#include "../include/QuickMedia.hpp" #include "../plugins/Manganelo.hpp" #include "../plugins/Mangadex.hpp" #include "../plugins/LocalManga.hpp" #include "../plugins/LocalAnime.hpp" #include "../plugins/MangaGeneric.hpp" #include "../plugins/MangaCombined.hpp" #include "../plugins/MediaGeneric.hpp" #include "../plugins/Youtube.hpp" #include "../plugins/Peertube.hpp" #include "../plugins/Fourchan.hpp" #include "../plugins/NyaaSi.hpp" #include "../plugins/Matrix.hpp" #include "../plugins/Soundcloud.hpp" #include "../plugins/Lbry.hpp" #include "../plugins/FileManager.hpp" #include "../plugins/Pipe.hpp" #include "../plugins/Saucenao.hpp" #include "../plugins/Info.hpp" #include "../plugins/HotExamples.hpp" #include "../plugins/MyAnimeList.hpp" #include "../plugins/AniList.hpp" #include "../include/Scale.hpp" #include "../include/Program.hpp" #include "../include/VideoPlayer.hpp" #include "../include/StringUtils.hpp" #include "../include/GoogleCaptcha.hpp" #include "../include/Notification.hpp" #include "../include/ImageViewer.hpp" #include "../include/ImageUtils.hpp" #include "../external/cppcodec/base64_url.hpp" #include "../include/Entry.hpp" #include "../include/NetUtils.hpp" #include "../include/ResourceLoader.hpp" #include "../include/Config.hpp" #include "../include/Tabs.hpp" #include "../include/Theme.hpp" #include "../include/Utils.hpp" #include "../include/Downloader.hpp" #include "../include/Storage.hpp" #include "../include/AsyncImageLoader.hpp" #include #include "../include/gui/Button.hpp" #include "../external/hash-library/sha256.h" #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include } #include #include #include #include #include #include static int FPS_IDLE; static const double IDLE_TIMEOUT_SEC = 2.0; static const mgl::vec2i AVATAR_THUMBNAIL_SIZE(std::floor(32), std::floor(32)); static const float more_items_height = 2.0f; static const int FPS_SYNC_TO_VSYNC = 0; static const std::pair valid_plugins[] = { std::make_pair("launcher", nullptr), std::make_pair("manganelo", "manganelo_logo.png"), std::make_pair("manganelos", "manganelos_logo.png"), std::make_pair("mangatown", "mangatown_logo.png"), std::make_pair("mangakatana", "mangakatana_logo.png"), std::make_pair("mangadex", "mangadex_logo.png"), std::make_pair("onimanga", nullptr), std::make_pair("readm", "readm_logo.png"), std::make_pair("local-manga", nullptr), std::make_pair("local-anime", nullptr), std::make_pair("manga", nullptr), std::make_pair("youtube", "yt_logo_rgb_dark_small.png"), std::make_pair("peertube", "peertube_logo.png"), std::make_pair("soundcloud", "soundcloud_logo.png"), std::make_pair("lbry", "lbry_logo.png"), std::make_pair("pornhub", "pornhub_logo.png"), std::make_pair("spankbang", "spankbang_logo.png"), std::make_pair("xvideos", "xvideos_logo.png"), std::make_pair("xhamster", "xhamster_logo.png"), std::make_pair("4chan", "4chan_logo.png"), std::make_pair("nyaa.si", "nyaa_si_logo.png"), std::make_pair("matrix", "matrix_logo.png"), std::make_pair("anilist", "anilist_logo.png"), std::make_pair("hotexamples", nullptr), std::make_pair("file-manager", nullptr), std::make_pair("stdin", nullptr), std::make_pair("saucenao", nullptr), std::make_pair("download", nullptr) }; static const char* get_plugin_logo_name(const char *plugin_name) { for(const auto &valid_plugin : valid_plugins) { if(strcmp(plugin_name, valid_plugin.first) == 0) return valid_plugin.second; } return nullptr; } // Prevent writing to broken pipe from exiting the program static void sigpipe_handler(int) { } static int x_error_handler(Display*, XErrorEvent*) { return 0; } static int x_io_error_handler(Display*) { return 0; } static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) { for(int i = 0; i < sr->nmode; ++i) { if(sr->modes[i].id == id) return &sr->modes[i]; } return nullptr; } static void for_each_active_monitor_output(Display *display, std::function callback_func) { XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display)); if(!screen_res) return; for(int i = 0; i < screen_res->noutput; ++i) { XRROutputInfo *out_info = XRRGetOutputInfo(display, screen_res, screen_res->outputs[i]); if(out_info && out_info->crtc && out_info->connection == RR_Connected) { XRRCrtcInfo *crt_info = XRRGetCrtcInfo(display, screen_res, out_info->crtc); if(crt_info && crt_info->mode) { const XRRModeInfo *mode_info = get_mode_info(screen_res, crt_info->mode); if(mode_info) callback_func(crt_info, mode_info); } if(crt_info) XRRFreeCrtcInfo(crt_info); } if(out_info) XRRFreeOutputInfo(out_info); } XRRFreeScreenResources(screen_res); } static int get_largest_monitor_height(Display *display) { int max_height = 0; for_each_active_monitor_output(display, [&max_height](const XRRCrtcInfo *crtc_info, const XRRModeInfo*) { // Need to get the min of width or height because we want to get the smallest size for monitors in portrait mode, for mobile devices such as pinephone int width_or_height = std::min((int)crtc_info->width, (int)crtc_info->height); max_height = std::max(max_height, width_or_height); }); if(max_height == 0) max_height = DefaultScreenOfDisplay(display)->height; return std::max(max_height, 240); } static void get_screen_resolution(Display *display, int *width, int *height) { *width = DefaultScreenOfDisplay(display)->width; *height = DefaultScreenOfDisplay(display)->height; } static mgl::Color interpolate_colors(mgl::Color source, mgl::Color target, double progress) { int diff_r = (int)target.r - (int)source.r; int diff_g = (int)target.g - (int)source.g; int diff_b = (int)target.b - (int)source.b; int diff_a = (int)target.a - (int)source.a; return mgl::Color( source.r + diff_r * progress, source.g + diff_g * progress, source.b + diff_b * progress, source.a + diff_a * progress); } static std::string base64_url_encode(const std::string &data) { return cppcodec::base64_url::encode(data); } static std::string base64_url_decode(const std::string &data) { return cppcodec::base64_url::decode(data); } static void set_window_icon(Display *display, Window window, const std::string &icon_filepath) { mgl::Image image; if(!image.load_from_file(icon_filepath.c_str()) || image.get_num_channels() != 4) { fprintf(stderr, "Warning: failed to load window icon: %s\n", icon_filepath.c_str()); return; } const size_t icon_buffer_size = 2 + image.get_size().x * image.get_size().y; unsigned long *icon_buffer = new unsigned long[icon_buffer_size]; icon_buffer[0] = image.get_size().x; icon_buffer[1] = image.get_size().y; const unsigned char *image_data = image.data(); for(size_t i = 0; i < (size_t)image.get_size().x * (size_t)image.get_size().y; ++i) { const size_t image_data_index = i * 4; icon_buffer[2 + i] = (long)image_data[image_data_index + 0] << 16 | (long)image_data[image_data_index + 1] << 8 | (long)image_data[image_data_index + 2] << 0 | (long)image_data[image_data_index + 3] << 24; } Atom net_wm_icon = XInternAtom(display, "_NET_WM_ICON", False); XChangeProperty(display, window, net_wm_icon, XA_CARDINAL, 32, PropModeReplace, (const unsigned char*)icon_buffer, icon_buffer_size); XFlush(display); delete []icon_buffer; } namespace QuickMedia { static Json::Value load_recommended_json(const char *plugin_name); static void fill_recommended_items_from_json(const char *plugin_name, const Json::Value &recommended_json, BodyItems &body_items); enum class HistoryType { YOUTUBE, MANGA }; class HistoryPage : public LazyFetchPage { public: HistoryPage(Program *program, Page *search_page, HistoryType history_type, bool local_thumbnail = false) : LazyFetchPage(program), search_page(search_page), history_type(history_type), local_thumbnail(local_thumbnail) {} const char* get_title() const override { return "History"; } PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override { return search_page->submit(args, result_tabs); } PluginResult lazy_fetch(BodyItems &result_items) override { switch(history_type) { case HistoryType::YOUTUBE: program->youtube_get_watch_history(result_items); break; case HistoryType::MANGA: program->manga_get_watch_history(program->get_plugin_name(), result_items, local_thumbnail); break; } return PluginResult::OK; } bool reload_on_page_change() override { return true; } const char* get_bookmark_name() const override { return search_page->get_bookmark_name(); } private: Page *search_page; HistoryType history_type; bool local_thumbnail; }; class RecommendedPage : public LazyFetchPage { public: RecommendedPage(Program *program, Page *search_page, const char *plugin_name) : LazyFetchPage(program), search_page(search_page), plugin_name(plugin_name) {} const char* get_title() const override { return "Recommended"; } PluginResult submit(const SubmitArgs &args, std::vector &result_tabs) override { return search_page->submit(args, result_tabs); } PluginResult lazy_fetch(BodyItems &result_items) override { fill_recommended_items_from_json(plugin_name, load_recommended_json(plugin_name), result_items); return PluginResult::OK; } bool reload_on_page_change() override { return true; } private: Page *search_page; const char *plugin_name; }; using OptionsPageHandler = std::function; class OptionsPage : public Page { public: OptionsPage(Program *program, std::string title) : Page(program), title(std::move(title)) {} const char* get_title() const override { return title.c_str(); } PluginResult submit(const SubmitArgs &args, std::vector&) override { const int handlers_index = atoi(args.url.c_str()); handlers[handlers_index](); program->set_go_to_previous_page(); return PluginResult::OK; } bool submit_is_async() const override { return false; } void add_option(Body *body, std::string title, std::string description, OptionsPageHandler handler) { assert(handler); auto body_item = BodyItem::create(std::move(title)); if(!description.empty()) { body_item->set_description(std::move(description)); body_item->set_description_color(get_theme().faded_text_color); } body_item->url = std::to_string(handlers.size()); handlers.push_back(std::move(handler)); body->append_item(std::move(body_item)); } private: std::string title; std::vector handlers; }; Program::Program() : disp(nullptr), window_size(1280, 720), current_page(PageType::EXIT), image_index(0) { } Program::~Program() { window.close(); images_to_upscale_queue.close(); if(image_upscale_thead.joinable()) image_upscale_thead.join(); if(matrix) delete matrix; if(disp) XCloseDisplay(disp); } static void usage() { fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--dir ] [-e ] [youtube-url]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); fprintf(stderr, " --no-video Only play audio when playing a video. 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-always 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. Default is the the users home directory\n"); fprintf(stderr, " --instance The instance to use for peertube\n"); fprintf(stderr, " -e Embed QuickMedia into another window\n"); fprintf(stderr, " --video-max-height Media plugins will try to select a video source that is this size or smaller\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, " quickmedia\n"); fprintf(stderr, " quickmedia --upscale-images-always manganelo\n"); fprintf(stderr, " quickmedia https://www.youtube.com/watch?v=jHg91NVHh3s\n"); fprintf(stderr, " echo -e \"hello\\nworld\" | quickmedia stdin\n"); fprintf(stderr, " tabbed -c -k quickmedia launcher -e\n"); } static bool is_manga_plugin(const char *plugin_name) { return strcmp(plugin_name, "manga") == 0 || strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "manganelos") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangakatana") == 0 || strcmp(plugin_name, "mangadex") == 0 || strcmp(plugin_name, "readm") == 0 || strcmp(plugin_name, "onimanga") == 0 || strcmp(plugin_name, "local-manga") == 0; } static std::shared_ptr create_launcher_body_item(const char *title, const char *plugin_name, const std::string &thumbnail_url) { auto body_item = BodyItem::create(title); body_item->url = plugin_name; if(!thumbnail_url.empty()) { body_item->thumbnail_url = thumbnail_url; body_item->thumbnail_is_local = true; } body_item->thumbnail_size.x = 32; body_item->thumbnail_size.y = 32; return body_item; } static bool convert_to_absolute_path(std::filesystem::path &path) { char resolved[PATH_MAX]; if(!realpath(path.c_str(), resolved)) return false; path = resolved; return true; } int Program::run(int argc, char **argv) { if(argc < 1) { usage(); return -1; } if(argc == 1) plugin_name = "launcher"; Window parent_window = None; video_max_height = 0; std::vector tabs; const char *url = nullptr; std::string program_path = dirname(argv[0]); std::string instance; for(int i = 1; i < argc; ++i) { if(!plugin_name) { std::string youtube_video_id_dummy; std::string youtube_url_converted = invidious_url_to_youtube_url(argv[i]); if(youtube_url_extract_id(youtube_url_converted, youtube_video_id_dummy)) { youtube_url = std::move(youtube_url_converted); plugin_name = "youtube"; } for(const auto &valid_plugin : valid_plugins) { if(strcmp(argv[i], valid_plugin.first) == 0) { plugin_name = argv[i]; break; } } } if(strcmp(argv[i], "--no-video") == 0) { force_no_video = true; } else if(strcmp(argv[i], "--upscale-images") == 0) { upscale_image_action = UpscaleImageAction::LOW_RESOLUTION; } else if(strcmp(argv[i], "--upscale-images-force") == 0 || strcmp(argv[i], "--upscale-images-always") == 0) { upscale_image_action = UpscaleImageAction::FORCE; } else if(strcmp(argv[i], "--instance") == 0) { if(i < argc - 1) { instance = argv[i + 1]; ++i; } else { fprintf(stderr, "Missing instance after --instance argument\n"); usage(); return -1; } } else if(strcmp(argv[i], "--dir") == 0) { if(i < argc - 1) { file_manager_start_dir = argv[i + 1]; ++i; } else { fprintf(stderr, "Missing directory after --dir argument\n"); usage(); return -1; } } else if(strcmp(argv[i], "--low-cpu-mode") == 0) { low_cpu_mode = true; } else if(strcmp(argv[i], "-u") == 0) { if(i < argc - 1) { url = argv[i + 1]; ++i; } else { fprintf(stderr, "Missing url after -u argument\n"); usage(); return -1; } } else if(strcmp(argv[i], "-e") == 0) { if(i < argc - 1) { parent_window = strtoll(argv[i + 1], nullptr, 0); if(parent_window == None && errno == EINVAL) { fprintf(stderr, "Invalid -e argument. Argument has to be a number\n"); usage(); return -1; } ++i; } else { fprintf(stderr, "Missing window id after -e argument\n"); usage(); return -1; } } else if(strcmp(argv[i], "--video-max-height") == 0) { if(i < argc - 1) { errno = 0; video_max_height = strtoll(argv[i + 1], nullptr, 0); if(errno == EINVAL) { fprintf(stderr, "Invalid --video-max-height argument. Argument has to be a number\n"); usage(); return -1; } ++i; } else { fprintf(stderr, "Missing number after --video-max-height argument\n"); usage(); return -1; } } else if(argv[i][0] == '-') { fprintf(stderr, "Invalid option %s\n", argv[i]); usage(); return -1; } } if(!plugin_name) { fprintf(stderr, "Missing plugin argument\n"); usage(); return -1; } if(low_cpu_mode) FPS_IDLE = 3; else FPS_IDLE = 20; if(upscale_image_action != UpscaleImageAction::NO) { if(!is_manga_plugin(plugin_name)) { fprintf(stderr, "Option --upscale-images/-upscale-images-force is only valid for manga plugins\n"); return -2; } if(!is_program_executable_by_name("waifu2x-ncnn-vulkan")) { fprintf(stderr, "waifu2x-ncnn-vulkan needs to be installed when using the --upscale-images/--upscale-images-always option\n"); return -2; } image_upscale_thead = std::thread([this]{ std::optional copy_op_opt; while(true) { copy_op_opt = images_to_upscale_queue.pop_wait(); if(!copy_op_opt) break; CopyOp ©_op = copy_op_opt.value(); Path tmp_file = copy_op.source; tmp_file.append(".tmp.png"); fprintf(stderr, "Upscaling %s\n", copy_op.source.data.c_str()); const char *args[] = { "waifu2x-ncnn-vulkan", "-n", "3", "-i", copy_op.source.data.c_str(), "-o", tmp_file.data.c_str(), nullptr }; if(exec_program(args, nullptr, nullptr) != 0) { fprintf(stderr, "Warning: failed to upscale %s with waifu2x-ncnn-vulkan\n", copy_op.source.data.c_str()); // No conversion, but we need the file to have the destination name to see that the operation completed (and read it) if(rename_atomic(copy_op.source.data.c_str(), copy_op.destination.data.c_str()) != 0) perror(tmp_file.data.c_str()); continue; } if(rename_atomic(tmp_file.data.c_str(), copy_op.destination.data.c_str()) != 0) perror(tmp_file.data.c_str()); copy_op.destination.append(".upscaled"); file_overwrite(copy_op.destination.data.c_str(), "1"); } }); } std::filesystem::path home_path = get_home_dir().data; if(file_manager_start_dir.empty()) { file_manager_start_dir = home_path; } else { if(!convert_to_absolute_path(file_manager_start_dir)) fprintf(stderr, "Warning: failed to get absolute path for path: %s\n", file_manager_start_dir.c_str()); } int start_tab_index = 0; FileManagerMimeType fm_mine_type = FILE_MANAGER_MIME_TYPE_ALL; FileSelectionHandler file_selection_handler = nullptr; FileSelectionHandler saucenao_file_selection_handler = [this](FileManagerPage*, const std::filesystem::path&) { std::vector tabs; tabs.push_back(Tab{create_body(), std::make_unique(this, selected_files[0], true), nullptr}); return tabs; }; no_video = force_no_video; mgl_init(); init(parent_window, program_path); if(strcmp(plugin_name, "download") == 0) { if(!url) { fprintf(stderr, "-u argument has to be set when using the download plugin\n"); usage(); return -1; } download_page(url); return exit_code; } if(strcmp(plugin_name, "saucenao") == 0) { plugin_name = "file-manager"; fm_mine_type = FILE_MANAGER_MIME_TYPE_IMAGE; file_selection_handler = std::move(saucenao_file_selection_handler); } load_plugin_by_name(tabs, start_tab_index, fm_mine_type, std::move(file_selection_handler), std::move(instance)); while(!tabs.empty() || matrix) { if(matrix) { if(matrix->load_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; chat_login_page(); } after_matrix_login_page(); return exit_code; } page_loop(tabs, start_tab_index); tabs.clear(); if(strcmp(plugin_name, "launcher") == 0) { plugin_name = pipe_selected_text.c_str(); if(strcmp(plugin_name, "saucenao") == 0) { plugin_name = "file-manager"; fm_mine_type = FILE_MANAGER_MIME_TYPE_IMAGE; file_selection_handler = std::move(saucenao_file_selection_handler); } load_plugin_by_name(tabs, start_tab_index, fm_mine_type, std::move(file_selection_handler), ""); } } return exit_code; } // TODO: Move to mgl static mgl::vec2i get_global_mouse_position(Display *display) { Window dummy_w; int dummy_i; unsigned int dummy_u; mgl::vec2i mouse_pos; XQueryPointer(display, DefaultRootWindow(display), &dummy_w, &dummy_w, &mouse_pos.x, &mouse_pos.y, &dummy_i, &dummy_i, &dummy_u); return mouse_pos; } static mgl::vec2i get_focused_monitor_center(Display *disp, mgl::vec2i &monitor_size) { int screen = DefaultScreen(disp); monitor_size.x = DisplayWidth(disp, screen); monitor_size.y = DisplayHeight(disp, screen); int screen_center_x = monitor_size.x / 2; int screen_center_y = monitor_size.y / 2; mgl::vec2i focused_monitor_center(screen_center_x, screen_center_y); mgl::vec2i mouse_pos = get_global_mouse_position(disp); for_each_active_monitor_output(disp, [&focused_monitor_center, mouse_pos, &monitor_size](const XRRCrtcInfo *crtc_info, const XRRModeInfo*){ if(mgl::Rect(mgl::vec2i(crtc_info->x, crtc_info->y), mgl::vec2i(crtc_info->width, crtc_info->height)).contains(mouse_pos)) { monitor_size.x = crtc_info->width; monitor_size.y = crtc_info->height; focused_monitor_center = mgl::vec2i(crtc_info->x + crtc_info->width/2, crtc_info->y + crtc_info->height/2); } }); return focused_monitor_center; } void Program::init(mgl::WindowHandle parent_window, std::string &program_path) { disp = XOpenDisplay(NULL); if (!disp) { show_notification("QuickMedia", "Failed to open display to X11 server", Urgency::CRITICAL); abort(); } XSetErrorHandler(x_error_handler); XSetIOErrorHandler(x_io_error_handler); mgl::vec2i monitor_size; mgl::vec2i focused_monitor_center = get_focused_monitor_center(disp, monitor_size); if(strcmp(plugin_name, "download") == 0) { window_size.x = std::min(900, monitor_size.x); window_size.y = std::min(900, monitor_size.y); } mgl::Window::CreateParams window_create_params; window_create_params.position = { focused_monitor_center.x - window_size.x / 2, focused_monitor_center.y - window_size.y / 2 }; window_create_params.size = window_size; if(strcmp(plugin_name, "download") == 0) { window_create_params.min_size = window_size; window_create_params.max_size = window_size; } window_create_params.parent_window = parent_window; if(!window.create("QuickMedia", std::move(window_create_params))) { show_notification("QuickMedia", "Failed to create opengl window", Urgency::CRITICAL); abort(); } if(!program_path.empty() && program_path.back() != '/') program_path += '/'; resources_root = "/usr/share/quickmedia/"; if(get_file_type(program_path + "../../../images/manganelo_logo.png") == FileType::REGULAR) { resources_root = program_path + "../../../"; } // Initialize config and theme early to prevent possible race condition on initialize get_config(); get_theme(); set_resource_loader_root_path(resources_root.c_str()); set_use_system_fonts(get_config().use_system_fonts); init_body_themes(); set_window_icon(disp, window.get_system_handle(), resources_root + "icons/qm_logo.png"); if(!is_touch_enabled()) { if(!circle_mask_shader.load_from_file((resources_root + "shaders/circle_mask.glsl").c_str(), mgl::Shader::Type::Fragment)) { show_notification("QuickMedia", "Failed to load " + resources_root + "/shaders/circle_mask.glsl", Urgency::CRITICAL); abort(); } if(get_theme().drop_shadow) { if(!rounded_rectangle_shader.load_from_file((resources_root + "shaders/rounded_rectangle.glsl").c_str(), mgl::Shader::Type::Fragment)) { show_notification("QuickMedia", "Failed to load " + resources_root + "/shaders/rounded_rectangle.glsl", Urgency::CRITICAL); abort(); } } else { if(!rounded_rectangle_shader.load_from_file((resources_root + "shaders/rounded_rectangle_no_shadow.glsl").c_str(), mgl::Shader::Type::Fragment)) { show_notification("QuickMedia", "Failed to load " + resources_root + "/shaders/rounded_rectangle_no_shadow.glsl", Urgency::CRITICAL); abort(); } } if(!rounded_rectangle_mask_shader.load_from_file((resources_root + "shaders/rounded_rectangle_mask.glsl").c_str(), mgl::Shader::Type::Fragment)) { show_notification("QuickMedia", "Failed to load " + resources_root + "/shaders/rounded_rectangle_mask.glsl", Urgency::CRITICAL); abort(); } } if(!loading_icon.load_from_file((resources_root + "images/loading_icon.png").c_str())) { show_notification("QuickMedia", "Failed to load " + resources_root + "/images/loading_icon.png", Urgency::CRITICAL); abort(); } load_sprite.set_texture(&loading_icon); mgl::vec2i loading_icon_size = loading_icon.get_size(); load_sprite.set_origin(mgl::vec2f(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f)); TextureLoader::get_texture("images/search_icon.png"); TextureLoader::get_texture("images/arrow.png"); struct sigaction action; action.sa_handler = sigpipe_handler; sigemptyset(&action.sa_mask); action.sa_flags = 0; sigaction(SIGPIPE, &action, NULL); XSetErrorHandler(x_error_handler); XSetIOErrorHandler(x_io_error_handler); window.set_framerate_limit(FPS_SYNC_TO_VSYNC); idle = false; if(create_directory_recursive(get_cache_dir().join("media")) != 0) { show_notification("QuickMedia", "Failed to create media directory", Urgency::CRITICAL); abort(); } if(create_directory_recursive(get_cache_dir().join("thumbnails")) != 0) { show_notification("QuickMedia", "Failed to create thumbnails directory", Urgency::CRITICAL); abort(); } if(create_directory_recursive(get_storage_dir()) != 0) { show_notification("QuickMedia", "Failed to create storage directory", Urgency::CRITICAL); abort(); } //if(create_directory_recursive(get_storage_dir().join("file-manager")) != 0) { // show_notification("QuickMedia", "Failed to create file-manager directory", Urgency::CRITICAL); // abort(); //} const char *qm_phone_factor = getenv("QM_PHONE_FACTOR"); if(qm_phone_factor && atoi(qm_phone_factor) == 1) show_room_side_panel = false; else show_room_side_panel = true; main_thread_id = std::this_thread::get_id(); auto window_size_u = window.get_size(); window_size.x = window_size_u.x; window_size.y = window_size_u.y; } // Returns size_t(-1) if not found static size_t find_end_of_json_array(const char *str, size_t start, size_t size) { if(size <= start || str[start] != '[') return size_t(-1); bool inside_string = false; bool escape = false; int array_depth = 0; for(size_t i = start; i < size; ++i) { char c = str[i]; if(c == '"' && !escape) { inside_string = !inside_string; } else if(c == '\\') { escape = !escape; } else if(c == '[' && !inside_string && !escape) { ++array_depth; } else if(c == ']' && !inside_string && !escape) { --array_depth; if(array_depth == 0) return i + 1; } else { escape = false; } } return size_t(-1); } static void add_manganelos_handlers(MangaGenericSearchPage *manga_generic_search_page) { manga_generic_search_page->search_handler("http://manganelos.com/search?q=%s&page=%p", 1) .text_handler({{"//div[class='media-left cover-manga']//a", "title", "href", "/manga/"}}) .thumbnail_handler({{"//div[class='media-left cover-manga']//img[class='media-object']", "src", "/mangaimage/"}}) .list_chapters_handler("//section[id='examples']//div[class='chapter-list']//a", "text", "href", nullptr) .list_page_images_handler("//p[id='arraydata']", "text", nullptr, [](std::vector &urls) { if(urls.size() != 1) return; std::string urls_combined = urls.front(); urls.clear(); string_split(urls_combined, ',', [&urls](const char *str, size_t size) { std::string url(str, size); url = strip(url); if(!url.empty()) urls.push_back(std::move(url)); return true; }); }) .manga_id_handler("/manga/", "?"); } static void add_mangatown_handlers(MangaGenericSearchPage *manga_generic_search_page) { manga_generic_search_page->search_handler("https://www.mangatown.com/search?name=%s&page=%p", 1) .text_handler({{"//p[class='title']/a", "title", "href", "/manga/"}}) .thumbnail_handler({{"//a[class='manga_cover']/img", "src", nullptr}}) .authors_handler({ {"//div[class='detail_content']//a", "text", "href", "/author/"}, {"//div[class='detail_content']//a", "text", "href", "/artist/"} }) .list_chapters_handler("//ul[class='chapter_list']//a", "text", "href", "/manga/") .list_chapters_uploaded_time_handler("//ul[class='chapter_list']//span[class='time']", "text", nullptr) .list_page_images_pagination_handler( "//div[class='page_select']//option", "text", "//img[id='image']", "src", nullptr, "//a[class='next_page']", "href", nullptr) .manga_id_handler("/manga/", "/"); } static void add_mangakatana_handlers(MangaGenericSearchPage *manga_generic_search_page) { manga_generic_search_page->search_handler("https://mangakatana.com/page/%p?search=%s&search_by=book_name", 1) .text_handler({ {"//div[id='book_list']//h3[class='title']//a", "text", "href", "/manga/"}, {"//div[id='single_book']//h1[class='heading']", "text", nullptr, nullptr} }) .thumbnail_handler({ {"//div[id='book_list']//div[class='media']//img", "src", nullptr}, {"//div[id='single_book']//div[class='cover']//img", "src", nullptr} }) .description_handler({ {"//div[id='book_list']//div[class='*summary*']", "text"}, {"//div[id='single_book']//div[class='*summary*']", "text"} }) .authors_handler({{"//div[id='single_book']//a[class='author']", "text", "href", "/author/"}}) .list_chapters_handler("//div[class='chapters']//div[class='chapter']//a[0]", "text", "href", "/manga/") .list_chapters_uploaded_time_handler("//div[class='chapters']//div[class='update_time']", "text", nullptr) .list_page_images_custom_handler([](const std::string &html_source) { std::vector urls; size_t sources_start = html_source.find("ytaw=["); if(sources_start == std::string::npos) return urls; sources_start += 5; // just before [ size_t json_end = find_end_of_json_array(html_source.c_str(), sources_start, html_source.size()); if(json_end == size_t(-1)) return urls; sources_start += 1; json_end -= 1; std::string urls_str = html_source.substr(sources_start, json_end - sources_start); string_replace_all(urls_str, "'", ""); string_split(urls_str, ',', [&urls](const char *str, size_t size) { std::string url(str, size); url = strip(url); if(!url.empty()) urls.push_back(std::move(url)); return true; }); return urls; }) .manga_id_handler("/manga/", nullptr); } static void add_readm_handlers(MangaGenericSearchPage *manga_generic_search_page) { manga_generic_search_page->search_post_handler("https://readm.org/service/search", {{"dataType", "json"}, {"phrase", "%s"}}, [](Json::Value &json_root) { BodyItems result_items; if(!json_root.isObject()) return result_items; const Json::Value &manga_json = json_root["manga"]; if(!manga_json.isArray()) return result_items; for(const Json::Value &item_json : manga_json) { if(!item_json.isObject()) continue; const Json::Value &title_json = item_json["title"]; const Json::Value &url_json = item_json["url"]; const Json::Value &image_json = item_json["image"]; if(!title_json.isString() || !url_json.isString()) continue; auto body_item = BodyItem::create(strip(title_json.asString())); body_item->url = strip(url_json.asString()); if(image_json.isString()) body_item->thumbnail_url = strip(image_json.asString()); result_items.push_back(std::move(body_item)); } return result_items; }) .list_chapters_handler("//div[class='episodes-list']//a", "text", "href", "/manga/") .list_chapters_uploaded_time_handler("//div[class='episodes-list']//td[class='episode-date']", "text", nullptr) .list_page_images_handler("//div[id='content']//img", "src", "/chapter_files/") .manga_id_handler("/manga/", "/"); } static void add_onimanga_handlers(MangaGenericSearchPage *manga_generic_search_page) { manga_generic_search_page->search_handler("https://onimanga.com/search?search=%s", 1) .text_handler({{"//li[class='manga-name']/a", "text", "href", nullptr}}) .list_chapters_handler("//div[class='manga-chapters']//div[class='chapter']//a", "text", "href", nullptr) .list_page_images_handler("//img[class='page']", "src", "/scans/") .manga_id_handler("/", nullptr); } static void add_pornhub_handlers(MediaGenericSearchPage *media_generic_search_page) { media_generic_search_page->search_handler("https://www.pornhub.com/video/search?search=%s&page=%p", 1) .text_handler({{"//div[class='nf-videos']//div[class='phimage']//a", "title", "href", "/view_video.php"}}) .thumbnail_handler({{"//div[class='nf-videos']//div[class='phimage']//img", "data-src", "/videos/"}}) .related_media_text_handler({{"//div[class='phimage']//a", "title", "href", "/view_video.php"}}) .related_media_thumbnail_handler({{"//div[class='phimage']//img", "data-src", nullptr}}); } static void add_spankbang_handlers(MediaGenericSearchPage *media_generic_search_page) { media_generic_search_page->search_handler("https://spankbang.com/s/%s/%p/", 1) .text_handler({{"//div[class='main_results']//div[class='video-item']//a[class='n']", "text", "href", "/video/"}}) .thumbnail_handler({{"//div[class='main_results']//div[class='video-item']//img", "data-src", nullptr}}) .related_media_text_handler({{"//div[class='right']//div[class='video-item']//a[class='n']", "text", "href", "/video/"}}) .related_media_thumbnail_handler({{"//div[class='right']//div[class='video-item']//img", "data-src", nullptr}}); } static void add_xvideos_handlers(MediaGenericSearchPage *media_generic_search_page) { media_generic_search_page->search_handler("https://www.xvideos.com/?k=%s&p=%p", 0) .text_handler({{"//div[id='main']//div[class='thumb-under']//a", "title", "href", "/video"}}) .thumbnail_handler({{"//div[id='main']//div[class='thumb']//img", "data-src", "/videos"}}) .video_url_custom_handler([](const std::string &html_source) -> std::string { size_t start_index = html_source.find("html5player.setVideoHLS"); if(start_index == std::string::npos) return ""; start_index += 23; start_index = html_source.find("'", start_index); if(start_index == std::string::npos) return ""; start_index += 1; size_t end_index = html_source.find("'", start_index); if(end_index == std::string::npos) return ""; return html_source.substr(start_index, end_index - start_index); }) .related_media_custom_handler([](const std::string &html_source) { std::vector related_items; size_t related_start = html_source.find("video_related=["); if(related_start == std::string::npos) return related_items; related_start += 14; // just before [ size_t json_end = find_end_of_json_array(html_source.c_str(), related_start, html_source.size()); if(json_end == size_t(-1)) return related_items; Json::Value json_root; Json::CharReaderBuilder json_builder; std::unique_ptr json_reader(json_builder.newCharReader()); std::string json_errors; if(!json_reader->parse(html_source.c_str() + related_start, html_source.c_str() + json_end, &json_root, &json_errors)) { fprintf(stderr, "Failed to parse xvideos related json, error: %s\n", json_errors.c_str()); return related_items; } if(!json_root.isArray()) return related_items; for(const Json::Value &json_item : json_root) { if(!json_item.isObject()) continue; const Json::Value &title_json = json_item["tf"]; const Json::Value &url_json = json_item["u"]; const Json::Value &thumbnail_url_json = json_item["i"]; if(!title_json.isString() || !url_json.isString()) continue; MediaRelatedItem related_item; related_item.title = title_json.asString(); related_item.url = url_json.asString(); if(thumbnail_url_json.isString()) related_item.thumbnail_url = thumbnail_url_json.asString(); related_items.push_back(std::move(related_item)); } return related_items; }); } static void add_xhamster_handlers(MediaGenericSearchPage *media_generic_search_page) { media_generic_search_page->search_handler("https://xhamster.com/search/%s?page=%p", 1) .text_handler({{"//div[data-role='video-list']//div[class='video-thumb-info']//a", "text", "href", "/videos/"}}) .thumbnail_handler({{"//div[data-role='video-list']//img", "src", "/thumb-"}}) .related_media_text_handler({{"//div[data-role='video-relations']//div[class='video-thumb-info']//a", "text", "href", "/videos/"}}) .related_media_thumbnail_handler({{"//div[data-role='video-relations']//img", "src", "/thumb-"}}); } static void check_youtube_dl_installed(const std::string &plugin_name) { if(!is_program_executable_by_name("youtube-dl")) { show_notification("QuickMedia", "youtube-dl needs to be installed to play " + plugin_name + " videos", Urgency::CRITICAL); abort(); } } void Program::load_plugin_by_name(std::vector &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler, std::string instance) { if(!plugin_name || plugin_name[0] == '\0') return; window.set_title(("QuickMedia - " + std::string(plugin_name)).c_str()); no_video = force_no_video; if(strcmp(plugin_name, "youtube-audio") == 0) { plugin_name = "youtube"; no_video = true; } std::string plugin_logo_path; const char *plugin_logo_name = get_plugin_logo_name(plugin_name); if(plugin_logo_name) plugin_logo_path = resources_root + "images/" + plugin_logo_name; plugin_logo.clear(); if(!plugin_logo_path.empty()) { if(!plugin_logo.load_from_file(plugin_logo_path.c_str())) fprintf(stderr, "Failed to load plugin logo, path: %s\n", plugin_logo_path.c_str()); // TODO: Fix //plugin_logo.generateMipmap(); } if(strcmp(plugin_name, "launcher") == 0) { auto pipe_body = create_body(true); pipe_body->set_items({ create_launcher_body_item("4chan", "4chan", resources_root + "icons/4chan_launcher.png"), create_launcher_body_item("AniList", "anilist", resources_root + "images/anilist_logo.png"), create_launcher_body_item("Hot Examples", "hotexamples", ""), create_launcher_body_item("Lbry", "lbry", resources_root + "icons/lbry_launcher.png"), create_launcher_body_item("Local anime", "local-anime", ""), create_launcher_body_item("Local manga", "local-manga", ""), create_launcher_body_item("Manga (all)", "manga", ""), create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png"), create_launcher_body_item("Mangakatana", "mangakatana", resources_root + "icons/mangakatana_launcher.png"), create_launcher_body_item("Manganelo", "manganelo", resources_root + "icons/manganelo_launcher.png"), create_launcher_body_item("Manganelos", "manganelos", resources_root + "icons/manganelos_launcher.png"), create_launcher_body_item("Mangatown", "mangatown", resources_root + "icons/mangatown_launcher.png"), create_launcher_body_item("Onimanga", "onimanga", ""), create_launcher_body_item("Readm", "readm", resources_root + "icons/readm_launcher.png"), create_launcher_body_item("Matrix", "matrix", resources_root + "icons/matrix_launcher.png"), create_launcher_body_item("Nyaa.si", "nyaa.si", resources_root + "icons/nyaa_si_launcher.png"), create_launcher_body_item("PeerTube", "peertube", resources_root + "images/peertube_logo.png"), create_launcher_body_item("SauceNAO", "saucenao", ""), create_launcher_body_item("Soundcloud", "soundcloud", resources_root + "icons/soundcloud_launcher.png"), create_launcher_body_item("YouTube", "youtube", resources_root + "icons/yt_launcher.png"), create_launcher_body_item("YouTube (audio only)", "youtube-audio", resources_root + "icons/yt_launcher.png"), }); tabs.push_back(Tab{std::move(pipe_body), std::make_unique(this, "Select plugin to launch"), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "manganelo") == 0) { auto search_page = std::make_unique(this); tabs.push_back(Tab{create_body(), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "manganelos") == 0) { auto search_page = std::make_unique(this, plugin_name, "http://manganelos.com/"); add_manganelos_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "mangatown") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://www.mangatown.com/"); add_mangatown_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "mangakatana") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://mangakatana.com/", false); add_mangakatana_handlers(search_page.get()); auto bookmarks_body = create_body(); bookmarks_body->draw_thumbnails = false; auto history_body = create_body(); history_body->draw_thumbnails = false; tabs.push_back(Tab{std::move(bookmarks_body), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{std::move(history_body), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "mangadex") == 0) { auto search_page = std::make_unique(this); tabs.push_back(Tab{create_body(), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "readm") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://readm.org/"); add_readm_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "onimanga") == 0) { auto search_page = std::make_unique(this, plugin_name, "https://onimanga.com/"); add_onimanga_handlers(search_page.get()); tabs.push_back(Tab{create_body(), std::make_unique(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA); tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "local-manga") == 0) { auto search_page = std::make_unique(this, true); tabs.push_back(Tab{create_body(false, true), std::make_unique(this, search_page.get(), true), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); auto history_page = std::make_unique(this, tabs.back().page.get(), HistoryType::MANGA, true); tabs.push_back(Tab{create_body(false, true), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 1; } else if(strcmp(plugin_name, "local-anime") == 0) { if(get_config().local_anime.directory.empty()) { show_notification("QuickMedia", "local_anime.directory config is not set", Urgency::CRITICAL); exit(1); } if(get_file_type(get_config().local_anime.directory) != FileType::DIRECTORY) { show_notification("QuickMedia", "local_anime.directory config is not set to a valid directory", Urgency::CRITICAL); exit(1); } auto search_page = std::make_unique(this, get_anime_in_directory(get_config().local_anime.directory)); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); start_tab_index = 0; } else if(strcmp(plugin_name, "manga") == 0) { auto mangadex = std::make_unique(this); auto manganelo = std::make_unique(this); auto manganelos = std::make_unique(this, "manganelos", "http://manganelos.com/"); add_manganelos_handlers(manganelos.get()); auto mangatown = std::make_unique(this, "mangatown", "https://www.mangatown.com/"); add_mangatown_handlers(mangatown.get()); auto mangakatana = std::make_unique(this, "mangakatana", "https://mangakatana.com/", false); add_mangakatana_handlers(mangakatana.get()); auto onimanga = std::make_unique(this, "onimanga", "https://onimanga.com/"); add_onimanga_handlers(onimanga.get()); auto readm = std::make_unique(this, "readm", "https://readm.org/"); add_readm_handlers(readm.get()); auto local_manga = std::make_unique(this, false); // TODO: Use async task pool std::vector pages; pages.push_back({std::move(manganelo), "Manganelo", "manganelo"}); pages.push_back({std::move(manganelos), "Manganelos", "manganelos"}); pages.push_back({std::move(mangatown), "Mangatown", "mangatown"}); pages.push_back({std::move(mangakatana), "Mangakatana", "mangakatana"}); pages.push_back({std::move(onimanga), "Onimanga", "onimanga"}); pages.push_back({std::move(readm), "Readm", "readm"}); pages.push_back({std::move(mangadex), "Mangadex", "mangadex"}); if(!get_config().local_manga.directory.empty()) pages.push_back({std::move(local_manga), "Local manga", "local-manga", true}); tabs.push_back(Tab{create_body(), std::make_unique(this, std::move(pages)), create_search_bar("Search...", 400)}); } else if(strcmp(plugin_name, "nyaa.si") == 0) { auto categories_nyaa_si_body = create_body(); BodyItems body_items; get_nyaa_si_categories(body_items); categories_nyaa_si_body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(categories_nyaa_si_body), std::make_unique(this, false), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); auto categories_sukebei_body = create_body(); get_sukebei_categories(body_items); categories_sukebei_body->set_items(body_items); tabs.push_back(Tab{std::move(categories_sukebei_body), std::make_unique(this, true), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "4chan") == 0) { auto boards_page = std::make_unique(this, resources_root); FourchanBoardsPage *boards_page_ptr = boards_page.get(); auto boards_body = create_body(); BodyItems body_items; boards_page->get_boards(body_items); boards_body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(boards_body), std::move(boards_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); auto login_page = std::make_unique(this, "4chan pass login", boards_page_ptr, &tabs, 1); FourchanLoginPage *login_page_ptr = login_page.get(); tabs.push_back(Tab{ create_body(), std::move(login_page), nullptr, {} }); login_page_ptr->login_inputs = &tabs.back().login_inputs; page_loop(tabs); } else if(strcmp(plugin_name, "hotexamples") == 0) { auto body = create_body(); BodyItems body_items; hot_examples_front_page_fill(body_items); body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "anilist") == 0) { tabs.push_back(Tab{create_body(), std::make_unique(this, AniListMediaType::ANIME), create_search_bar("Search...", 300)}); tabs.push_back(Tab{create_body(), std::make_unique(this, AniListMediaType::MANGA), create_search_bar("Search...", 300)}); } else if(strcmp(plugin_name, "file-manager") == 0) { auto file_manager_page = std::make_unique(this, fm_mime_type, file_selection_handler); if(!file_manager_page->set_current_directory(file_manager_start_dir)) fprintf(stderr, "Warning: Invalid directory provided with --dir\n"); auto file_manager_body = create_body(); BodyItems body_items; file_manager_page->get_files_in_directory(body_items); file_manager_body->set_items(std::move(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, "stdin") == 0) { auto pipe_body = create_body(); BodyItems body_items; PipePage::load_body_items_from_stdin(body_items); pipe_body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(pipe_body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "youtube") == 0) { if(youtube_url.empty()) { start_tab_index = 1; tabs.push_back(Tab{create_body(false, true), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{create_body(false, false), std::make_unique(this), create_search_bar("Search...", 100)}); auto recommended_page = std::make_unique(this, tabs.back().page.get(), plugin_name); tabs.push_back(Tab{create_body(false, true), std::move(recommended_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); auto history_body = create_body(false, true); auto history_page = std::make_unique(this, tabs.front().page.get(), HistoryType::YOUTUBE); tabs.push_back(Tab{std::move(history_body), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else { current_page = PageType::VIDEO_CONTENT; auto youtube_video_page = std::make_unique(this, youtube_url); video_content_page(nullptr, youtube_video_page.get(), "", false, nullptr, 0); } } else if(strcmp(plugin_name, "peertube") == 0) { if(instance.empty()) { tabs.push_back(Tab{create_body(false, false), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else { tabs.push_back(Tab{create_body(false, true), std::make_unique(this, instance), create_search_bar("Search...", 500)}); } } else if(strcmp(plugin_name, "pornhub") == 0) { check_youtube_dl_installed(plugin_name); auto search_page = std::make_unique(this, "https://www.pornhub.com/", mgl::vec2i(320/1.5f, 180/1.5f), true); add_pornhub_handlers(search_page.get()); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 500)}); use_youtube_dl = true; } else if(strcmp(plugin_name, "spankbang") == 0) { check_youtube_dl_installed(plugin_name); auto search_page = std::make_unique(this, "https://spankbang.com/", mgl::vec2i(500/2.5f, 281/2.5f), true); add_spankbang_handlers(search_page.get()); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 500)}); use_youtube_dl = true; } else if(strcmp(plugin_name, "xvideos") == 0) { std::vector extra_commands = { { "--header", "Cookie: last_views=%5B%2236247565-" + std::to_string(time(nullptr)) + "%22%5D" } }; auto search_page = std::make_unique(this, "https://www.xvideos.com/", mgl::vec2i(352/1.5f, 198/1.5f), true, std::move(extra_commands)); add_xvideos_handlers(search_page.get()); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 500)}); } else if(strcmp(plugin_name, "xhamster") == 0) { check_youtube_dl_installed(plugin_name); auto search_page = std::make_unique(this, "https://xhamster.com/", mgl::vec2i(240, 135), true); add_xhamster_handlers(search_page.get()); tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 500)}); use_youtube_dl = true; } else if(strcmp(plugin_name, "soundcloud") == 0) { tabs.push_back(Tab{create_body(false, true), std::make_unique(this), create_search_bar("Search...", 500)}); no_video = true; } else if(strcmp(plugin_name, "lbry") == 0) { tabs.push_back(Tab{create_body(false, true), std::make_unique(this), create_search_bar("Search...", 500)}); } else if(strcmp(plugin_name, "matrix") == 0) { assert(!matrix); if(create_directory_recursive(get_cache_dir().join("matrix").join("events")) != 0) { show_notification("QuickMedia", "Failed to create events cache directory", Urgency::CRITICAL); abort(); } matrix = new Matrix(); } else { assert(false); } } void Program::common_event_handler(mgl::Event &event) { if(event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::Q && event.key.control) window.close(); } } void Program::handle_x11_events() { if(window.is_open()) { window_closed = false; } else { window_closed = true; current_page = PageType::EXIT; } } void Program::base_event_handler(mgl::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_keypress, bool handle_searchbar) { if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; } else if(handle_keypress && event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::Escape) { current_page = previous_page; } } else if(handle_searchbar) { assert(search_bar); search_bar->on_event(window, event); } } void Program::event_idle_handler(const mgl::Event &event) { if(event.type == mgl::Event::KeyPressed || event.type == mgl::Event::TextEntered) idle_active_handler(); } void Program::idle_active_handler() { if(idle) window.set_framerate_limit(FPS_SYNC_TO_VSYNC); idle = false; idle_timer.restart(); } void Program::update_idle_state() { if(idle) return; if(idle_timer.get_elapsed_time_seconds() > IDLE_TIMEOUT_SEC) { window.set_framerate_limit(FPS_IDLE); idle = true; } } static void fill_youtube_history_items_from_json(const Json::Value &history_json, BodyItems &history_items) { assert(history_json.isArray()); std::vector history_json_items; for(const Json::Value &item : history_json) { if(!item.isObject()) continue; const Json::Value ×tamp = item["timestamp"]; if(!timestamp.isNumeric()) continue; history_json_items.push_back(&item); } std::sort(history_json_items.begin(), history_json_items.end(), [](const Json::Value *val1, const Json::Value *val2) { const Json::Value ×tamp1 = (*val1)["timestamp"]; const Json::Value ×tamp2 = (*val2)["timestamp"]; return timestamp1.asInt64() > timestamp2.asInt64(); }); time_t time_now = time(NULL); for(const Json::Value *item : history_json_items) { const Json::Value &video_id = (*item)["id"]; if(!video_id.isString()) continue; const Json::Value &title = (*item)["title"]; if(!title.isString()) continue; const Json::Value ×tamp = (*item)["timestamp"]; std::string title_str = title.asString(); std::string video_id_str = video_id.asString(); auto body_item = BodyItem::create(std::move(title_str)); body_item->url = "https://www.youtube.com/watch?v=" + video_id_str; body_item->thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/mqdefault.jpg"; body_item->set_description("Watched " + seconds_to_relative_time_str(time_now - timestamp.asInt64())); body_item->set_description_color(get_theme().faded_text_color); body_item->thumbnail_size = mgl::vec2i(192, 108); history_items.push_back(std::move(body_item)); } } static Path get_history_filepath(const char *plugin_name) { Path history_dir = get_storage_dir().join("history"); if(create_directory_recursive(history_dir) != 0) { show_notification("QuickMedia", "Failed to create history directory " + history_dir.data, Urgency::CRITICAL); exit(1); } Path history_filepath = history_dir; return 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_history_json() { Path history_filepath = get_history_filepath(plugin_name); Json::Value json_result; FileType file_type = get_file_type(history_filepath); if(file_type == FileType::REGULAR) { if(!read_file_as_json(history_filepath, json_result) || !json_result.isArray()) { show_notification("QuickMedia", "Failed to read " + history_filepath.data, Urgency::CRITICAL); abort(); } } else { json_result = Json::Value(Json::arrayValue); } return json_result; } 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 "; err_msg += video_history_dir.data; show_notification("QuickMedia", err_msg.c_str(), Urgency::CRITICAL); exit(1); } Path video_history_filepath = video_history_dir; return video_history_filepath.join(plugin_name).append(".json"); } Json::Value load_recommended_json(const char *plugin_name) { 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 fill_recommended_items_from_json(const char *plugin_name, const Json::Value &recommended_json, BodyItems &body_items) { assert(recommended_json.isObject()); const int64_t recommendations_autodelete_period = 60*60*24*20; // 20 days time_t time_now = time(NULL); int num_items_deleted = 0; std::vector> recommended_items(recommended_json.size()); /* TODO: Optimize member access */ for(auto &member_name : recommended_json.getMemberNames()) { const Json::Value &recommended_item = recommended_json[member_name]; if(recommended_item.isObject()) { const Json::Value &recommended_timestamp_json = recommended_item["recommended_timestamp"]; const Json::Value &watched_timestamp_json = recommended_item["watched_timestamp"]; if(watched_timestamp_json.isNumeric() && time_now - watched_timestamp_json.asInt64() >= recommendations_autodelete_period) { ++num_items_deleted; } else if(recommended_timestamp_json.isNumeric() && time_now - recommended_timestamp_json.asInt64() >= recommendations_autodelete_period) { ++num_items_deleted; } else if(recommended_timestamp_json.isNull() && watched_timestamp_json.isNull()) { ++num_items_deleted; } else { recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); } } } if(num_items_deleted > 0) { // TODO: Is there a better way? Json::Value new_recommendations(Json::objectValue); for(auto &recommended : recommended_items) { new_recommendations[recommended.first] = recommended.second; } fprintf(stderr, "Number of old recommendations to delete: %d\n", num_items_deleted); save_json_to_file_atomic(get_recommended_filepath(plugin_name), new_recommendations); } /* TODO: Better algorithm for recommendations */ std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair &a, std::pair &b) { const Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; const 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(); const Json::Value &a_recommended_count_json = a.second["recommended_count"]; const 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; const 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 + "/mqdefault.jpg"; body_item->thumbnail_size = mgl::vec2i(192, 108); 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 void save_recommendations_from_related_videos(const char *plugin_name, const std::string &video_url, const std::string &video_title, const BodyItems &related_media_body_items) { std::string video_id; if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; err_msg += video_url; err_msg + ", video wont be saved in recommendations"; show_notification("QuickMedia", err_msg.c_str(), Urgency::LOW); return; } Json::Value recommended_json = load_recommended_json(plugin_name); time_t time_now = time(NULL); Json::Value &existing_recommended_json = recommended_json[video_id]; if(existing_recommended_json.isObject()) { int64_t watched_count = 0; Json::Value &watched_count_json = existing_recommended_json["watched_count"]; if(watched_count_json.isNumeric()) watched_count = watched_count_json.asInt64(); existing_recommended_json["watched_count"] = watched_count + 1; existing_recommended_json["watched_timestamp"] = time_now; save_json_to_file_atomic(get_recommended_filepath(plugin_name), recommended_json); return; } else { Json::Value new_content_object(Json::objectValue); 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; new_content_object["watched_timestamp"] = time_now; recommended_json[video_id] = std::move(new_content_object); } int saved_recommendation_count = 0; for(const auto &body_item : related_media_body_items) { std::string recommended_video_id; if(youtube_url_extract_id(body_item->url, recommended_video_id)) { Json::Value &existing_recommendation = recommended_json[recommended_video_id]; if(existing_recommendation.isObject()) { int64_t recommended_count = 0; Json::Value &count_json = existing_recommendation["recommended_count"]; if(count_json.isNumeric()) recommended_count = count_json.asInt64(); existing_recommendation["recommended_count"] = recommended_count + 1; existing_recommendation["recommended_timestamp"] = time_now; } else { Json::Value new_content_object(Json::objectValue); new_content_object["title"] = body_item->get_title(); new_content_object["recommended_timestamp"] = time_now; new_content_object["recommended_count"] = 1; recommended_json[recommended_video_id] = std::move(new_content_object); saved_recommendation_count++; /* TODO: Save more than the first 3 video that hasn't been watched yet? */ if(saved_recommendation_count == 3) break; } } else { 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(plugin_name), recommended_json); } void Program::set_clipboard(const std::string &str) { window.set_clipboard(str); } void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items, bool local_thumbnail) { // TOOD: Make generic, instead of checking for plugin Path content_storage_dir = get_storage_dir().join(plugin_name); if(create_directory_recursive(content_storage_dir) != 0) { show_notification("QuickMedia", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); abort(); } Json::Value history_json = load_history_json(); std::unordered_map manga_id_to_thumbnail_url_map; for(const Json::Value &history_item : history_json) { const Json::Value &id = history_item["id"]; const Json::Value &thumbnail_url = history_item["thumbnail_url"]; if(!id.isString() || !thumbnail_url.isString()) continue; manga_id_to_thumbnail_url_map[id.asString()] = thumbnail_url.asString(); } // TODO: Remove this once manga history file has been in use for a few months and is filled with history time_t now = time(NULL); for_files_in_dir_sort_last_modified(content_storage_dir, [&](const Path &filepath, FileType) { // 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(strcmp(filepath.ext(), ".tmp") == 0) return true; Json::Value body; if(!read_file_as_json(filepath, body) || !body.isObject()) { fprintf(stderr, "Failed to read json file: %s\n", filepath.data.c_str()); return true; } // TODO: Manga combined const char *filename = filepath.filename(); std::string manga_id = base64_url_decode(filename); const Json::Value &manga_name = body["name"]; if(!manga_name.isString()) return true; time_t last_modified_time = 0; file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_time); // TODO: Add thumbnail auto body_item = BodyItem::create(manga_name.asString()); body_item->set_description("Last read " + seconds_to_relative_time_str(now - last_modified_time)); body_item->set_description_color(get_theme().faded_text_color); auto thumbnail_it = manga_id_to_thumbnail_url_map.find(manga_id); if(thumbnail_it != manga_id_to_thumbnail_url_map.end()) { body_item->thumbnail_url = thumbnail_it->second; body_item->thumbnail_size = {101, 141}; body_item->thumbnail_is_local = local_thumbnail; } if(strcmp(plugin_name, "manganelo") == 0) body_item->url = "https://manganelo.com/manga/" + manga_id; else if(strcmp(plugin_name, "manganelos") == 0) body_item->url = "http://manganelos.com/manga/" + manga_id; else if(strcmp(plugin_name, "mangadex") == 0) body_item->url = manga_id; else if(strcmp(plugin_name, "mangatown") == 0) body_item->url = "https://mangatown.com/manga/" + manga_id; else if(strcmp(plugin_name, "mangakatana") == 0) body_item->url = "https://mangakatana.com/manga/" + manga_id; else if(strcmp(plugin_name, "onimanga") == 0) body_item->url = "https://onimanga.com/" + manga_id; else if(strcmp(plugin_name, "readm") == 0) body_item->url = "https://readm.org/manga/" + manga_id; else if(strcmp(plugin_name, "local-manga") == 0) body_item->url = manga_id; else fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); history_items.push_back(std::move(body_item)); return true; }); } void Program::youtube_get_watch_history(BodyItems &history_items) { fill_youtube_history_items_from_json(load_history_json(), history_items); } static void get_body_dimensions(const mgl::vec2i &window_size, SearchBar *search_bar, mgl::vec2f &body_pos, mgl::vec2f &body_size, bool has_tabs = false) { const float body_width = window_size.x; float tab_h = Tabs::get_shade_height(); if(!search_bar) tab_h += std::floor(10.0f * get_config().scale); if(!has_tabs) tab_h = 0.0f; float search_bottom = search_bar ? search_bar->getBottomWithoutShadow() : 0.0f; body_pos = mgl::vec2f(0.0f, search_bottom + tab_h); body_size = mgl::vec2f(body_width, window_size.y - search_bottom - tab_h); } std::unique_ptr Program::create_body(bool plain_text_list, bool prefer_card_view) { if(!rounded_rectangle_mask_shader.is_valid()) plain_text_list = true; auto body = std::make_unique(plain_text_list ? BODY_THEME_MINIMAL : BODY_THEME_MODERN_SPACIOUS, loading_icon, &rounded_rectangle_shader, &rounded_rectangle_mask_shader); body->card_view = prefer_card_view; body->thumbnail_mask_shader = &circle_mask_shader; return body; } std::unique_ptr Program::create_search_bar(const std::string &placeholder, int search_delay) { auto search_bar = std::make_unique(&plugin_logo, &rounded_rectangle_shader, placeholder, SearchBarType::Search); search_bar->text_autosearch_delay_ms = search_delay; return search_bar; } void Program::add_login_inputs(Tab *tab, std::vector login_inputs) { if(login_inputs.empty()) return; std::lock_guard lock(login_inputs_mutex); for(const LoginInput &login_input : login_inputs) { auto search_bar = std::make_unique(nullptr, &rounded_rectangle_shader, login_input.placeholder, login_input.type); search_bar->padding_top = 0.0f; search_bar->padding_bottom = 0.0f; search_bar->padding_x = 0.0f; search_bar->caret_visible = false; tab->login_inputs.inputs.push_back(std::move(search_bar)); } tab->login_inputs.inputs.front()->caret_visible = true; tab->login_inputs.needs_refresh = true; } bool Program::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_url, const std::string &manga_id) { Path content_storage_dir = get_storage_dir().join(service_name); this->manga_id = manga_id; manga_id_base64 = base64_url_encode(manga_id); content_storage_file = content_storage_dir.join(manga_id_base64); content_storage_json.clear(); content_storage_file_modified = true; bool result = true; FileType file_type = get_file_type(content_storage_file); if(file_type == FileType::REGULAR) { result = read_file_as_json(content_storage_file, content_storage_json) && content_storage_json.isObject(); if(!result) { show_notification("QuickMedia", "Failed to read " + content_storage_file.data, Urgency::CRITICAL); abort(); } } else { result = true; } if(!content_storage_json.isObject()) content_storage_json = Json::Value(Json::objectValue); content_storage_json["name"] = manga_title; content_storage_json["url"] = manga_url; return result; } void Program::select_file(const std::string &filepath) { puts(filepath.c_str()); selected_files.clear(); selected_files.push_back(filepath); } bool Program::is_window_focused() { return window.has_focus(); } RoomData* Program::get_current_chat_room() { return current_chat_room; } void Program::set_go_to_previous_page() { go_to_previous_page = true; } void Program::set_pipe_selected_text(const std::string &text) { pipe_selected_text = text; } static bool is_url_video(const std::string &url) { return string_ends_with(url, ".webm") || string_ends_with(url, ".mp4") || string_ends_with(url, ".mkv") || string_ends_with(url, ".gif"); } bool Program::show_info_page(BodyItem *body_item, bool include_reverse_image_search) { if(!body_item) return false; std::string title = body_item->get_title(); std::string description = body_item->get_description(); std::string text = std::move(title); if(!description.empty()) { if(!text.empty()) text += '\n'; text += std::move(description); } auto body = create_body(); if(include_reverse_image_search && !body_item->url.empty() && !body_item->thumbnail_url.empty()) { std::string image_url = body_item->url; if(is_url_video(body_item->url)) image_url = body_item->thumbnail_url; body->append_item(InfoPage::add_reverse_image_search(image_url)); } std::vector urls = ranges_get_strings(text, extract_urls(text)); for(const std::string &url : urls) { body->append_item(InfoPage::add_url(url)); } if(body->get_num_items() == 0) return false; std::vector info_tabs; info_tabs.push_back(Tab{std::move(body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); page_loop(info_tabs); return true; } // Returns -1 if not found int bookmark_find_item(Json::Value &root, const std::string &title, const std::string &author, const std::string &url) { if(!root.isArray()) return false; int index = -1; for(const Json::Value &item_json : root) { ++index; if(!item_json.isObject()) continue; const Json::Value &title_json = item_json["title"]; const Json::Value &author_json = item_json["author"]; const Json::Value &url_json = item_json["url"]; if((!title.empty() && title_json.isString() && strcmp(title.c_str(), title_json.asCString()) == 0) || (!author.empty() && author_json.isString() && strcmp(author.c_str(), author_json.asCString()) == 0) || (!url.empty() && url_json.isString() && strcmp(url.c_str(), url_json.asCString()) == 0)) { return index; } } return -1; } bool Program::toggle_bookmark(BodyItem *body_item, const char *bookmark_name) { assert(bookmark_name); Path bookmark_path = get_storage_dir().join("bookmarks"); if(create_directory_recursive(bookmark_path) != 0) { show_notification("QuickMedia", "Failed to update bookmark", Urgency::CRITICAL); return false; } bookmark_path.join(bookmark_name); Json::Value json_root; if(!read_file_as_json(bookmark_path, json_root) || !json_root.isArray()) json_root = Json::Value(Json::arrayValue); const int existing_index = bookmark_find_item(json_root, body_item->get_title(), body_item->get_author(), body_item->url); if(existing_index != -1) { Json::Value removed; json_root.removeIndex(existing_index, &removed); if(!save_json_to_file_atomic(bookmark_path, json_root)) { show_notification("QuickMedia", "Failed to update bookmark", Urgency::CRITICAL); return false; } std::string bookmark_title = body_item->get_title(); if(bookmark_title.empty()) bookmark_title = body_item->get_author(); show_notification("QuickMedia", "Removed " + bookmark_title + " from bookmarks"); removed = true; return true; } Json::Value new_item(Json::objectValue); if(!body_item->get_title().empty()) new_item["title"] = body_item->get_title(); if(!body_item->get_author().empty()) new_item["author"] = body_item->get_author(); if(!body_item->url.empty()) new_item["url"] = body_item->url; if(!body_item->thumbnail_url.empty()) new_item["thumbnail_url"] = body_item->thumbnail_url; new_item["timestamp"] = (int64_t)time(nullptr); json_root.append(std::move(new_item)); if(!save_json_to_file_atomic(bookmark_path, json_root)) { show_notification("QuickMedia", "Failed to update bookmark", Urgency::CRITICAL); return false; } std::string bookmark_title = body_item->get_title(); if(bookmark_title.empty()) bookmark_title = body_item->get_author(); show_notification("QuickMedia", "Added " + bookmark_title + " to bookmarks"); return true; } void Program::page_loop_render(mgl::Window &window, std::vector &tabs, int selected_tab, TabAssociatedData &tab_associated_data, const Json::Value *json_chapters, Tabs &ui_tabs) { if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, window_size.to_vec2f(), true); float shade_extra_height = 0.0f; if(!tabs[selected_tab].search_bar) { shade_extra_height = std::floor(10.0f * get_config().scale); mgl::Rectangle shade_top(mgl::vec2f(window_size.x, shade_extra_height)); shade_top.set_color(get_theme().shade_color); window.draw(shade_top); } float tab_vertical_offset = tabs[selected_tab].search_bar ? tabs[selected_tab].search_bar->getBottomWithoutShadow() : 0.0f; ui_tabs.draw(window, mgl::vec2f(0.0f, tab_vertical_offset + shade_extra_height), window_size.x); tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); if(tab_associated_data.fetching_next_page_running) window.draw(gradient_points, 4, mgl::PrimitiveType::Quads); // TODO: mgl::PrimitiveType::Quads doesn't work with egl if(!tab_associated_data.search_result_text.get_string().empty() && !tabs[selected_tab].page->search_is_suggestion()) { auto search_result_text_bounds = tab_associated_data.search_result_text.get_bounds(); tab_associated_data.search_result_text.set_position(mgl::vec2f( std::floor(body_pos.x + body_size.x * 0.5f - search_result_text_bounds.size.x * 0.5f), std::floor(body_pos.y + body_size.y * 0.5f - search_result_text_bounds.size.y * 0.5f))); window.draw(tab_associated_data.search_result_text); } if(!tabs[selected_tab].page->is_ready()) { mgl::Text loading_text("Loading...", *FontLoader::get_font(FontLoader::FontType::LATIN, 30 * get_config().scale * get_config().font_scale)); auto text_bounds = loading_text.get_bounds(); loading_text.set_position(mgl::vec2f( std::floor(body_pos.x + body_size.x * 0.5f - text_bounds.size.x * 0.5f), std::floor(body_pos.y + body_size.y * 0.5f - text_bounds.size.y * 0.5f))); window.draw(loading_text); } if(matrix) matrix->update(); if(matrix && !matrix->is_initial_sync_finished()) { // if(is_login_sync) { load_sprite.set_position(mgl::vec2f(body_pos.x + body_size.x * 0.5f, body_pos.y + body_size.y * 0.5f)); load_sprite.set_rotation(load_sprite_timer.get_elapsed_time_seconds() * 400.0); window.draw(load_sprite); // } std::string err_msg; if(matrix->did_initial_sync_fail(err_msg)) { show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); matrix->logout(); delete matrix; matrix = new Matrix(); current_page = PageType::CHAT_LOGIN; chat_login_page(); after_matrix_login_page(); window.close(); exit(exit_code); } } } static void set_search_bar_to_body_item_text(BodyItem *body_item, SearchBar *search_bar) { if(!body_item || !search_bar) return; if(!body_item->get_title().empty()) { search_bar->set_text(body_item->get_title()); return; } if(!body_item->get_author().empty()) { search_bar->set_text(body_item->get_author()); return; } } bool Program::page_loop(std::vector &tabs, int start_tab_index, PageLoopSubmitHandler after_submit_handler, bool go_to_previous_on_escape) { if(tabs.empty()) { show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL); return false; } malloc_trim(0); idle_active_handler(); bool loop_running = true; bool redraw = true; for(Tab &tab : tabs) { assert(tab.body.get()); assert(tab.page.get()); if(tab.body->attach_side == AttachSide::BOTTOM) tab.body->select_last_item(); tab.page->on_navigate_to_page(tab.body.get()); } Tabs ui_tabs(&rounded_rectangle_shader); for(auto &tab : tabs) { ui_tabs.add_tab(tab.page->get_title(), tab.body.get()); } ui_tabs.set_selected(start_tab_index); ui_tabs.on_change_tab = [&tabs, &redraw](int prev_tab, int) { tabs[prev_tab].body->clear_cache(); redraw = true; }; const Json::Value *json_chapters = &Json::Value::nullSingleton(); std::vector tab_associated_data; for(size_t i = 0; i < tabs.size(); ++i) { TabAssociatedData data; data.search_result_text = mgl::Text("", *FontLoader::get_font(FontLoader::FontType::LATIN, 30 * get_config().scale * get_config().font_scale)); tab_associated_data.push_back(std::move(data)); } double gradient_inc = 0.0; const float gradient_height = 5.0f; auto window_size_u = window.get_size(); window_size.x = window_size_u.x; window_size.y = window_size_u.y; std::function submit_handler = [this, &submit_handler, &after_submit_handler, &tabs, &tab_associated_data, &ui_tabs, &loop_running, &redraw](const std::string &search_text) { mgl::Event event; while(window.poll_event(event)) { common_event_handler(event); } const int selected_tab = ui_tabs.get_selected(); auto selected_item = tabs[selected_tab].body->get_selected_shared(); if(!selected_item && search_text.empty() && !tabs[selected_tab].page->allow_submit_no_selection()) return; if(tabs[selected_tab].page->allow_submit_no_selection() && (window.is_key_pressed(mgl::Keyboard::LControl) || window.is_key_pressed(mgl::Keyboard::RControl))) selected_item = nullptr; if(!selected_item && !tabs[selected_tab].page->allow_submit_no_selection()) return; hide_virtual_keyboard(); std::vector new_tabs; BodyItems new_body_items; const bool search_suggestion_submitted = tab_associated_data[selected_tab].search_suggestion_submitted; if((tabs[selected_tab].page->is_single_page() || (tabs[selected_tab].page->search_is_suggestion() && !search_suggestion_submitted)) && tab_associated_data[selected_tab].fetch_future.valid()) { tabs[selected_tab].page->cancel_operation(); tab_associated_data[selected_tab].fetch_future.cancel(); tab_associated_data[selected_tab].fetch_status = FetchStatus::NONE; tab_associated_data[selected_tab].search_text_updated = false; } auto plugin_submit_handler = [&tabs, selected_tab, &selected_item, &search_text, &new_tabs, &new_body_items, search_suggestion_submitted]() { SubmitArgs submit_args; submit_args.title = selected_item ? selected_item->get_title() : search_text; submit_args.url = selected_item ? selected_item->url : search_text; submit_args.thumbnail_url = selected_item ? selected_item->thumbnail_url : ""; submit_args.userdata = selected_item ? selected_item->userdata : nullptr; submit_args.extra = selected_item ? selected_item->extra : nullptr; if(tabs[selected_tab].page->search_is_suggestion() && !search_suggestion_submitted) { PluginResult plugin_result = tabs[selected_tab].page->submit_suggestion(submit_args, new_body_items); return plugin_result == PluginResult::OK; } else { PluginResult plugin_result = tabs[selected_tab].page->submit(submit_args, new_tabs); return plugin_result == PluginResult::OK; } }; TaskResult submit_result; if(tabs[selected_tab].page->submit_is_async()) { submit_result = run_task_with_loading_screen(std::move(plugin_submit_handler)); } else { submit_result = plugin_submit_handler() ? TaskResult::TRUE : TaskResult::FALSE; } if(submit_result == TaskResult::CANCEL) { return; } else if(submit_result != TaskResult::TRUE) { // TODO: Show the exact cause of error (get error message from curl). show_notification("QuickMedia", std::string("Submit failed for page ") + tabs[selected_tab].page->get_title(), Urgency::CRITICAL); return; } idle_active_handler(); if(tabs[selected_tab].page->clear_search_after_submit() && tabs[selected_tab].search_bar) { if(!tabs[selected_tab].search_bar->get_text().empty()) { tabs[selected_tab].search_bar->clear(); tabs[selected_tab].search_bar->onTextUpdateCallback(""); } else if(tabs[selected_tab].body->attach_side == AttachSide::TOP) { int selected_item_index = tabs[selected_tab].body->get_selected_item(); tabs[selected_tab].body->select_first_item(); tabs[selected_tab].body->set_selected_item(selected_item_index, false); } } if(tabs[selected_tab].page->is_single_page() && !tabs[selected_tab].page->search_is_suggestion()) { if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->clear(); if(new_tabs.size() == 1 && !new_tabs[0].page) { tabs[selected_tab].body = std::move(new_tabs[0].body); return; } else if(new_tabs.empty()) { loop_running = false; return; } } if(tabs[selected_tab].page->search_is_suggestion() && !search_suggestion_submitted) { if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_text(selected_item ? selected_item->get_title() : search_text, false); tabs[selected_tab].body->set_items(std::move(new_body_items)); tab_associated_data[selected_tab].search_suggestion_submitted = true; return; } if(new_tabs.empty()) { return; } if(after_submit_handler) after_submit_handler(new_tabs); for(size_t i = 0; i < tabs.size(); ++i) { tabs[i].body->clear_cache(); if(tabs[i].page->is_lazy_fetch_page() && static_cast(tabs[i].page.get())->reload_on_page_change()) { tab_associated_data[i].lazy_fetch_finished = false; tab_associated_data[i].fetched_page = 0; const BodyItem *selected_item = tabs[i].body->get_selected(); tab_associated_data[i].body_item_url_before_refresh = selected_item ? selected_item->url : ""; tabs[i].body->clear_items(); } } if(tabs[selected_tab].page->allow_submit_no_selection()) { page_loop(new_tabs, 0, after_submit_handler); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) { page_stack.push(current_page); select_episode(selected_item.get(), false); Body *chapters_body = tabs[selected_tab].body.get(); tabs[selected_tab].search_bar->clear(); 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(new_tabs[0].page.get()); window.set_key_repeat_enabled(false); downloading_chapter_url.clear(); Path manga_progress_dir = get_storage_dir().join(manga_images_page->get_service_name()); if(create_directory_recursive(manga_progress_dir) != 0) { show_notification("QuickMedia", "Failed to create directory: " + manga_progress_dir.data, Urgency::CRITICAL); current_page = pop_page_stack(); } else { while(window.is_open() && (current_page == PageType::IMAGES || current_page == PageType::IMAGES_CONTINUOUS)) { int page_navigation = 0; bool continue_left_off = false; if(current_page == PageType::IMAGES) { page_navigation = image_page(manga_images_page, chapters_body, continue_left_off); } else if(current_page == PageType::IMAGES_CONTINUOUS) { page_navigation = image_continuous_page(manga_images_page); } if(page_navigation == -1) { // previous chapter // TODO: Make this work if the list is sorted differently than from newest to oldest. if(chapters_body->select_next_item()) { select_episode(chapters_body->get_selected(), !continue_left_off); if(!continue_left_off) 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(!tab_associated_data[selected_tab].fetching_next_page_failed) { BodyItems new_body_items; const int fetch_page = tab_associated_data[selected_tab].fetched_page + 1; TaskResult load_next_page_result = run_task_with_loading_screen([&] { if(tabs[selected_tab].page->get_page("", fetch_page, new_body_items) != PluginResult::OK) { fprintf(stderr, "Failed to get next page (page %d)\n", fetch_page); return false; } return true; }); fprintf(stderr, "Finished fetching page %d, num new items: %zu\n", fetch_page, new_body_items.size()); size_t num_new_messages = new_body_items.size(); if(num_new_messages > 0) { tabs[selected_tab].body->append_items(new_body_items); tab_associated_data[selected_tab].fetched_page++; select_episode(chapters_body->get_selected(), !continue_left_off); if(!continue_left_off) 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 { tab_associated_data[selected_tab].fetching_next_page_failed = true; } if(load_next_page_result == TaskResult::CANCEL) { current_page = pop_page_stack(); break; } } } else if(page_navigation == 1) { // next chapter // TODO: Make this work if the list is sorted differently than from newest to oldest. if(chapters_body->select_previous_item()) { select_episode(chapters_body->get_selected(), !continue_left_off); manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); } } } } content_storage_file_modified = true; image_download_cancel = true; image_download_future.cancel(); image_download_cancel = false; images_to_upscale_queue.clear(); num_manga_pages = 0; window.set_key_repeat_enabled(true); malloc_trim(0); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::IMAGE_BOARD_THREAD) { page_stack.push(current_page); current_page = PageType::IMAGE_BOARD_THREAD; image_board_thread_page(static_cast(new_tabs[0].page.get()), new_tabs[0].body.get()); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) { page_stack.push(current_page); current_page = PageType::VIDEO_CONTENT; int selected_index = tabs[selected_tab].body->get_selected_item(); video_content_page(tabs[selected_tab].page.get(), static_cast(new_tabs[0].page.get()), "", false, tabs[selected_tab].body.get(), selected_index, &tab_associated_data[selected_tab].fetched_page, tab_associated_data[selected_tab].update_search_text); } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::CHAT) { MatrixChatPage *tmp_matrix_chat_page = static_cast(new_tabs[0].page.get()); std::string jump_to_event_id = tmp_matrix_chat_page->jump_to_event_id; MatrixRoomsPage *rooms_page = tmp_matrix_chat_page->rooms_page; Body *room_list_body = rooms_page->body; rooms_page->clear_search(); body_set_selected_item_by_url(room_list_body, tmp_matrix_chat_page->room_id); current_page = PageType::CHAT; current_chat_room = matrix->get_room_by_id(tmp_matrix_chat_page->room_id); rooms_page->body->show_drop_shadow = false; while(window.is_open() && current_chat_room) { auto matrix_chat_page = std::make_unique(this, current_chat_room->id, rooms_page, jump_to_event_id); bool move_room = chat_page(matrix_chat_page.get(), current_chat_room); matrix_chat_page->messages_tab_visible = false; if(!move_room) break; BodyItem *selected_item = room_list_body->get_selected(); if(!selected_item) break; current_chat_room = matrix->get_room_by_id(selected_item->url); jump_to_event_id.clear(); } rooms_page->body->show_drop_shadow = true; room_list_body->body_item_select_callback = [&submit_handler](BodyItem *body_item) { submit_handler(body_item->get_title()); }; current_chat_room = nullptr; } else { page_loop(new_tabs, 0, after_submit_handler); } for(Tab &tab : tabs) { tab.page->on_navigate_to_page(tab.body.get()); } redraw = true; idle_active_handler(); hide_virtual_keyboard(); }; std::function on_reached_end = [&ui_tabs, &tabs, &tab_associated_data, &gradient_inc] { const int selected_tab = ui_tabs.get_selected(); if(tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].fetching_next_page_running && !tab_associated_data[selected_tab].fetching_next_page_failed && (!tabs[selected_tab].search_bar || !tabs[selected_tab].page->search_is_filter() || tabs[selected_tab].search_bar->is_empty()) && tabs[selected_tab].body->get_num_visible_items() > 0 && (!tabs[selected_tab].page->search_is_suggestion() || tab_associated_data[selected_tab].search_suggestion_submitted) && tabs[selected_tab].page->is_ready() && (!tabs[selected_tab].page->is_lazy_fetch_page() || tab_associated_data[selected_tab].lazy_fetch_finished)) { 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 = AsyncTask([update_search_text{std::move(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; }); } }; for(size_t i = 0; i < tabs.size(); ++i) { Tab &tab = tabs[i]; tab.body->body_item_select_callback = [&submit_handler](BodyItem *body_item) { submit_handler(body_item->get_title()); }; if(tab.body->attach_side == AttachSide::TOP) tab.body->on_bottom_reached = on_reached_end; else if(tab.body->attach_side == AttachSide::BOTTOM) tab.body->on_top_reached = on_reached_end; TabAssociatedData &associated_data = tab_associated_data[i]; if(tab.search_bar) { 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); if(tabs[i].body->attach_side == AttachSide::TOP) tabs[i].body->select_first_item(); else if(tabs[i].body->attach_side == AttachSide::BOTTOM) tabs[i].body->select_last_item(); } associated_data.typing = false; }; tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string &search_text) { if(associated_data.typing) return; submit_handler(search_text); }; } } RoundedRectangle login_inputs_background(mgl::vec2f(1.0f, 1.0f), 10.0f * get_config().scale, get_theme().shade_color, &rounded_rectangle_shader); for(auto &tab : tabs) { for(auto &login_input : tab.login_inputs.inputs) { login_input->padding_top = 0.0f; login_input->padding_bottom = 0.0f; login_input->padding_x = 0.0f; login_input->caret_visible = false; } if(!tab.login_inputs.inputs.empty()) tab.login_inputs.inputs.front()->caret_visible = true; } const float login_input_padding_x = std::floor(20.0f * get_config().scale * get_config().spacing_scale); const float login_input_padding_y = std::floor(20.0f * get_config().scale * get_config().spacing_scale); const float login_input_spacing_y = std::floor(20.0f * get_config().scale * get_config().spacing_scale); mgl::Event event; mgl::Clock frame_timer; while (window.is_open() && loop_running) { int32_t frame_time_ms = frame_timer.restart() * 1000.0; while (window.poll_event(event)) { common_event_handler(event); const int selected_tab = ui_tabs.get_selected(); if(tabs[selected_tab].body->on_event(window, event)) idle_active_handler(); else event_idle_handler(event); if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; idle_active_handler(); } if(tabs[selected_tab].search_bar) { tabs[selected_tab].search_bar->on_event(window, event); } ui_tabs.on_event(event); if(event.type == mgl::Event::Resized || event.type == mgl::Event::GainedFocus) redraw = true; else if(event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::Escape && go_to_previous_on_escape) { return false; } else if(event.key.code == mgl::Keyboard::Enter) { if(!tabs[selected_tab].search_bar) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); submit_handler(selected_item ? selected_item->get_title() : ""); } } else if(event.key.code == mgl::Keyboard::T && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(selected_item && tabs[selected_tab].page->is_trackable()) { TrackablePage *trackable_page = dynamic_cast(tabs[selected_tab].page.get()); run_task_with_loading_screen([trackable_page, selected_item](){ return trackable_page->track(selected_item->get_title()) == TrackResult::OK; }); } } else if(event.key.code == mgl::Keyboard::B && event.key.control) { auto bookmark_item = tabs[selected_tab].page->get_bookmark_body_item(tabs[selected_tab].body->get_selected()); if(!bookmark_item) bookmark_item = tabs[selected_tab].body->get_selected_shared(); if(bookmark_item) { const char *bookmark_name = tabs[selected_tab].page->get_bookmark_name(); if(bookmark_name) { if(toggle_bookmark(bookmark_item.get(), bookmark_name)) { for(Tab &tab : tabs) { if(tab.page && tab.page->is_bookmark_page()) tab.page->needs_refresh = true; } } } } } else if(event.key.code == mgl::Keyboard::R && event.key.control) { tabs[selected_tab].page->toggle_read(tabs[selected_tab].body->get_selected()); } else if(event.key.code == mgl::Keyboard::C && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(selected_item) tabs[selected_tab].page->copy_to_clipboard(selected_item); } else if(event.key.code == mgl::Keyboard::I && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(show_info_page(selected_item, false)) redraw = true; } else if(event.key.code == mgl::Keyboard::Tab && !event.key.control) { set_search_bar_to_body_item_text(tabs[selected_tab].body->get_selected(), tabs[selected_tab].search_bar.get()); std::lock_guard lock(login_inputs_mutex); if(!tabs[selected_tab].login_inputs.inputs.empty()) { for(auto &login_input : tabs[selected_tab].login_inputs.inputs) { login_input->caret_visible = false; } tabs[selected_tab].login_inputs.focused_input = (tabs[selected_tab].login_inputs.focused_input + 1) % tabs[selected_tab].login_inputs.inputs.size(); tabs[selected_tab].login_inputs.inputs[tabs[selected_tab].login_inputs.focused_input]->caret_visible = true; idle_active_handler(); } } } std::lock_guard lock(login_inputs_mutex); if(!tabs[selected_tab].login_inputs.inputs.empty()) tabs[selected_tab].login_inputs.inputs[tabs[selected_tab].login_inputs.focused_input]->on_event(window, event); } update_idle_state(); handle_x11_events(); if(!loop_running || !window.is_open()) break; const int selected_tab = ui_tabs.get_selected(); if(redraw || tabs[selected_tab].login_inputs.needs_refresh) { redraw = false; tabs[selected_tab].login_inputs.needs_refresh = false; if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->onWindowResize(window_size.to_vec2f()); // 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); if(tabs[selected_tab].body->attach_side == AttachSide::TOP) { 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; } else if(tabs[selected_tab].body->attach_side == AttachSide::BOTTOM) { gradient_points[0].position.x = 0.0f; gradient_points[0].position.y = body_pos.y; gradient_points[1].position.x = window_size.x; gradient_points[1].position.y = body_pos.y; gradient_points[2].position.x = window_size.x; gradient_points[2].position.y = body_pos.y + gradient_height; gradient_points[3].position.x = 0.0f; gradient_points[3].position.y = body_pos.y + gradient_height; } std::lock_guard lock(login_inputs_mutex); const int num_inputs = tabs[selected_tab].login_inputs.inputs.size(); const int first_input_height = tabs[selected_tab].login_inputs.inputs.empty() ? 0 : tabs[selected_tab].login_inputs.inputs.front()->getBottomWithoutShadow(); login_inputs_background.set_size(mgl::vec2f( std::min((float)window_size.x, std::max(640.0f, window_size.x * 0.5f)), num_inputs * first_input_height + login_input_padding_y * 2.0f + login_input_spacing_y * std::max(0, num_inputs - 1))); login_inputs_background.set_position(window_size.to_vec2f() * 0.5f - login_inputs_background.get_size() * 0.5f); mgl::vec2f pos = login_inputs_background.get_position() + mgl::vec2f(login_input_padding_x, login_input_padding_y); for(auto &login_input : tabs[selected_tab].login_inputs.inputs) { login_input->set_position(pos); pos.y += login_input->getBottomWithoutShadow() + login_input_spacing_y; } } 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); mgl::Color bottom_color = interpolate_colors(get_theme().background_color, get_theme().loading_bar_color, progress); if(tabs[selected_tab].body->attach_side == AttachSide::TOP) { gradient_points[0].color = get_theme().background_color; gradient_points[1].color = get_theme().background_color; gradient_points[2].color = bottom_color; gradient_points[3].color = bottom_color; } else if(tabs[selected_tab].body->attach_side == AttachSide::BOTTOM) { gradient_points[0].color = bottom_color; gradient_points[1].color = bottom_color; gradient_points[2].color = get_theme().background_color; gradient_points[3].color = get_theme().background_color; } } if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update(); if(tabs[selected_tab].page->needs_refresh && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].fetching_next_page_running) { tabs[selected_tab].page->needs_refresh = false; if(tabs[selected_tab].page->is_lazy_fetch_page()) { tab_associated_data[selected_tab].lazy_fetch_finished = false; tab_associated_data[selected_tab].fetched_page = 0; } else if(!tabs[selected_tab].page->search_is_filter()) { tab_associated_data[selected_tab].search_text_updated = true; } const BodyItem *selected_item = tabs[selected_tab].body->get_selected(); tab_associated_data[selected_tab].body_item_url_before_refresh = selected_item ? selected_item->url : ""; tabs[selected_tab].body->clear_items(); } if(tabs[selected_tab].page->is_ready() && tabs[selected_tab].page->is_lazy_fetch_page() && tab_associated_data[selected_tab].fetch_status == FetchStatus::NONE && !tab_associated_data[selected_tab].lazy_fetch_finished) { tab_associated_data[selected_tab].fetch_status = FetchStatus::LOADING; tab_associated_data[selected_tab].fetch_type = FetchType::LAZY; tab_associated_data[selected_tab].search_result_text.set_string("Loading..."); LazyFetchPage *lazy_fetch_page = static_cast(tabs[selected_tab].page.get()); tab_associated_data[selected_tab].fetch_future = AsyncTask([lazy_fetch_page]() { FetchResult fetch_result; fetch_result.result = lazy_fetch_page->lazy_fetch(fetch_result.body_items); return fetch_result; }); } for(size_t i = 0; i < tabs.size(); ++i) { TabAssociatedData &associated_data = tab_associated_data[i]; if(!tabs[i].page->is_ready()) continue; if(associated_data.fetching_next_page_running && associated_data.next_page_future.ready()) { const bool body_was_empty = tabs[i].body->get_num_items() == 0; BodyItems new_body_items = associated_data.next_page_future.get(); fprintf(stderr, "Finished fetching page %d, num new items: %zu\n", associated_data.fetched_page + 1, new_body_items.size()); int prev_selected_item = tabs[i].body->get_selected_item(); size_t num_new_messages = new_body_items.size(); if(num_new_messages > 0) { if(tabs[i].body->attach_side == AttachSide::TOP) tabs[i].body->append_items(std::move(new_body_items)); else if(tabs[i].body->attach_side == AttachSide::BOTTOM) tabs[i].body->prepend_items_reverse(std::move(new_body_items)); associated_data.fetched_page++; } else { associated_data.fetching_next_page_failed = true; } associated_data.fetching_next_page_running = false; if(tabs[i].body->attach_side == AttachSide::BOTTOM) { if(body_was_empty) { tabs[i].body->select_last_item(); } else { // TODO: Use select_next_item in a loop instead for |num_new_messages|? tabs[i].body->set_selected_item(prev_selected_item + num_new_messages, true); } } idle_active_handler(); } if(associated_data.search_text_updated && associated_data.fetch_status == FetchStatus::LOADING && associated_data.fetch_type == FetchType::SEARCH && associated_data.fetch_future.valid()) { tabs[i].page->cancel_operation(); associated_data.fetch_future.cancel(); associated_data.fetch_status = FetchStatus::NONE; } if(associated_data.search_text_updated && associated_data.fetch_status == FetchStatus::NONE && !associated_data.fetching_next_page_running) { std::string update_search_text = associated_data.update_search_text; if(!tabs[i].page->search_is_suggestion() || associated_data.search_suggestion_submitted) tabs[i].body->clear_items(); associated_data.search_text_updated = false; associated_data.fetch_status = FetchStatus::LOADING; associated_data.fetch_type = FetchType::SEARCH; associated_data.search_result_text.set_string("Searching..."); associated_data.search_suggestion_submitted = false; Page *page = tabs[i].page.get(); associated_data.fetch_future = AsyncTask([update_search_text{std::move(update_search_text)}, page]() { FetchResult fetch_result; fetch_result.result = search_result_to_plugin_result(page->search(update_search_text, fetch_result.body_items)); return fetch_result; }); } if(associated_data.fetch_status == FetchStatus::LOADING && associated_data.fetch_type == FetchType::SEARCH && associated_data.fetch_future.ready()) { if(!associated_data.search_text_updated) { FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->set_items(std::move(fetch_result.body_items)); if(tabs[i].body->attach_side == AttachSide::TOP) { tabs[i].body->select_first_item(); } else if(tabs[i].body->attach_side == AttachSide::BOTTOM) { tabs[i].body->reverse_items(); tabs[i].body->select_last_item(); } associated_data.fetched_page = 0; associated_data.fetching_next_page_failed = false; if(fetch_result.result != PluginResult::OK) associated_data.search_result_text.set_string("Search failed!"); else if(tabs[i].body->get_num_items() == 0) associated_data.search_result_text.set_string("No results found"); else associated_data.search_result_text.set_string(""); idle_active_handler(); } else { associated_data.fetch_future.get(); } associated_data.fetch_status = FetchStatus::NONE; } if(associated_data.fetch_status == FetchStatus::LOADING && associated_data.fetch_type == FetchType::LAZY && associated_data.fetch_future.ready()) { LazyFetchPage *lazy_fetch_page = static_cast(tabs[i].page.get()); associated_data.lazy_fetch_finished = true; FetchResult fetch_result = associated_data.fetch_future.get(); tabs[i].body->set_items(std::move(fetch_result.body_items)); if(tabs[i].search_bar && tabs[i].page->search_is_filter()) { tabs[i].body->filter_search_fuzzy(tabs[i].search_bar->get_text()); } if(lazy_fetch_page->reseek_to_body_item_by_url()) { const auto &tab_ass = tab_associated_data[i]; const int item_index = tabs[i].body->find_item_index([&tab_ass](const std::shared_ptr &item) { return item->visible && item->url == tab_ass.body_item_url_before_refresh; }); if(item_index != -1) tabs[i].body->set_selected_item(item_index); } else { if(tabs[i].body->attach_side == AttachSide::TOP) { tabs[i].body->select_first_item(); } if(tabs[i].body->attach_side == AttachSide::BOTTOM) { tabs[i].body->reverse_items(); tabs[i].body->select_last_item(); } } tab_associated_data[i].body_item_url_before_refresh.clear(); if(fetch_result.result != PluginResult::OK) associated_data.search_result_text.set_string("Failed to fetch page!"); else if(tabs[i].body->get_num_items() == 0 && !lazy_fetch_page->lazy_fetch_is_loader()) associated_data.search_result_text.set_string("No results found"); else associated_data.search_result_text.set_string(""); associated_data.fetch_status = FetchStatus::NONE; idle_active_handler(); } } if(content_storage_file_modified) { content_storage_file_modified = false; if(content_storage_json.isObject()) { const Json::Value &chapters_json = content_storage_json["chapters"]; if(chapters_json.isObject()) json_chapters = &chapters_json; else json_chapters = &Json::Value::nullSingleton(); } else { json_chapters = &Json::Value::nullSingleton(); } } for(size_t i = 0; i < tabs.size(); ++i) { Tab &tab = tabs[i]; if(tab.page) ui_tabs.set_text(i, tab.page->get_title()); } window.clear(get_theme().background_color); page_loop_render(window, tabs, selected_tab, tab_associated_data[selected_tab], json_chapters, ui_tabs); { std::lock_guard lock(login_inputs_mutex); if(!tabs[selected_tab].login_inputs.inputs.empty()) { login_inputs_background.draw(window); for(auto &login_input : tabs[selected_tab].login_inputs.inputs) { login_input->update(); login_input->draw(window, login_inputs_background.get_size() - mgl::vec2f(login_input_padding_x * 2.0f, 0.0f), false); } } } if(tabs[selected_tab].body->get_num_items() > 0) { if(tabs[selected_tab].body->attach_side == AttachSide::TOP && !tabs[selected_tab].body->is_bottom_cut_off()) on_reached_end(); else if(tabs[selected_tab].body->attach_side == AttachSide::BOTTOM && !tabs[selected_tab].body->is_top_cut_off()) on_reached_end(); } AsyncImageLoader::get_instance().update(); window.display(); if(go_to_previous_page) { go_to_previous_page = false; return true; } } return false; } static Json::Value* history_get_item_by_id(Json::Value &history_json, const char *id) { assert(history_json.isArray()); for(Json::Value &item : history_json) { if(!item.isObject()) continue; const Json::Value &id_json = item["id"]; if(!id_json.isString()) continue; if(strcmp(id, id_json.asCString()) == 0) return &item; } return nullptr; } enum class WindowFullscreenState { UNSET, SET, TOGGLE }; static bool window_set_fullscreen(Display *display, Window window, WindowFullscreenState state) { Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); if(wm_state_atom == False || wm_state_fullscreen_atom == False) { fprintf(stderr, "Failed to fullscreen the window\n"); return false; } XEvent xev; xev.type = ClientMessage; xev.xclient.window = window; xev.xclient.message_type = wm_state_atom; xev.xclient.format = 32; xev.xclient.data.l[0] = (int)state; xev.xclient.data.l[1] = wm_state_fullscreen_atom; xev.xclient.data.l[2] = 0; xev.xclient.data.l[3] = 1; xev.xclient.data.l[4] = 0; if(!XSendEvent(display, XDefaultRootWindow(display), 0, SubstructureRedirectMask | SubstructureNotifyMask, &xev)) { fprintf(stderr, "Failed to fullscreen window\n"); return false; } XFlush(display); return true; } static bool window_is_fullscreen(Display *display, Window window) { Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); if(wm_state_atom == False || wm_state_fullscreen_atom == False) { fprintf(stderr, "Failed to get window atoms\n"); return false; } Atom type; int format = 0; unsigned long num_items = 0; unsigned long bytes_after = 0; unsigned char *properties = nullptr; if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) { fprintf(stderr, "Failed to get window wm state property\n"); return false; } if(!properties) return false; bool is_fullscreen = false; Atom *atoms = (Atom*)properties; for(unsigned long i = 0; i < num_items; ++i) { if(atoms[i] == wm_state_fullscreen_atom) { is_fullscreen = true; break; } } XFree(properties); return is_fullscreen; } static const char *useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"; static int accumulate_string_limit_head(char *data, int size, void *userdata) { std::string *str = (std::string*)userdata; str->append(data, size); if(str->size() >= 42) return 1; return 0; } static bool video_url_is_non_streamable_mp4(const char *url) { std::string result; const char *args[] = { "curl", "-sLf", "-r", "0-40", "--no-buffer", "-H", useragent_str, "--", url, nullptr }; exec_program(args, accumulate_string_limit_head, &result, 42); return (result.size() >= 42) && (memcmp(&result[4], "ftypisom", 8) == 0 || memcmp(&result[4], "ftypmp42", 8) == 0 || memcmp(&result[4], "ftymp42", 7) == 0 || memcmp(&result[4], "ftyp3gp4", 8) == 0 || memcmp(&result[4], "ftyp3gp5", 8) == 0 || memcmp(&result[4], "fty3gp5", 7) == 0 || memcmp(&result[4], "ftypqt", 6) == 0) && (memmem(&result[0], result.size(), "moov", 4) == NULL); } const char* Program::get_plugin_name() const { return plugin_name; } TaskResult Program::run_task_with_loading_screen(std::function callback) { if(running_task_with_loading_screen) return callback() ? TaskResult::TRUE : TaskResult::FALSE; running_task_with_loading_screen = true; assert(std::this_thread::get_id() == main_thread_id); idle_active_handler(); AsyncTask task = callback; TaskResult task_result = TaskResult::TRUE; window_size.x = window.get_size().x; window_size.y = window.get_size().y; mgl::Event event; while(window.is_open()) { while(window.poll_event(event)) { common_event_handler(event); if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; } else if(event.type == mgl::Event::KeyPressed && (event.key.code == mgl::Keyboard::Escape || event.key.code == mgl::Keyboard::Backspace)) { task.cancel(); task_result = TaskResult::CANCEL; goto task_end; } } handle_x11_events(); if(window_closed) { task.cancel(); task_result = TaskResult::CANCEL; goto task_end; } if(task.ready()) { task_result = task.get() ? TaskResult::TRUE : TaskResult::FALSE; goto task_end; } window.clear(get_theme().background_color); load_sprite.set_position(mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.5f)); load_sprite.set_rotation(load_sprite_timer.get_elapsed_time_seconds() * 400.0); window.draw(load_sprite); window.display(); } task_end: running_task_with_loading_screen = false; return task_result; } static bool video_url_supports_timestamp(const std::string &url) { std::string dummy_id; if(youtube_url_extract_id(url, dummy_id)) return true; if(url.find("pornhub.com/view_video.php") != std::string::npos) return true; return false; } // TODO: Remove when youtube-dl is no longer required to download soundcloud music static bool is_soundcloud(const std::string &url) { return url.find("soundcloud.com") != std::string::npos; } static bool url_should_download_with_youtube_dl(const std::string &url) { return url.find("pornhub.com") != std::string::npos || url.find("xhamster.com") != std::string::npos || url.find("spankbang.com") != std::string::npos // TODO: Remove when youtube-dl is no longer required to download soundcloud music || is_soundcloud(url); } void Program::redirect_focus_to_video_player_window(mgl::WindowHandle video_player_window) { Window focused_window = None; int dummy; XGetInputFocus(disp, &focused_window, &dummy); if(focused_window != window.get_system_handle()) return; XRaiseWindow(disp, video_player_window); XSetInputFocus(disp, video_player_window, RevertToParent, CurrentTime); XSync(disp, False); XFlush(disp); } void Program::show_video_player_window(mgl::WindowHandle video_player_window) { XMapWindow(disp, video_player_window); XSync(disp, False); redirect_focus_to_video_player_window(video_player_window); } void Program::video_page_download_video(const std::string &url, mgl::WindowHandle video_player_window) { bool separate_audio_option = url_should_download_with_youtube_dl(url); std::string video_id; separate_audio_option |= youtube_url_extract_id(url, video_id); if(is_soundcloud(url)) separate_audio_option = false; if(!separate_audio_option) { download_async_gui(url, file_manager_start_dir.string(), no_video); return; } bool audio_only = false; auto body = create_body(); auto options_page = std::make_unique(this, "Select download option"); options_page->add_option(body.get(), "Download video and audio", "", [&audio_only](){ audio_only = false; }); options_page->add_option(body.get(), "Download only audio", "", [&audio_only](){ audio_only = true; }); if(video_player_window) { XUnmapWindow(disp, video_player_window); XSync(disp, False); } std::vector tabs; tabs.push_back(Tab{ std::move(body), std::move(options_page), nullptr }); bool selected = page_loop(tabs); if(video_player_window) show_video_player_window(video_player_window); if(!selected) return; download_async_gui(url, file_manager_start_dir.string(), audio_only); } bool Program::video_download_if_non_streamable(std::string &video_url, std::string &audio_url, bool &is_audio_only, bool &has_embedded_audio, PageType previous_page) { Path video_cache_dir = get_cache_dir().join("media"); Path video_path = video_cache_dir; SHA256 sha256; sha256.add(video_url.data(), video_url.size()); video_path.join(sha256.getHash()); if(get_file_type(video_path) == FileType::REGULAR) { fprintf(stderr, "%s is found in cache. Playing from cache...\n", video_url.c_str()); video_url = std::move(video_path.data); audio_url.clear(); if(no_video) { is_audio_only = true; has_embedded_audio = false; } else { is_audio_only = false; has_embedded_audio = true; } } else { TaskResult video_is_not_streamble_result = run_task_with_loading_screen([video_url]() { return video_url_is_non_streamable_mp4(video_url.c_str()); }); if(video_is_not_streamble_result == TaskResult::TRUE) { fprintf(stderr, "%s is detected to be a non-streamable mp4 file, downloading it before playing it...\n", video_url.c_str()); if(create_directory_recursive(video_cache_dir) != 0) { show_notification("QuickMedia", "Failed to create video cache directory", Urgency::CRITICAL); current_page = previous_page; return false; } TaskResult download_file_result = run_task_with_loading_screen([&video_path, video_url]() { return download_to_file(video_url, video_path.data, {}, true) == DownloadResult::OK; }); switch(download_file_result) { case TaskResult::TRUE: { video_url = std::move(video_path.data); audio_url.clear(); if(no_video) { is_audio_only = true; has_embedded_audio = false; } else { is_audio_only = false; has_embedded_audio = true; } break; } case TaskResult::FALSE: { show_notification("QuickMedia", "Failed to download " + video_url, Urgency::CRITICAL); current_page = previous_page; return false; } case TaskResult::CANCEL: { current_page = previous_page; return false; } } } else if(video_is_not_streamble_result == TaskResult::CANCEL) { current_page = previous_page; return false; } } return true; } static bool youtube_url_is_live_stream(const std::string &url) { return url.find("yt_live_broadcast") != std::string::npos || url.find("manifest/") != std::string::npos; } int Program::video_get_max_height() { if(video_max_height > 0) return video_max_height; if(get_config().video.max_height > 0) return get_config().video.max_height; return get_largest_monitor_height(disp); } #define CLEANMASK(mask) ((mask) & (ShiftMask|ControlMask|Mod1Mask|Mod4Mask|Mod5Mask)) void Program::video_content_page(Page *parent_page, VideoPage *video_page, std::string video_title, bool download_if_streaming_fails, Body *parent_body, int play_index, int *parent_body_page, const std::string &parent_page_search) { PageType previous_page = pop_page_stack(); bool video_loaded = false; double video_time_pos = 0.0; // Time in media in seconds. Updates every 5 seconds and when starting to watch the video and when seeking. double video_duration = 0.0; // Time in seconds. 0 if unknown bool successfully_fetched_video_duration = false; bool successfully_fetched_time_pos = false; bool update_time_pos = false; bool update_duration = false; bool update_window_focus = false; bool update_window_focus_timer = false; mgl::Clock video_time_pos_clock; mgl::Clock update_window_focus_time; // HACK! std::string youtube_video_id_dummy; const bool is_youtube = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy); const bool is_matrix = strcmp(plugin_name, "matrix") == 0; const bool is_youtube_plugin = strcmp(plugin_name, "youtube") == 0; bool added_recommendations = false; mgl::Clock time_watched_timer; idle_active_handler(); video_player.reset(); BodyItems related_videos; bool move_in_parent = false; if(parent_body && video_page->autoplay_next_item() && play_index + 1 >= 0 && play_index + 1 < (int)parent_body->get_num_items()) { parent_body->copy_range(play_index + 1, (size_t)-1, related_videos); move_in_parent = true; } mgl::WindowHandle video_player_window = None; auto on_window_create = [&](mgl::WindowHandle _video_player_window) mutable { video_player_window = _video_player_window; XSelectInput(disp, video_player_window, KeyPressMask | PointerMotionMask | FocusChangeMask | StructureNotifyMask); redirect_focus_to_video_player_window(video_player_window); XSync(disp, False); SubtitleData subtitle_data; video_page->get_subtitles(subtitle_data); if(!subtitle_data.url.empty()) video_player->add_subtitle(subtitle_data.url, subtitle_data.title, "eng"); update_time_pos = true; update_window_focus_timer = true; update_window_focus_time.restart(); }; int64_t youtube_video_content_length = 0; int64_t youtube_audio_content_length = 0; std::string channel_url; AsyncTask related_videos_task; EventCallbackFunc video_event_callback; bool go_to_previous_page = false; std::string video_url; std::string audio_url; bool has_embedded_audio = true; std::vector media_chapters; auto load_video_error_check = [&](std::string start_time = "", bool reuse_media_source = false) mutable { video_player.reset(); channel_url.clear(); video_loaded = false; successfully_fetched_video_duration = false; successfully_fetched_time_pos = false; video_player_window = None; video_duration = 0.0; bool is_audio_only = no_video; const int video_max_height = video_get_max_height(); if(!reuse_media_source) { std::string new_title; video_url.clear(); audio_url.clear(); has_embedded_audio = true; std::string err_str; const int num_retries = is_youtube ? 3 : 1; bool load_successful = false; for(int i = 0; i < num_retries; ++i) { bool cancelled = false; TaskResult load_result = run_task_with_loading_screen([&]() { video_duration = 0.0; if(video_page->load(new_title, channel_url, video_duration, media_chapters, err_str) != PluginResult::OK) return false; if(video_duration > 0.001) successfully_fetched_video_duration = true; std::string ext; if(!no_video) video_url = video_page->get_video_url(video_max_height, has_embedded_audio, ext); if(video_url.empty() || no_video) { video_url = video_page->get_audio_url(ext); if(video_url.empty()) { video_url = video_page->get_url(); has_embedded_audio = true; } else { is_audio_only = true; has_embedded_audio = false; } } else if(!has_embedded_audio) { audio_url = video_page->get_audio_url(ext); } if(!is_youtube && download_if_streaming_fails) { if(!video_download_if_non_streamable(video_url, audio_url, is_audio_only, has_embedded_audio, previous_page)) return false; } if(is_youtube && !youtube_url_is_live_stream(video_url) && !youtube_url_is_live_stream(audio_url)) { youtube_video_content_length = 0; youtube_audio_content_length = 0; std::string new_video_url = video_url; std::string new_audio_url = audio_url; auto current_thread_id = std::this_thread::get_id(); if(!youtube_custom_redirect(new_video_url, new_audio_url, youtube_video_content_length, youtube_audio_content_length, [current_thread_id]{ return !program_is_dead_in_thread(current_thread_id); })) { if(program_is_dead_in_current_thread()) cancelled = true; return false; } video_url = std::move(new_video_url); audio_url = std::move(new_audio_url); } return true; }); if(!new_title.empty()) video_title = std::move(new_title); if(load_result == TaskResult::CANCEL || cancelled) { current_page = previous_page; go_to_previous_page = true; return; } else if(load_result == TaskResult::FALSE) { continue; } load_successful = true; break; } if(!load_successful) { show_notification("QuickMedia", "Failed to load media" + (err_str.empty() ? "" : ", error: " + err_str), Urgency::CRITICAL); current_page = previous_page; go_to_previous_page = true; return; } } const bool is_resume_go_back = !start_time.empty(); if(start_time.empty()) start_time = video_page->get_url_timestamp(); watched_videos.insert(video_page->get_url()); // TODO: Sync sequences //audio_url.clear(); //video_url.clear(); //is_audio_only = true; std::string v = video_url; std::string a = audio_url; if(is_youtube) { if(!v.empty() && !youtube_url_is_live_stream(v)) v = "qm-yt://" + v; if(!a.empty() && !youtube_url_is_live_stream(a)) a = "qm-yt://" + a; } VideoPlayer::StartupArgs startup_args; startup_args.path = v; startup_args.audio_path = a; startup_args.parent_window = window.get_system_handle(); startup_args.no_video = is_audio_only; startup_args.use_system_mpv_config = get_config().use_system_mpv_config || video_page->is_local(); startup_args.use_system_input_config = video_page->is_local(); startup_args.keep_open = is_matrix && !is_youtube; startup_args.resume = false; startup_args.resource_root = resources_root; startup_args.monitor_height = video_max_height; startup_args.use_youtube_dl = use_youtube_dl && !video_page->is_local(); startup_args.title = video_title; startup_args.start_time = start_time; startup_args.chapters = std::move(media_chapters); startup_args.plugin_name = plugin_name; startup_args.cache_on_disk = !video_page->is_local(); video_player = std::make_unique(std::move(startup_args), video_event_callback, on_window_create); VideoPlayer::Error err = video_player->load_video(); if(err != VideoPlayer::Error::OK) { std::string err_msg = "Failed to play url: "; err_msg += video_page->get_url(); show_notification("QuickMedia", err_msg.c_str(), Urgency::CRITICAL); current_page = previous_page; go_to_previous_page = true; } else { if(video_page->autoplay_next_item()) return; std::string url = video_page->get_url(); related_videos.clear(); related_videos_task = AsyncTask([&related_videos, url, video_page]() { video_page->mark_watched(); related_videos = video_page->get_related_media(url); }); // TODO: Make this also work for other video plugins if(strcmp(plugin_name, "youtube") != 0 || is_resume_go_back) return; std::string video_id; if(!youtube_url_extract_id(video_page->get_url(), video_id)) { std::string err_msg = "Failed to extract id of youtube url "; err_msg += video_page->get_url(); err_msg + ", video wont be saved in history"; show_notification("QuickMedia", err_msg.c_str(), Urgency::LOW); return; } Json::Value video_history_json = load_history_json(); time_t time_now = time(NULL); Json::Value *json_item = history_get_item_by_id(video_history_json, video_id.c_str()); if(json_item) { (*json_item)["timestamp"] = (Json::Int64)time_now; } else { Json::Value new_content_object(Json::objectValue); new_content_object["id"] = video_id; new_content_object["title"] = video_title; new_content_object["timestamp"] = (Json::Int64)time_now; video_history_json.append(std::move(new_content_object)); } Path video_history_filepath = get_history_filepath(plugin_name); save_json_to_file_atomic(video_history_filepath, video_history_json); } }; video_event_callback = [&](const char *event_name, const std::vector &args) mutable { if(strcmp(event_name, "pause") == 0) { //double time_remaining = 9999.0; //if(video_player->get_time_remaining(&time_remaining) == VideoPlayer::Error::OK && time_remaining <= 1.0) // end_of_file = true; } else if(strcmp(event_name, "playback-restart") == 0) { //video_player->set_paused(false); } else if(strcmp(event_name, "start-file") == 0) { update_duration = true; added_recommendations = false; time_watched_timer.restart(); video_loaded = true; update_time_pos = true; update_window_focus = true; } else if(strcmp(event_name, "file-loaded") == 0) { video_loaded = true; update_window_focus = true; } else if(strcmp(event_name, "video-reconfig") == 0 || strcmp(event_name, "audio-reconfig") == 0) { video_loaded = true; update_window_focus = true; } else if(strcmp(event_name, "seek") == 0) { update_time_pos = true; } else if(strcmp(event_name, "fullscreen") == 0 && args.size() == 1) { window_set_fullscreen(disp, window.get_system_handle(), args[0] == "yes" ? WindowFullscreenState::SET : WindowFullscreenState::UNSET); } //fprintf(stderr, "event name: %s\n", event_name); }; load_video_error_check(); mgl::Event event; XEvent xev; bool cursor_visible = true; mgl::Clock cursor_hide_timer; auto save_video_url_to_clipboard = [this, video_page]() { std::string url = video_page->get_download_url(video_get_max_height()); if(video_url_supports_timestamp(url)) { double time_in_file = 0.0; if(video_player && (video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK)) time_in_file = 0.0; std::string clipboard = std::move(url); if((int)time_in_file > 0) clipboard += "&t=" + std::to_string((int)time_in_file); set_clipboard(clipboard); } else { set_clipboard(url); } }; while (current_page == PageType::VIDEO_CONTENT && window.is_open() && !go_to_previous_page) { while (window.poll_event(event)) { common_event_handler(event); if(event.type == mgl::Event::GainedFocus) { update_window_focus = true; } else if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; } else if(event.type == mgl::Event::KeyPressed && (event.key.code == mgl::Keyboard::Escape || event.key.code == mgl::Keyboard::Q || event.key.code == mgl::Keyboard::Backspace)) { // To be able to close the video player while the video is loading if(window_is_fullscreen(disp, window.get_system_handle())) { if(video_player && video_player_window && event.key.code != mgl::Keyboard::Escape) video_player->cycle_fullscreen(); } else { current_page = previous_page; go_to_previous_page = true; } } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::C && event.key.control && !video_page->is_local()) { save_video_url_to_clipboard(); } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::F5 && !video_page->is_local()) { load_video_error_check(); } } handle_x11_events(); if(video_player_window && XCheckTypedWindowEvent(disp, video_player_window, MapNotify, &xev)) { update_window_focus = true; } if(video_player_window && XCheckTypedWindowEvent(disp, video_player_window, FocusIn, &xev)) {} if(video_player_window && XCheckTypedWindowEvent(disp, video_player_window, FocusOut, &xev)) { update_window_focus = true; } if(video_player_window && update_window_focus) { update_window_focus = false; redirect_focus_to_video_player_window(video_player_window); } if(video_player_window && update_window_focus_timer && update_window_focus_time.get_elapsed_time_seconds() >= 0.5) { update_window_focus_timer = false; update_window_focus = false; redirect_focus_to_video_player_window(video_player_window); } if(video_player && video_player_window && XCheckTypedWindowEvent(disp, video_player_window, KeyPress, &xev)/* && xev.xkey.subwindow == video_player_window*/) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" KeySym pressed_keysym = XKeycodeToKeysym(disp, xev.xkey.keycode, 0); #pragma GCC diagnostic pop bool pressing_ctrl = (CLEANMASK(xev.xkey.state) == ControlMask); if(pressed_keysym == XK_q && pressing_ctrl) { window.close(); } else if(pressed_keysym == XK_Escape || pressed_keysym == XK_q || pressed_keysym == XK_BackSpace) { if(window_is_fullscreen(disp, window.get_system_handle())) { if(pressed_keysym != XK_Escape) video_player->cycle_fullscreen(); } else { current_page = previous_page; go_to_previous_page = true; break; } } else if(pressed_keysym == XK_f && pressing_ctrl) { video_player->cycle_fullscreen(); } else if(pressed_keysym == XK_s && pressing_ctrl && !video_page->is_local()) { video_page_download_video(video_page->get_download_url(video_get_max_height()), video_player_window); } else if(pressed_keysym == XK_F5 && !video_page->is_local()) { double resume_start_time = 0.0; video_player->get_time_in_file(&resume_start_time); load_video_error_check(std::to_string((int)resume_start_time)); } else if(pressed_keysym == XK_r && pressing_ctrl && !video_page->is_local()) { cursor_hide_timer.restart(); if(!cursor_visible) window.set_cursor_visible(true); cursor_visible = true; bool cancelled = false; if(related_videos_task.valid()) { XUnmapWindow(disp, video_player_window); XSync(disp, False); XFlush(disp); TaskResult task_result = run_task_with_loading_screen([&]() { while(!program_is_dead_in_current_thread()) { if(related_videos_task.ready()) { related_videos_task.get(); return true; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return true; }); if(task_result == TaskResult::CANCEL) { show_video_player_window(video_player_window); cancelled = true; } } if(!cancelled) { XUnmapWindow(disp, video_player_window); XSync(disp, False); XFlush(disp); std::vector related_pages; TaskResult related_pages_result = run_task_with_loading_screen([&video_page, &related_videos, &channel_url, &related_pages]{ return video_page->get_related_pages(related_videos, channel_url, related_pages) == PluginResult::OK; }); if(related_pages_result == TaskResult::FALSE) { show_video_player_window(video_player_window); show_notification("QuickMedia", "Failed to get related pages", Urgency::CRITICAL); } else if(related_pages_result == TaskResult::TRUE && !related_pages.empty()) { if(successfully_fetched_time_pos && successfully_fetched_video_duration) video_page->set_watch_progress(video_time_pos, video_duration); bool page_changed = false; double resume_start_time = 0.0; page_loop(related_pages, video_page->get_related_pages_first_tab(), [&](const std::vector &new_tabs) { if(!page_changed && new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) { video_player->get_time_in_file(&resume_start_time); video_player.reset(); page_changed = true; } }); if(!window.is_open() || current_page == PageType::EXIT) { video_player.reset(); return; } if(!video_player) { current_page = PageType::VIDEO_CONTENT; load_video_error_check(resume_start_time > 0.1 ? std::to_string((int)resume_start_time) : ""); } else { show_video_player_window(video_player_window); } } else { show_video_player_window(video_player_window); } } } else if(pressed_keysym == XK_c && pressing_ctrl) { save_video_url_to_clipboard(); } } if(video_player_window && XCheckTypedWindowEvent(disp, video_player_window, MotionNotify, &xev)) { while(XCheckTypedWindowEvent(disp, video_player_window, MotionNotify, &xev)); cursor_hide_timer.restart(); if(!cursor_visible) window.set_cursor_visible(true); cursor_visible = true; } /* Only save recommendations for the video if we have been watching it for 15 seconds */ if(is_youtube_plugin && video_loaded && !added_recommendations && time_watched_timer.get_elapsed_time_seconds() >= 15.0) { added_recommendations = true; save_recommendations_from_related_videos(plugin_name, video_page->get_url(), video_title, related_videos); } VideoPlayer::Error update_err = video_player ? video_player->update() : VideoPlayer::Error::OK; if(update_err == VideoPlayer::Error::FAIL_TO_CONNECT_TIMEOUT) { show_notification("QuickMedia", "Failed to connect to mpv ipc after 10 seconds", Urgency::CRITICAL); current_page = previous_page; go_to_previous_page = true; break; } else if(update_err == VideoPlayer::Error::EXITED && video_player->exit_status == 0 && (!is_matrix || is_youtube)) { std::string new_video_url; if(related_videos_task.valid()) { TaskResult task_result = run_task_with_loading_screen([&]() { while(!program_is_dead_in_current_thread()) { if(related_videos_task.ready()) { related_videos_task.get(); return true; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return true; }); if(task_result == TaskResult::CANCEL) { current_page = previous_page; go_to_previous_page = true; break; } } // Find video that hasn't been played before in this video session auto find_next_video = [this, parent_body, move_in_parent, &related_videos, &video_page, &new_video_url]() { for(auto it = related_videos.begin(), end = related_videos.end(); it != end; ++it) { if((*it)->visible && !(*it)->url.empty() && watched_videos.find((*it)->url) == watched_videos.end() && !video_page->video_should_be_skipped((*it)->url)) { if(parent_body && move_in_parent) parent_body->set_selected_item(it->get()); new_video_url = (*it)->url; related_videos.erase(it); break; } } }; find_next_video(); if(new_video_url.empty() && parent_page && parent_body_page && video_page->autoplay_next_item()) { BodyItems new_body_items; const int fetch_page = (*parent_body_page) + 1; TaskResult load_next_page_result = run_task_with_loading_screen([parent_page, parent_page_search, fetch_page, &new_body_items] { if(parent_page->get_page(parent_page_search, fetch_page, new_body_items) != PluginResult::OK) { fprintf(stderr, "Failed to get next page (page %d)\n", fetch_page); return false; } return true; }); fprintf(stderr, "Finished fetching page %d, num new items: %zu\n", fetch_page, new_body_items.size()); size_t num_new_messages = new_body_items.size(); if(num_new_messages > 0) { if(parent_body) parent_body->append_items(new_body_items); (*parent_body_page)++; related_videos = std::move(new_body_items); find_next_video(); } if(load_next_page_result == TaskResult::CANCEL) { current_page = previous_page; go_to_previous_page = true; break; } } // If there are no videos to play, then dont play any... if(new_video_url.empty()) { if(!video_page->is_local()) show_notification("QuickMedia", "No more related videos to play"); current_page = previous_page; go_to_previous_page = true; break; } TaskResult get_playable_url_result = run_task_with_loading_screen([video_page, &new_video_url]() { video_page->set_url(video_page->url_get_playable_url(new_video_url)); return true; }); if(get_playable_url_result == TaskResult::CANCEL) { current_page = previous_page; go_to_previous_page = true; break; } else if(get_playable_url_result == TaskResult::FALSE) { show_notification("QuickMedia", "Failed to get playable url", Urgency::CRITICAL); current_page = previous_page; go_to_previous_page = true; break; } load_video_error_check(); } else if(update_err != VideoPlayer::Error::OK) { show_notification("QuickMedia", "Failed to play the video (error code " + std::to_string((int)update_err) + ")", Urgency::CRITICAL); current_page = previous_page; go_to_previous_page = true; break; } AsyncImageLoader::get_instance().update(); if(matrix) matrix->update(); if(!video_loaded) { window.clear(get_theme().background_color); load_sprite.set_position(mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.5f)); load_sprite.set_rotation(load_sprite_timer.get_elapsed_time_seconds() * 400.0); window.draw(load_sprite); window.display(); if(!cursor_visible) { cursor_visible = true; window.set_cursor_visible(true); } cursor_hide_timer.restart(); continue; } if(video_player) { if(video_time_pos_clock.get_elapsed_time_seconds() >= 5.0) { video_time_pos_clock.restart(); update_time_pos = true; } if(update_time_pos) { update_time_pos = false; if(video_player->get_time_in_file(&video_time_pos) == VideoPlayer::Error::OK) successfully_fetched_time_pos = true; } if(update_duration) { update_duration = false; successfully_fetched_video_duration = true; double file_duration = 0.0; video_player->get_duration_in_file(&file_duration); video_duration = std::max(video_duration, file_duration); } } if(video_player_window) { if(!cursor_visible) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); continue; } const double UI_HIDE_TIMEOUT_SEC = 2.5; if(cursor_hide_timer.get_elapsed_time_seconds() > UI_HIDE_TIMEOUT_SEC) { cursor_visible = false; window.set_cursor_visible(false); } } std::this_thread::sleep_for(std::chrono::milliseconds(50)); } video_player.reset(); window.set_cursor_visible(true); window_set_fullscreen(disp, window.get_system_handle(), WindowFullscreenState::UNSET); auto window_size_u = window.get_size(); window_size.x = window_size_u.x; window_size.y = window_size_u.y; if(successfully_fetched_time_pos && successfully_fetched_video_duration) video_page->set_watch_progress(video_time_pos, video_duration); } void Program::select_episode(BodyItem *item, bool start_from_beginning) { image_index = 0; switch(image_view_mode) { case ImageViewMode::SINGLE: current_page = PageType::IMAGES; break; case ImageViewMode::SCROLL: current_page = PageType::IMAGES_CONTINUOUS; break; } if(start_from_beginning) return; const Json::Value &json_chapters = content_storage_json["chapters"]; if(json_chapters.isObject()) { const Json::Value &json_chapter = json_chapters[item->get_title()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) image_index = current.asInt() - 1; } } } // TODO: Remove PageType Program::pop_page_stack() { if(!page_stack.empty()) { PageType previous_page = page_stack.top(); page_stack.pop(); return previous_page; } return PageType::EXIT; } // TODO: Do the same for thumbnails? static bool is_symlink_valid(const char *filepath) { struct stat buf; return lstat(filepath, &buf) != -1; } // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image // becomes 40mb (private memory, almost 100mb in total!) Unacceptable! Program::LoadImageResult Program::load_image_by_index(int image_index, mgl::Texture &image_texture, std::string &error_message) { Path image_path = content_cache_dir; image_path.join(std::to_string(image_index + 1)); bool upscaled_ok = true; if(upscale_image_action != UpscaleImageAction::NO) { Path image_filepath_upcaled = image_path; image_filepath_upcaled.append(".upscaled"); if(get_file_type(image_filepath_upcaled) == FileType::FILE_NOT_FOUND && image_index < (int)image_upscale_status.size() && image_upscale_status[image_index] == 0) upscaled_ok = false; } if(get_file_type(image_path) == FileType::REGULAR && is_symlink_valid(image_path.data.c_str()) && upscaled_ok) { if(image_texture.load_from_file(image_path.data.c_str())) { return LoadImageResult::OK; } else { show_notification("QuickMedia", "Failed to load image for page " + std::to_string(image_index + 1) + ". Image filepath: " + image_path.data, Urgency::CRITICAL); error_message = std::string("Failed to load image for page ") + std::to_string(image_index + 1); return LoadImageResult::FAILED; } } else { error_message = "Downloading page " + std::to_string(image_index + 1) + "..."; return LoadImageResult::DOWNLOAD_IN_PROGRESS; } } void Program::download_chapter_images_if_needed(MangaImagesPage *images_page) { if(downloading_chapter_url == images_page->get_url()) return; downloading_chapter_url = images_page->get_url(); image_download_cancel = true; image_download_future.cancel(); image_download_cancel = false; num_manga_pages = 0; std::promise num_manga_pages_promise; num_manga_pages_future = num_manga_pages_promise.get_future(); Path content_cache_dir_ = content_cache_dir; image_download_future = AsyncTask>([images_page, content_cache_dir_, this](std::promise num_manga_pages_promise) { int num_pages = 0; if(images_page->update_image_urls(num_pages) != ImageResult::OK) { num_manga_pages_promise.set_value(0); if(!image_download_cancel) show_notification("QuickMedia", "Failed to fetch page images", Urgency::CRITICAL); return; } else { num_manga_pages_promise.set_value(num_pages); image_upscale_status.resize(num_pages, 0); } if(num_pages == 0) return; // TODO: Download images in parallel int page = 1; images_page->for_each_page_in_chapter([this, images_page, &page, content_cache_dir_](const std::string &url) { if(image_download_cancel) return false; int image_index = page - 1; // TODO: Save image with the file extension that url says it has? right now the file is saved without any extension Path image_filepath = content_cache_dir_; image_filepath.join(std::to_string(page++)); bool upscaled_ok = true; if(upscale_image_action != UpscaleImageAction::NO) { Path image_filepath_upcaled = image_filepath; image_filepath_upcaled.append(".upscaled"); if(get_file_type(image_filepath_upcaled) == FileType::FILE_NOT_FOUND && image_upscale_status[image_index] == 0) upscaled_ok = false; } if(get_file_type(image_filepath) != FileType::FILE_NOT_FOUND && is_symlink_valid(image_filepath.data.c_str()) && upscaled_ok) return true; std::vector extra_args; const bool is_manganelo = (strcmp(images_page->get_service_name(), "manganelo") == 0); const char *website_url = images_page->get_website_url(); const bool cloudflare_bypass = (strcmp(images_page->get_service_name(), "manganelo") == 0 || strcmp(images_page->get_service_name(), "readm") == 0); if(is_manganelo) { extra_args = { CommandArg { "-H", "accept: image/jpeg,image/png,image/*,*/*;q=0.8" }, CommandArg { "-H", "sec-fetch-site: cross-site" }, CommandArg { "-H", "sec-fetch-mode: no-cors" }, CommandArg { "-H", "sec-fetch-dest: image" }, CommandArg { "-H", "referer: https://manganelo.com/" }, CommandArg { "-m", "30" }, CommandArg { "--connect-timeout", "30" } }; } else if(website_url && website_url[0] != '\0') { std::string website_url_str = website_url; if(!website_url_str.empty() && website_url_str.back() != '/') website_url_str.push_back('/'); extra_args = { CommandArg { "-H", "referer: " + std::move(website_url_str) }, }; } Path image_filepath_tmp(image_filepath.data + ".tmpz"); // TODO: Move to page if(images_page->is_local()) { int res = symlink(url.c_str(), image_filepath_tmp.data.c_str()); if(res == -1 && errno != EEXIST) { show_notification("QuickMedia", "Failed to symlink " + image_filepath_tmp.data + " to " + url); return true; } } else { int64_t file_size = 0; if(download_to_file(url, image_filepath_tmp.data, extra_args, true, cloudflare_bypass) != DownloadResult::OK || (is_manganelo && file_get_size(image_filepath_tmp, &file_size) == 0 && file_size < 255)) { if(!image_download_cancel) show_notification("QuickMedia", "Failed to download image: " + url, Urgency::CRITICAL); return true; } } bool rename_immediately = true; if(upscale_image_action == UpscaleImageAction::LOW_RESOLUTION) { int screen_width, screen_height; get_screen_resolution(disp, &screen_width, &screen_height); int image_width, image_height; if(image_get_resolution(image_filepath_tmp, &image_width, &image_height)) { if(image_height < screen_height * 0.65) { rename_immediately = false; CopyOp copy_op; copy_op.source = image_filepath_tmp; copy_op.destination = image_filepath; images_to_upscale_queue.push(std::move(copy_op)); } else { fprintf(stderr, "Info: not upscaling %s because the file is already large on your monitor (screen height: %d, image height: %d)\n", image_filepath_tmp.data.c_str(), screen_height, image_height); image_upscale_status[image_index] = 1; } } else { fprintf(stderr, "Warning: failed to upscale %s because QuickMedia failed to recognize the resolution of the image\n", image_filepath_tmp.data.c_str()); image_upscale_status[image_index] = 1; } } else if(upscale_image_action == UpscaleImageAction::FORCE) { rename_immediately = false; CopyOp copy_op; copy_op.source = image_filepath_tmp; copy_op.destination = image_filepath; images_to_upscale_queue.push(std::move(copy_op)); } if(rename_immediately) { if(rename_atomic(image_filepath_tmp.data.c_str(), image_filepath.data.c_str()) != 0) { perror(image_filepath_tmp.data.c_str()); show_notification("QuickMedia", "Failed to save image to file: " + image_filepath.data, Urgency::CRITICAL); return true; } } return true; }); }, std::move(num_manga_pages_promise)); mgl::Event event; PageType current_manga_page = current_page; while (current_page == current_manga_page && window.is_open()) { while(window.poll_event(event)) { common_event_handler(event); if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::Escape) { current_page = pop_page_stack(); } } handle_x11_events(); if(num_manga_pages_future.valid() && num_manga_pages_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { num_manga_pages = num_manga_pages_future.get(); break; } window.clear(get_theme().background_color); load_sprite.set_position(mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.5f)); load_sprite.set_rotation(load_sprite_timer.get_elapsed_time_seconds() * 400.0); window.draw(load_sprite); AsyncImageLoader::get_instance().update(); window.display(); } } void Program::update_manga_history(const std::string &manga_id, const std::string &thumbnail_url) { Json::Value history_json = load_history_json(); const time_t time_now = time(NULL); Json::Value *json_item = history_get_item_by_id(history_json, manga_id.c_str()); if(json_item) { (*json_item)["timestamp"] = (Json::Int64)time_now; if(!thumbnail_url.empty()) (*json_item)["thumbnail_url"] = thumbnail_url; } else { Json::Value new_content_object(Json::objectValue); new_content_object["id"] = manga_id; new_content_object["timestamp"] = (Json::Int64)time_now; if(!thumbnail_url.empty()) new_content_object["thumbnail_url"] = thumbnail_url; history_json.append(std::move(new_content_object)); } Path history_filepath = get_history_filepath(plugin_name); save_json_to_file_atomic(history_filepath, history_json); } void Program::save_manga_progress(MangaImagesPage *images_page, Json::Value &json_chapters, Json::Value &json_chapter, int &latest_read) { image_index = std::max(0, std::min(image_index, num_manga_pages)); json_chapters = content_storage_json["chapters"]; latest_read = image_index + 1; if(json_chapters.isObject()) { json_chapter = json_chapters[images_page->get_chapter_name()]; if(!json_chapter.isObject()) json_chapter = Json::Value(Json::objectValue); } else { json_chapters = Json::Value(Json::objectValue); json_chapter = Json::Value(Json::objectValue); } json_chapter["current"] = std::min(latest_read, num_manga_pages); json_chapter["total"] = num_manga_pages; json_chapter["url"] = images_page->get_url(); json_chapters[images_page->get_chapter_name()] = json_chapter; content_storage_json["chapters"] = json_chapters; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("QuickMedia", "Failed to save manga progress", Urgency::CRITICAL); } } int Program::image_page(MangaImagesPage *images_page, Body *chapters_body, bool &continue_left_off) { int page_navigation = 0; image_download_cancel = false; continue_left_off = false; content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_url_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("QuickMedia", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); current_page = pop_page_stack(); return 0; } mgl::Texture image_texture; mgl::Sprite image; mgl::Text error_message("", *FontLoader::get_font(FontLoader::FontType::LATIN, 30 * get_config().scale * get_config().font_scale)); error_message.set_color(get_theme().text_color); bool download_in_progress = false; mgl::Event event; download_chapter_images_if_needed(images_page); if(num_manga_pages == 0) { current_page = pop_page_stack(); return 0; } if(current_page != PageType::IMAGES || !window.is_open()) return 0; // TODO: Dont do this every time we change page? Json::Value json_chapters; Json::Value json_chapter; int latest_read; save_manga_progress(images_page, json_chapters, json_chapter, latest_read); update_manga_history(manga_id, images_page->thumbnail_url); if(image_index < num_manga_pages) { std::string error_msg; LoadImageResult load_image_result = load_image_by_index(image_index, image_texture, error_msg); if(load_image_result == LoadImageResult::OK) image.set_texture(&image_texture); else if(load_image_result == LoadImageResult::DOWNLOAD_IN_PROGRESS) download_in_progress = true; error_message.set_string(std::move(error_msg)); } else if(image_index == num_manga_pages) { error_message.set_string("End of " + images_page->get_chapter_name()); } bool error = !error_message.get_string().empty(); bool redraw = true; const int chapter_text_character_size = 14 * get_config().scale * get_config().font_scale; mgl::Text chapter_text(images_page->manga_name + " | " + images_page->get_chapter_name() + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_manga_pages), *FontLoader::get_font(FontLoader::FontType::LATIN, chapter_text_character_size)); if(image_index == num_manga_pages) chapter_text.set_string(images_page->manga_name + " | " + images_page->get_chapter_name() + " | End"); chapter_text.set_color(mgl::Color(255, 255, 255, 255)); mgl::Rectangle chapter_text_background; chapter_text_background.set_color(mgl::Color(0, 0, 0, 150)); mgl::vec2i texture_size; mgl::vec2f texture_size_f; if(!error) { texture_size = image.get_texture()->get_size(); texture_size_f = mgl::vec2f(texture_size.x, texture_size.y); } mgl::Clock check_downloaded_timer; const double check_downloaded_timeout_sec = 0.5; malloc_trim(0); mgl::Clock force_redraw_timer; window.set_framerate_limit(20); idle = true; // TODO: Show to user if a certain page is missing (by checking page name (number) and checking if some are skipped) while (current_page == PageType::IMAGES && window.is_open()) { while(window.poll_event(event)) { common_event_handler(event); if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; redraw = true; } else if(event.type == mgl::Event::GainedFocus) { redraw = true; } else if(event.type == mgl::Event::KeyPressed) { if((!event.key.control && event.key.code == mgl::Keyboard::Up) || (!event.key.alt && event.key.control && event.key.code == mgl::Keyboard::K)) { if(image_index > 0) { --image_index; goto end_of_images_page; } else if(image_index == 0 && chapters_body->get_selected_item() < (int)chapters_body->get_num_items() - 1) { page_navigation = -1; goto end_of_images_page; } } else if((!event.key.control && event.key.code == mgl::Keyboard::Down) || (!event.key.alt && event.key.control && event.key.code == mgl::Keyboard::J)) { if(image_index < num_manga_pages) { ++image_index; goto end_of_images_page; } else if(image_index == num_manga_pages && chapters_body->get_selected_item() > 0) { page_navigation = 1; goto end_of_images_page; } } else if(event.key.code == mgl::Keyboard::PageUp) { if(image_index > 0) { image_index = std::max(0, image_index - 10); goto end_of_images_page; } } else if(event.key.code == mgl::Keyboard::PageDown) { if(image_index < num_manga_pages) { image_index = std::min(num_manga_pages, image_index + 10); goto end_of_images_page; } } else if(event.key.code == mgl::Keyboard::Home) { if(image_index > 0) { image_index = 0; goto end_of_images_page; } } else if(event.key.code == mgl::Keyboard::End) { if(image_index < num_manga_pages) { image_index = num_manga_pages; goto end_of_images_page; } } else if(event.key.code == mgl::Keyboard::Escape) { current_page = pop_page_stack(); } else if(event.key.code == mgl::Keyboard::I) { current_page = PageType::IMAGES_CONTINUOUS; image_view_mode = ImageViewMode::SCROLL; } else if(event.key.code == mgl::Keyboard::F) { fit_image_to_window = !fit_image_to_window; redraw = true; } } } handle_x11_events(); if(download_in_progress && check_downloaded_timer.get_elapsed_time_seconds() >= check_downloaded_timeout_sec) { std::string error_msg; LoadImageResult load_image_result = load_image_by_index(image_index, image_texture, error_msg); if(load_image_result == LoadImageResult::OK) { image.set_texture(&image_texture); download_in_progress = false; error = false; texture_size = image.get_texture()->get_size(); texture_size_f = mgl::vec2f(texture_size.x, texture_size.y); } else if(load_image_result == LoadImageResult::FAILED) { download_in_progress = false; error = true; } error_message.set_string(std::move(error_msg)); redraw = true; check_downloaded_timer.restart(); } const float font_height = chapter_text_character_size + 8.0f; const float bottom_panel_height = font_height + 6.0f; mgl::vec2f content_size; content_size.x = window_size.x; content_size.y = window_size.y - bottom_panel_height; // TODO: Track x11 window damage instead if(force_redraw_timer.get_elapsed_time_seconds() >= 1.0f) { force_redraw_timer.restart(); redraw = true; } if(redraw) { redraw = false; if(error) { auto bounds = error_message.get_bounds(); error_message.set_position(vec2f_floor(content_size.x * 0.5f - bounds.size.x * 0.5f, content_size.y * 0.5f - bounds.size.y)); } else { mgl::vec2f image_scale; if(fit_image_to_window) image_scale = get_ratio(texture_size_f, wrap_to_size(texture_size_f, content_size)); else image_scale = get_ratio(texture_size_f, clamp_to_size(texture_size_f, content_size)); image.set_scale(image_scale); auto image_size = texture_size_f; image_size.x *= image_scale.x; image_size.y *= image_scale.y; image.set_position(vec2f_floor(content_size.x * 0.5f - image_size.x * 0.5f, content_size.y * 0.5f - image_size.y * 0.5f)); } window.clear(get_theme().background_color); if(error) { window.draw(error_message); } else { window.draw(image); } chapter_text_background.set_size(mgl::vec2f(window_size.x, bottom_panel_height)); chapter_text_background.set_position(mgl::vec2f(0.0f, std::floor(window_size.y - bottom_panel_height))); window.draw(chapter_text_background); auto text_bounds = chapter_text.get_bounds(); chapter_text.set_position(vec2f_floor(window_size.x * 0.5f - text_bounds.size.x * 0.5f, window_size.y - bottom_panel_height * 0.5f - font_height * 0.7f)); window.draw(chapter_text); window.display(); } else { std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } end_of_images_page: return page_navigation; } int Program::image_continuous_page(MangaImagesPage *images_page) { int page_navigation = 0; image_download_cancel = false; content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_url_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("QuickMedia", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); current_page = pop_page_stack(); return page_navigation; } download_chapter_images_if_needed(images_page); if(num_manga_pages == 0) { current_page = pop_page_stack(); return page_navigation; } if(current_page != PageType::IMAGES_CONTINUOUS || !window.is_open()) return page_navigation; Json::Value json_chapters; Json::Value json_chapter; int current_read_page; save_manga_progress(images_page, json_chapters, json_chapter, current_read_page); ImageViewer image_viewer(&window, num_manga_pages, images_page->manga_name, images_page->get_chapter_name(), std::min(num_manga_pages - 1, image_index), content_cache_dir, &fit_image_to_window); idle_active_handler(); while(current_page == PageType::IMAGES_CONTINUOUS && window.is_open()) { handle_x11_events(); window.clear(get_theme().background_color); ImageViewerAction action = image_viewer.draw(); switch(action) { case ImageViewerAction::NONE: break; case ImageViewerAction::RETURN: current_page = pop_page_stack(); break; case ImageViewerAction::SWITCH_TO_SINGLE_IMAGE_MODE: image_view_mode = ImageViewMode::SINGLE; current_page = PageType::IMAGES; break; case ImageViewerAction::PREVIOUS_CHAPTER: { page_navigation = -1; goto end_of_continuous_images_page; } case ImageViewerAction::NEXT_CHAPTER: { page_navigation = 1; goto end_of_continuous_images_page; } } AsyncImageLoader::get_instance().update(); window.display(); int focused_page = image_viewer.get_focused_page(); image_index = focused_page - 1; if(focused_page != current_read_page) { current_read_page = focused_page; json_chapter["current"] = std::max(0, std::min(current_read_page, num_manga_pages)); json_chapters[images_page->get_chapter_name()] = json_chapter; content_storage_json["chapters"] = json_chapters; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("QuickMedia", "Failed to save manga progress", Urgency::CRITICAL); } update_manga_history(manga_id, images_page->thumbnail_url); } } end_of_continuous_images_page: window_size.x = window.get_size().x; window_size.y = window.get_size().y; return page_navigation; } static bool get_image_board_last_posted_filepath(const char *plugin_name, Path &path) { Path dir = get_storage_dir().join(plugin_name); if(create_directory_recursive(dir) != 0) return false; path = std::move(dir); path.join("last_posted_time"); return true; } struct ImageControl { float zoom = 1.0f; bool pressed = false; bool moved = false; mgl::vec2f offset; // relative to center mgl::vec2i prev_mouse_pos; }; static mgl::vec2f floor(mgl::vec2f vec) { return { std::floor(vec.x), std::floor(vec.y) }; } 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, REPLYING, REQUESTING_CAPTCHA, SOLVING_POST_CAPTCHA, POSTING_COMMENT, VIEWING_ATTACHED_IMAGE }; thread_body->title_mark_urls = true; NavigationStage navigation_stage = NavigationStage::VIEWING_COMMENTS; mgl::Texture captcha_texture; mgl::Sprite captcha_sprite; mgl::Texture captcha_bg_texture; mgl::Sprite captcha_bg_sprite; bool has_captcha_bg = false; float captcha_slide = 0.0f; std::string attached_image_url; ImageBoardCaptchaChallenge captcha_challenge; ImageControl image_control; const float captcha_slide_padding_x = std::floor(4.0f * get_config().scale * get_config().spacing_scale); const float captcha_slide_padding_y = std::floor(4.0f * get_config().scale * get_config().spacing_scale); mgl::Color background_color_darker = get_theme().background_color; background_color_darker.r = std::max(0, (int)background_color_darker.r - 20); background_color_darker.g = std::max(0, (int)background_color_darker.g - 20); background_color_darker.b = std::max(0, (int)background_color_darker.b - 20); RoundedRectangle captcha_slide_bg(mgl::vec2f(1.0f, 1.0f), std::floor(10.0f * get_config().scale), background_color_darker, &rounded_rectangle_shader); RoundedRectangle captcha_slide_fg(mgl::vec2f(1.0f, 1.0f), std::floor(10.0f * get_config().scale - captcha_slide_padding_y), get_theme().loading_bar_color, &rounded_rectangle_shader); auto attached_image_texture = std::make_unique(); mgl::Sprite attached_image_sprite; std::string captcha_post_id; std::string captcha_solution; std::string comment_to_post; const int captcha_solution_text_height = 18 * get_config().scale * get_config().font_scale; mgl::Text captcha_solution_text("", *FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, captcha_solution_text_height)); int solved_captcha_ttl = 0; int64_t last_posted_time = time(nullptr); int64_t seconds_until_post_again = 60; // TODO: Timeout for other imageboards bool has_post_timeout = thread_page->get_pass_id().empty(); bool has_posted_before = false; Path last_posted_time_filepath; if(get_image_board_last_posted_filepath(plugin_name, last_posted_time_filepath)) { std::string d; if(file_get_content(last_posted_time_filepath, d) == 0) { last_posted_time = strtoll(d.c_str(), nullptr, 10); has_posted_before = true; } } if(!has_posted_before) last_posted_time -= (seconds_until_post_again + 1); bool redraw = true; Entry comment_input("Press i to start writing a comment...", &rounded_rectangle_shader); comment_input.draw_background = false; comment_input.set_editable(false); std::string selected_file_for_upload; auto post_comment = [this, &comment_input, &selected_file_for_upload, &navigation_stage, &thread_page, &captcha_post_id, &captcha_solution, &last_posted_time, &has_post_timeout](std::string comment_to_post, std::string file_to_upload) { comment_input.set_editable(false); PostResult post_result = thread_page->post_comment(captcha_post_id, captcha_solution, comment_to_post, file_to_upload); if(post_result == PostResult::OK) { show_notification("QuickMedia", "Comment posted!"); navigation_stage = NavigationStage::VIEWING_COMMENTS; comment_input.set_text(""); // 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. selected_file_for_upload.clear(); // TODO: Remove from here, this is async has_post_timeout = thread_page->get_pass_id().empty(); last_posted_time = time(nullptr); Path last_posted_time_filepath; if(get_image_board_last_posted_filepath(plugin_name, last_posted_time_filepath)) file_overwrite_atomic(last_posted_time_filepath, std::to_string(last_posted_time)); } else if(post_result == PostResult::TRY_AGAIN) { show_notification("QuickMedia", "Please wait before posting again"); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::INVALID_CAPTCHA) { show_notification("QuickMedia", "Invalid captcha, please try again"); navigation_stage = NavigationStage::REQUESTING_CAPTCHA; // TODO: Need to wait before requesting need captcha? } else if(post_result == PostResult::BANNED) { show_notification("QuickMedia", "Failed to post comment because you are banned", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; //} else if(post_result == PostResult::FILE_TOO_LARGE) { // show_notification("QuickMedia", "Failed to post comment because the file you are trying to upload is larger than " + std::to_string((double)thread_page->get_max_upload_file_size() * 1024.0 * 1024.0) + " mb", Urgency::CRITICAL); // navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::NO_SUCH_FILE) { show_notification("QuickMedia", "Failed to post comment because the file you are trying to upload no longer exists", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::FILE_TYPE_NOT_ALLOWED) { show_notification("QuickMedia", "Failed to post comment because you are trying to upload a file of a type that is not allowed", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::UPLOAD_FAILED) { show_notification("QuickMedia", "Failed to post comment because file upload failed", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::ERR) { show_notification("QuickMedia", "Failed to post comment", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else { assert(false && "Unhandled post result"); show_notification("QuickMedia", "Failed to post comment. Unknown error", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } }; bool frame_skip_text_entry = false; comment_input.on_submit_callback = [&frame_skip_text_entry, &comment_input, &navigation_stage, &comment_to_post, &selected_file_for_upload, &thread_page, &has_post_timeout, &seconds_until_post_again, &last_posted_time](std::string text) -> bool { if(text.empty() && selected_file_for_upload.empty()) return false; if(has_post_timeout && seconds_until_post_again - (time(nullptr) - last_posted_time) > 0) return false; comment_input.set_editable(false); frame_skip_text_entry = true; assert(navigation_stage == NavigationStage::REPLYING); comment_to_post = std::move(text); if(thread_page->get_pass_id().empty()) { navigation_stage = NavigationStage::REQUESTING_CAPTCHA; } else { navigation_stage = NavigationStage::POSTING_COMMENT; } return false; }; mgl::Rectangle comment_input_shade; comment_input_shade.set_color(get_theme().shade_color); mgl::Sprite logo_sprite(&plugin_logo); logo_sprite.set_scale(mgl::vec2f(0.8f * get_config().scale, 0.8f * get_config().scale)); mgl::vec2f logo_size(std::floor(plugin_logo.get_size().x * logo_sprite.get_scale().x), std::floor(plugin_logo.get_size().y * logo_sprite.get_scale().y)); mgl::Sprite file_to_upload_sprite; bool sprite_applied_texture = false; std::shared_ptr file_to_upload_thumbnail_data; const float logo_file_to_upload_spacing = std::floor(10.0f * get_config().scale * get_config().spacing_scale); float prev_chat_height = comment_input.get_height(); float chat_input_height_full = 0.0f; const float logo_padding_x = std::floor(15.0f * get_config().scale * get_config().spacing_scale); const float chat_input_padding_x = std::floor(10.0f * get_config().scale * get_config().spacing_scale); const float chat_input_padding_y = std::floor(10.0f * get_config().scale * get_config().spacing_scale); mgl::vec2f body_pos; mgl::vec2f body_size; mgl::Event event; std::deque comment_navigation_stack; std::deque comment_page_scroll_stack; mgl::Clock frame_timer; while (current_page == PageType::IMAGE_BOARD_THREAD && window.is_open()) { const float frame_elapsed_time_sec = frame_timer.restart(); while (window.poll_event(event)) { common_event_handler(event); if(navigation_stage == NavigationStage::REPLYING || navigation_stage == NavigationStage::VIEWING_COMMENTS) { if(thread_body->on_event(window, event, navigation_stage == NavigationStage::VIEWING_COMMENTS)) idle_active_handler(); } event_idle_handler(event); if(!frame_skip_text_entry) comment_input.process_event(window, event); if(navigation_stage == NavigationStage::REPLYING && !frame_skip_text_entry) { if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::Escape) { comment_input.set_editable(false); navigation_stage = NavigationStage::VIEWING_COMMENTS; break; } } if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; idle_active_handler(); } if(event.type == mgl::Event::Resized || event.type == mgl::Event::GainedFocus) redraw = true; else if(navigation_stage == NavigationStage::VIEWING_COMMENTS && event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::Escape) { current_page = pop_page_stack(); } else if(event.key.code == mgl::Keyboard::P) { BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->url.empty()) { if(is_url_video(selected_item->url)) { page_stack.push(PageType::IMAGE_BOARD_THREAD); current_page = PageType::VIDEO_CONTENT; watched_videos.clear(); thread_page->set_url(selected_item->url); // TODO: Use real title video_content_page(thread_page, thread_page, "", true, thread_body, thread_body->get_selected_item()); redraw = true; idle_active_handler(); } else { BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->url.empty()) { attached_image_url = selected_item->url; mgl::Image image; TaskResult task_result = run_task_with_loading_screen([&attached_image_url, &image]{ SHA256 sha256; sha256.add(attached_image_url.data(), attached_image_url.size()); Path media_file_path = get_cache_dir().join("media").join(sha256.getHash()); if(get_file_type(media_file_path) == FileType::FILE_NOT_FOUND && download_to_file(attached_image_url, media_file_path.data, {}, true) != DownloadResult::OK) { show_notification("QuickMedia", "Failed to download image: " + attached_image_url, Urgency::CRITICAL); return false; } if(!image.load_from_file(media_file_path.data.c_str())) { show_notification("QuickMedia", "Failed to load image: " + attached_image_url, Urgency::CRITICAL); return false; } return true; }); if(task_result == TaskResult::TRUE) { attached_image_texture = std::make_unique(); if(attached_image_texture->load_from_image(image)) { attached_image_sprite.set_texture(attached_image_texture.get()); attached_image_sprite.set_origin(mgl::vec2f(0.0f, 0.0f)); attached_image_sprite.set_scale(mgl::vec2f(1.0f, 1.0f)); navigation_stage = NavigationStage::VIEWING_ATTACHED_IMAGE; image_control.zoom = 1.0f; image_control.offset = attached_image_texture->get_size().to_vec2f() * 0.5f; image_control.pressed = window.is_mouse_button_pressed(mgl::Mouse::Left); image_control.pressed = false; image_control.moved = false; if(image_control.pressed) image_control.prev_mouse_pos = window.get_mouse_position(); } else { show_notification("QuickMedia", "Failed to load image: " + attached_image_url, Urgency::CRITICAL); } } } } } } else if(event.key.code == mgl::Keyboard::U) { std::filesystem::path &fm_dir = file_manager_start_dir; auto file_manager_page = std::make_unique(this, (FileManagerMimeType)(FILE_MANAGER_MIME_TYPE_IMAGE|FILE_MANAGER_MIME_TYPE_VIDEO)); file_manager_page->set_current_directory(fm_dir.string()); auto file_manager_body = create_body(); BodyItems body_items; file_manager_page->get_files_in_directory(body_items); file_manager_body->set_items(std::move(body_items)); std::vector file_manager_tabs; file_manager_tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); mgl::Event event; while(window.poll_event(event)) { common_event_handler(event); } selected_files.clear(); page_loop(file_manager_tabs); if(selected_files.empty()) { fprintf(stderr, "No files selected!\n"); } else { selected_file_for_upload = selected_files[0]; } redraw = true; frame_skip_text_entry = true; } else if(event.key.code == mgl::Keyboard::D && event.key.control) { selected_file_for_upload.clear(); } else if(event.key.code == mgl::Keyboard::C && event.key.control) { BodyItem *selected_item = thread_body->get_selected(); if(selected_item) thread_page->copy_to_clipboard(selected_item); } else if(event.key.code == mgl::Keyboard::I && event.key.control) { BodyItem *selected_item = thread_body->get_selected(); if(show_info_page(selected_item, true)) { redraw = true; frame_skip_text_entry = true; } } else if(event.key.code == mgl::Keyboard::S && event.key.control) { BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->url.empty()) download_async_gui(selected_item->url, file_manager_start_dir.string(), false); } BodyItem *selected_item = thread_body->get_selected(); if(event.key.code == mgl::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || thread_body->get_selected_item() != comment_navigation_stack.back()) && (!selected_item->replies_to.empty() || !selected_item->replies.empty())) { thread_body->for_each_item([](std::shared_ptr &body_item) { body_item->visible = false; }); selected_item->visible = true; for(size_t reply_to_index : selected_item->replies_to) { thread_body->get_item_by_index(reply_to_index)->visible = true; } for(size_t reply_index : selected_item->replies) { thread_body->get_item_by_index(reply_index)->visible = true; } comment_navigation_stack.push_back(thread_body->get_selected_item()); comment_page_scroll_stack.push_back(thread_body->get_page_scroll()); //thread_body->clamp_selection(); thread_body->set_page_scroll(0.0f); } else if(event.key.code == mgl::Keyboard::Backspace && !comment_navigation_stack.empty()) { size_t previous_selected = comment_navigation_stack.back(); float previous_page_scroll = comment_page_scroll_stack.back(); comment_navigation_stack.pop_back(); comment_page_scroll_stack.pop_back(); if(comment_navigation_stack.empty()) { thread_body->for_each_item([](std::shared_ptr &body_item) { body_item->visible = true; }); thread_body->set_selected_item(previous_selected); thread_body->clamp_selection(); } else { thread_body->for_each_item([](std::shared_ptr &body_item) { body_item->visible = false; }); thread_body->set_selected_item(previous_selected); selected_item = thread_body->get_item_by_index(comment_navigation_stack.back()).get(); selected_item->visible = true; for(size_t reply_to_index : selected_item->replies_to) { thread_body->get_item_by_index(reply_to_index)->visible = true; } for(size_t reply_index : selected_item->replies) { thread_body->get_item_by_index(reply_index)->visible = true; } thread_body->clamp_selection(); } thread_body->set_page_scroll(previous_page_scroll); } else if(event.key.code == mgl::Keyboard::R && selected_item) { std::string text_to_add = ">>" + selected_item->post_number + "\n"; comment_input.insert_text_at_caret_position(std::move(text_to_add)); comment_input.move_caret_to_end(); } if(event.key.code == mgl::Keyboard::I && !event.key.control) { frame_skip_text_entry = true; navigation_stage = NavigationStage::REPLYING; comment_input.set_editable(true); comment_input.move_caret_to_end(); } } if(event.type == mgl::Event::KeyPressed && navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA && !frame_skip_text_entry) { if(event.key.code == mgl::Keyboard::Escape) { navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(event.key.code == mgl::Keyboard::Enter) { navigation_stage = NavigationStage::POSTING_COMMENT; captcha_solution = captcha_solution_text.get_string(); } else if(event.key.code == mgl::Keyboard::Backspace) { std::string str = captcha_solution_text.get_string(); if(!str.empty()) { str.erase(str.size() - 1); captcha_solution_text.set_string(std::move(str)); } } else { const int alpha = (int)event.key.code - (int)mgl::Keyboard::A; const int num = (int)event.key.code - (int)mgl::Keyboard::Num0; const int numpad = (int)event.key.code - (int)mgl::Keyboard::Numpad0; if(alpha >= 0 && alpha <= mgl::Keyboard::Z - mgl::Keyboard::A) { captcha_solution_text.set_string(captcha_solution_text.get_string() + (char)to_upper(alpha + 'a')); } else if(num >= 0 && num <= mgl::Keyboard::Num9 - mgl::Keyboard::Num0) { captcha_solution_text.set_string(captcha_solution_text.get_string() + (char)(num + '0')); } else if(numpad >= 0 && numpad <= mgl::Keyboard::Numpad9 - mgl::Keyboard::Numpad0) { captcha_solution_text.set_string(captcha_solution_text.get_string() + (char)(numpad + '0')); } } } if(navigation_stage == NavigationStage::VIEWING_ATTACHED_IMAGE) { if(event.type == mgl::Event::MouseWheelScrolled) { image_control.zoom *= (1.0f + event.mouse_wheel_scroll.delta * 0.1f); if(image_control.zoom < 0.01f) image_control.zoom = 0.01f; image_control.moved = true; } else if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) { image_control.pressed = true; image_control.prev_mouse_pos.x = event.mouse_button.x; image_control.prev_mouse_pos.y = event.mouse_button.y; } else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) { image_control.pressed = false; } else if(event.type == mgl::Event::MouseMoved && image_control.pressed) { const mgl::vec2i mouse_diff = mgl::vec2i(event.mouse_move.x, event.mouse_move.y) - image_control.prev_mouse_pos; image_control.prev_mouse_pos.x = event.mouse_move.x; image_control.prev_mouse_pos.y = event.mouse_move.y; image_control.offset -= (mouse_diff.to_vec2f() / image_control.zoom); image_control.moved = true; idle_active_handler(); } else if(event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::W) { image_control.zoom = 1.0f; image_control.moved = false; image_control.offset = attached_image_texture->get_size().to_vec2f() * 0.5f; attached_image_sprite.set_origin(mgl::vec2f(0.0f, 0.0f)); attached_image_sprite.set_scale(mgl::vec2f(1.0f, 1.0f)); } } } if(event.type == mgl::Event::KeyPressed && navigation_stage == NavigationStage::VIEWING_ATTACHED_IMAGE) { if(event.key.code == mgl::Keyboard::Escape || event.key.code == mgl::Keyboard::Backspace) { navigation_stage = NavigationStage::VIEWING_COMMENTS; attached_image_texture.reset(new mgl::Texture()); redraw = true; } else if(event.key.code == mgl::Keyboard::I && event.key.control && !attached_image_url.empty()) { std::vector saucenao_tabs; saucenao_tabs.push_back(Tab{create_body(), std::make_unique(this, attached_image_url, false), nullptr}); page_loop(saucenao_tabs); redraw = true; frame_skip_text_entry = true; } else if(event.key.code == mgl::Keyboard::S && event.key.control) { download_async_gui(attached_image_url, file_manager_start_dir.string(), false); } } } frame_skip_text_entry = false; update_idle_state(); handle_x11_events(); if(navigation_stage == NavigationStage::REQUESTING_CAPTCHA) { captcha_challenge = ImageBoardCaptchaChallenge(); TaskResult task_result = run_task_with_loading_screen([thread_page, &captcha_challenge]{ return thread_page->request_captcha_challenge(captcha_challenge) == PluginResult::OK; }); if(task_result == TaskResult::TRUE) { comment_input.set_editable(false); captcha_post_id = std::move(captcha_challenge.challenge_id); solved_captcha_ttl = 0;// captcha_challenge.ttl; // TODO: Support ttl to not have to solve captcha again captcha_solution_text.set_string(""); captcha_slide = 0.0f; if(captcha_post_id == "noop") { // TODO: Fix for other imageboard than 4chan in the future captcha_solution.clear(); navigation_stage = NavigationStage::POSTING_COMMENT; } else { captcha_texture.clear(); captcha_bg_texture.clear(); bool failed = false; mgl::Image image; if(!image.load_from_memory((const unsigned char*)captcha_challenge.img_data.data(), captcha_challenge.img_data.size()) || !captcha_texture.load_from_image(image)) { show_notification("QuickMedia", "Failed to load captcha image", Urgency::CRITICAL); failed = true; } captcha_challenge.img_data = std::string(); has_captcha_bg = !failed && !captcha_challenge.bg_data.empty(); mgl::Image bg_image; if(has_captcha_bg && (!bg_image.load_from_memory((const unsigned char*)captcha_challenge.bg_data.data(), captcha_challenge.bg_data.size()) || !captcha_bg_texture.load_from_image(bg_image))) { show_notification("QuickMedia", "Failed to load captcha image", Urgency::CRITICAL); failed = true; } captcha_challenge.bg_data = std::string(); if(failed) { navigation_stage = NavigationStage::VIEWING_COMMENTS; } else { navigation_stage = NavigationStage::SOLVING_POST_CAPTCHA; captcha_sprite.set_texture(&captcha_texture); if(has_captcha_bg) captcha_bg_sprite.set_texture(&captcha_bg_texture); } } } else if(task_result == TaskResult::CANCEL) { comment_input.set_editable(false); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(task_result == TaskResult::FALSE) { show_notification("QuickMedia", "Failed to get captcha", Urgency::CRITICAL); comment_input.set_editable(false); navigation_stage = NavigationStage::VIEWING_COMMENTS; } } else if(navigation_stage == NavigationStage::POSTING_COMMENT) { TaskResult task_result = run_task_with_loading_screen([&post_comment, &comment_to_post, &selected_file_for_upload]{ post_comment(comment_to_post, selected_file_for_upload); return true; }); if(task_result == TaskResult::CANCEL) { comment_input.set_editable(false); navigation_stage = NavigationStage::VIEWING_COMMENTS; } } if(selected_file_for_upload.empty()) { if(file_to_upload_thumbnail_data) { file_to_upload_thumbnail_data.reset(); redraw = true; } } else { file_to_upload_thumbnail_data = AsyncImageLoader::get_instance().get_thumbnail(selected_file_for_upload, true, mgl::vec2i(logo_size.x, logo_size.y * 4)); } if(file_to_upload_thumbnail_data && file_to_upload_thumbnail_data->loading_state == LoadingState::FINISHED_LOADING && file_to_upload_thumbnail_data->image->get_size().x > 0 && file_to_upload_thumbnail_data->image->get_size().y > 0) { if(!file_to_upload_thumbnail_data->texture.load_from_image(*file_to_upload_thumbnail_data->image)) fprintf(stderr, "Warning: failed to load texture for attached file\n"); //room_avatar_thumbnail_data->texture.generateMipmap(); file_to_upload_thumbnail_data->image.reset(); file_to_upload_thumbnail_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; sprite_applied_texture = false; } if(file_to_upload_thumbnail_data && file_to_upload_thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && !sprite_applied_texture) { file_to_upload_sprite.set_texture(&file_to_upload_thumbnail_data->texture); sprite_applied_texture = true; mgl::vec2f texture_size_f(file_to_upload_thumbnail_data->texture.get_size().x, file_to_upload_thumbnail_data->texture.get_size().y); mgl::vec2f image_scale = get_ratio(texture_size_f, clamp_to_size_x(texture_size_f, logo_size.x)); file_to_upload_sprite.set_scale(image_scale); redraw = true; } float chat_input_height_full_images = logo_size.y; if(file_to_upload_thumbnail_data && file_to_upload_thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE) { const float file_to_upload_height = std::floor(logo_file_to_upload_spacing + file_to_upload_sprite.get_texture()->get_size().y * file_to_upload_sprite.get_scale().y); chat_input_height_full_images += file_to_upload_height; } chat_input_height_full = chat_input_padding_y + std::max(comment_input.get_height(), chat_input_height_full_images) + chat_input_padding_y; const float chat_height = comment_input.get_height(); if(std::abs(chat_height - prev_chat_height) > 1.0f) { prev_chat_height = chat_height; redraw = true; } if(redraw) { redraw = false; comment_input.set_max_width(window_size.x - (logo_padding_x + logo_size.x + chat_input_padding_x + logo_padding_x)); comment_input.set_position(vec2f_floor(logo_padding_x + logo_size.x + chat_input_padding_x, chat_input_padding_y)); const float body_width = window_size.x; comment_input_shade.set_size(mgl::vec2f(window_size.x, chat_input_height_full)); comment_input_shade.set_position(mgl::vec2f(0.0f, 0.0f)); body_pos = mgl::vec2f(0.0f, comment_input_shade.get_size().y); body_size = mgl::vec2f(body_width, window_size.y - comment_input_shade.get_size().y); logo_sprite.set_position(mgl::vec2f(logo_padding_x, chat_input_padding_y)); file_to_upload_sprite.set_position(logo_sprite.get_position() + mgl::vec2f(0.0f, logo_size.y + logo_file_to_upload_spacing)); } //comment_input.update(); window.clear(get_theme().background_color); if(navigation_stage == NavigationStage::SOLVING_POST_CAPTCHA && captcha_texture.is_valid()) { const float slide_speed = 0.5f; const bool window_has_focus = window.has_focus(); if(window_has_focus && window.is_key_pressed(mgl::Keyboard::Left)) { captcha_slide -= (slide_speed * frame_elapsed_time_sec); if(captcha_slide < 0.0f) captcha_slide = 0.0f; } else if(window_has_focus && window.is_key_pressed(mgl::Keyboard::Right)) { captcha_slide += (slide_speed * frame_elapsed_time_sec); if(captcha_slide > 1.0f) captcha_slide = 1.0f; } mgl::vec2f content_size = window_size.to_vec2f(); int image_height = 0; mgl::vec2i captcha_texture_size = captcha_texture.get_size(); mgl::vec2f captcha_texture_size_f(captcha_texture_size.x, captcha_texture_size.y); auto image_scale = get_ratio(captcha_texture_size_f, clamp_to_size(captcha_texture_size_f, content_size)); captcha_sprite.set_scale(image_scale); auto captcha_image_size = captcha_texture_size_f; captcha_image_size.x *= image_scale.x; captcha_image_size.y *= image_scale.y; captcha_sprite.set_position(vec2f_floor(content_size.x * 0.5f - captcha_image_size.x * 0.5f, content_size.y * 0.5f - captcha_image_size.y * 0.5f)); image_height = (int)captcha_image_size.y; if(has_captcha_bg && captcha_bg_texture.is_valid()) { mgl::vec2f content_size = window_size.to_vec2f(); mgl::vec2i captcha_bg_texture_size = captcha_bg_texture.get_size(); mgl::vec2f captcha_bg_texture_size_f(captcha_bg_texture_size.x, captcha_bg_texture_size.y); auto image_scale = get_ratio(captcha_bg_texture_size_f, clamp_to_size(captcha_bg_texture_size_f, content_size)); captcha_bg_sprite.set_scale(image_scale); auto image_size = captcha_bg_texture_size_f; image_size.x *= image_scale.x; image_size.y *= image_scale.y; const float width_diff = image_size.x - captcha_image_size.x; captcha_bg_sprite.set_position(vec2f_floor(captcha_sprite.get_position().x + width_diff*1.0f - captcha_slide*(width_diff + width_diff*2.0f), captcha_sprite.get_position().y)); window.draw(captcha_bg_sprite); image_height = std::max(image_height, (int)image_size.y); } window.draw(captcha_sprite); // TODO: Cut off ends with sf::View instead mgl::Rectangle cut_off_rectangle(captcha_image_size); cut_off_rectangle.set_color(get_theme().background_color); cut_off_rectangle.set_position(captcha_sprite.get_position() - mgl::vec2f(cut_off_rectangle.get_size().x, 0.0f)); window.draw(cut_off_rectangle); cut_off_rectangle.set_position(captcha_sprite.get_position() + mgl::vec2f(captcha_image_size.x, 0.0f)); window.draw(cut_off_rectangle); const float captcha_slide_bg_height = std::floor(20.0f * get_config().scale); captcha_slide_bg.set_size(mgl::vec2f(captcha_image_size.x, captcha_slide_bg_height)); captcha_slide_bg.set_position(mgl::vec2f(captcha_sprite.get_position().x, captcha_sprite.get_position().y + image_height + 10.0f)); const mgl::vec2f captcha_slide_fg_size = captcha_slide_bg.get_size() - mgl::vec2f(captcha_slide_padding_x * 2.0f, captcha_slide_padding_y * 2.0f); captcha_slide_fg.set_size(vec2f_floor(captcha_slide_fg_size.x * captcha_slide, captcha_slide_fg_size.y)); captcha_slide_fg.set_position(captcha_slide_bg.get_position() + mgl::vec2f(captcha_slide_padding_x, captcha_slide_padding_y)); if(has_captcha_bg) { captcha_slide_bg.draw(window); captcha_slide_fg.draw(window); } captcha_solution_text.set_position( mgl::vec2f( std::floor(window_size.x * 0.5f - captcha_solution_text.get_bounds().size.x * 0.5f), std::floor(captcha_slide_bg.get_position().y + captcha_slide_bg.get_size().y + 10.0f))); window.draw(captcha_solution_text); } else if(navigation_stage == NavigationStage::VIEWING_ATTACHED_IMAGE) { if(attached_image_texture->is_valid()) { if(image_control.moved) { attached_image_sprite.set_origin(floor(image_control.offset)); attached_image_sprite.set_scale(mgl::vec2f(image_control.zoom, image_control.zoom)); attached_image_sprite.set_position(floor(window_size.to_vec2f() * 0.5f)); window.draw(attached_image_sprite); } else { auto content_size = window_size.to_vec2f(); mgl::vec2i texture_size = attached_image_texture->get_size(); mgl::vec2f texture_size_f(texture_size.x, texture_size.y); auto image_scale = get_ratio(texture_size_f, clamp_to_size(texture_size_f, content_size)); image_control.zoom = std::min(image_scale.x, image_scale.y); attached_image_sprite.set_scale(image_scale); auto image_size = texture_size_f; image_size.x *= image_scale.x; image_size.y *= image_scale.y; attached_image_sprite.set_position(vec2f_floor(content_size.x * 0.5f - image_size.x * 0.5f, content_size.y * 0.5f - image_size.y * 0.5f)); window.draw(attached_image_sprite); } } else { mgl::Rectangle rect(mgl::vec2f(640.0f, 480.0f)); rect.set_color(get_theme().image_loading_background_color); auto content_size = window_size.to_vec2f(); auto rect_size = clamp_to_size(rect.get_size(), content_size); rect.set_size(rect_size); rect.set_position(vec2f_floor(content_size.x * 0.5f - rect_size.x * 0.5f, content_size.y * 0.5f - rect_size.y * 0.5f)); window.draw(rect); load_sprite.set_position(mgl::vec2f(window_size.x * 0.5f, window_size.y * 0.5f)); load_sprite.set_rotation(load_sprite_timer.get_elapsed_time_seconds() * 400.0); window.draw(load_sprite); } } else if(navigation_stage == NavigationStage::REPLYING) { window.draw(comment_input_shade); window.draw(logo_sprite); if(file_to_upload_thumbnail_data && file_to_upload_thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE) window.draw(file_to_upload_sprite); comment_input.draw(window); thread_body->draw(window, body_pos, body_size); } else if(navigation_stage == NavigationStage::VIEWING_COMMENTS) { window.draw(comment_input_shade); window.draw(logo_sprite); if(file_to_upload_thumbnail_data && file_to_upload_thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE) window.draw(file_to_upload_sprite); comment_input.draw(window); thread_body->draw(window, body_pos, body_size); } if((navigation_stage == NavigationStage::REPLYING || navigation_stage == NavigationStage::VIEWING_COMMENTS) && has_post_timeout) { int64_t time_left_until_post_again = seconds_until_post_again - (time(nullptr) - last_posted_time); if(time_left_until_post_again > 0) { mgl::Rectangle time_left_bg(comment_input_shade.get_size()); time_left_bg.set_position(comment_input_shade.get_position()); time_left_bg.set_color(mgl::Color(0, 0, 0, 100)); window.draw(time_left_bg); mgl::Text time_left_text("Wait " + std::to_string(time_left_until_post_again) + " second(s) before posting again", *FontLoader::get_font(FontLoader::FontType::LATIN, 18 * get_config().scale * get_config().font_scale)); time_left_text.set_position(time_left_bg.get_position() + mgl::vec2f( std::floor(time_left_bg.get_size().x * 0.5f - time_left_text.get_bounds().size.x * 0.5f), std::floor(time_left_bg.get_size().y * 0.5f - time_left_text.get_bounds().size.y * 0.5f))); window.draw(time_left_text); } } AsyncImageLoader::get_instance().update(); window.display(); } } class MatrixLoginPage : public LoginPage { public: MatrixLoginPage(Program *program, std::string title, Matrix *matrix) : LoginPage(program), title(std::move(title)), matrix(matrix) {} const char* get_title() const override { return title.c_str(); } PluginResult submit(const SubmitArgs&, std::vector&) override { for(const auto &login_input : login_inputs->inputs) { if(login_input->get_text().empty()) { show_notification("QuickMedia", "All fields need to be filled in", Urgency::CRITICAL); return PluginResult::OK; } } std::string homeserver = login_inputs->inputs[2]->get_text(); if(!string_starts_with(homeserver, "http://") && !string_starts_with(homeserver, "https://")) homeserver = "https://" + homeserver; std::string err_msg; if(matrix->login(login_inputs->inputs[0]->get_text(), login_inputs->inputs[1]->get_text(), homeserver, err_msg) == PluginResult::OK) { login_finish(); return PluginResult::OK; } else { // TODO: Do a proper check for this if(err_msg.find("Failed to parse") != std::string::npos) show_notification("QuickMedia", "Failed to login, error: " + err_msg + ". Did you perhaps specify an invalid homeserver?", Urgency::CRITICAL); else show_notification("QuickMedia", "Failed to login, error: " + err_msg, Urgency::CRITICAL); return PluginResult::OK; } } const LoginInputs *login_inputs; private: std::string title; Matrix *matrix; }; void Program::chat_login_page() { assert(strcmp(plugin_name, "matrix") == 0); LoginInputs login_inputs; login_inputs.inputs.push_back(std::make_unique(nullptr, &rounded_rectangle_shader, "Username", SearchBarType::Text)); login_inputs.inputs.push_back(std::make_unique(nullptr, &rounded_rectangle_shader, "Password", SearchBarType::Password)); login_inputs.inputs.push_back(std::make_unique(nullptr, &rounded_rectangle_shader, "Homeserver", SearchBarType::Text)); auto login_page = std::make_unique(this, "Matrix login", matrix); MatrixLoginPage *login_page_ptr = login_page.get(); std::vector tabs; tabs.push_back(Tab{ create_body(), std::move(login_page), nullptr, std::move(login_inputs) }); login_page_ptr->login_inputs = &tabs.back().login_inputs; page_loop(tabs); if(login_page_ptr->logged_in()) current_page = PageType::CHAT; } struct ChatTab { std::unique_ptr body; AsyncTask future; }; static const mgl::vec2i CHAT_MESSAGE_THUMBNAIL_MAX_SIZE(600, 337); // TODO: Optimize static std::shared_ptr find_body_item_by_event_id(const std::shared_ptr *body_items, size_t num_body_items, const std::string &event_id, size_t *index_result = nullptr) { if(event_id.empty()) return nullptr; for(size_t i = 0; i < num_body_items; ++i) { auto &body_item = body_items[i]; if(body_item->userdata && static_cast(body_item->userdata)->event_id == event_id) { if(index_result) *index_result = i; return body_item; } } return nullptr; } // TODO: Optimize static std::shared_ptr find_body_item_by_event_id(BodyItemList body_items, const std::string &event_id, int *index_result) { if(event_id.empty()) return nullptr; for(int i = 0; i < (int)body_items.size(); ++i) { auto &body_item = body_items[i]; if(body_item->userdata && static_cast(body_item->userdata)->event_id == event_id) { if(index_result) *index_result = i; return body_item; } } return nullptr; } // Returns true if cached and loaded static bool load_cached_related_embedded_item(BodyItem *body_item, Message *message, UserInfo *me, const std::string &my_display_name, const std::string &my_user_id, const BodyItemList &message_body_items) { // Check if we already have the referenced message as a body item, so we dont create a new one. // TODO: Optimize from linear search to hash map auto related_body_item = find_body_item_by_event_id(message_body_items.data(), message_body_items.size(), message->related_event_id); if(!related_body_item) return false; body_item->embedded_item = BodyItem::create(""); *body_item->embedded_item = *related_body_item; body_item->embedded_item->embedded_item = nullptr; body_item->embedded_item->reactions.clear(); if(message->user->user_id != my_user_id && ((related_body_item->userdata && static_cast(related_body_item->userdata)->user.get() == me) || message_contains_user_mention(body_item->get_description(), my_display_name) || message_contains_user_mention(body_item->get_description(), my_user_id))) body_item->set_description_color(get_theme().attention_alert_text_color); else body_item->set_description_color(get_theme().text_color); body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; return true; } static bool load_cached_related_embedded_item(BodyItem *body_item, Message *message, const std::shared_ptr &me, RoomData *current_room, const BodyItemList &message_body_items) { return load_cached_related_embedded_item(body_item, message, me.get(), current_room->get_user_display_name(me), me->user_id, message_body_items); } static std::shared_ptr message_to_body_item(RoomData *room, Message *message, const std::string &my_display_name, const std::string &my_user_id) { auto body_item = BodyItem::create(""); body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH)); body_item->set_description(strip(message_get_body_remove_formatting(message))); body_item->set_timestamp(message->timestamp); if(!message->thumbnail_url.empty()) { body_item->thumbnail_url = message->thumbnail_url; body_item->thumbnail_size = message->thumbnail_size; } else if(!message->url.empty() && message->type == MessageType::IMAGE) { body_item->thumbnail_url = message->url; body_item->thumbnail_size = message->thumbnail_size; } else { body_item->thumbnail_url = room->get_user_avatar_url(message->user); if(body_item->thumbnail_url.empty()) { body_item->thumbnail_url = get_resource_loader_root_path() + std::string("images/no_avatar.png"); body_item->thumbnail_is_local = true; } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; // if construct is not configured to use ImageMagic then it wont give thumbnails of size 32x32 even when requested and the spec says that the server SHOULD do that body_item->thumbnail_size = AVATAR_THUMBNAIL_SIZE; } // TODO: Show image thumbnail inline instead of url to image and showing it as the thumbnail of the body item body_item->url = message->url; body_item->set_author_color(message->user->display_name_color); body_item->userdata = (void*)message; // Note: message has to be valid as long as body_item is used! if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT || message->related_event_type == RelatedEventType::REACTION) body_item->visible = false; if(is_system_message_type(message->type)) { body_item->set_author("System"); body_item->set_author_color(get_theme().text_color); body_item->set_description_color(get_theme().faded_text_color); body_item->thumbnail_url.clear(); } if(message->user->user_id != my_user_id && (message_contains_user_mention(body_item->get_description(), my_display_name) || message_contains_user_mention(body_item->get_description(), my_user_id))) body_item->set_description_color(get_theme().attention_alert_text_color); return body_item; } static BodyItems messages_to_body_items(RoomData *room, const Messages &messages, const std::string &my_display_name, const std::string &my_user_id) { BodyItems result_items(messages.size()); for(size_t i = 0; i < messages.size(); ++i) { result_items[i] = message_to_body_item(room, messages[i].get(), my_display_name, my_user_id); } return result_items; } static void messages_load_cached_related_embedded_item(BodyItems &new_body_items, const BodyItemList &all_body_items, const std::shared_ptr &me, RoomData *current_room) { std::string my_display_name = current_room->get_user_display_name(me); for(auto &body_item : new_body_items) { Message *message = static_cast(body_item->userdata); if(message) load_cached_related_embedded_item(body_item.get(), message, me.get(), my_display_name, me->user_id, all_body_items); } } static bool is_state_message_type(const Message *message) { if(!message) return true; switch(message->type) { case MessageType::TEXT: return false; case MessageType::IMAGE: return false; case MessageType::VIDEO: return false; case MessageType::AUDIO: return false; case MessageType::FILE: return false; default: return true; } return true; } struct PinnedEventData { std::string event_id; FetchStatus status = FetchStatus::NONE; Message *message = nullptr; }; static void user_update_display_info(BodyItem *body_item, RoomData *room, Message *message) { if(is_system_message_type(message->type)) return; body_item->set_author(extract_first_line_remove_newline_elipses(room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH)); if(!is_visual_media_message_type(message->type)) { body_item->thumbnail_url = room->get_user_avatar_url(message->user); if(body_item->thumbnail_url.empty()) { body_item->thumbnail_url = get_resource_loader_root_path() + std::string("images/no_avatar.png"); body_item->thumbnail_is_local = true; } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; // if construct is not configured to use ImageMagic then it wont give thumbnails of size 32x32 even when requested and the spec says that the server SHOULD do that body_item->thumbnail_size = AVATAR_THUMBNAIL_SIZE; } if(body_item->embedded_item_status == FetchStatus::FINISHED_LOADING && body_item->embedded_item && body_item->userdata) user_update_display_info(body_item->embedded_item.get(), room, (Message*)body_item->embedded_item->userdata); } static Message* get_original_message(Message *message) { if(!message) return nullptr; Message *replaces = message->replaces; while(replaces) { if(!replaces->replaces) return replaces; replaces = replaces->replaces; } return message; } static std::string pantalaimon_image_proxy_url_to_remote_image_url(Matrix *matrix, const std::string &image_url) { std::string remote_homeserver_url = matrix->get_remote_homeserver_url(); if(!remote_homeserver_url.empty() && remote_homeserver_url.back() == '/') remote_homeserver_url.pop_back(); std::string result_url = image_url; if(string_starts_with(result_url, "http://")) result_url.erase(result_url.begin(), result_url.begin() + 7); else if(string_starts_with(result_url, "https://")) result_url.erase(result_url.begin(), result_url.begin() + 8); size_t path_index = result_url.find('/'); if(path_index == std::string::npos) return remote_homeserver_url; result_url.replace(0, path_index, remote_homeserver_url); return result_url; } bool Program::chat_page(MatrixChatPage *matrix_chat_page, RoomData *current_room) { assert(current_room); assert(strcmp(plugin_name, "matrix") == 0); if(!current_room) { show_notification("QuickMedia", "Bug: current room empty", Urgency::CRITICAL); abort(); } window.set_title(("QuickMedia - matrix - " + current_room->get_name()).c_str()); auto video_page = std::make_unique(this); bool move_room = false; std::vector tabs; ChatTab pinned_tab; pinned_tab.body = create_body(true); pinned_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; pinned_tab.body->attach_side = AttachSide::BOTTOM; tabs.push_back(std::move(pinned_tab)); ChatTab messages_tab; messages_tab.body = create_body(true); messages_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; messages_tab.body->attach_side = AttachSide::BOTTOM; tabs.push_back(std::move(messages_tab)); ChatTab users_tab; users_tab.body = create_body(true); users_tab.body->thumbnail_max_size = CHAT_MESSAGE_THUMBNAIL_MAX_SIZE; users_tab.body->attach_side = AttachSide::TOP; tabs.push_back(std::move(users_tab)); for(ChatTab &tab : tabs) { tab.body->show_drop_shadow = false; } Tabs ui_tabs(&rounded_rectangle_shader, is_touch_enabled() ? mgl::Color(0, 0, 0, 0) : get_theme().background_color); const int PINNED_TAB_INDEX = ui_tabs.add_tab("Pinned messages (0)", tabs[0].body.get()); const int MESSAGES_TAB_INDEX = ui_tabs.add_tab("Messages", tabs[1].body.get()); const int USERS_TAB_INDEX = ui_tabs.add_tab("Users (0)", tabs[2].body.get()); ui_tabs.set_selected(MESSAGES_TAB_INDEX); matrix_chat_page->chat_body = tabs[MESSAGES_TAB_INDEX].body.get(); matrix_chat_page->messages_tab_visible = true; matrix_chat_page->set_current_room(current_room, tabs[USERS_TAB_INDEX].body.get()); size_t prev_num_users_in_room = 0; bool redraw = true; mgl::Clock read_marker_timer; double read_marker_timeout_sec = 0; AsyncTask set_read_marker_future; bool setting_read_marker = false; mgl::Clock start_typing_timer; const double typing_timeout_seconds = 5.0; bool typing = false; MessageQueue typing_state_queue; auto typing_state_handler = [this, ¤t_room, &typing_state_queue]() { while(true) { std::optional state_opt = typing_state_queue.pop_wait(); if(!state_opt) break; bool state = state_opt.value(); if(state) matrix->on_start_typing(current_room); else matrix->on_stop_typing(current_room); } }; std::thread typing_state_thread(typing_state_handler); ui_tabs.on_change_tab = [matrix_chat_page, &redraw, &typing, &typing_state_queue, &read_marker_timer, &tabs, MESSAGES_TAB_INDEX](int prev_tab, int new_tab) { tabs[prev_tab].body->clear_cache(); if(new_tab == MESSAGES_TAB_INDEX) matrix_chat_page->messages_tab_visible = true; else matrix_chat_page->messages_tab_visible = false; read_marker_timer.restart(); redraw = true; if(typing) { fprintf(stderr, "Stopped typing\n"); typing = false; typing_state_queue.push(false); } }; bool is_window_focused = window.has_focus(); enum class ChatState { NAVIGATING, TYPING_MESSAGE, REPLYING, EDITING, URL_SELECTION }; PageType new_page = PageType::CHAT; ChatState chat_state = ChatState::NAVIGATING; std::shared_ptr currently_operating_on_item; mgl::Text replying_to_text("Replying to:", *FontLoader::get_font(FontLoader::FontType::LATIN, 18 * get_config().scale * get_config().font_scale)); mgl::Sprite logo_sprite(&plugin_logo); logo_sprite.set_scale(mgl::vec2f(0.8f * get_config().scale, 0.8f * get_config().scale)); mgl::vec2f logo_size(plugin_logo.get_size().x * logo_sprite.get_scale().x, plugin_logo.get_size().y * logo_sprite.get_scale().y); const float room_name_text_height = std::floor(18.0f * get_config().scale * get_config().font_scale); mgl::Text room_name_text("", *FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, room_name_text_height)); const float room_name_text_padding_y = std::floor(10.0f * get_config().scale); const float room_name_total_height = room_name_text_height + room_name_text_padding_y * 2.0f; const float room_avatar_height = 32.0f; const float room_topic_text_height = std::floor(12.0f * get_config().scale * get_config().font_scale); mgl::Text room_topic_text("", *FontLoader::get_font(FontLoader::FontType::LATIN, room_topic_text_height)); room_topic_text.set_color(get_theme().faded_text_color); mgl::Text room_label(matrix_chat_page->rooms_page->get_title(), *FontLoader::get_font(FontLoader::FontType::LATIN_BOLD, 18 * get_config().scale * get_config().font_scale)); room_label.set_position(mgl::vec2f(15.0f, room_name_text_padding_y + 4.0f)); mgl::Sprite room_avatar_sprite; auto room_avatar_thumbnail_data = std::make_shared(); bool draw_room_list = show_room_side_panel; // TODO: What if these never end up referencing events? clean up automatically after a while? Messages unreferenced_events; auto set_body_as_deleted = [¤t_room](Message *message, BodyItem *body_item) { //body_item->embedded_item = nullptr; //body_item->embedded_item_status = FetchStatus::NONE; message->type = MessageType::REDACTION; //message->related_event_id.clear(); //message->related_event_type = RelatedEventType::NONE; Message *original_message = static_cast(body_item->userdata); if(original_message && !is_system_message_type(original_message->type)) { body_item->thumbnail_url = current_room->get_user_avatar_url(original_message->user); if(body_item->thumbnail_url.empty()) { body_item->thumbnail_url = get_resource_loader_root_path() + std::string("images/no_avatar.png"); body_item->thumbnail_is_local = true; } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; } body_item->set_description("Message deleted"); if(original_message && is_system_message_type(original_message->type)) body_item->set_description_color(get_theme().faded_text_color); else body_item->set_description_color(get_theme().text_color); body_item->thumbnail_size = AVATAR_THUMBNAIL_SIZE; body_item->url.clear(); }; // TODO: Optimize with hash map? auto resolve_unreferenced_events_with_body_items = [this, ¤t_room, &set_body_as_deleted, &unreferenced_events](std::shared_ptr *body_items, size_t num_body_items) { if(num_body_items == 0) return; auto me = matrix->get_me(current_room); auto my_display_name = current_room->get_user_display_name(me); for(auto it = unreferenced_events.begin(); it != unreferenced_events.end(); ) { auto &message = *it; // TODO: Make redacted/edited events as (redacted)/(edited) in the body if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) { auto body_item = find_body_item_by_event_id(body_items, num_body_items, message->related_event_id); if(body_item) { // TODO: Append the new message to the body item so the body item should have a list of edit events //body_item->userdata = message.get(); if(message->related_event_type == RelatedEventType::REDACTION) { set_body_as_deleted(message.get(), body_item.get()); } else { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *reply_to_message = static_cast(body_item->userdata); body_item->set_description(strip(message_get_body_remove_formatting(message.get()))); if(message->user != me && (message_contains_user_mention(reply_to_message->body, my_display_name) || message_contains_user_mention(reply_to_message->body, me->user_id))) body_item->set_description_color(get_theme().attention_alert_text_color); else body_item->set_description_color(get_theme().text_color); message->replaces = reply_to_message; reply_to_message->replaced_by = message; } it = unreferenced_events.erase(it); } else { ++it; } } else { ++it; } } }; // TODO: Optimize find_body_item_by_event_id hash map? auto modify_related_messages_in_current_room = [this, ¤t_room, &set_body_as_deleted, &unreferenced_events, &tabs, MESSAGES_TAB_INDEX](Messages &messages) { if(messages.empty()) return; auto me = matrix->get_me(current_room); auto my_display_name = current_room->get_user_display_name(me); auto body_items = tabs[MESSAGES_TAB_INDEX].body->get_items(); for(auto &message : messages) { // TODO: Make redacted/edited events as (redacted)/(edited) in the body if(message->related_event_type == RelatedEventType::REDACTION || message->related_event_type == RelatedEventType::EDIT) { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), message->related_event_id); if(body_item) { // TODO: Append the new message to the body item so the body item should have a list of edit events //body_item->userdata = message.get(); if(message->related_event_type == RelatedEventType::REDACTION) { set_body_as_deleted(message.get(), body_item.get()); } else { // TODO: Properly check reply message objects for mention of user instead of message data, but only when synapse fixes that notifications // are not triggered by reply to a message with our display name/user id. Message *reply_to_message = static_cast(body_item->userdata); body_item->set_description(strip(message_get_body_remove_formatting(message.get()))); if(message->user != me && (message_contains_user_mention(reply_to_message->body, my_display_name) || message_contains_user_mention(reply_to_message->body, me->user_id))) body_item->set_description_color(get_theme().attention_alert_text_color); else body_item->set_description_color(get_theme().text_color); message->replaces = reply_to_message; reply_to_message->replaced_by = message; } } else { unreferenced_events.push_back(message); } } } }; std::vector> unresolved_reactions; // TODO: Optimize find_body_item_by_event_id hash map? auto process_reactions = [&tabs, &unresolved_reactions, ¤t_room, MESSAGES_TAB_INDEX](Messages &messages) { if(messages.empty()) return; auto body_items = tabs[MESSAGES_TAB_INDEX].body->get_items(); // TODO: Check in |messages| instead for(auto it = unresolved_reactions.begin(); it != unresolved_reactions.end();) { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), (*it)->related_event_id); if(body_item) { body_item->add_reaction(extract_first_line_remove_newline_elipses(current_room->get_user_display_name((*it)->user), AUTHOR_MAX_LENGTH) + ": " + (*it)->body, (*it).get()); it = unresolved_reactions.erase(it); } else { ++it; } } for(auto &message : messages) { if(message->type == MessageType::REACTION) { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), message->related_event_id); if(body_item) body_item->add_reaction(extract_first_line_remove_newline_elipses(current_room->get_user_display_name(message->user), AUTHOR_MAX_LENGTH) + ": " + message->body, message.get()); else unresolved_reactions.push_back(message); } else if(message->type == MessageType::REDACTION) { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), message->related_event_id); if(body_item && static_cast(body_item->userdata)) { Message *reaction_message = static_cast(body_item->userdata); if(reaction_message->type == MessageType::REACTION) { auto body_item = find_body_item_by_event_id(body_items.data(), body_items.size(), reaction_message->related_event_id); if(body_item) body_item->remove_reaction_by_userdata(reaction_message); } } else { for(auto it = unresolved_reactions.begin(); it != unresolved_reactions.end(); ++it) { if(message->related_event_id == (*it)->event_id) { unresolved_reactions.erase(it); break; } } } } } }; auto pinned_body_items_contains_event = [&tabs, PINNED_TAB_INDEX](const std::string &event_id) { const int found_item_index = tabs[PINNED_TAB_INDEX].body->find_item_index([&event_id](std::shared_ptr &body_item) { return static_cast(body_item->userdata)->event_id == event_id; }); return found_item_index != -1; }; auto process_pinned_events = [&tabs, &ui_tabs, &pinned_body_items_contains_event, PINNED_TAB_INDEX](const std::optional> &pinned_events) { if(!pinned_events) return; bool empty_before = tabs[PINNED_TAB_INDEX].body->get_num_items() == 0; int selected_before = tabs[PINNED_TAB_INDEX].body->get_selected_item(); auto prev_pinned_body_items = tabs[PINNED_TAB_INDEX].body->get_items_copy(); tabs[PINNED_TAB_INDEX].body->clear_items(); // TODO: Add message to rooms messages when there are new pinned events for(const std::string &event : pinned_events.value()) { if(pinned_body_items_contains_event(event)) continue; auto body = BodyItem::create(""); body->set_description("Loading message..."); PinnedEventData *event_data = new PinnedEventData(); event_data->event_id = event; event_data->status = FetchStatus::NONE; event_data->message = nullptr; body->userdata = event_data; tabs[PINNED_TAB_INDEX].body->append_item(std::move(body)); } for(auto &prev_body_item : prev_pinned_body_items) { if(!pinned_body_items_contains_event(static_cast(prev_body_item->userdata)->event_id)) delete (PinnedEventData*)prev_body_item->userdata; } if(empty_before || ui_tabs.get_selected() != PINNED_TAB_INDEX) tabs[PINNED_TAB_INDEX].body->select_last_item(); else tabs[PINNED_TAB_INDEX].body->set_selected_item(selected_before); ui_tabs.set_text(PINNED_TAB_INDEX, "Pinned messages (" + std::to_string(tabs[PINNED_TAB_INDEX].body->get_num_items()) + ")"); }; Body url_selection_body(BODY_THEME_MINIMAL, loading_icon, &rounded_rectangle_shader, &rounded_rectangle_mask_shader); std::unordered_set fetched_messages_set; auto filter_existing_messages = [&fetched_messages_set](Messages &messages) { for(auto it = messages.begin(); it != messages.end();) { if((*it)->event_id.empty()) { ++it; continue; } auto res = fetched_messages_set.insert((*it)->event_id); if(!res.second) it = messages.erase(it); else ++it; } }; Messages all_messages; matrix->get_all_synced_room_messages(current_room, all_messages); for(auto &message : all_messages) { fetched_messages_set.insert(message->event_id); } auto me = matrix->get_me(current_room); auto new_body_items = messages_to_body_items(current_room, all_messages, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); modify_related_messages_in_current_room(all_messages); process_reactions(all_messages); tabs[MESSAGES_TAB_INDEX].body->select_last_item(); if(!all_messages.empty() && current_room->initial_prev_messages_fetch) { current_room->initial_prev_messages_fetch = false; } std::vector pinned_events; matrix->get_all_pinned_events(current_room, pinned_events); process_pinned_events(std::move(pinned_events)); tabs[PINNED_TAB_INDEX].body->select_last_item(); std::string room_name = current_room->get_name(); std::string room_topic = current_room->get_topic(); string_replace_all(room_name, '\n', ' '); string_replace_all(room_topic, '\n', ' '); room_name_text.set_string(std::move(room_name)); room_topic_text.set_string(std::move(room_topic)); read_marker_timeout_sec = 0; redraw = true; Entry chat_input("Press i to start writing a message...", &rounded_rectangle_shader); chat_input.draw_background = false; chat_input.set_editable(false); struct ProvisionalMessage { std::shared_ptr body_item; std::shared_ptr message; std::string event_id; }; std::unordered_map> pending_sent_replies; // This is needed to keep the message shared ptr alive. TODO: Remove this shit, maybe even use raw pointers. std::unordered_map sent_messages; // |event_id| is always empty in this. Use |message->event_id| instead std::optional provisional_message; MessageQueue provisional_message_queue; MessageQueue> post_task_queue; auto post_thread_handler = [&provisional_message_queue, &post_task_queue]() { while(true) { std::optional> post_task_opt = post_task_queue.pop_wait(); if(!post_task_opt) break; provisional_message_queue.push(post_task_opt.value()()); } }; std::thread post_thread(post_thread_handler); auto message_set_replaced_by = [&tabs, &pending_sent_replies, MESSAGES_TAB_INDEX](std::shared_ptr message) { if(message->related_event_type == RelatedEventType::EDIT) { auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), message->related_event_id); if(body_item) { Message *reply_to_message = static_cast(body_item->userdata); if(!reply_to_message) { show_notification("QuickMedia", "Unexpected error, failed to set replaced by message", Urgency::CRITICAL); return; } message->replaces = reply_to_message; reply_to_message->replaced_by = message; } } else if(message->related_event_type == RelatedEventType::REPLY) { auto pending_sent_reply_it = pending_sent_replies.find(message->transaction_id); if(pending_sent_reply_it != pending_sent_replies.end()) { pending_sent_reply_it->second->replaced_by = message; pending_sent_replies.erase(pending_sent_reply_it); } } }; auto filter_sent_messages = [&sent_messages, &message_set_replaced_by](Messages &messages) { for(auto it = messages.begin(); it != messages.end();) { if(!(*it)->event_id.empty() && sent_messages.find((*it)->event_id) != sent_messages.end()) { message_set_replaced_by(*it); it = messages.erase(it); } else { ++it; } } }; auto upload_file = [this, ¤t_room](const std::string &filepath) { run_task_with_loading_screen([this, ¤t_room, filepath]() { std::string event_id_response; std::string err_msg; if(matrix->post_file(current_room, filepath, event_id_response, err_msg) == PluginResult::OK) { return true; } else { show_notification("QuickMedia", "Failed to upload media to room, error: " + err_msg, Urgency::CRITICAL); return false; } }); }; struct Mention { mgl::Clock filter_timer; bool visible = false; bool filter_updated = false; std::string filter; Body *users_tab_body = nullptr; void show() { visible = true; } void hide() { visible = false; filter_updated = false; filter.clear(); users_tab_body->filter_search_fuzzy(""); users_tab_body->select_first_item(); users_tab_body->clear_cache(); } void handle_event(const mgl::Event &event) { if(visible) { if(event.type == mgl::Event::TextEntered) { filter_timer.restart(); if(event.text.codepoint > 32) { filter.append(event.text.str, event.text.size); filter_updated = true; } else if(event.text.codepoint == ' ' || event.text.codepoint == '\t') { hide(); } } else if(event.type == mgl::Event::KeyPressed) { if(event.key.code == mgl::Keyboard::Up || (event.key.control && event.key.code == mgl::Keyboard::K)) { users_tab_body->select_previous_item(true); } else if(event.key.code == mgl::Keyboard::Down || (event.key.control && event.key.code == mgl::Keyboard::J)) { users_tab_body->select_next_item(true); } else if(event.key.code == mgl::Keyboard::Enter && event.key.shift) { hide(); } else if(event.key.code == mgl::Keyboard::Backspace) { if(filter.empty()) { hide(); } else { filter.erase(filter.size() - 1, 1); filter_updated = true; } } } } if(event.type == mgl::Event::TextEntered && event.text.codepoint == '@' && !visible) show(); } void update() { if(visible && filter_updated && filter_timer.get_elapsed_time_seconds() > 0.05) { filter_updated = false; users_tab_body->filter_search_fuzzy(filter); users_tab_body->select_first_item(); } } }; Mention mention; mention.users_tab_body = tabs[USERS_TAB_INDEX].body.get(); const float user_mention_body_height = std::floor(300.0f * get_config().scale * get_config().font_scale); bool frame_skip_text_entry = false; chat_input.on_submit_callback = [&](std::string text) mutable { if(mention.visible) { BodyItem *selected_mention_item = tabs[USERS_TAB_INDEX].body->get_selected(); if(selected_mention_item) { std::string str_to_append = selected_mention_item->get_description(); if(!str_to_append.empty()) str_to_append.erase(0, 1); str_to_append += " "; chat_input.replace(chat_input.get_caret_index() - mention.filter.size(), mention.filter.size(), str_to_append); mention.hide(); } return false; } frame_skip_text_entry = true; const int selected_tab = ui_tabs.get_selected(); int num_items = tabs[MESSAGES_TAB_INDEX].body->get_num_items(); bool scroll_to_end = num_items == 0; if(tabs[MESSAGES_TAB_INDEX].body->is_selected_item_last_visible_item() && selected_tab == MESSAGES_TAB_INDEX) scroll_to_end = true; if(selected_tab == MESSAGES_TAB_INDEX) { if(text.empty()) return false; std::string msgtype; if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') { if(text == "/upload") { new_page = PageType::FILE_MANAGER; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(strncmp(text.c_str(), "/join ", 6) == 0) { text.erase(text.begin(), text.begin() + 6); text = strip(text); if(text.empty()) { return false; } else { TaskResult task_result = run_task_with_loading_screen([this, text{std::move(text)}] { return matrix->join_room(text) == PluginResult::OK; }); if(task_result == TaskResult::TRUE) { chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else { return false; } } } else if(text == "/invite") { new_page = PageType::CHAT_INVITE; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(text == "/logout") { new_page = PageType::CHAT_LOGIN; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(text == "/leave") { TaskResult task_result = run_task_with_loading_screen([this, ¤t_room]() { return matrix->leave_room(current_room->id) == PluginResult::OK; }); if(task_result != TaskResult::FALSE) { go_to_previous_page = true; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; } return true; } else if(text == "/help") { auto message = std::make_shared(); message->type = MessageType::SYSTEM; message->user = me; message->body = "/upload: Bring up the file manager and select a file to upload to the room, `Esc` to cancel.\n" "/join [room]: Join a room by name or id.\n" "/invite: Invite a user to the room.\n" "/logout: Logout.\n" "/leave: Leave the current room.\n" "/me [text]: Send a message of type \"m.emote\".\n" "/react [text]: React to the selected message (also works if you are replying to a message).\n" "/id: Show the room id."; message->timestamp = time(nullptr) * 1000; matrix->append_system_message(current_room, std::move(message)); chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(text == "/id") { auto message = std::make_shared(); message->type = MessageType::SYSTEM; message->user = me; message->body = current_room->id; message->timestamp = time(nullptr) * 1000; matrix->append_system_message(current_room, std::move(message)); chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(strncmp(text.c_str(), "/me ", 4) == 0) { msgtype = "m.emote"; text.erase(text.begin(), text.begin() + 4); } else if(strncmp(text.c_str(), "/react ", 7) == 0) { msgtype = "m.reaction"; text.erase(text.begin(), text.begin() + 7); } else { show_notification("QuickMedia", "Error: invalid command: " + text + ", type /help to see a list of valid commands.", Urgency::NORMAL); return false; } } else if(chat_state == ChatState::REPLYING && text[0] == '/') { if(strncmp(text.c_str(), "/react ", 7) == 0) { msgtype = "m.reaction"; text.erase(text.begin(), text.begin() + 7); } } auto message = std::make_shared(); message->user = matrix->get_me(current_room); if(msgtype == "m.emote") message->body = "*" + current_room->get_user_display_name(me) + "* " + text; else message->body = text; message->type = MessageType::TEXT; message->timestamp = time(NULL) * 1000; if(chat_state == ChatState::TYPING_MESSAGE || (chat_state == ChatState::REPLYING && msgtype == "m.reaction")) { BodyItem *selected_item = tabs[MESSAGES_TAB_INDEX].body->get_selected(); if(chat_state == ChatState::REPLYING) selected_item = currently_operating_on_item.get(); if(msgtype == "m.reaction" && selected_item) { void *related_to_message = selected_item->userdata; if(chat_state == ChatState::REPLYING) related_to_message = currently_operating_on_item->userdata; message->type = MessageType::REACTION; message->related_event_type = RelatedEventType::REACTION; message->related_event_id = static_cast(related_to_message)->event_id; auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); Messages messages; messages.push_back(message); process_reactions(messages); post_task_queue.push([this, ¤t_room, text, body_item, message, related_to_message]() { ProvisionalMessage provisional_message; provisional_message.body_item = body_item; provisional_message.message = message; if(matrix->post_reaction(current_room, text, related_to_message, provisional_message.event_id) != PluginResult::OK) fprintf(stderr, "Failed to post matrix reaction\n"); return provisional_message; }); } else { auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); body_item->set_description_color(get_theme().provisional_message_color); load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); post_task_queue.push([this, ¤t_room, text, msgtype, body_item, message]() { ProvisionalMessage provisional_message; provisional_message.body_item = body_item; provisional_message.message = message; if(matrix->post_message(current_room, text, provisional_message.event_id, std::nullopt, std::nullopt, msgtype) != PluginResult::OK) fprintf(stderr, "Failed to post matrix message\n"); return provisional_message; }); } chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; if(scroll_to_end) tabs[MESSAGES_TAB_INDEX].body->select_last_item(); return true; } else if(chat_state == ChatState::REPLYING) { std::string transaction_id = create_transaction_id(); pending_sent_replies[transaction_id] = message; void *related_to_message = currently_operating_on_item->userdata; message->related_event_type = RelatedEventType::REPLY; message->related_event_id = static_cast(related_to_message)->event_id; auto body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); body_item->set_description_color(get_theme().provisional_message_color); load_cached_related_embedded_item(body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({body_item}); post_task_queue.push([this, ¤t_room, text, related_to_message, body_item, message, transaction_id]() { ProvisionalMessage provisional_message; provisional_message.body_item = body_item; provisional_message.message = message; if(matrix->post_reply(current_room, text, related_to_message, provisional_message.event_id, transaction_id) != PluginResult::OK) fprintf(stderr, "Failed to post matrix reply\n"); return provisional_message; }); chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; currently_operating_on_item = nullptr; if(scroll_to_end) tabs[MESSAGES_TAB_INDEX].body->select_last_item(); return true; } else if(chat_state == ChatState::EDITING) { void *related_to_message = currently_operating_on_item->userdata; message->related_event_type = RelatedEventType::EDIT; message->related_event_id = static_cast(related_to_message)->event_id; size_t body_item_index = 0; auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), message->related_event_id, &body_item_index); if(body_item) { auto body_item_shared_ptr = tabs[MESSAGES_TAB_INDEX].body->get_item_by_index(body_item_index); body_item_shared_ptr->set_description(text); body_item_shared_ptr->set_description_color(get_theme().provisional_message_color); auto edit_body_item = message_to_body_item(current_room, message.get(), current_room->get_user_avatar_url(me), me->user_id); edit_body_item->visible = false; load_cached_related_embedded_item(edit_body_item.get(), message.get(), me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items()); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps({edit_body_item}); //unreferenced_events.push_back(message); post_task_queue.push([this, ¤t_room, text, related_to_message, message, body_item_shared_ptr]() { ProvisionalMessage provisional_message; provisional_message.message = message; provisional_message.body_item = body_item_shared_ptr; if(matrix->post_edit(current_room, text, related_to_message, provisional_message.event_id) != PluginResult::OK) fprintf(stderr, "Failed to post matrix edit\n"); return provisional_message; }); chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; currently_operating_on_item = nullptr; return true; } else { show_notification("QuickMedia", "Failed to edit message. Message refers to a non-existing message", Urgency::CRITICAL); return false; } } } return false; }; struct FetchMessagesResult { Messages messages; MessageDirection message_dir; bool reached_end = false; }; AsyncTask fetch_messages_future; MessageDirection fetch_messages_dir = MessageDirection::BEFORE; enum class FetchMessageType { MESSAGE, USER_UPDATE }; struct FetchMessageResult { FetchMessageType type; std::shared_ptr message; }; //const int num_fetch_message_threads = 4; AsyncTask fetch_users_future; AsyncTask fetch_message_future; Message *fetch_message = nullptr; std::shared_ptr fetch_body_item = nullptr; int fetch_message_tab = -1; // TODO: How about instead fetching all messages we have, not only the visible ones? also fetch with multiple threads. tabs[PINNED_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &me, &fetch_message_future, &tabs, &fetch_body_item, &fetch_message_tab, PINNED_TAB_INDEX, MESSAGES_TAB_INDEX](std::shared_ptr &body_item) { if(fetch_message_future.valid()) return; PinnedEventData *event_data = static_cast(body_item->userdata); if(!event_data) return; // Fetch replied to message if(event_data->status == FetchStatus::FINISHED_LOADING && event_data->message) { if(event_data->message->related_event_id.empty() || event_data->message->related_event_type != RelatedEventType::REPLY || (body_item->embedded_item_status != FetchStatus::NONE && body_item->embedded_item_status != FetchStatus::QUEUED_LOADING)) return; if(load_cached_related_embedded_item(body_item.get(), event_data->message, me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items())) return; std::string message_event_id = event_data->message->related_event_id; fetch_body_item = body_item; body_item->embedded_item_status = FetchStatus::LOADING; fetch_message_tab = MESSAGES_TAB_INDEX; // TODO: Check if the message is already cached before calling async? is this needed? is async creation expensive? fetch_message_future = AsyncTask([this, ¤t_room, message_event_id]() { return FetchMessageResult{FetchMessageType::MESSAGE, matrix->get_message_by_id(current_room, message_event_id)}; }); return; } if(event_data->status != FetchStatus::NONE) return; // Fetch embed message // Check if we already have the referenced message as a body item in the messages list, so we dont create a new one. // TODO: Optimize from linear search to hash map auto related_body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size(), event_data->event_id); if(related_body_item) { *body_item = *related_body_item; body_item->reactions.clear(); if(message_contains_user_mention(related_body_item->get_description(), current_room->get_user_display_name(me)) || message_contains_user_mention(related_body_item->get_description(), me->user_id)) body_item->set_description_color(get_theme().attention_alert_text_color); else body_item->set_description_color(get_theme().text_color); event_data->status = FetchStatus::FINISHED_LOADING; event_data->message = static_cast(related_body_item->userdata); body_item->userdata = event_data; return; } std::string message_event_id = event_data->event_id; fetch_body_item = body_item; event_data->status = FetchStatus::LOADING; fetch_message_tab = PINNED_TAB_INDEX; // TODO: Check if the message is already cached before calling async? is this needed? is async creation expensive? fetch_message_future = AsyncTask([this, ¤t_room, message_event_id]() { return FetchMessageResult{FetchMessageType::MESSAGE, matrix->get_message_by_id(current_room, message_event_id)}; }); }; // TODO: How about instead fetching all messages we have, not only the visible ones? also fetch with multiple threads. tabs[MESSAGES_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &me, &fetch_message_future, &tabs, &fetch_body_item, &fetch_message_tab, MESSAGES_TAB_INDEX](std::shared_ptr &body_item) { Message *message = static_cast(body_item->userdata); if(!message) return; if(message->related_event_id.empty() || message->related_event_type != RelatedEventType::REPLY || (body_item->embedded_item_status != FetchStatus::NONE && body_item->embedded_item_status != FetchStatus::QUEUED_LOADING)) return; if(fetch_message_future.valid()) { body_item->embedded_item_status = FetchStatus::QUEUED_LOADING; return; } if(load_cached_related_embedded_item(body_item.get(), message, me, current_room, tabs[MESSAGES_TAB_INDEX].body->get_items())) return; std::string message_event_id = message->related_event_id; fetch_body_item = body_item; body_item->embedded_item_status = FetchStatus::LOADING; fetch_message_tab = MESSAGES_TAB_INDEX; // TODO: Check if the message is already cached before calling async? is this needed? is async creation expensive? fetch_message_future = AsyncTask([this, ¤t_room, message_event_id]() { return FetchMessageResult{FetchMessageType::MESSAGE, matrix->get_message_by_id(current_room, message_event_id)}; }); }; tabs[MESSAGES_TAB_INDEX].body->body_item_merge_handler = [](BodyItem *prev_item, BodyItem *this_item) { Message *message = static_cast(this_item->userdata); if(!message || !prev_item || !prev_item->userdata) return false; if(is_visual_media_message_type(message->type) && !this_item->thumbnail_url.empty()) return false; Message *prev_message = static_cast(prev_item->userdata); if(is_system_message_type(prev_message->type) && is_system_message_type(message->type)) return true; else if(is_system_message_type(prev_message->type) || is_system_message_type(message->type)) return false; if(is_visual_media_message_type(prev_message->type) && !prev_item->thumbnail_url.empty()) return false; if(message->user == prev_message->user) return true; return false; }; mgl::vec2f body_pos; mgl::vec2f body_size; mgl::Event event; const float gradient_height = 5.0f; mgl::Vertex gradient_points[4]; double gradient_inc = 0; std::string before_token; std::string after_token; bool fetched_enough_messages_top = false; bool fetched_enough_messages_bottom = false; bool has_unread_messages = false; mgl::Rectangle more_messages_below_rect; more_messages_below_rect.set_color(get_theme().new_items_alert_color); mgl::Rectangle chat_input_shade; chat_input_shade.set_color(get_theme().shade_color); float tab_vertical_offset = 0.0f; mgl::Clock frame_timer; float prev_chat_height = chat_input.get_height(); float chat_input_height_full = 0.0f; const float logo_padding_x = std::floor(10.0f * get_config().scale * get_config().spacing_scale); const float chat_input_padding_x = std::floor(10.0f * get_config().scale * get_config().spacing_scale); const float chat_input_padding_y = std::floor(10.0f * get_config().scale * get_config().spacing_scale); bool avatar_applied = false; auto jump_to_message = [&](const std::string &event_id) { const int selected_tab = ui_tabs.get_selected(); if(selected_tab != MESSAGES_TAB_INDEX || event_id.empty()) return false; int body_item_index = -1; auto body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items(), event_id, &body_item_index); if(body_item) { tabs[MESSAGES_TAB_INDEX].body->set_selected_item(body_item_index, false); return true; } else { std::shared_ptr fetched_message; Messages before_messages; Messages after_messages; std::string new_before_token; std::string new_after_token; TaskResult task_result = run_task_with_loading_screen([this, current_room, &event_id, &fetched_message, &before_messages, &after_messages, &new_before_token, &new_after_token]{ std::string message_to_fetch = event_id; for(int i = 0; i < 10; ++i) { // TODO: Delay between tries? auto message = matrix->get_message_by_id(current_room, message_to_fetch); if(!message) return false; message_to_fetch = message->event_id; if(message->related_event_type != RelatedEventType::EDIT) break; } return matrix->get_message_context(current_room, message_to_fetch, fetched_message, before_messages, after_messages, new_before_token, new_after_token) == PluginResult::OK; }); if(task_result == TaskResult::TRUE) { Messages this_message = { fetched_message }; Messages *messages[3] = { &before_messages, &this_message, &after_messages }; before_token = std::move(new_before_token); after_token = std::move(new_after_token); fetch_messages_future.cancel(); fetched_enough_messages_top = false; fetched_enough_messages_bottom = false; unresolved_reactions.clear(); unreferenced_events.clear(); sent_messages.clear(); fetched_messages_set.clear(); tabs[MESSAGES_TAB_INDEX].body->clear_items(); matrix->clear_previous_messages_token(current_room); for(Messages *message_list : messages) { for(auto &message : *message_list) { fetched_messages_set.insert(message->event_id); } all_messages.insert(all_messages.end(), message_list->begin(), message_list->end()); auto new_body_items = messages_to_body_items(current_room, *message_list, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); modify_related_messages_in_current_room(*message_list); process_reactions(*message_list); } int fetched_message_index = -1; find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items(), fetched_message->event_id, &fetched_message_index); tabs[MESSAGES_TAB_INDEX].body->set_selected_item(fetched_message_index, false); return true; } else { return false; } } }; auto launch_url = [this, matrix_chat_page, &tabs, MESSAGES_TAB_INDEX, &redraw, &avatar_applied](std::string url) mutable { if(url.empty()) return; url = invidious_url_to_youtube_url(url); std::string video_id; if(youtube_url_extract_id(url, video_id)) { watched_videos.clear(); page_stack.push(PageType::CHAT); current_page = PageType::VIDEO_CONTENT; auto youtube_video_page = std::make_unique(this, std::move(url)); // TODO: Use real title video_content_page(matrix_chat_page, youtube_video_page.get(), "", false, tabs[MESSAGES_TAB_INDEX].body.get(), tabs[MESSAGES_TAB_INDEX].body->get_selected_item()); redraw = true; avatar_applied = false; } else { const char *launch_program = "xdg-open"; if(!is_program_executable_by_name("xdg-open")) { launch_program = getenv("BROWSER"); if(!launch_program) { show_notification("QuickMedia", "xdg-utils which provides xdg-open needs to be installed to open urls. Alternatively set the $BROWSER environment variable to a browser", Urgency::CRITICAL); return; } } std::string url_modified = url; if(strncmp(url.c_str(), "http://", 7) != 0 && strncmp(url.c_str(), "https://", 8) != 0) url_modified = "https://" + url; const char *args[] = { launch_program, url_modified.c_str(), nullptr }; exec_program_async(args, nullptr); } }; auto filter_provisional_messages = [&message_set_replaced_by](Messages &messages) { for(auto it = messages.begin(); it != messages.end();) { if(!(*it)->transaction_id.empty()) { message_set_replaced_by(*it); it = messages.erase(it); } else { ++it; } } }; auto add_new_messages_to_current_room = [&me, &tabs, &ui_tabs, ¤t_room, MESSAGES_TAB_INDEX, &after_token](Messages &messages) { if(messages.empty()) return; const int selected_tab = ui_tabs.get_selected(); int num_items = tabs[MESSAGES_TAB_INDEX].body->get_num_items(); bool scroll_to_end = num_items == 0; if(selected_tab == MESSAGES_TAB_INDEX && (tabs[MESSAGES_TAB_INDEX].body->is_selected_item_last_visible_item() || !tabs[MESSAGES_TAB_INDEX].body->get_selected())) scroll_to_end = true; if(current_room->initial_prev_messages_fetch) { current_room->initial_prev_messages_fetch = false; if(selected_tab == MESSAGES_TAB_INDEX) scroll_to_end = true; } if(!after_token.empty()) scroll_to_end = false; auto new_body_items = messages_to_body_items(current_room, messages, current_room->get_user_display_name(me), me->user_id); messages_load_cached_related_embedded_item(new_body_items, tabs[MESSAGES_TAB_INDEX].body->get_items(), me, current_room); tabs[MESSAGES_TAB_INDEX].body->insert_items_by_timestamps(std::move(new_body_items)); if(scroll_to_end) tabs[MESSAGES_TAB_INDEX].body->select_last_item(); }; auto display_url_or_image = [this, matrix_chat_page, &ui_tabs, &redraw, &video_page, &launch_url, &chat_state, &url_selection_body, &avatar_applied, PINNED_TAB_INDEX, MESSAGES_TAB_INDEX](BodyItem *selected) { if(!selected) return false; const int selected_tab = ui_tabs.get_selected(); Message *selected_item_message = nullptr; if(selected_tab == MESSAGES_TAB_INDEX) { selected_item_message = static_cast(selected->userdata); } else if(selected_tab == PINNED_TAB_INDEX && static_cast(selected->userdata)->status == FetchStatus::FINISHED_LOADING) { selected_item_message = static_cast(selected->userdata)->message; } if(selected_item_message) { MessageType message_type = selected_item_message->type; if(!selected->url.empty()) { if(message_type == MessageType::VIDEO || message_type == MessageType::IMAGE || message_type == MessageType::AUDIO) { page_stack.push(PageType::CHAT); watched_videos.clear(); current_page = PageType::VIDEO_CONTENT; bool is_audio = (message_type == MessageType::AUDIO); bool prev_no_video = no_video; no_video = is_audio; video_page->set_url(selected->url); video_content_page(matrix_chat_page, video_page.get(), selected_item_message->body, message_type == MessageType::VIDEO || message_type == MessageType::AUDIO, nullptr, 0); no_video = prev_no_video; redraw = true; avatar_applied = false; return true; } else if(message_type == MessageType::FILE) { download_async_gui(selected->url, file_manager_start_dir.string(), no_video); return true; } launch_url(selected->url); return true; } } // TODO: If content type is a file, show file-manager prompt where it should be saved and asynchronously save it instead std::vector urls = ranges_get_strings(selected->get_description(), extract_urls(selected->get_description())); if(urls.size() == 1) { launch_url(urls[0]); return true; } else if(urls.size() > 1) { chat_state = ChatState::URL_SELECTION; url_selection_body.clear_items(); for(const std::string &url : urls) { auto body_item = BodyItem::create(url); url_selection_body.append_item(std::move(body_item)); } return true; } return false; }; auto download_selected_item = [this, &ui_tabs, PINNED_TAB_INDEX, MESSAGES_TAB_INDEX](BodyItem *selected) { if(!selected) return false; const int selected_tab = ui_tabs.get_selected(); Message *selected_item_message = nullptr; if(selected_tab == MESSAGES_TAB_INDEX) { selected_item_message = static_cast(selected->userdata); } else if(selected_tab == PINNED_TAB_INDEX && static_cast(selected->userdata)->status == FetchStatus::FINISHED_LOADING) { selected_item_message = static_cast(selected->userdata)->message; } if(selected_item_message) { MessageType message_type = selected_item_message->type; if(!selected->url.empty() && message_type >= MessageType::IMAGE && message_type <= MessageType::FILE) { download_async_gui(selected->url, file_manager_start_dir.string(), no_video); return true; } } return false; }; auto update_pinned_messages_author = [&tabs, ¤t_room, PINNED_TAB_INDEX](const std::shared_ptr &user) { fprintf(stderr, "updated pinned messages author for user: %s\n", user->user_id.c_str()); tabs[PINNED_TAB_INDEX].body->for_each_item([¤t_room, &user](std::shared_ptr &pinned_body_item) { Message *message = static_cast(pinned_body_item->userdata)->message; // Its fine if we dont set it now. When the message is fetches, it will have updated user info since its fetched later if(!message || message->user != user) return; user_update_display_info(pinned_body_item.get(), current_room, message); }); }; auto update_messages_author = [&tabs, ¤t_room, MESSAGES_TAB_INDEX](const std::shared_ptr &user) { fprintf(stderr, "updated messages author for user: %s\n", user->user_id.c_str()); tabs[MESSAGES_TAB_INDEX].body->for_each_item([¤t_room, &user](std::shared_ptr &message_body_items) { Message *message = static_cast(message_body_items->userdata); if(!message || message->user != user) return; user_update_display_info(message_body_items.get(), current_room, message); }); }; // TODO: Optimize auto update_pinned_messages_authors = [&tabs, ¤t_room, PINNED_TAB_INDEX]() { fprintf(stderr, "updated pinned messages author for all users in room: %s\n", current_room->id.c_str()); tabs[PINNED_TAB_INDEX].body->for_each_item([¤t_room](std::shared_ptr &pinned_body_item) { Message *message = static_cast(pinned_body_item->userdata)->message; // Its fine if we dont set it now. When the message is fetches, it will have updated user info since its fetched later if(!message) return; user_update_display_info(pinned_body_item.get(), current_room, message); }); }; // TODO: Optimize auto update_messages_authors = [&tabs, ¤t_room, MESSAGES_TAB_INDEX]() { fprintf(stderr, "updated messages author for all users in room: %s\n", current_room->id.c_str()); tabs[MESSAGES_TAB_INDEX].body->for_each_item([¤t_room](std::shared_ptr &message_body_items) { Message *message = static_cast(message_body_items->userdata); if(!message) return; user_update_display_info(message_body_items.get(), current_room, message); }); }; auto cleanup_tasks = [&set_read_marker_future, &fetch_message_future, &fetch_users_future, &typing_state_queue, &typing_state_thread, &post_task_queue, &provisional_message_queue, &fetched_messages_set, &sent_messages, &pending_sent_replies, &post_thread, &tabs, MESSAGES_TAB_INDEX, PINNED_TAB_INDEX, USERS_TAB_INDEX]() { set_read_marker_future.cancel(); fetch_message_future.cancel(); fetch_users_future.cancel(); typing_state_queue.close(); if(typing_state_thread.joinable()) { program_kill_in_thread(typing_state_thread.get_id()); typing_state_thread.join(); } post_task_queue.close(); if(post_thread.joinable()) { program_kill_in_thread(post_thread.get_id()); post_thread.join(); } provisional_message_queue.clear(); fetched_messages_set.clear(); sent_messages.clear(); pending_sent_replies.clear(); //unreferenced_event_by_room.clear(); if(!tabs.empty()) { tabs[MESSAGES_TAB_INDEX].body->clear_items(); tabs[PINNED_TAB_INDEX].body->for_each_item([](std::shared_ptr &pinned_body_item) { delete (PinnedEventData*)pinned_body_item->userdata; pinned_body_item->userdata = nullptr; }); tabs[PINNED_TAB_INDEX].body->clear_items(); tabs[USERS_TAB_INDEX].body->clear_items(); } //tabs.clear(); }; // TODO: Remove this once synapse bug has been resolved where /sync does not include user info for new messages when using message filter that limits number of messages for initial sync, // and then only call this when viewing the users tab for the first time. // Note that this is not needed when new users join the room, as those will be included in the sync timeline (with membership events) if(current_room->users_fetched) { //TODO BLABLA //update_ } else { // TODO: Race condition? maybe use matrix /members instead which has a since parameter to make the members list match current sync fetch_users_future = AsyncTask([this, ¤t_room]() { matrix->update_room_users(current_room); return true; }); } float tab_shade_height = 0.0f; SyncData sync_data; matrix_chat_page->rooms_page->body->body_item_select_callback = [&move_room](BodyItem *body_item) { move_room = true; }; std::function on_top_reached = [this, &fetch_messages_future, &fetch_messages_dir, &ui_tabs, &MESSAGES_TAB_INDEX, &gradient_inc, current_room, &before_token, &after_token, &fetched_enough_messages_top] { const int selected_tab = ui_tabs.get_selected(); if(fetched_enough_messages_top || fetch_messages_future.valid() || selected_tab != MESSAGES_TAB_INDEX) return; gradient_inc = 0; if(before_token.empty() && after_token.empty()) { fetch_messages_dir = MessageDirection::BEFORE; fetch_messages_future = AsyncTask([this, current_room]() { FetchMessagesResult messages; messages.message_dir = MessageDirection::BEFORE; messages.reached_end = false; if(matrix->get_previous_room_messages(current_room, messages.messages, false, &messages.reached_end) != PluginResult::OK) fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); return messages; }); } else if(!before_token.empty()) { fetch_messages_dir = MessageDirection::BEFORE; fetch_messages_future = AsyncTask([this, current_room, &before_token]() { std::string token = before_token; FetchMessagesResult messages; messages.message_dir = MessageDirection::BEFORE; messages.reached_end = false; if(matrix->get_messages_in_direction(current_room, token, MessageDirection::BEFORE, messages.messages, before_token) != PluginResult::OK) fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); messages.reached_end = before_token.empty(); return messages; }); } }; std::function on_bottom_reached = [this, &fetch_messages_future, &fetch_messages_dir, &ui_tabs, &MESSAGES_TAB_INDEX, &gradient_inc, current_room, &after_token, &fetched_enough_messages_bottom] { const int selected_tab = ui_tabs.get_selected(); if(!fetched_enough_messages_bottom && !after_token.empty() && !fetch_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { fetch_messages_dir = MessageDirection::AFTER; gradient_inc = 0; fetch_messages_future = AsyncTask([this, current_room, &after_token]() { std::string token = after_token; FetchMessagesResult messages; messages.message_dir = MessageDirection::AFTER; messages.reached_end = false; if(matrix->get_messages_in_direction(current_room, token, MessageDirection::AFTER, messages.messages, after_token) != PluginResult::OK) fprintf(stderr, "Failed to get next matrix messages in room: %s\n", current_room->id.c_str()); messages.reached_end = after_token.empty(); return messages; }); return; } }; for(size_t i = 0; i < tabs.size(); ++i) { tabs[i].body->on_top_reached = on_top_reached; tabs[i].body->on_bottom_reached = on_bottom_reached; } if(!matrix_chat_page->jump_to_event_id.empty()) { if(!jump_to_message(matrix_chat_page->jump_to_event_id)) goto chat_page_end; } while (current_page == PageType::CHAT && window.is_open() && !move_room) { int32_t frame_time_ms = frame_timer.restart() * 1000.0; while (window.poll_event(event)) { common_event_handler(event); const int selected_tab = ui_tabs.get_selected(); if(chat_state == ChatState::NAVIGATING) ui_tabs.on_event(event); if(chat_state == ChatState::URL_SELECTION) { if(url_selection_body.on_event(window, event)) idle_active_handler(); } else { if(tabs[selected_tab].body->on_event(window, event, chat_state == ChatState::NAVIGATING)) idle_active_handler(); } base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); event_idle_handler(event); if(!frame_skip_text_entry) { if(!mention.visible || event.type != mgl::Event::KeyPressed || (event.key.code != mgl::Keyboard::Up && event.key.code != mgl::Keyboard::Down && event.key.code != mgl::Keyboard::Left && event.key.code != mgl::Keyboard::Right)) chat_input.process_event(window, event); if(chat_input.is_editable()) mention.handle_event(event); } if(draw_room_list) { if(matrix_chat_page->rooms_page->body->on_event(window, event, false)) idle_active_handler(); } if(event.type == mgl::Event::KeyPressed && event.key.alt && (chat_state == ChatState::NAVIGATING || chat_state == ChatState::URL_SELECTION)) { if(event.key.code == mgl::Keyboard::Up || (event.key.control && event.key.code == mgl::Keyboard::K)) { matrix_chat_page->rooms_page->body->select_previous_item(true); move_room = true; goto chat_page_end; } else if(event.key.code == mgl::Keyboard::Down || (event.key.control && event.key.code == mgl::Keyboard::J)) { matrix_chat_page->rooms_page->body->select_next_item(true); move_room = true; goto chat_page_end; } else if(event.key.code == mgl::Keyboard::PageUp) { matrix_chat_page->rooms_page->body->select_previous_page(); move_room = true; goto chat_page_end; } else if(event.key.code == mgl::Keyboard::PageDown) { matrix_chat_page->rooms_page->body->select_next_page(); move_room = true; goto chat_page_end; } else if(event.key.code == mgl::Keyboard::Home) { matrix_chat_page->rooms_page->body->select_first_item(false); move_room = true; goto chat_page_end; } else if(event.key.code == mgl::Keyboard::End) { matrix_chat_page->rooms_page->body->select_last_item(); move_room = true; goto chat_page_end; } else if(event.key.code == mgl::Keyboard::Escape) { move_room = false; goto chat_page_end; } continue; } if(event.type == mgl::Event::GainedFocus) { is_window_focused = true; redraw = true; } else if(event.type == mgl::Event::LostFocus) { is_window_focused = false; } else if(event.type == mgl::Event::Resized) { redraw = true; idle_active_handler(); } else if(event.type == mgl::Event::KeyPressed && chat_state == ChatState::NAVIGATING) { if(event.key.code == mgl::Keyboard::Escape) { goto chat_page_end; } else if(event.key.code == mgl::Keyboard::I && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(selected_item && selected_item->url.empty()) selected_item = selected_item->embedded_item.get(); if(selected_item && !selected_item->url.empty() && !selected_item->thumbnail_url.empty()) { Message *selected_item_message = nullptr; if(selected_tab == MESSAGES_TAB_INDEX) { selected_item_message = static_cast(selected_item->userdata); } else if(selected_tab == PINNED_TAB_INDEX && static_cast(selected_item->userdata)->status == FetchStatus::FINISHED_LOADING) { selected_item_message = static_cast(selected_item->userdata)->message; } if(selected_item_message && (selected_item_message->type == MessageType::IMAGE || selected_item_message->type == MessageType::VIDEO)) { std::string image_url = selected_item->url; if(selected_item_message->type == MessageType::VIDEO) image_url = selected_item->thumbnail_url; image_url = pantalaimon_image_proxy_url_to_remote_image_url(matrix, image_url); std::vector saucenao_tabs; saucenao_tabs.push_back(Tab{create_body(), std::make_unique(this, image_url, false), nullptr}); page_loop(saucenao_tabs); redraw = true; frame_skip_text_entry = true; } } } if((selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX) && !frame_skip_text_entry) { if(event.key.code == mgl::Keyboard::Enter) { BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { if(!display_url_or_image(selected)) display_url_or_image(selected->embedded_item.get()); } } else if(event.key.code == mgl::Keyboard::S && event.key.control) { BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { if(!download_selected_item(selected)) download_selected_item(selected->embedded_item.get()); } } } if(event.key.control && event.key.code == mgl::Keyboard::C) { BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) set_clipboard(selected->get_description()); } if(selected_tab == MESSAGES_TAB_INDEX) { if(event.key.code == mgl::Keyboard::U) { frame_skip_text_entry = true; new_page = PageType::FILE_MANAGER; chat_input.set_editable(false); } if(event.key.code == mgl::Keyboard::I && !event.key.control) { frame_skip_text_entry = true; chat_input.set_editable(true); chat_state = ChatState::TYPING_MESSAGE; } if(event.key.control && event.key.code == mgl::Keyboard::V) { frame_skip_text_entry = true; // TODO: Upload multiple files. upload_file(window.get_clipboard()); } if(event.key.code == mgl::Keyboard::R && tabs[selected_tab].body->get_selected_shared()) { std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); Message *selected_message = static_cast(selected->userdata); const bool go_to_replied_message = event.key.control; if(!is_state_message_type(selected_message)) { if(go_to_replied_message && selected_message->related_event_type == RelatedEventType::REPLY) { int replied_to_body_item_index = -1; auto replied_to_body_item = find_body_item_by_event_id(tabs[MESSAGES_TAB_INDEX].body->get_items(), selected_message->related_event_id, &replied_to_body_item_index); if(replied_to_body_item) { assert(replied_to_body_item->userdata); Message *orig_message = get_original_message(static_cast(replied_to_body_item->userdata)); jump_to_message(orig_message->event_id); } else { jump_to_message(selected_message->related_event_id); } } else if(!go_to_replied_message) { if(static_cast(selected->userdata)->event_id.empty()) { // TODO: Show inline notification show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet"); } else { chat_state = ChatState::REPLYING; currently_operating_on_item = selected; chat_input.set_editable(true); replying_to_text.set_string("Replying to:"); frame_skip_text_entry = true; } } } } if(event.key.code == mgl::Keyboard::B && event.key.control) { // Reload room, goes to latest message l0l move_room = true; goto chat_page_end; } if(event.key.code == mgl::Keyboard::E) { frame_skip_text_entry = true; std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); if(selected) { if(!is_state_message_type(static_cast(selected->userdata))) { if(static_cast(selected->userdata)->event_id.empty()) { // TODO: Show inline notification show_notification("QuickMedia", "You can't edit a message that hasn't been sent yet"); } else if(!selected->url.empty()) { // cant edit messages that are image/video posts // TODO: Show inline notification show_notification("QuickMedia", "You can't edit messages with files attached to them"); } else if(!matrix->was_message_posted_by_me(selected->userdata)) { // TODO: Show inline notification show_notification("QuickMedia", "You can't edit a message that was posted by somebody else"); } else { chat_state = ChatState::EDITING; currently_operating_on_item = selected; chat_input.set_editable(true); chat_input.set_text(selected->get_description()); // TODO: Description? it may change in the future, in which case this should be edited chat_input.move_caret_to_end(); replying_to_text.set_string("Editing message:"); } } } else { // TODO: Show inline notification show_notification("QuickMedia", "No message selected for editing"); } } if(event.key.control && event.key.code == mgl::Keyboard::D && !chat_input.is_editable()) { frame_skip_text_entry = true; BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { if(!is_state_message_type(static_cast(selected->userdata))) { if(static_cast(selected->userdata)->event_id.empty()) { // TODO: Show inline notification show_notification("QuickMedia", "You can't delete a message that hasn't been sent yet"); } else { //set_body_as_deleted(static_cast(selected->userdata), selected); void *selected_message = selected->userdata; post_task_queue.push([this, ¤t_room, selected_message]() { ProvisionalMessage provisional_message; std::string err_msg; if(matrix->delete_message(current_room, selected_message, err_msg) != PluginResult::OK) { // TODO: Show inline notification fprintf(stderr, "Failed to delete message, reason: %s\n", err_msg.c_str()); } return provisional_message; }); } } } else { // TODO: Show inline notification show_notification("QuickMedia", "No message selected for deletion"); } } if(event.key.control && event.key.code == mgl::Keyboard::P && !chat_input.is_editable()) { frame_skip_text_entry = true; BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { Message *selected_message = static_cast(selected->userdata); if(!is_state_message_type(selected_message)) { if(selected_message->event_id.empty()) { // TODO: Show inline notification show_notification("QuickMedia", "You can't pin a message that hasn't been sent yet"); } else { run_task_with_loading_screen([this, current_room, selected_message] { return matrix->pin_message(current_room, selected_message->event_id) == PluginResult::OK; }); } } } else { // TODO: Show inline notification show_notification("QuickMedia", "No message selected for pinning"); } } } else if(selected_tab == PINNED_TAB_INDEX) { if(event.key.control && event.key.code == mgl::Keyboard::D && !chat_input.is_editable()) { frame_skip_text_entry = true; BodyItem *selected = tabs[selected_tab].body->get_selected(); if(selected) { PinnedEventData *selected_pinned_event_data = static_cast(selected->userdata); if(selected_pinned_event_data) { run_task_with_loading_screen([this, current_room, selected_pinned_event_data] { return matrix->unpin_message(current_room, selected_pinned_event_data->event_id) == PluginResult::OK; }); } } else { // TODO: Show inline notification show_notification("QuickMedia", "No message selected for unpinning"); } } if(event.key.code == mgl::Keyboard::R && event.key.control && tabs[selected_tab].body->get_selected_shared()) { std::shared_ptr selected = tabs[selected_tab].body->get_selected_shared(); PinnedEventData *selected_event_data = static_cast(selected->userdata); if(selected_event_data && !selected_event_data->event_id.empty()) { ui_tabs.set_selected(MESSAGES_TAB_INDEX); jump_to_message(selected_event_data->event_id); } } } } else if(event.type == mgl::Event::KeyPressed && chat_state == ChatState::URL_SELECTION) { if(event.key.code == mgl::Keyboard::Escape) { url_selection_body.clear_items(); chat_state = ChatState::NAVIGATING; } else if(event.key.code == mgl::Keyboard::Enter) { BodyItem *selected_item = url_selection_body.get_selected(); if(!selected_item) continue; launch_url(selected_item->get_title()); } } if((chat_state == ChatState::TYPING_MESSAGE || chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) && selected_tab == MESSAGES_TAB_INDEX && !frame_skip_text_entry) { frame_skip_text_entry = false; if(event.type == mgl::Event::TextEntered) { // TODO: Also show typing event when ctrl+v pasting? if(event.text.codepoint != 13) { // Return key start_typing_timer.restart(); if(!typing && current_room) { fprintf(stderr, "Started typing\n"); typing_state_queue.push(true); } typing = true; } } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::Escape) { if(mention.visible) { mention.hide(); } else { chat_input.set_editable(false); chat_input.set_text(""); chat_state = ChatState::NAVIGATING; currently_operating_on_item = nullptr; if(typing && current_room) { fprintf(stderr, "Stopped typing\n"); typing = false; typing_state_queue.push(false); } } } } } frame_skip_text_entry = false; update_idle_state(); handle_x11_events(); matrix->update(); mention.update(); const size_t num_users_in_room = matrix_chat_page->get_num_users_in_current_room(); if(num_users_in_room != prev_num_users_in_room) { prev_num_users_in_room = num_users_in_room; ui_tabs.set_text(USERS_TAB_INDEX, "Users (" + std::to_string(num_users_in_room) + ")"); } while((provisional_message = provisional_message_queue.pop_if_available()) != std::nullopt) { if(!provisional_message->body_item || !provisional_message->message) continue; if(!provisional_message->event_id.empty()) { provisional_message->message->event_id = std::move(provisional_message->event_id); provisional_message->body_item->set_description_color(get_theme().text_color); sent_messages[provisional_message->message->event_id] = std::move(provisional_message.value()); } else if(provisional_message->body_item) { provisional_message->body_item->set_description("Failed to send: " + provisional_message->body_item->get_description()); provisional_message->body_item->set_description_color(get_theme().failed_text_color); provisional_message->body_item->userdata = nullptr; } } switch(new_page) { case PageType::FILE_MANAGER: { new_page = PageType::CHAT; for(ChatTab &tab : tabs) { tab.body->clear_cache(); } std::filesystem::path &fm_dir = file_manager_start_dir; auto file_manager_page = std::make_unique(this); file_manager_page->set_current_directory(fm_dir.string()); auto file_manager_body = create_body(); BodyItems body_items; file_manager_page->get_files_in_directory(body_items); file_manager_body->set_items(std::move(body_items)); std::vector file_manager_tabs; file_manager_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(file_manager_tabs); if(selected_files.empty()) { fprintf(stderr, "No files selected!\n"); } else { // TODO: Upload multiple files. upload_file(selected_files[0]); } redraw = true; avatar_applied = false; break; } case PageType::CHAT_LOGIN: { matrix_chat_page->set_current_room(nullptr, nullptr); fetch_messages_future.cancel(); cleanup_tasks(); tabs.clear(); unreferenced_events.clear(); unresolved_reactions.clear(); all_messages.clear(); new_page = PageType::CHAT; matrix->stop_sync(); matrix->logout(); delete matrix; matrix = new Matrix(); // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. //delete current_plugin; //current_plugin = new Matrix(); window.set_title("QuickMedia - matrix"); current_page = PageType::CHAT_LOGIN; chat_login_page(); if(current_page == PageType::CHAT) after_matrix_login_page(); exit(exit_code); break; } case PageType::CHAT_INVITE: { new_page = PageType::CHAT; for(ChatTab &tab : tabs) { tab.body->clear_cache(); } std::vector new_tabs; new_tabs.push_back(Tab{create_body(), std::make_unique(this, matrix, current_room->id), create_search_bar("Search...", 350)}); page_loop(new_tabs); redraw = true; avatar_applied = false; break; } default: break; } if(typing && start_typing_timer.get_elapsed_time_seconds() >= typing_timeout_seconds && current_room) { fprintf(stderr, "Stopped typing\n"); typing = false; typing_state_queue.push(false); } if(!current_room->body_item->thumbnail_url.empty()) room_avatar_thumbnail_data = AsyncImageLoader::get_instance().get_thumbnail(current_room->body_item->thumbnail_url, false, AVATAR_THUMBNAIL_SIZE); if(room_avatar_thumbnail_data->loading_state == LoadingState::FINISHED_LOADING && room_avatar_thumbnail_data->image->get_size().x > 0 && room_avatar_thumbnail_data->image->get_size().y > 0) { if(!room_avatar_thumbnail_data->texture.load_from_image(*room_avatar_thumbnail_data->image)) fprintf(stderr, "Warning: failed to load texture for room avatar\n"); //room_avatar_thumbnail_data->texture.generateMipmap(); room_avatar_thumbnail_data->image.reset(); room_avatar_thumbnail_data->loading_state = LoadingState::APPLIED_TO_TEXTURE; avatar_applied = false; } if(room_avatar_thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && !avatar_applied) { avatar_applied = true; room_avatar_sprite.set_texture(&room_avatar_thumbnail_data->texture); auto texture_size = room_avatar_sprite.get_texture()->get_size(); if(texture_size.x > 0 && texture_size.y > 0) { float width_ratio = (float)texture_size.x / (float)texture_size.y; float height_scale = room_avatar_height / (float)texture_size.y; float width_scale = height_scale * width_ratio; room_avatar_sprite.set_scale(mgl::vec2f(width_scale * get_config().scale, height_scale * get_config().scale)); } redraw = true; } const int selected_tab = ui_tabs.get_selected(); float room_name_padding_y = 0.0f; if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX || selected_tab == USERS_TAB_INDEX) room_name_padding_y = room_name_total_height; chat_input_height_full = chat_input.get_height() + chat_input_padding_y * 2.0f; if(selected_tab != MESSAGES_TAB_INDEX) chat_input_height_full = 0.0f; const float chat_height = chat_input.get_height(); if(std::abs(chat_height - prev_chat_height) > 1.0f) { prev_chat_height = chat_height; redraw = true; } if(redraw) { redraw = false; if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX || selected_tab == USERS_TAB_INDEX) { tab_vertical_offset = std::floor(10.0f * get_config().scale); } tab_shade_height = std::floor(tab_vertical_offset) + Tabs::get_shade_height() + room_name_padding_y; const float body_width = window_size.x; this->body_pos = mgl::vec2f(0.0f, tab_shade_height); if(window_size.x > 900.0f * get_config().scale * get_config().font_scale && show_room_side_panel) { this->body_size = vec2f_floor(300.0f * get_config().scale * get_config().font_scale, window_size.y - tab_shade_height); draw_room_list = true; } else { this->body_size = mgl::vec2f(0.0f, 0.0f); draw_room_list = false; } body_pos = mgl::vec2f(this->body_pos.x + this->body_size.x, tab_shade_height); body_size = mgl::vec2f(body_width - this->body_pos.x - this->body_size.x, window_size.y - chat_input_height_full - tab_shade_height); chat_input_shade.set_size(mgl::vec2f(window_size.x - body_pos.x, chat_input_height_full)); chat_input_shade.set_position(mgl::vec2f(body_pos.x, window_size.y - chat_input_shade.get_size().y)); chat_input.set_max_width(window_size.x - (logo_padding_x + logo_size.x + chat_input_padding_x + logo_padding_x + body_pos.x)); chat_input.set_position(vec2f_floor(body_pos.x + logo_padding_x + logo_size.x + chat_input_padding_x, window_size.y - chat_height - chat_input_padding_y - 5.0f * get_config().scale)); more_messages_below_rect.set_size(mgl::vec2f(chat_input_shade.get_size().x, gradient_height)); more_messages_below_rect.set_position(mgl::vec2f(chat_input_shade.get_position().x, std::floor(window_size.y - chat_input_height_full - gradient_height))); logo_sprite.set_position(mgl::vec2f(body_pos.x + logo_padding_x, std::floor(window_size.y - chat_input_height_full * 0.5f - logo_size.y * 0.5f))); } sync_data.messages.clear(); sync_data.pinned_events = std::nullopt; matrix->get_room_sync_data(current_room, sync_data); if(!sync_data.messages.empty() && after_token.empty()) { all_messages.insert(all_messages.end(), sync_data.messages.begin(), sync_data.messages.end()); filter_existing_messages(sync_data.messages); } filter_provisional_messages(sync_data.messages); if(after_token.empty()) { add_new_messages_to_current_room(sync_data.messages); modify_related_messages_in_current_room(sync_data.messages); process_reactions(sync_data.messages); has_unread_messages = false; } else { auto it = std::find_if(sync_data.messages.begin(), sync_data.messages.end(), [](const std::shared_ptr &message) { return message->related_event_type == RelatedEventType::NONE || message->related_event_type == RelatedEventType::REPLY; }); if(it != sync_data.messages.end()) has_unread_messages = true; } process_pinned_events(sync_data.pinned_events); if(set_read_marker_future.ready()) { set_read_marker_future.get(); read_marker_timer.restart(); setting_read_marker = false; } if(fetch_messages_future.ready()) { FetchMessagesResult new_messages_result = fetch_messages_future.get(); all_messages.insert(all_messages.end(), new_messages_result.messages.begin(), new_messages_result.messages.end()); if(new_messages_result.message_dir == MessageDirection::BEFORE) fetched_enough_messages_top = new_messages_result.reached_end; if(new_messages_result.message_dir == MessageDirection::AFTER) fetched_enough_messages_bottom = new_messages_result.reached_end; filter_sent_messages(new_messages_result.messages); filter_existing_messages(new_messages_result.messages); size_t num_new_messages = new_messages_result.messages.size(); if(num_new_messages > 0) { add_new_messages_to_current_room(new_messages_result.messages); modify_related_messages_in_current_room(new_messages_result.messages); process_reactions(new_messages_result.messages); // TODO: Do not loop all items, only loop the new items resolve_unreferenced_events_with_body_items(tabs[MESSAGES_TAB_INDEX].body->get_items().data(), tabs[MESSAGES_TAB_INDEX].body->get_items().size()); } if(num_new_messages > 0 && current_room->initial_prev_messages_fetch) { current_room->initial_prev_messages_fetch = false; if(selected_tab == MESSAGES_TAB_INDEX) tabs[MESSAGES_TAB_INDEX].body->select_last_item(); } } if(fetch_users_future.ready()) { fetch_users_future.get(); current_room->users_fetched = true; update_pinned_messages_authors(); update_messages_authors(); } if(fetch_message_future.ready()) { FetchMessageResult fetch_message_result = fetch_message_future.get(); if(fetch_message_result.type == FetchMessageType::USER_UPDATE) { update_pinned_messages_author(fetch_message->user); update_messages_author(fetch_message->user); fetch_message = nullptr; } else if(fetch_message_result.type == FetchMessageType::MESSAGE && fetch_body_item) { fprintf(stderr, "Finished fetching message: %s\n", fetch_message_result.message ? fetch_message_result.message->event_id.c_str() : "(null)"); if(fetch_message_tab == PINNED_TAB_INDEX) { PinnedEventData *event_data = static_cast(fetch_body_item->userdata); if(fetch_message_result.message) { *fetch_body_item = *message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); event_data->status = FetchStatus::FINISHED_LOADING; event_data->message = fetch_message_result.message.get(); fetch_body_item->userdata = event_data; } else { fetch_body_item->set_description("Failed to load message!"); event_data->status = FetchStatus::FAILED_TO_LOAD; } } else if(fetch_message_tab == MESSAGES_TAB_INDEX) { if(fetch_message_result.message) { fetch_body_item->embedded_item = message_to_body_item(current_room, fetch_message_result.message.get(), current_room->get_user_display_name(me), me->user_id); fetch_body_item->embedded_item_status = FetchStatus::FINISHED_LOADING; if(fetch_message_result.message->user == me) fetch_body_item->set_description_color(get_theme().attention_alert_text_color); } else { fetch_body_item->embedded_item_status = FetchStatus::FAILED_TO_LOAD; } } fetch_body_item = nullptr; } fetch_message_tab = -1; } window.clear(get_theme().background_color); if(chat_state == ChatState::URL_SELECTION) { url_selection_body.draw(window, body_pos, body_size); } else { tabs[selected_tab].body->draw(window, body_pos, body_size); if(selected_tab == MESSAGES_TAB_INDEX && mention.visible && chat_state == ChatState::TYPING_MESSAGE) { mgl::Rectangle user_mention_background(mgl::vec2f(body_size.x, user_mention_body_height)); user_mention_background.set_position(mgl::vec2f(body_pos.x, body_pos.y + body_size.y - user_mention_body_height)); user_mention_background.set_color(get_theme().shade_color); window.draw(user_mention_background); tabs[USERS_TAB_INDEX].body->draw(window, user_mention_background.get_position(), user_mention_background.get_size()); } } //tab_shade.set_size(mgl::vec2f(window_size.x, tab_shade_height)); //window.draw(tab_shade); if(selected_tab == MESSAGES_TAB_INDEX || selected_tab == PINNED_TAB_INDEX || selected_tab == USERS_TAB_INDEX) { float room_name_text_offset_x = std::floor(10.0f * get_config().scale); if(room_avatar_thumbnail_data->loading_state == LoadingState::APPLIED_TO_TEXTURE && room_avatar_sprite.get_texture() && room_avatar_sprite.get_texture()->is_valid()) { auto room_avatar_texture_size = room_avatar_sprite.get_texture()->get_size(); room_avatar_texture_size.x *= room_avatar_sprite.get_scale().x; room_avatar_texture_size.y *= room_avatar_sprite.get_scale().y; room_avatar_sprite.set_position(mgl::vec2f(body_pos.x + std::floor(10.0f * get_config().scale), room_name_total_height * 0.5f - room_avatar_texture_size.y * 0.5f + 5.0f)); if(circle_mask_shader.is_valid()) circle_mask_shader.set_uniform("resolution", mgl::vec2f(room_avatar_texture_size.x, room_avatar_texture_size.y)); window.draw(room_avatar_sprite, &circle_mask_shader); room_name_text_offset_x += room_avatar_texture_size.x + 10.0f; } room_name_text.set_position(mgl::vec2f(body_pos.x + room_name_text_offset_x, room_name_text_padding_y - 3.0f)); window.draw(room_name_text); room_topic_text.set_position(mgl::vec2f(room_name_text.get_position().x + room_name_text.get_bounds().size.x + 15.0f, room_name_text_padding_y - 1.0f + room_name_text_height * 0.5f - room_topic_text_height * 0.5f)); window.draw(room_topic_text); } if(draw_room_list) { mgl::Rectangle room_list_background(mgl::vec2f(this->body_size.x, window_size.y)); //room_list_background.set_position(this->body_pos); room_list_background.set_color(get_theme().shade_color); window.draw(room_list_background); window.draw(room_label); const float tab_y = std::floor(tab_vertical_offset) + room_name_padding_y; matrix_chat_page->rooms_page->body->draw(window, mgl::vec2f(0.0f, tab_y), mgl::vec2f(this->body_size.x, window_size.y - tab_y), Json::Value::nullSingleton()); } ui_tabs.draw(window, mgl::vec2f(body_pos.x, std::floor(tab_vertical_offset) + room_name_padding_y), body_size.x); if(chat_state == ChatState::REPLYING || chat_state == ChatState::EDITING) { const float margin = 5.0f; const float replying_to_text_height = replying_to_text.get_bounds().size.y + margin; float item_height = std::min(body_size.y - replying_to_text_height - margin, tabs[MESSAGES_TAB_INDEX].body->get_item_height(currently_operating_on_item.get(), body_size.x) + margin); if(item_height < 0.0f) item_height = 0.0f; float extra_user_mention_height = 0.0f; if(mention.visible) extra_user_mention_height = user_mention_body_height; mgl::Rectangle overlay(mgl::vec2f(window_size.x, window_size.y - chat_input_height_full - extra_user_mention_height)); overlay.set_color(mgl::Color(0, 0, 0, 240)); window.draw(overlay); const float padding_x = std::floor(10.0f * get_config().scale * get_config().spacing_scale); mgl::vec2f body_item_pos(body_pos.x + padding_x, window_size.y - chat_input_height_full - item_height); mgl::vec2f body_item_size(body_size.x - padding_x * 2.0f, item_height); mgl::Rectangle item_background(mgl::vec2f(window_size.x, body_item_size.y + chat_input_height_full + replying_to_text_height + margin)); item_background.set_position(mgl::vec2f(0.0f, window_size.y - (body_item_size.y + chat_input_height_full + replying_to_text_height + margin))); item_background.set_color(get_theme().background_color); window.draw(item_background); if(mention.visible) { mgl::Rectangle user_mention_background(mgl::vec2f(window_size.x, user_mention_body_height)); user_mention_background.set_position(mgl::vec2f(0.0f, item_background.get_position().y - user_mention_body_height)); user_mention_background.set_color(get_theme().shade_color); window.draw(user_mention_background); tabs[USERS_TAB_INDEX].body->draw(window, mgl::vec2f(body_pos.x + padding_x, item_background.get_position().y - user_mention_body_height), mgl::vec2f(body_size.x - padding_x * 2.0f, user_mention_body_height)); } replying_to_text.set_position(mgl::vec2f(body_item_pos.x, body_item_pos.y - replying_to_text_height)); window.draw(replying_to_text); tabs[MESSAGES_TAB_INDEX].body->draw_item(window, currently_operating_on_item, body_item_pos, body_item_size); } if(selected_tab == MESSAGES_TAB_INDEX && current_room && current_room->body_item && (!current_room->last_message_read || has_unread_messages) && matrix->is_initial_sync_finished()) { if(after_token.empty() && !tabs[selected_tab].body->is_bottom_cut_off() && is_window_focused && chat_state != ChatState::URL_SELECTION && !setting_read_marker && read_marker_timer.get_elapsed_time_seconds() >= read_marker_timeout_sec) { // TODO: Only set read marker once every second if the message is not the last message in the room auto body_items = tabs[selected_tab].body->get_items(); int last_timeline_message = (int)body_items.size() - 1; for(int i = last_timeline_message - 1; i >= 0; --i) { BodyItem *item = body_items[i].get(); Message *message = static_cast(item->userdata); if(item->visible && message && message_is_timeline(message)) break; } if(last_timeline_message != -1) { std::string room_desc = current_room->body_item->get_description(); if(strncmp(room_desc.c_str(), "Unread: ", 8) == 0) room_desc = room_desc.substr(8); size_t last_line_start = room_desc.rfind('\n'); if(last_line_start != std::string::npos && last_line_start != room_desc.size()) { ++last_line_start; size_t last_line_size = room_desc.size() - last_line_start; if(last_line_size >= 23 && memcmp(&room_desc[last_line_start], "** ", 3) == 0 && memcmp(&room_desc[room_desc.size() - 20], "unread mention(s) **", 20) == 0) room_desc.erase(room_desc.begin() + last_line_start - 1, room_desc.end()); } current_room->body_item->set_description(std::move(room_desc)); current_room->body_item->set_description_color(get_theme().faded_text_color); // TODO: Show a line like nheko instead for unread messages, or something else current_room->body_item->set_title_color(get_theme().text_color); current_room->last_message_read = true; // TODO: Maybe set this instead when the mention is visible on the screen? current_room->unread_notification_count = 0; Message *read_message = static_cast(body_items[last_timeline_message]->userdata); if(read_message->replaced_by) read_message = read_message->replaced_by.get(); // TODO: What if two messages have the same timestamp? if(!read_message->event_id.empty() && read_message->timestamp > current_room->last_read_message_timestamp) { matrix_chat_page->set_room_as_read(current_room); current_room->last_read_message_timestamp = read_message->timestamp; // TODO: What if the message is no longer valid? setting_read_marker = true; RoomData *room = current_room; std::string event_id = read_message->event_id; int64_t event_timestamp = read_message->timestamp; set_read_marker_future = AsyncTask([this, room, event_id, event_timestamp]() mutable { if(matrix->set_read_marker(room, event_id, event_timestamp) != PluginResult::OK) { fprintf(stderr, "Warning: failed to set read marker to %s\n", event_id.c_str()); } }); } } } else if(tabs[selected_tab].body->is_bottom_cut_off() || has_unread_messages) { window.draw(more_messages_below_rect); } } // TODO: Have one for each room. Also add bottom one? for fetching new messages (currently not implemented, is it needed?) if(fetch_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { // TODO: fetch_messages_dir 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); mgl::Color top_color = interpolate_colors(get_theme().background_color, get_theme().loading_page_color, progress); gradient_points[0].position.x = chat_input_shade.get_position().x; gradient_points[0].position.y = tab_shade_height; gradient_points[1].position.x = window_size.x; gradient_points[1].position.y = tab_shade_height; gradient_points[2].position.x = window_size.x; gradient_points[2].position.y = tab_shade_height + gradient_height; gradient_points[3].position.x = chat_input_shade.get_position().x; gradient_points[3].position.y = tab_shade_height + gradient_height; gradient_points[0].color = top_color; gradient_points[1].color = top_color; gradient_points[2].color = get_theme().background_color; gradient_points[3].color = get_theme().background_color; if(fetch_messages_dir == MessageDirection::AFTER) { gradient_points[0].position.y = more_messages_below_rect.get_position().y; gradient_points[1].position.y = more_messages_below_rect.get_position().y; gradient_points[2].position.y = more_messages_below_rect.get_position().y + gradient_height; gradient_points[3].position.y = more_messages_below_rect.get_position().y + gradient_height; gradient_points[0].color = get_theme().background_color; gradient_points[1].color = get_theme().background_color; gradient_points[2].color = top_color; gradient_points[3].color = top_color; } window.draw(gradient_points, 4, mgl::PrimitiveType::Quads); // Note: mgl::PrimitiveType::Quads doesn't work with egl } if(selected_tab == MESSAGES_TAB_INDEX) { //window.draw(chat_input_shade); chat_input.draw(window); //chat_input.draw(window, false); window.draw(logo_sprite); } if(matrix && !matrix->is_initial_sync_finished()) { std::string err_msg; if(matrix->did_initial_sync_fail(err_msg)) { matrix_chat_page->set_current_room(nullptr, nullptr); fetch_messages_future.cancel(); cleanup_tasks(); tabs.clear(); unreferenced_events.clear(); unresolved_reactions.clear(); all_messages.clear(); show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); matrix->logout(); delete matrix; matrix = new Matrix(); current_page = PageType::CHAT_LOGIN; chat_login_page(); after_matrix_login_page(); window.close(); goto chat_page_end; } } AsyncImageLoader::get_instance().update(); window.display(); if(selected_tab == MESSAGES_TAB_INDEX) { if(!tabs[selected_tab].body->is_top_cut_off()) tabs[selected_tab].body->on_top_reached(); if(!tabs[selected_tab].body->is_bottom_cut_off()) tabs[selected_tab].body->on_bottom_reached(); } if(matrix_chat_page->should_clear_data) { matrix_chat_page->should_clear_data = false; std::string err_msg; while(!matrix->is_initial_sync_finished()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); if(matrix->did_initial_sync_fail(err_msg)) { matrix_chat_page->set_current_room(nullptr, nullptr); fetch_messages_future.cancel(); cleanup_tasks(); tabs.clear(); unreferenced_events.clear(); unresolved_reactions.clear(); all_messages.clear(); show_notification("QuickMedia", "Initial matrix sync failed, error: " + err_msg, Urgency::CRITICAL); matrix->logout(); delete matrix; matrix = new Matrix(); current_page = PageType::CHAT_LOGIN; chat_login_page(); after_matrix_login_page(); window.close(); goto chat_page_end; } } //all_messages.clear(); tabs[MESSAGES_TAB_INDEX].body->clear_items(); Messages all_messages_new; matrix->get_all_synced_room_messages(current_room, all_messages_new); for(auto &message : all_messages_new) { fetched_messages_set.insert(message->event_id); } all_messages.insert(all_messages.end(), all_messages_new.begin(), all_messages_new.end()); //me = matrix->get_me(current_room); filter_sent_messages(all_messages_new); add_new_messages_to_current_room(all_messages_new); modify_related_messages_in_current_room(all_messages_new); unresolved_reactions.clear(); after_token.clear(); before_token.clear(), fetched_enough_messages_top = false; fetched_enough_messages_bottom = false; fetch_messages_future.cancel(); process_reactions(all_messages_new); if(current_room->initial_prev_messages_fetch) { current_room->initial_prev_messages_fetch = false; tabs[MESSAGES_TAB_INDEX].body->select_last_item(); } std::vector pinned_events; matrix->get_all_pinned_events(current_room, pinned_events); process_pinned_events(std::move(pinned_events)); } if(go_to_previous_page) { go_to_previous_page = false; goto chat_page_end; } } chat_page_end: matrix_chat_page->set_current_room(nullptr, nullptr); fetch_messages_future.cancel(); cleanup_tasks(); window.set_title("QuickMedia - matrix"); return move_room; } static void add_body_item_unique_title(BodyItems &body_items, const std::string &title) { for(auto &body_item : body_items) { if(body_item->get_title() == title) return; } body_items.push_back(BodyItem::create(title)); } void Program::after_matrix_login_page() { if(!window.is_open()) exit(exit_code); auto rooms_tags_body = create_body(); auto matrix_rooms_tag_page = std::make_unique(this, rooms_tags_body.get()); auto rooms_body = create_body(true); auto rooms_page_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); auto matrix_rooms_page = std::make_unique(this, rooms_body.get(), "All rooms", nullptr, rooms_page_search_bar.get()); auto notifications_body = create_body(); //notifications_body->attach_side = AttachSide::BOTTOM; auto matrix_notifications_page = std::make_unique(this, matrix, notifications_body.get(), matrix_rooms_page.get()); auto invites_body = create_body(); auto matrix_invites_page = std::make_unique(this, matrix, invites_body.get()); BodyItems room_dir_body_items; add_body_item_unique_title(room_dir_body_items, matrix->get_homeserver_domain()); add_body_item_unique_title(room_dir_body_items, "midov.pl"); add_body_item_unique_title(room_dir_body_items, "matrix.org"); add_body_item_unique_title(room_dir_body_items, "kde.org"); add_body_item_unique_title(room_dir_body_items, "librem.one"); add_body_item_unique_title(room_dir_body_items, "maunium.net"); add_body_item_unique_title(room_dir_body_items, "halogen.city"); add_body_item_unique_title(room_dir_body_items, "gnome.org"); add_body_item_unique_title(room_dir_body_items, "shivering-isles.com"); add_body_item_unique_title(room_dir_body_items, "nerdsin.space"); add_body_item_unique_title(room_dir_body_items, "glowers.club"); add_body_item_unique_title(room_dir_body_items, "privacytools.io"); add_body_item_unique_title(room_dir_body_items, "linuxdelta.com"); add_body_item_unique_title(room_dir_body_items, "tchncs.de"); add_body_item_unique_title(room_dir_body_items, "jupiterbroadcasting.com"); auto room_directory_body = create_body(); room_directory_body->set_items(std::move(room_dir_body_items)); auto matrix_room_directory_page = std::make_unique(this, matrix); MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get(), matrix_invites_page.get(), matrix_notifications_page.get()); bool sync_cached = false; if(!matrix->start_sync(&matrix_handler, sync_cached)) { show_notification("QuickMedia", "Failed to start sync", Urgency::CRITICAL); exit_code = 1; return; } is_login_sync = !sync_cached; std::vector tabs; tabs.push_back(Tab{std::move(notifications_body), std::move(matrix_notifications_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{std::move(rooms_tags_body), std::move(matrix_rooms_tag_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{std::move(rooms_body), std::move(matrix_rooms_page), std::move(rooms_page_search_bar)}); tabs.push_back(Tab{std::move(invites_body), std::move(matrix_invites_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{std::move(room_directory_body), std::move(matrix_room_directory_page), create_search_bar("Server to search on...", SEARCH_DELAY_FILTER)}); page_loop(tabs, 2, nullptr, false); matrix->stop_sync(); } static int accumulate_string(char *data, int size, void *userdata) { std::string *str = (std::string*)userdata; if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable return 1; str->append(data, size); return 0; } void Program::download_page(std::string url) { window.set_title(("QuickMedia - Select where you want to save " + std::string(url)).c_str()); url = invidious_url_to_youtube_url(url); const bool download_use_youtube_dl = url_should_download_with_youtube_dl(url); std::string filename; std::string video_id; const bool url_is_youtube = youtube_url_extract_id(url, video_id); std::unique_ptr youtube_video_page; std::string video_url; std::string audio_url; int64_t video_content_length = 0; int64_t audio_content_length = 0; TaskResult task_result; if(download_use_youtube_dl) { if(!is_program_executable_by_name("youtube-dl")) { show_notification("QuickMedia", "youtube-dl needs to be installed to download the video/music", Urgency::CRITICAL); abort(); } task_result = run_task_with_loading_screen([this, url, &filename]{ std::string json_str; std::vector args = { "youtube-dl", "--skip-download", "--print-json", "--no-warnings" }; if(no_video) { args.push_back("-f"); args.push_back("bestaudio/best"); args.push_back("-x"); } else { args.push_back("-f"); args.push_back("bestvideo+bestaudio/best"); } args.insert(args.end(), { "--", url.c_str(), nullptr }); if(exec_program(args.data(), accumulate_string, &json_str) != 0) return false; Json::Value result; Json::CharReaderBuilder json_builder; std::unique_ptr json_reader(json_builder.newCharReader()); std::string json_errors; if(!json_reader->parse(json_str.data(), json_str.data() + json_str.size(), &result, &json_errors)) { fprintf(stderr, "Failed to json response, error: %s\n", json_errors.c_str()); return false; } const Json::Value &title_json = result["title"]; const Json::Value &ext_json = result["ext"]; if(title_json.isString()) filename = title_json.asString(); if(ext_json.isString()) { if(ext_json.asCString()[0] != '.' && (filename.empty() || filename.back() != '.')) filename += "."; filename += ext_json.asString(); } return !filename.empty(); }); } else if(url_is_youtube) { youtube_video_page = std::make_unique(this, url); bool cancelled = false; bool load_successful = false; const int video_max_height = video_get_max_height(); std::string err_str; for(int i = 0; i < 3; ++i) { task_result = run_task_with_loading_screen([&]{ std::string channel_url; std::vector chapters; filename.clear(); double duration; if(youtube_video_page->load(filename, channel_url, duration, chapters, err_str) != PluginResult::OK) return false; std::string ext; bool has_embedded_audio = true; video_url = no_video ? "" : youtube_video_page->get_video_url(video_max_height, has_embedded_audio, ext); audio_url.clear(); if(!has_embedded_audio || no_video) audio_url = youtube_video_page->get_audio_url(ext); if(video_url.empty() && audio_url.empty()) return false; if(!youtube_url_is_live_stream(video_url) && !youtube_url_is_live_stream(audio_url)) { video_content_length = 0; audio_content_length = 0; std::string new_video_url = video_url; std::string new_audio_url = audio_url; auto current_thread_id = std::this_thread::get_id(); if(!youtube_custom_redirect(new_video_url, new_audio_url, video_content_length, audio_content_length, [current_thread_id]{ return !program_is_dead_in_thread(current_thread_id); })) { if(program_is_dead_in_current_thread()) cancelled = true; return false; } video_url = std::move(new_video_url); audio_url = std::move(new_audio_url); } if(!video_url.empty() && !audio_url.empty()) filename += ".mkv"; else filename += ext; return true; }); if(task_result == TaskResult::CANCEL || cancelled) { exit_code = 1; return; } else if(task_result == TaskResult::FALSE) { continue; } load_successful = true; break; } if(!load_successful) { show_notification("QuickMedia", "Download failed" + (err_str.empty() ? "" : ", error: " + err_str), Urgency::CRITICAL); exit_code = 1; return; } if(youtube_url_is_live_stream(video_url) || youtube_url_is_live_stream(audio_url)) { show_notification("QuickMedia", "Downloading youtube live streams is currently not supported", Urgency::CRITICAL); exit_code = 1; return; } } else { task_result = run_task_with_loading_screen([url, &filename]{ return url_get_remote_name(url, filename, true) == DownloadResult::OK; }); } if(task_result == TaskResult::CANCEL) { exit_code = 1; return; } else if(task_result == TaskResult::FALSE) { show_notification("QuickMedia", "Download failed", Urgency::CRITICAL); exit_code = 1; return; } string_replace_all(filename, '/', '_'); std::string output_filepath = file_save_page(filename); if(!window.is_open() || output_filepath.empty()) { exit_code = 1; return; } mgl::vec2i monitor_size; mgl::vec2i focused_monitor_center = get_focused_monitor_center(disp, monitor_size); window_size.x = std::min(monitor_size.x, (int)(300.0f + 380.0f * get_config().scale)); window_size.y = std::min(monitor_size.y, (int)(50.0f + 130.0f * get_config().scale)); window.set_size(mgl::vec2i(window_size.x, window_size.y)); window.set_size_limits(window_size, window_size); window.set_position(mgl::vec2i(focused_monitor_center.x - window_size.x * 0.5f, focused_monitor_center.y - window_size.y * 0.5f)); std::string output_filepath_s = output_filepath; char *output_dir = dirname(output_filepath_s.data()); if(create_directory_recursive(output_dir) != 0) { show_notification("QuickMedia", std::string("Failed to download ") + url + " to " + output_filepath, Urgency::CRITICAL); exit_code = 1; return; } idle_active_handler(); window.clear(get_theme().background_color); window.display(); const float loading_bar_padding_x = std::floor(4.0f * get_config().scale * get_config().spacing_scale); const float loading_bar_padding_y = std::floor(4.0f * get_config().scale * get_config().spacing_scale); RoundedRectangle loading_bar_background(mgl::vec2f(1.0f, 1.0f), std::floor(10.0f * get_config().scale), get_theme().background_color, &rounded_rectangle_shader); RoundedRectangle loading_bar(mgl::vec2f(1.0f, 1.0f), std::floor(10.0f * get_config().scale - loading_bar_padding_y), get_theme().loading_bar_color, &rounded_rectangle_shader); const float padding_x = std::floor(30.0f * get_config().scale * get_config().spacing_scale); const float spacing_y = std::floor(15.0f * get_config().scale * get_config().spacing_scale); const float loading_bar_height = std::floor(20.0f * get_config().scale); mgl::Text progress_text("0kb/Unknown", *FontLoader::get_font(FontLoader::FontType::LATIN, 20.0f * get_config().scale * get_config().font_scale)); mgl::Text status_text("Downloading", *FontLoader::get_font(FontLoader::FontType::LATIN, 20.0f * get_config().scale * get_config().font_scale)); mgl::Text filename_text(filename.c_str(), *FontLoader::get_font(FontLoader::FontType::LATIN, 14.0f * get_config().scale * get_config().font_scale)); filename_text.set_color(get_theme().faded_text_color); mgl::Text download_speed_text("0 bytes/s", *FontLoader::get_font(FontLoader::FontType::LATIN, 14.0f * get_config().scale * get_config().font_scale)); download_speed_text.set_color(get_theme().faded_text_color); bool redraw = true; mgl::Event event; std::unique_ptr downloader; if(download_use_youtube_dl) { downloader = std::make_unique(url, output_filepath, no_video); } else if(url_is_youtube) { MediaMetadata video_metadata; video_metadata.url = std::move(video_url); video_metadata.content_length = video_content_length; MediaMetadata audio_metadata; audio_metadata.url = std::move(audio_url); audio_metadata.content_length = audio_content_length; downloader = std::make_unique(video_metadata, audio_metadata, output_filepath); } else { downloader = std::make_unique(url, output_filepath); } if(!downloader->start()) { show_notification("QuickMedia", std::string("Failed to download ") + url + " to " + output_filepath, Urgency::CRITICAL); exit_code = 1; return; } mgl::Clock frame_timer; mgl::Clock progress_update_timer; bool download_completed = false; float progress = 0.0f; float ui_progress = 0.0f; while(window.is_open()) { while (window.poll_event(event)) { common_event_handler(event); if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; redraw = true; } } handle_x11_events(); if(window_closed) { show_notification("QuickMedia", "Download cancelled!"); downloader->stop(false); exit_code = 1; exit(exit_code); } if(progress_update_timer.get_elapsed_time_seconds() >= 1.0f) { progress_update_timer.restart(); DownloadUpdateStatus update_status = downloader->update(); switch(update_status) { case DownloadUpdateStatus::DOWNLOADING: break; case DownloadUpdateStatus::FINISHED: download_completed = true; goto cleanup; case DownloadUpdateStatus::ERROR: fprintf(stderr, "Download error on update\n"); goto cleanup; } progress = downloader->get_progress(); progress = std::max(0.0f, std::min(1.0f, progress)); progress_text.set_string(downloader->get_progress_text()); download_speed_text.set_string(downloader->get_download_speed_text()); redraw = true; } if(redraw) { redraw = false; loading_bar_background.set_size(mgl::vec2f(window_size.x - padding_x * 2.0f, loading_bar_height)); loading_bar_background.set_position(window_size.to_vec2f() * 0.5f - loading_bar_background.get_size() * 0.5f + mgl::vec2f(0.0f, download_speed_text.get_bounds().size.y * 0.5f)); loading_bar_background.set_position(vec2f_floor(loading_bar_background.get_position().x, loading_bar_background.get_position().y)); loading_bar.set_position(loading_bar_background.get_position() + mgl::vec2f(loading_bar_padding_x, loading_bar_padding_y)); filename_text.set_position( loading_bar_background.get_position() + mgl::vec2f(0.0f, -(filename_text.get_bounds().size.y + spacing_y))); progress_text.set_position( filename_text.get_position() + mgl::vec2f(loading_bar_background.get_size().x - progress_text.get_bounds().size.x, -(progress_text.get_bounds().size.y + spacing_y))); status_text.set_position( filename_text.get_position() + mgl::vec2f(0.0f, -(status_text.get_bounds().size.y + spacing_y))); download_speed_text.set_position( loading_bar_background.get_position() + mgl::vec2f(0.0f, loading_bar_height + spacing_y)); } const float progress_diff = progress - ui_progress; const float progress_move = frame_timer.get_elapsed_time_seconds() * 2500.0f * std::abs(progress_diff); if(std::abs(progress_diff) < progress_move) { ui_progress = progress; } else { if(progress_diff > 0.0f) ui_progress += progress_move; else ui_progress -= progress_move; } loading_bar.set_size(mgl::vec2f( std::floor((loading_bar_background.get_size().x - loading_bar_padding_x * 2.0f) * ui_progress), loading_bar_height - loading_bar_padding_y * 2.0f)); window.clear(get_theme().shade_color); loading_bar_background.draw(window); loading_bar.draw(window); window.draw(progress_text); window.draw(status_text); window.draw(filename_text); window.draw(download_speed_text); AsyncImageLoader::get_instance().update(); window.display(); frame_timer.restart(); } cleanup: const bool stop_successful = downloader->stop(download_completed); if(download_completed && stop_successful) { show_notification("QuickMedia", std::string("Download finished! Downloaded ") + Path(filename).filename() + " to " + output_filepath); exit_code = 0; } else { show_notification("QuickMedia", std::string("Failed to download ") + url + " to " + output_filepath, Urgency::CRITICAL); exit_code = 1; } exit(exit_code); } std::string Program::file_save_page(const std::string &filename) { mgl::vec2f body_pos; mgl::vec2f body_size; bool redraw = true; mgl::Event event; auto file_manager_page = std::make_unique(this); file_manager_page->set_current_directory(file_manager_start_dir); auto file_manager_body = create_body(); BodyItems body_items; file_manager_page->get_files_in_directory(body_items); file_manager_body->set_items(std::move(body_items)); auto search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); Tabs ui_tabs(&rounded_rectangle_shader); const int tab_path_index = ui_tabs.add_tab(file_manager_start_dir, file_manager_body.get()); search_bar->onTextUpdateCallback = [&file_manager_body](const std::string &text) { file_manager_body->filter_search_fuzzy(text); file_manager_body->select_first_item(); }; search_bar->onTextSubmitCallback = [this, &search_bar, &file_manager_body, &file_manager_page, &ui_tabs, tab_path_index](const std::string&) { if(window.is_key_pressed(mgl::Keyboard::LControl) || window.is_key_pressed(mgl::Keyboard::RControl)) return; auto selected = file_manager_body->get_selected_shared(); if(!selected) return; std::vector new_tabs; TaskResult task_result = run_task_with_loading_screen([&]() { SubmitArgs submit_args; submit_args.title = selected->get_title(); submit_args.url = selected->url; submit_args.thumbnail_url = selected->thumbnail_url; submit_args.userdata = selected->userdata; submit_args.extra = selected->extra; return file_manager_page->submit(submit_args, new_tabs) == PluginResult::OK; }); if(task_result == TaskResult::TRUE) { if(!new_tabs.empty()) { file_manager_body = std::move(new_tabs[0].body); search_bar->clear(); } } else if(task_result == TaskResult::FALSE) { show_notification("QuickMedia", "Failed to change directory", Urgency::CRITICAL); } ui_tabs.set_text(tab_path_index, file_manager_page->get_current_directory().string()); idle_active_handler(); }; const float bottom_panel_padding = std::floor(10.0f * get_config().spacing_scale); const float bottom_panel_spacing = std::floor(10.0f * get_config().spacing_scale); Button cancel_button("Cancel", FontLoader::get_font(FontLoader::FontType::LATIN, 16 * get_config().scale), 100.0f, &rounded_rectangle_shader, get_config().scale * get_config().font_scale); cancel_button.set_background_color(get_theme().cancel_button_background_color); Button save_button("Save", FontLoader::get_font(FontLoader::FontType::LATIN, 16 * get_config().scale), 100.0f, &rounded_rectangle_shader, get_config().scale * get_config().font_scale); save_button.set_background_color(get_theme().confirm_button_background_color); mgl::Text file_name_label("File name:", *FontLoader::get_font(FontLoader::FontType::LATIN, 16.0f * get_config().scale * get_config().font_scale)); Entry file_name_entry("", &rounded_rectangle_shader); file_name_entry.set_text(filename); file_name_entry.set_single_line(true); file_name_entry.set_editable(false); mgl::Rectangle bottom_panel_background; bottom_panel_background.set_color(get_theme().shade_color); const float gradient_height = 5.0f; mgl::Vertex gradient_points[4]; auto save_file = [this, &file_name_entry, &file_manager_page]() -> std::string { const std::string *filename = &file_name_entry.get_text(); Path filename_full_path = file_manager_page->get_current_directory().string(); filename_full_path.join(*filename); if(filename->empty()) { show_notification("QuickMedia", "The file name can't be empty", Urgency::CRITICAL); } else if(*filename == "." || *filename == ".." || filename->find('/') != std::string::npos) { show_notification("QuickMedia", "Invalid file name. File can't be ., .. or contain /", Urgency::CRITICAL); } else if(filename->size() >= 255 || filename_full_path.data.size() >= 4096) { show_notification("QuickMedia", "The file name has to be less than 255 characters and the full path has to be less than 4096 characters", Urgency::CRITICAL); } else { if(std::filesystem::exists(filename_full_path.data)) { bool overwrite = false; auto body = create_body(); auto options_page = std::make_unique(this, "Are you sure you want to overwrite " + filename_full_path.data + "?"); options_page->add_option(body.get(), "No", "", [&overwrite](){ overwrite = false; }); options_page->add_option(body.get(), "Yes", "", [&overwrite](){ overwrite = true; }); std::vector tabs; tabs.push_back(Tab{ std::move(body), std::move(options_page), nullptr }); page_loop(tabs); if(overwrite) return std::move(filename_full_path.data); } else { return std::move(filename_full_path.data); } } return ""; }; float prev_entry_height = file_name_entry.get_height(); while (window.is_open()) { while (window.poll_event(event)) { common_event_handler(event); if(file_manager_body->on_event(window, event, !file_name_entry.is_editable())) idle_active_handler(); else event_idle_handler(event); search_bar->on_event(window, event); if(cancel_button.on_event(event) & BUTTON_EVENT_CLICKED) return ""; if(save_button.on_event(event) & BUTTON_EVENT_CLICKED) { std::string save_path = save_file(); if(!save_path.empty()) return save_path; } file_name_entry.process_event(window, event); if(event.type == mgl::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; redraw = true; idle_active_handler(); } else if(event.type == mgl::Event::GainedFocus) { redraw = true; } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::Tab) { file_name_entry.set_editable(!file_name_entry.is_editable()); search_bar->set_editable(!search_bar->is_editable()); } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::Enter && event.key.control) { std::string save_path = save_file(); if(!save_path.empty()) return save_path; } else if(event.type == mgl::Event::KeyPressed && event.key.code == mgl::Keyboard::Escape) { window.close(); } } update_idle_state(); handle_x11_events(); search_bar->update(); if(std::abs(file_name_entry.get_height() - prev_entry_height) >= 1.0f) { prev_entry_height = file_name_entry.get_height(); redraw = true; } if(redraw) { redraw = false; get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); body_pos.y += Tabs::get_shade_height(); body_size.y -= Tabs::get_shade_height(); save_button.set_position(window_size.to_vec2f() - mgl::vec2f(save_button.get_width(), save_button.get_height()) - mgl::vec2f(bottom_panel_padding, bottom_panel_padding)); cancel_button.set_position(save_button.get_position() - mgl::vec2f(cancel_button.get_width() + bottom_panel_spacing, 0.0f)); file_name_label.set_position(mgl::vec2f(bottom_panel_spacing, std::floor(window_size.y - bottom_panel_padding - file_name_entry.get_height() * 0.5f - file_name_label.get_bounds().size.y * 0.5f - 5.0f * get_config().scale))); file_name_entry.set_position(mgl::vec2f(file_name_label.get_position().x + file_name_label.get_bounds().size.x + bottom_panel_spacing, window_size.y - file_name_entry.get_height() - bottom_panel_padding)); file_name_entry.set_max_width(std::floor(cancel_button.get_position().x - bottom_panel_spacing - file_name_label.get_bounds().size.x - bottom_panel_spacing - bottom_panel_spacing)); bottom_panel_background.set_position(mgl::vec2f(0.0f, window_size.y - std::floor(bottom_panel_padding * 2.0f + file_name_entry.get_height()))); bottom_panel_background.set_size(mgl::vec2f(window_size.x, std::floor(bottom_panel_padding * 2.0f + file_name_entry.get_height()))); const mgl::Color color(0, 0, 0, 50); gradient_points[0] = mgl::Vertex(bottom_panel_background.get_position() + mgl::vec2f(0.0f, -gradient_height), mgl::Color(color.r, color.g, color.b, 0)); gradient_points[1] = mgl::Vertex(bottom_panel_background.get_position() + mgl::vec2f(bottom_panel_background.get_size().x, -gradient_height), mgl::Color(color.r, color.g, color.b, 0)); gradient_points[2] = mgl::Vertex(bottom_panel_background.get_position() + mgl::vec2f(bottom_panel_background.get_size().x, 0.0f), color); gradient_points[3] = mgl::Vertex(bottom_panel_background.get_position() + mgl::vec2f(0.0f, 0.0f), color); } window.clear(get_theme().background_color); ui_tabs.draw(window, mgl::vec2f(0.0f, search_bar->getBottomWithoutShadow()), window_size.x); search_bar->draw(window, window_size.to_vec2f(), true); file_manager_body->draw(window, body_pos, body_size - mgl::vec2f(0.0f, bottom_panel_background.get_size().y)); window.draw(bottom_panel_background); window.draw(gradient_points, 4, mgl::PrimitiveType::Quads); window.draw(file_name_label); cancel_button.draw(window); save_button.draw(window); file_name_entry.draw(window); AsyncImageLoader::get_instance().update(); window.display(); } return ""; } }