diff options
43 files changed, 2236 insertions, 2863 deletions
@@ -11,18 +11,16 @@ Cache is stored under `$HOME/.cache/quickmedia`. ``` usage: QuickMedia <plugin> [--tor] [--use-system-mpv-config] [--dir <directory>] [-p <placeholder-text>] OPTIONS: - plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube, nyaa.si, matrix, file-manager or dmenu + plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube, nyaa.si, matrix, file-manager --no-video Only play audio when playing a video. Disabled by default --tor Use tor. Disabled by default --use-system-mpv-config Use system mpv config instead of no config. Disabled by default --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default --upscale-images-force Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default --dir Set the start directory when using file-manager - -p Change the placeholder text for dmenu EXAMPLES: QuickMedia manganelo QuickMedia youtube --tor -echo -e "hello\nworld" | QuickMedia dmenu ``` ## Installation If you are running arch linux then you can install QuickMedia from aur (https://aur.archlinux.org/packages/quickmedia-git/), otherwise you will need to use [sibs](https://git.dec05eba.com/sibs/) to build QuickMedia manually.\ @@ -44,7 +42,6 @@ Press `P` to preview the 4chan image of the selected row in full screen view, pr Press `I` to switch between single image and scroll image view mode when reading manga.\ Press `F` to fit image to window size when reading manga. Press `F` again to show original window size.\ Press `Middle mouse button` to "autoscroll" in scrolling image view mode.\ -Press `Tab` to autocomplete a search when autocomplete is available (currently only available for youtube).\ Press `Tab` to switch between username/password field in login panel.\ Press `Ctrl + C` to copy the url of the currently playing video to the clipboard (with timestamp).\ Press `Ctrl + V` to paste the content of your clipboard into the search bar.\ @@ -1,5 +1,4 @@ -If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions. -Give user the option to start where they left off or from the start or from the start. +Give user the option to start where they left off or from the start or from the start (for manga). Add grid-view when thumbnails are visible. Add scrollbar. Add option to scale image to window size. @@ -63,7 +62,7 @@ Merge body items in matrix if they are posted by the same author (there is a git Add joining/leaving room in matrix, and also show invites and add command to ban users. Also add joining by invite, and show invites in the rooms list. Support peertube (works with mpv, but need to implement search and related videos). Scroll to bottom when receiving a new message even if the selected message is not the last one. It should instead school if the last message is visible on the screen. -Add ".." directory to file-manager, to go up one directory. Also add a tab for common directories and recently accessed files/directories (the directories would be the directory of used files). +Also add a tab for common directories and recently accessed files/directories (the directories would be the directory of used files). Provide a way to go to the first unread message in matrix and also show a marker in the body (maybe a red line?) where the first unread message is. Load already downloaded images/thumbnails in a separate thread, to instantly load them instead of waiting for new downloads... Make text that mentions us red in matrix. @@ -93,4 +92,16 @@ Some services such as 4chan, youtube, matrix and nyaa supports knowing the size Show google recaptcha on youtube when search/play fails, which can happen when using tor. Show notifications when we receive a message in a matrix room even if we are not mentioned. This happens when we have set to receive notifications for all messages. Some mp4 videos fail to play because +faststart is not set, so the metadata is not at the beginning of the file. In such cases the video needs to be fully downloaded before it can play. QuickMedia should detect such files and download them (with progress bar) and then play them. -If there are multiple users with the same name in a matrix room, then display the user id beside the displayname.
\ No newline at end of file +If there are multiple users with the same name in a matrix room, then display the user id beside the displayname. +Show 4chan warnings as warnings instead of ban when posting a message (show the warning message) and then post the message. +Add tabs. Using tabs with tabbed is not as good of a solution as it would use much more memory (opengl context cost) and with our own tabs, we can clear thumbnails and other cache when a tab is in the background. Changing tab either with ctrl+tab or mouse click. ctrl+enter to open a new tab. Ctrl+q or ctrl+w to close a tab. +Remove related videos that have already been watched (except the first related video, which is the "watch next" video, usually the next part of a serie). +Use GET /_matrix/client/r0/notifications to get the exact messages that notified us in matrix. +Add F5 to refresh page. +Support m.sticker, m.direct, and other matrix events. +Show invites for us in matrix in a seperate tag and show notification when receiving invite. +Allow choosing which translation/scanlation to use on mangadex. Right now it uses the latest one, which is most likely to be the best. +Add file upload to 4chan. +Retry download if it fails, at least 3 times (observed to be needed for mangadex images). +Readd autocomplete, but make it better with a proper list. Also readd 4chan login page and manganelo creators page. +Fix logout/login in matrix. Currently it doesn't work because data is cleared while sync is in progress, leading to the first sync sometimes being with previous data...
\ No newline at end of file diff --git a/include/Body.hpp b/include/Body.hpp index 3d5c870..875ad61 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -101,7 +101,6 @@ namespace QuickMedia { class Body { public: Body(Program *program, sf::Font *font, sf::Font *bold_font, sf::Font *cjk_font); - ~Body(); // Select previous page, ignoring invisible items. Returns true if the item was changed. This can be used to check if the top was hit when wrap_around is set to false bool select_previous_page(); @@ -126,6 +125,8 @@ namespace QuickMedia { void append_items(BodyItems new_items); void insert_item_by_timestamp(std::shared_ptr<BodyItem> body_item); void insert_items_by_timestamps(BodyItems new_items); + void clear_cache(); + void clear_text_cache(); void clear_thumbnails(); BodyItem* get_selected() const; @@ -188,7 +189,6 @@ namespace QuickMedia { sf::RectangleShape item_background_shadow; sf::RoundedRectangleShape item_background; sf::Sprite image; - std::future<void> load_thumbnail_future; int num_visible_items; bool last_item_fully_visible; int last_fully_visible_item; diff --git a/include/ImageViewer.hpp b/include/ImageViewer.hpp index b8063ee..2cf3ac2 100644 --- a/include/ImageViewer.hpp +++ b/include/ImageViewer.hpp @@ -15,7 +15,7 @@ namespace sf { } namespace QuickMedia { - class Manga; + class MangaImagesPage; enum class ImageStatus { WAITING, @@ -46,7 +46,7 @@ namespace QuickMedia { class ImageViewer { public: - ImageViewer(Manga *manga, const std::string &images_url, const std::string &content_title, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font); + ImageViewer(MangaImagesPage *manga_images_page, const std::string &content_title, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font); ImageViewerAction draw(sf::RenderWindow &window); // Returns page as 1 indexed int get_focused_page() const; diff --git a/include/Page.hpp b/include/Page.hpp index f8af3c0..d0e40b3 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -1,16 +1,11 @@ #pragma once namespace QuickMedia { - enum class Page { + enum class PageType { EXIT, - SEARCH_SUGGESTION, VIDEO_CONTENT, - EPISODE_LIST, IMAGES, IMAGES_CONTINUOUS, - CONTENT_LIST, - CONTENT_DETAILS, - IMAGE_BOARD_THREAD_LIST, IMAGE_BOARD_THREAD, CHAT_LOGIN, CHAT, diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 016202c..0a0b509 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -4,6 +4,7 @@ #include "SearchBar.hpp" #include "Page.hpp" #include "Storage.hpp" +#include "Tab.hpp" #include <vector> #include <memory> #include <SFML/Graphics/Font.hpp> @@ -20,9 +21,10 @@ #include <X11/Xatom.h> namespace QuickMedia { - class Plugin; + class Matrix; class FileManager; - class Manga; + class MangaImagesPage; + class ImageBoardThreadPage; enum class ImageViewMode { SINGLE, @@ -40,24 +42,23 @@ namespace QuickMedia { ~Program(); int run(int argc, char **argv); - Plugin* get_current_plugin() { return current_plugin; } + bool is_tor_enabled(); + std::unique_ptr<Body> create_body(); + std::unique_ptr<SearchBar> create_search_bar(const std::string &placeholder, int search_delay); + + bool load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id); + + void select_file(const std::string &filepath); private: - void base_event_handler(sf::Event &event, Page previous_page, bool handle_key_press = true, bool clear_on_escape = true, bool handle_searchbar = true); - void search_suggestion_page(); - void search_result_page(); - void video_content_page(); - void episode_list_page(); - void image_page(); - void image_continuous_page(); - void content_list_page(); - void content_details_page(); - void image_board_thread_list_page(); - void image_board_thread_page(); + void base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_key_press = true, bool handle_searchbar = true); + void page_loop(std::vector<Tab> tabs); + void video_content_page(Page *page, std::string video_url, std::string video_title); + // Returns -1 to go to previous chapter, 0 to stay on same chapter and 1 to go to next chapter + int image_page(MangaImagesPage *images_page, Body *chapters_body); + void image_continuous_page(MangaImagesPage *images_page); + void image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body); void chat_login_page(); void chat_page(); - void file_manager_page(); - - bool on_search_suggestion_submit_text(Body *input_body, Body *output_body); enum class LoadImageResult { OK, @@ -66,17 +67,18 @@ namespace QuickMedia { }; LoadImageResult load_image_by_index(int image_index, sf::Texture &image_texture, sf::String &error_message); - void download_chapter_images_if_needed(Manga *image_plugin); + void download_chapter_images_if_needed(MangaImagesPage *images_page); void select_episode(BodyItem *item, bool start_from_beginning); - // Returns Page::EXIT if empty - Page pop_page_stack(); + // Returns PageType::EXIT if empty + PageType pop_page_stack(); - void plugin_get_watch_history(Plugin *plugin, BodyItems &history_items); - Json::Value load_video_history_json(Plugin *plugin); - Json::Value load_recommended_json(Plugin *plugin); + void manga_get_watch_history(const char *plugin_name, BodyItems &history_items); + void youtube_get_watch_history(BodyItems &history_items); + Json::Value load_video_history_json(); + Json::Value load_recommended_json(); - void save_recommendations_from_related_videos(); + void save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const Body *related_media_body); private: enum class UpscaleImageAction { NO, @@ -86,25 +88,16 @@ namespace QuickMedia { Display *disp; sf::RenderWindow window; + Matrix *matrix = nullptr; int monitor_hz; sf::Vector2f window_size; std::unique_ptr<sf::Font> font; std::unique_ptr<sf::Font> bold_font; std::unique_ptr<sf::Font> cjk_font; - Body *body; - Plugin *current_plugin; + const char *plugin_name = nullptr; sf::Texture plugin_logo; - std::unique_ptr<SearchBar> search_bar; - Page current_page; - std::stack<Page> page_stack; - // TODO: Combine these - std::string images_url; - std::string content_title; - std::string content_episode; - std::string content_url; - std::string content_list_url; - std::string image_board_thread_list_url; - std::string chapter_title; + PageType current_page; + std::stack<PageType> page_stack; int image_index; Path content_storage_file; Path content_cache_dir; @@ -130,9 +123,7 @@ namespace QuickMedia { bool running = false; // TODO: Save this to config file when switching modes ImageViewMode image_view_mode = ImageViewMode::SINGLE; - Body *related_media_body; std::vector<std::string> selected_files; - FileManager *file_manager = nullptr; bool fit_image_to_window = false; }; }
\ No newline at end of file diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp index e5245be..6b7ca6d 100644 --- a/include/SearchBar.hpp +++ b/include/SearchBar.hpp @@ -15,8 +15,7 @@ namespace sf { namespace QuickMedia { using TextUpdateCallback = std::function<void(const std::string &text)>; - // Return true to consume the search (clear the search field) - using TextSubmitCallback = std::function<bool(const std::string &text)>; + using TextSubmitCallback = std::function<void(const std::string &text)>; using TextBeginTypingCallback = std::function<void()>; using AutocompleteRequestCallback = std::function<void(const std::string &text)>; @@ -69,6 +68,7 @@ namespace QuickMedia { bool draw_logo; bool needs_update; bool input_masked; + bool typing; float vertical_pos; sf::Clock time_since_search_update; }; diff --git a/include/Storage.hpp b/include/Storage.hpp index 38c083e..8a1bbfa 100644 --- a/include/Storage.hpp +++ b/include/Storage.hpp @@ -29,4 +29,6 @@ namespace QuickMedia { bool read_file_as_json(const Path &filepath, Json::Value &result); bool save_json_to_file_atomic(const Path &path, const Json::Value &json); + + bool is_program_executable_by_name(const char *name); }
\ No newline at end of file diff --git a/include/Tab.hpp b/include/Tab.hpp new file mode 100644 index 0000000..ccb8c85 --- /dev/null +++ b/include/Tab.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include <memory> + +namespace QuickMedia { + class Body; + class Page; + class SearchBar; + + struct Tab { + std::unique_ptr<Body> body; + std::unique_ptr<Page> page; // Only null when current page has |is_single_page()| set to true + std::unique_ptr<SearchBar> search_bar; // Nullable + }; +}
\ No newline at end of file diff --git a/plugins/Dmenu.hpp b/plugins/Dmenu.hpp deleted file mode 100644 index 32fdad1..0000000 --- a/plugins/Dmenu.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "Plugin.hpp" - -namespace QuickMedia { - class Dmenu : public Plugin { - public: - Dmenu(); - bool search_is_filter() override { return true; } - bool search_suggestions_has_thumbnails() const override { return false; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 50; } - bool search_suggestion_is_search() const override { return true; } - Page get_page_after_search() const override { return Page::EXIT; } - PluginResult get_front_page(BodyItems &result_items) override; - SearchResult search(const std::string &text, BodyItems &result_items) override; - private: - std::vector<std::string> stdin_data; - }; -}
\ No newline at end of file diff --git a/plugins/FileManager.hpp b/plugins/FileManager.hpp index f13184b..38babc1 100644 --- a/plugins/FileManager.hpp +++ b/plugins/FileManager.hpp @@ -1,22 +1,18 @@ #pragma once -#include "Plugin.hpp" +#include "Page.hpp" #include <filesystem> namespace QuickMedia { - class FileManager : public Plugin { + class FileManagerPage : public Page { public: - FileManager(); - virtual ~FileManager() = default; - PluginResult get_files_in_directory(BodyItems &result_items); - bool set_current_directory(const std::string &path); - bool set_child_directory(const std::string &filename); - const std::filesystem::path& get_current_dir() const; + FileManagerPage(Program *program) : Page(program), current_dir("/") {} + const char* get_title() const override { return current_dir.c_str(); } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + bool is_single_page() const override { return true; } - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return true; } - int get_search_delay() const override { return 50; } - Page get_page_after_search() const override { return Page::FILE_MANAGER; } + bool set_current_directory(const std::string &path); + PluginResult get_files_in_directory(BodyItems &result_items); private: std::filesystem::path current_dir; }; diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp index bc336bf..3ee07dd 100644 --- a/plugins/Fourchan.hpp +++ b/plugins/Fourchan.hpp @@ -1,49 +1,38 @@ #pragma once #include "ImageBoard.hpp" -#include <thread> -#include <mutex> -#include <condition_variable> namespace QuickMedia { - class Program; + class FourchanBoardsPage : public Page { + public: + FourchanBoardsPage(Program *program, std::string resources_root) : Page(program), resources_root(std::move(resources_root)) {} + const char* get_title() const override { return "Select board"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + void get_boards(BodyItems &result_items); + + const std::string resources_root; + }; - class Fourchan : public ImageBoard { + class FourchanThreadListPage : public Page { public: - Fourchan(const std::string &resources_root); - ~Fourchan() override; - PluginResult get_front_page(BodyItems &result_items) override; - PluginResult get_threads(const std::string &url, BodyItems &result_items) override; - PluginResult get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) override; - PostResult post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) override; - bool search_suggestions_has_thumbnails() const override { return false; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 150; } - Page get_page_after_search() const override { return Page::IMAGE_BOARD_THREAD_LIST; } - bool search_is_filter() override { return true; } - BodyItems get_related_media(const std::string &url) override; + FourchanThreadListPage(Program *program, std::string title, std::string board_id) : Page(program), title(std::move(title)), board_id(std::move(board_id)) {} + const char* get_title() const override { return title.c_str(); } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; - PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg); - const std::string& get_pass_id() const override; + const std::string title; + const std::string board_id; private: - PluginResult get_threads_internal(const std::string &url, BodyItems &result_items); - void set_board_url(const std::string &new_url); - std::string get_board_url(); - void set_board_thread_list(BodyItems body_items); - BodyItems get_board_thread_list(); - private: - std::string current_board_url; - std::thread thread_list_update_thread; - BodyItems cached_thread_list_items; - std::mutex board_url_mutex; - std::mutex board_list_mutex; - std::condition_variable thread_list_cached_cv; - std::mutex thread_list_cache_mutex; - std::condition_variable thread_list_update_cv; - bool thread_list_cached = false; - bool running = true; std::vector<std::string> cached_media_urls; - std::string resources_root; + }; + + class FourchanThreadPage : public ImageBoardThreadPage { + public: + FourchanThreadPage(Program *program, std::string board_id, std::string thread_id, std::vector<std::string> cached_media_urls) : ImageBoardThreadPage(program, std::move(board_id), std::move(thread_id), std::move(cached_media_urls)) {} + + PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg) override; + PostResult post_comment(const std::string &captcha_id, const std::string &comment) override; + const std::string& get_pass_id() override; + private: std::string pass_id; }; }
\ No newline at end of file diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index abab22e..6f2a276 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -1,6 +1,6 @@ #pragma once -#include "Plugin.hpp" +#include "Page.hpp" namespace QuickMedia { enum class PostResult { @@ -10,16 +10,26 @@ namespace QuickMedia { ERR }; - class ImageBoard : public Plugin { + class ImageBoardThreadPage : public Page { public: - ImageBoard(const std::string &name) : Plugin(name) {} - virtual ~ImageBoard() = default; + ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id, std::vector<std::string> cached_media_urls) : Page(program), board_id(std::move(board_id)), thread_id(std::move(thread_id)), cached_media_urls(std::move(cached_media_urls)) {} + const char* get_title() const override { return ""; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::ERR; + } - bool is_image_board() override { return true; } - virtual const std::string& get_pass_id() const = 0; + bool is_image_board_thread_page() const override { return true; } - virtual PluginResult get_threads(const std::string &url, BodyItems &result_items) = 0; - virtual PluginResult get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) = 0; - virtual PostResult post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) = 0; + virtual BodyItems get_related_media(const std::string &url) override; + virtual PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg); + virtual PostResult post_comment(const std::string &captcha_id, const std::string &comment) = 0; + virtual const std::string& get_pass_id(); + + const std::string board_id; + const std::string thread_id; + const std::vector<std::string> cached_media_urls; }; }
\ No newline at end of file diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp index 13c494e..732e208 100644 --- a/plugins/Manga.hpp +++ b/plugins/Manga.hpp @@ -1,8 +1,7 @@ #pragma once -#include "Plugin.hpp" +#include "Page.hpp" #include <functional> -#include <mutex> namespace QuickMedia { // Return false to stop iteration @@ -13,18 +12,46 @@ namespace QuickMedia { std::string url; }; - class Manga : public Plugin { + class MangaImagesPage : public Page { public: - Manga(const std::string &plugin_name) : Plugin(plugin_name) {} - bool is_manga() override { return true; } - virtual ImageResult get_number_of_images(const std::string &url, int &num_images) = 0; - virtual ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) = 0; - virtual bool extract_id_from_url(const std::string &url, std::string &manga_id) = 0; + MangaImagesPage(Program *program, std::string manga_name, std::string chapter_name, std::string url) : Page(program), manga_name(std::move(manga_name)), chapter_name(std::move(chapter_name)), url(std::move(url)) {} + virtual ~MangaImagesPage() = default; + const char* get_title() const override { return chapter_name.c_str(); } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::OK; + } - virtual PluginResult get_creators_manga_list(const std::string &url, BodyItems &result_items) { (void)url; (void)result_items; return {}; } + bool is_manga_images_page() const override { return true; } - const std::vector<Creator>& get_creators() const; + virtual ImageResult get_number_of_images(int &num_images) = 0; + virtual ImageResult for_each_page_in_chapter(PageCallback callback) = 0; + + virtual void change_chapter(std::string new_chapter_name, std::string new_url) { + chapter_name = std::move(new_chapter_name); + if(url != new_url) { + url = std::move(new_url); + chapter_image_urls.clear(); + } + } + + const std::string& get_chapter_name() const { return chapter_name; } + const std::string& get_url() const { return url; } + + virtual const char* get_service_name() const = 0; + + const std::string manga_name; protected: - std::vector<Creator> creators; + std::string chapter_name; + std::string url; + std::vector<std::string> chapter_image_urls; + }; + + class MangaChaptersPage : public TrackablePage { + public: + MangaChaptersPage(Program *program, std::string manga_name, std::string manga_url) : TrackablePage(program, std::move(manga_name), std::move(manga_url)) {} + TrackResult track(const std::string &str) override; }; }
\ No newline at end of file diff --git a/plugins/Mangadex.hpp b/plugins/Mangadex.hpp index aaca18d..ba43498 100644 --- a/plugins/Mangadex.hpp +++ b/plugins/Mangadex.hpp @@ -2,32 +2,37 @@ #include "Manga.hpp" #include <functional> -#include <mutex> namespace QuickMedia { - class Mangadex : public Manga { + class MangadexSearchPage : public Page { public: - Mangadex() : Manga("mangadex") {} - SearchResult search(const std::string &url, BodyItems &result_items) override; - SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; - ImageResult get_number_of_images(const std::string &url, int &num_images) override; - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 300; } - Page get_page_after_search() const override { return Page::EPISODE_LIST; } + MangadexSearchPage(Program *program) : Page(program) {} + const char* get_title() const override { return "All"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + private: + bool get_rememberme_token(std::string &rememberme_token); + std::optional<std::string> rememberme_token; + }; + + class MangadexChaptersPage : public MangaChaptersPage { + public: + MangadexChaptersPage(Program *program, std::string manga_name, std::string manga_url) : MangaChaptersPage(program, std::move(manga_name), std::move(manga_url)) {} + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + }; + + class MangadexImagesPage : public MangaImagesPage { + public: + MangadexImagesPage(Program *program, std::string manga_name, std::string chapter_name, std::string url) : MangaImagesPage(program, std::move(manga_name), std::move(chapter_name), std::move(url)) {} - ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override; + ImageResult get_number_of_images(int &num_images) override; + ImageResult for_each_page_in_chapter(PageCallback callback) override; - bool extract_id_from_url(const std::string &url, std::string &manga_id) override; + const char* get_service_name() const override { return "mangadex"; } private: - // Caches url. If the same url is requested multiple times then the cache is used + // Cached ImageResult get_image_urls_for_chapter(const std::string &url); - bool get_rememberme_token(std::string &rememberme_token); bool save_mangadex_cookies(const std::string &url, const std::string &cookie_filepath); - private: - std::string last_chapter_url; - std::vector<std::string> last_chapter_image_urls; - std::mutex image_urls_mutex; - std::optional<std::string> rememberme_token; }; }
\ No newline at end of file diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp index c2ad693..d79f814 100644 --- a/plugins/Manganelo.hpp +++ b/plugins/Manganelo.hpp @@ -2,31 +2,44 @@ #include "Manga.hpp" #include <functional> -#include <mutex> namespace QuickMedia { - class Manganelo : public Manga { + class ManganeloSearchPage : public Page { public: - Manganelo() : Manga("manganelo") {} - SearchResult search(const std::string &url, BodyItems &result_items) override; - SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; - ImageResult get_number_of_images(const std::string &url, int &num_images) override; - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 200; } - Page get_page_after_search() const override { return Page::EPISODE_LIST; } + ManganeloSearchPage(Program *program) : Page(program) {} + const char* get_title() const override { return "All"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + private: + bool extract_id_from_url(const std::string &url, std::string &manga_id) const; + }; - ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override; + class ManganeloChaptersPage : public MangaChaptersPage { + public: + ManganeloChaptersPage(Program *program, std::string manga_name, std::string manga_url) : MangaChaptersPage(program, std::move(manga_name), std::move(manga_url)) {} + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + }; - bool extract_id_from_url(const std::string &url, std::string &manga_id) override; + class ManganeloCreatorPage : public Page { + public: + ManganeloCreatorPage(Program *program, Creator creator) : Page(program), creator(std::move(creator)) {} + const char* get_title() const override { return creator.name.c_str(); } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + private: + Creator creator; + }; + + class ManganeloImagesPage : public MangaImagesPage { + public: + ManganeloImagesPage(Program *program, std::string manga_name, std::string chapter_name, std::string url) : MangaImagesPage(program, std::move(manga_name), std::move(chapter_name), std::move(url)) {} - PluginResult get_creators_manga_list(const std::string &url, BodyItems &result_items) override; + ImageResult get_number_of_images(int &num_images) override; + ImageResult for_each_page_in_chapter(PageCallback callback) override; + + const char* get_service_name() const override { return "manganelo"; } private: - // Caches url. If the same url is requested multiple times then the cache is used + // Cached ImageResult get_image_urls_for_chapter(const std::string &url); - private: - std::string last_chapter_url; - std::vector<std::string> last_chapter_image_urls; - std::mutex image_urls_mutex; }; }
\ No newline at end of file diff --git a/plugins/Mangatown.hpp b/plugins/Mangatown.hpp index 2a22ccd..c85b5b7 100644 --- a/plugins/Mangatown.hpp +++ b/plugins/Mangatown.hpp @@ -2,26 +2,34 @@ #include "Manga.hpp" #include <functional> -#include <mutex> namespace QuickMedia { - class Mangatown : public Manga { + class MangatownSearchPage : public Page { public: - Mangatown() : Manga("mangatown") {} - SearchResult search(const std::string &url, BodyItems &result_items) override; - SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; - ImageResult get_number_of_images(const std::string &url, int &num_images) override; - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 200; } - Page get_page_after_search() const override { return Page::EPISODE_LIST; } + MangatownSearchPage(Program *program) : Page(program) {} + const char* get_title() const override { return "All"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + private: + bool extract_id_from_url(const std::string &url, std::string &manga_id) const; + }; + + class MangatownChaptersPage : public MangaChaptersPage { + public: + MangatownChaptersPage(Program *program, std::string manga_name, std::string manga_url) : MangaChaptersPage(program, std::move(manga_name), std::move(manga_url)) {} + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + }; + + class MangatownImagesPage : public MangaImagesPage { + public: + MangatownImagesPage(Program *program, std::string manga_name, std::string chapter_name, std::string url) : MangaImagesPage(program, std::move(manga_name), std::move(chapter_name), std::move(url)) {} - ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override; + ImageResult get_number_of_images(int &num_images) override; + ImageResult for_each_page_in_chapter(PageCallback callback) override; - bool extract_id_from_url(const std::string &url, std::string &manga_id) override; + const char* get_service_name() const override { return "mangatown"; } private: - std::string last_chapter_url_num_images; - int last_num_pages = 0; - std::mutex image_urls_mutex; + ImageResult get_image_urls_for_chapter(const std::string &url); }; }
\ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 4d7bc11..5819420 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -2,10 +2,27 @@ #include "../include/FileAnalyzer.hpp" #include "Plugin.hpp" +#include "Page.hpp" +#include <SFML/Graphics/Color.hpp> #include <unordered_map> +#include <mutex> #include <json/value.h> namespace QuickMedia { + // Dummy, only play one video. TODO: Play all videos in room, as related videos? + class MatrixVideoPage : public Page { + public: + MatrixVideoPage(Program *program) : Page(program) {} + const char* get_title() const override { return ""; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::ERR; + } + bool is_video_page() const override { return true; } + }; + struct RoomData; struct UserInfo { @@ -91,22 +108,13 @@ namespace QuickMedia { using RoomSyncMessages = std::unordered_map<std::shared_ptr<RoomData>, std::vector<std::shared_ptr<Message>>>; - class Matrix : public Plugin { + class Matrix { public: - Matrix(); - virtual ~Matrix() = default; - bool search_is_filter() override { return true; } - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 0; } - bool search_suggestion_is_search() const override { return true; } - Page get_page_after_search() const override { return Page::EXIT; } PluginResult sync(RoomSyncMessages &room_messages); PluginResult get_joined_rooms(BodyItems &result_items); PluginResult get_all_synced_room_messages(const std::string &room_id, BodyItems &result_items); PluginResult get_new_room_messages(const std::string &room_id, BodyItems &result_items); PluginResult get_previous_room_messages(const std::string &room_id, BodyItems &result_items); - SearchResult search(const std::string &text, BodyItems &result_items) override; // |url| should only be set when uploading media. // TODO: Make api better. @@ -139,6 +147,8 @@ namespace QuickMedia { PluginResult get_config(int *upload_size); std::shared_ptr<UserInfo> get_me(const std::string &room_id); + + bool use_tor = false; private: PluginResult sync_response_to_body_items(const Json::Value &root, RoomSyncMessages &room_messages); PluginResult get_previous_room_messages(std::shared_ptr<RoomData> &room_data); @@ -152,6 +162,7 @@ namespace QuickMedia { std::shared_ptr<RoomData> get_room_by_id(const std::string &id); void add_room(std::shared_ptr<RoomData> room); + DownloadResult download_json(Json::Value &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent = false, std::string *err_msg = nullptr) const; private: std::unordered_map<std::string, std::shared_ptr<RoomData>> room_data_by_id; std::mutex room_data_mutex; diff --git a/plugins/NyaaSi.hpp b/plugins/NyaaSi.hpp index 97b69e7..bad5863 100644 --- a/plugins/NyaaSi.hpp +++ b/plugins/NyaaSi.hpp @@ -1,29 +1,33 @@ #pragma once -#include "Plugin.hpp" -#include <thread> -#include <mutex> -#include <condition_variable> +#include "Page.hpp" namespace QuickMedia { - class Program; + class NyaaSiCategoryPage : public Page { + public: + NyaaSiCategoryPage(Program *program) : Page(program) {} + const char* get_title() const override { return "Select category"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + void get_categories(BodyItems &result_items); + }; + + class NyaaSiSearchPage : public Page { + public: + NyaaSiSearchPage(Program *program, std::string category_name, std::string category_id) : Page(program), category_name(std::move(category_name)), category_id(std::move(category_id)) {} + const char* get_title() const override { return category_name.c_str(); } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + + const std::string category_name; + const std::string category_id; + }; - class NyaaSi : public Plugin { + class NyaaSiTorrentPage : public Page { public: - NyaaSi(); - ~NyaaSi() override; - PluginResult get_front_page(BodyItems &result_items) override; - SearchResult content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) override; - SearchResult content_list_search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) override; - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 150; } - Page get_page_after_search() const override { return Page::CONTENT_LIST; } - bool search_is_filter() override { return true; } - bool content_list_search_is_filter() const override { return false; } - PluginResult get_content_list(const std::string &url, BodyItems &result_items) override; - PluginResult get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) override; - private: - SearchResult search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items); + NyaaSiTorrentPage(Program *program) : Page(program) {} + const char* get_title() const override { return "Torrent"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; }; }
\ No newline at end of file diff --git a/plugins/Page.hpp b/plugins/Page.hpp new file mode 100644 index 0000000..947f045 --- /dev/null +++ b/plugins/Page.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "Plugin.hpp" + +#include "../include/Tab.hpp" +#include "../include/SearchBar.hpp" +#include "../include/Body.hpp" + +namespace QuickMedia { + constexpr int SEARCH_DELAY_FILTER = 50; + + class Page { + public: + Page(Program *program) : program(program) {} + virtual ~Page() = default; + + virtual const char* get_title() const = 0; + virtual bool search_is_filter() { return true; } + // This show be overriden if search_is_filter is overriden to return false + virtual SearchResult search(const std::string &str, BodyItems &result_items) { (void)str; (void)result_items; return SearchResult::ERR; } + + // Return empty |result_tabs| and PluginResult::OK to do nothing; which is useful for implementing custom actions on item submit + virtual PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) = 0; + virtual PluginResult get_page(const std::string &str, int page, BodyItems &result_items) { (void)str; (void)page; (void)result_items; return PluginResult::OK; } + + virtual BodyItems get_related_media(const std::string &url); + + DownloadResult download_json(Json::Value &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent = false, std::string *err_msg = nullptr); + + virtual bool is_manga_images_page() const { return false; } + virtual bool is_image_board_thread_page() const { return false; } + virtual bool is_video_page() const { return false; } + // Mutually exclusive with |is_manga_images_page|, |is_image_board_thread_page| and |is_video_page| + virtual bool is_single_page() const { return false; } + virtual bool is_trackable() const { return false; } + + bool is_tor_enabled(); + std::unique_ptr<Body> create_body(); + std::unique_ptr<SearchBar> create_search_bar(const std::string &placeholder_text, int search_delay); + + bool load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id); + + Program *program; + }; + + enum class TrackResult { + OK, + ERR + }; + + class TrackablePage : public Page { + public: + TrackablePage(Program *program, std::string content_title, std::string content_url) : Page(program), content_title(std::move(content_title)), content_url(std::move(content_url)) {} + const char* get_title() const override { return content_title.c_str(); } + bool is_trackable() const override { return true; } + virtual TrackResult track(const std::string &str) = 0; + + const std::string content_title; + const std::string content_url; + }; +}
\ No newline at end of file diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index 438b4ea..1427233 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -1,12 +1,9 @@ #pragma once -#include "../include/Page.hpp" #include "../include/Body.hpp" -#include "../include/StringUtils.hpp" #include "../include/DownloadUtils.hpp" +#include <stddef.h> #include <string> -#include <vector> -#include <memory> namespace QuickMedia { enum class PluginResult { @@ -34,64 +31,17 @@ namespace QuickMedia { NET_ERR }; + struct BodyItemImageContext { + BodyItems *body_items; + size_t index; + }; + void html_escape_sequences(std::string &str); void html_unescape_sequences(std::string &str); + std::string url_param_encode(const std::string ¶m); - class Plugin { - public: - Plugin(const std::string &name) : name(name) {} - virtual ~Plugin() = default; - - virtual bool is_image_board() { return false; } - virtual bool is_manga() { return false; } - // Return true if searching should update the search result (which could be a remote server search) or filter the existing list locally - virtual bool search_is_filter() { return false; } - - virtual PluginResult get_front_page(BodyItems &result_items) { - (void)result_items; return PluginResult::OK; - } - virtual SearchResult search(const std::string &text, BodyItems &result_items); - virtual SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items); - virtual SearchResult content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items); - // TODO: Merge with above? - // page 0 is the first page - virtual SearchResult content_list_search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { - (void)list_url; - (void)text; - (void)page; - (void)result_items; - return SearchResult::OK; - } - virtual BodyItems get_related_media(const std::string &url); - virtual PluginResult get_content_list(const std::string &url, BodyItems &result_items) { - (void)url; - (void)result_items; - return PluginResult::OK; - } - virtual PluginResult get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) { - (void)list_url; - (void)url; - (void)result_items; - return PluginResult::OK; - } - virtual bool search_suggestions_has_thumbnails() const = 0; - virtual bool search_results_has_thumbnails() const = 0; - virtual std::string autocomplete_search(const std::string &query) { return query; } - virtual int get_autocomplete_delay() const { return 100; } - virtual int get_search_delay() const = 0; - virtual int get_content_list_search_delay() const { return 350; } - virtual bool search_suggestion_is_search() const { return false; } - virtual bool content_list_search_is_filter() const { return true; } - virtual Page get_page_after_search() const = 0; - - const std::string name; - bool use_tor = false; - protected: - std::string url_param_encode(const std::string ¶m) const; - DownloadResult download_json(Json::Value &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent = false, std::string *err_msg = nullptr) const; - SuggestionResult download_result_to_suggestion_result(DownloadResult download_result) const { return (SuggestionResult)download_result; } - PluginResult download_result_to_plugin_result(DownloadResult download_result) const { return (PluginResult)download_result; } - SearchResult download_result_to_search_result(DownloadResult download_result) const { return (SearchResult)download_result; } - ImageResult download_result_to_image_result(DownloadResult download_result) const { return (ImageResult)download_result; } - }; + SuggestionResult download_result_to_suggestion_result(DownloadResult download_result); + PluginResult download_result_to_plugin_result(DownloadResult download_result); + SearchResult download_result_to_search_result(DownloadResult download_result); + ImageResult download_result_to_image_result(DownloadResult download_result); }
\ No newline at end of file diff --git a/plugins/Pornhub.hpp b/plugins/Pornhub.hpp index 188a68e..195845f 100644 --- a/plugins/Pornhub.hpp +++ b/plugins/Pornhub.hpp @@ -1,17 +1,28 @@ #pragma once -#include "Plugin.hpp" +#include "Page.hpp" namespace QuickMedia { - class Pornhub : public Plugin { + class PornhubSearchPage : public Page { public: - Pornhub() : Plugin("pornhub") {} - SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; + PornhubSearchPage(Program *program) : Page(program) {} + const char* get_title() const override { return "All"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; + }; + + class PornhubVideoPage : public Page { + public: + PornhubVideoPage(Program *program) : Page(program) {} + const char* get_title() const override { return ""; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::ERR; + } BodyItems get_related_media(const std::string &url) override; - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 500; } - bool search_suggestion_is_search() const override { return true; } - Page get_page_after_search() const override { return Page::VIDEO_CONTENT; } + bool is_video_page() const override { return true; } }; }
\ No newline at end of file diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index 685d4b0..0922b9d 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -1,24 +1,30 @@ #pragma once -#include "Plugin.hpp" +#include "Page.hpp" namespace QuickMedia { - class Youtube : public Plugin { + class YoutubeSearchPage : public Page { public: - Youtube(); - PluginResult get_front_page(BodyItems &result_items) override; - SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; - BodyItems get_related_media(const std::string &url) override; - bool search_suggestions_has_thumbnails() const override { return true; } - bool search_results_has_thumbnails() const override { return false; } - std::string autocomplete_search(const std::string &query) override; - int get_search_delay() const override { return 350; } - bool search_suggestion_is_search() const override { return true; } - Page get_page_after_search() const override { return Page::VIDEO_CONTENT; } + YoutubeSearchPage(Program *program) : Page(program) {} + const char* get_title() const override { return "All"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override; private: void search_suggestions_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items); - std::vector<CommandArg> get_cookies() const; - private: - std::string last_autocomplete_result; + }; + + class YoutubeVideoPage : public Page { + public: + YoutubeVideoPage(Program *program) : Page(program) {} + const char* get_title() const override { return ""; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + (void)title; + (void)url; + (void)result_tabs; + return PluginResult::ERR; + } + BodyItems get_related_media(const std::string &url) override; + bool is_video_page() const override { return true; } }; }
\ No newline at end of file diff --git a/src/Body.cpp b/src/Body.cpp index 260e90d..ac63e0f 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -98,11 +98,6 @@ namespace QuickMedia { item_background.setFillColor(sf::Color(55, 60, 68)); } - Body::~Body() { - if(load_thumbnail_future.valid()) - load_thumbnail_future.get(); - } - // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_previous_page() { @@ -250,6 +245,17 @@ namespace QuickMedia { } } + void Body::clear_cache() { + clear_text_cache(); + clear_thumbnails(); + } + + void Body::clear_text_cache() { + for(auto &body_item : items) { + clear_body_item_cache(body_item.get()); + } + } + void Body::clear_thumbnails() { item_thumbnail_textures.clear(); } @@ -566,7 +572,7 @@ namespace QuickMedia { if(draw_thumbnails) { if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED) { - async_image_loader.load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size, program->get_current_plugin()->use_tor, item_thumbnail); + async_image_loader.load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size, program->is_tor_enabled(), item_thumbnail); } if(item_thumbnail->loading_state == LoadingState::FINISHED_LOADING && item_thumbnail->image->getSize().x > 0 && item_thumbnail->image->getSize().y > 0) { diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp index dc99028..c44bed5 100644 --- a/src/DownloadUtils.cpp +++ b/src/DownloadUtils.cpp @@ -8,6 +8,8 @@ static const bool debug_download = false; 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; } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 77c53b4..b544165 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -8,7 +8,7 @@ #include <SFML/Graphics/RectangleShape.hpp> namespace QuickMedia { - ImageViewer::ImageViewer(Manga *manga, const std::string &images_url, const std::string &content_title, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font) : + ImageViewer::ImageViewer(MangaImagesPage *manga_images_page, const std::string &content_title, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font) : current_page(current_page), num_pages(0), content_title(content_title), @@ -18,7 +18,7 @@ namespace QuickMedia { font(font), page_text("", *font, 14) { - if(manga->get_number_of_images(images_url, num_pages) != ImageResult::OK) { + if(manga_images_page->get_number_of_images(num_pages) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); return; } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 3922ceb..01e244a 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5,7 +5,6 @@ #include "../plugins/Youtube.hpp" #include "../plugins/Pornhub.hpp" #include "../plugins/Fourchan.hpp" -#include "../plugins/Dmenu.hpp" #include "../plugins/NyaaSi.hpp" #include "../plugins/Matrix.hpp" #include "../plugins/FileManager.hpp" @@ -166,13 +165,102 @@ static sf::Color interpolate_colors(sf::Color source, sf::Color target, double p } namespace QuickMedia { + class HistoryPage : public Page { + public: + HistoryPage(Program *program, Page *search_page) : Page(program), search_page(search_page) {} + const char* get_title() const override { return "History"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + return search_page->submit(title, url, result_tabs); + } + private: + Page *search_page; + }; + + class RecommendedPage : public Page { + public: + RecommendedPage(Program *program, Page *search_page) : Page(program), search_page(search_page) {} + const char* get_title() const override { return "Recommended"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override { + return search_page->submit(title, url, result_tabs); + } + private: + Page *search_page; + }; + + // TODO: Make asynchronous + static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) { + assert(recommended_json.isObject()); + + std::vector<std::pair<std::string, Json::Value>> recommended_items(recommended_json.size()); + /* TODO: Optimize member access */ + for(auto &member_name : recommended_json.getMemberNames()) { + Json::Value recommended_item = recommended_json[member_name]; + if(recommended_item.isObject()) + recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); + } + + /* TODO: Better algorithm for recommendations */ + std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &b) { + Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; + Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; + int64_t a_timestamp = 0; + int64_t b_timestamp = 0; + if(a_timestamp_json.isNumeric()) + a_timestamp = a_timestamp_json.asInt64(); + if(b_timestamp_json.isNumeric()) + b_timestamp = b_timestamp_json.asInt64(); + + Json::Value &a_recommended_count_json = a.second["recommended_count"]; + Json::Value &b_recommended_count_json = b.second["recommended_count"]; + int64_t a_recommended_count = 0; + int64_t b_recommended_count = 0; + if(a_recommended_count_json.isNumeric()) + a_recommended_count = a_recommended_count_json.asInt64(); + if(b_recommended_count_json.isNumeric()) + b_recommended_count = b_recommended_count_json.asInt64(); + + /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ + a_timestamp += (300 * a_recommended_count); + b_timestamp += (300 * b_recommended_count); + + return a_timestamp > b_timestamp; + }); + + for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { + const std::string &recommended_item_id = it->first; + Json::Value &recommended_item = it->second; + + int64_t watched_count = 0; + const Json::Value &watched_count_json = recommended_item["watched_count"]; + if(watched_count_json.isNumeric()) + watched_count = watched_count_json.asInt64(); + + /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ + if(watched_count != 0) + continue; + + const Json::Value &recommended_title_json = recommended_item["title"]; + if(!recommended_title_json.isString()) + continue; + + auto body_item = BodyItem::create(recommended_title_json.asString()); + body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; + body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/hqdefault.jpg"; + body_items.push_back(std::move(body_item)); + + // We dont want more than 150 recommendations + if(body_items.size() == 150) + break; + } + + std::random_shuffle(body_items.begin(), body_items.end()); + } + Program::Program() : disp(nullptr), window(sf::VideoMode(1280, 720), "QuickMedia", sf::Style::Default, sf::ContextSettings(0, 0, 0, 3, 3)), window_size(1280, 720), - body(nullptr), - current_plugin(nullptr), - current_page(Page::SEARCH_SUGGESTION), + current_page(PageType::EXIT), image_index(0) { disp = XOpenDisplay(NULL); @@ -223,10 +311,6 @@ namespace QuickMedia { abort(); } - body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - related_media_body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - related_media_body->draw_thumbnails = true; - struct sigaction action; action.sa_handler = sigpipe_handler; sigemptyset(&action.sa_mask); @@ -262,69 +346,29 @@ namespace QuickMedia { } else { running = false; } - if(related_media_body) - delete related_media_body; - if(body) - delete body; - if(file_manager) - delete file_manager; - if(current_plugin && current_plugin != file_manager) - delete current_plugin; + if(matrix) + delete matrix; if(disp) XCloseDisplay(disp); } - static SearchResult search_selected_suggestion(Body *input_body, Body *output_body, Plugin *plugin, std::string &selected_title, std::string &selected_url, bool skip_search) { - BodyItem *selected_item = input_body->get_selected(); - if(!selected_item) - return SearchResult::ERR; - - selected_title = selected_item->get_title(); - selected_url = selected_item->url; - if(!skip_search) { - output_body->clear_items(); - SearchResult search_result = plugin->search(!selected_url.empty() ? selected_url : selected_title, output_body->items); - output_body->reset_selected(); - return search_result; - } else { - return SearchResult::OK; - } - } - static void usage() { fprintf(stderr, "usage: QuickMedia <plugin> [--tor] [--no-video] [--use-system-mpv-config] [--dir <directory>] [-p <placeholder-text>]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si, matrix, file-manager or dmenu\n"); + fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si, matrix, file-manager\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --tor Use tor. Disabled by default\n"); fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n"); fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n"); fprintf(stderr, " --upscale-images-force Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n"); fprintf(stderr, " --dir Set the start directory when using file-manager\n"); - fprintf(stderr, " -p Change the placeholder text for dmenu\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, "QuickMedia manganelo\n"); fprintf(stderr, "QuickMedia youtube --tor\n"); - fprintf(stderr, "echo \"hello\\nworld\" | QuickMedia dmenu\n"); } - static bool is_program_executable_by_name(const char *name) { - // TODO: Implement for Windows. Windows also uses semicolon instead of colon as a separator - char *env = getenv("PATH"); - std::unordered_set<std::string> paths; - string_split(env, ':', [&paths](const char *str, size_t size) { - paths.insert(std::string(str, size)); - return true; - }); - - for(const std::string &path_str : paths) { - Path path(path_str); - path.join(name); - if(get_file_type(path) == FileType::REGULAR) - return true; - } - - return false; + static bool is_manga_plugin(const char *plugin_name) { + return strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangadex") == 0; } int Program::run(int argc, char **argv) { @@ -333,45 +377,40 @@ namespace QuickMedia { return -1; } - current_plugin = nullptr; std::string plugin_logo_path; - std::string search_placeholder; const char *start_dir = nullptr; + std::vector<Tab> tabs; for(int i = 1; i < argc; ++i) { - if(!current_plugin) { + if(!plugin_name) { if(strcmp(argv[i], "manganelo") == 0) { - current_plugin = new Manganelo(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/manganelo_logo.png"; } else if(strcmp(argv[i], "mangatown") == 0) { - current_plugin = new Mangatown(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/mangatown_logo.png"; } else if(strcmp(argv[i], "mangadex") == 0) { - current_plugin = new Mangadex(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/mangadex_logo.png"; } else if(strcmp(argv[i], "youtube") == 0) { - current_plugin = new Youtube(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/yt_logo_rgb_dark_small.png"; } else if(strcmp(argv[i], "pornhub") == 0) { - current_plugin = new Pornhub(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/pornhub_logo.png"; + plugin_name = argv[i]; } else if(strcmp(argv[i], "4chan") == 0) { - current_plugin = new Fourchan(resources_root); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/4chan_logo.png"; } else if(strcmp(argv[i], "nyaa.si") == 0) { - current_plugin = new NyaaSi(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/nyaa_si_logo.png"; } else if(strcmp(argv[i], "matrix") == 0) { - current_plugin = new Matrix(); + plugin_name = argv[i]; + matrix = new Matrix(); plugin_logo_path = resources_root + "images/matrix_logo.png"; } else if(strcmp(argv[i], "file-manager") == 0) { - current_plugin = new FileManager(); - } else if(strcmp(argv[i], "dmenu") == 0) { - current_plugin = new Dmenu(); - } else { - fprintf(stderr, "Invalid plugin %s\n", argv[i]); - usage(); - return -1; + plugin_name = argv[i]; } } @@ -390,11 +429,6 @@ namespace QuickMedia { start_dir = argv[i + 1]; ++i; } - } else if(strcmp(argv[i], "-p") == 0) { - if(i < argc - 1) { - search_placeholder = argv[i + 1]; - ++i; - } } else if(argv[i][0] == '-') { fprintf(stderr, "Invalid option %s\n", argv[i]); usage(); @@ -402,43 +436,19 @@ namespace QuickMedia { } } - if(!current_plugin) { + if(!plugin_name) { fprintf(stderr, "Missing plugin argument\n"); usage(); return -1; } - if(!search_placeholder.empty() && current_plugin->name == "dmenu") { - fprintf(stderr, "Option -p is only valid with dmenu\n"); - usage(); - return -1; - } - - if(current_plugin->name == "file-manager") { - current_page = Page::FILE_MANAGER; - file_manager = static_cast<FileManager*>(current_plugin); - } else { - if(start_dir) { - fprintf(stderr, "Option --dir is only valid with file-manager\n"); - usage(); - return -1; - } - } - - if(start_dir) { - if(!static_cast<FileManager*>(current_plugin)->set_current_directory(start_dir)) { - fprintf(stderr, "Invalid directory provided with --dir: %s\n", start_dir); - return -3; - } - } - if(use_tor && !is_program_executable_by_name("torsocks")) { fprintf(stderr, "torsocks needs to be installed (and accessible from PATH environment variable) when using the --tor option\n"); return -2; } if(upscale_image_action != UpscaleImageAction::NO) { - if(!current_plugin->is_manga()) { + if(!is_manga_plugin(plugin_name)) { fprintf(stderr, "Option --upscale-images/-upscale-images-force is only valid for manganelo, mangatown and mangadex\n"); return -2; } @@ -485,8 +495,13 @@ namespace QuickMedia { running = true; } - current_plugin->use_tor = use_tor; - window.setTitle("QuickMedia - " + current_plugin->name); + if(strcmp(plugin_name, "file-manager") != 0 && start_dir) { + fprintf(stderr, "Option --dir is only valid with file-manager\n"); + usage(); + return -1; + } + + window.setTitle("QuickMedia - " + std::string(plugin_name)); if(!plugin_logo_path.empty()) { if(!plugin_logo.loadFromFile(plugin_logo_path)) { @@ -497,99 +512,94 @@ namespace QuickMedia { plugin_logo.setSmooth(true); } - if(current_plugin->name == "matrix") { - Matrix *matrix = static_cast<Matrix*>(current_plugin); - if(matrix->load_and_verify_cached_session() == PluginResult::OK) { - current_page = Page::CHAT; - } else { - fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); - current_page = Page::CHAT_LOGIN; + if(strcmp(plugin_name, "manganelo") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<ManganeloSearchPage>(this), create_search_bar("Search...", 200)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mangatown") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<MangatownSearchPage>(this), create_search_bar("Search...", 200)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mangadex") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<MangadexSearchPage>(this), create_search_bar("Search...", 300)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "nyaa.si") == 0) { + auto category_page = std::make_unique<NyaaSiCategoryPage>(this); + auto categories_body = create_body(); + category_page->get_categories(categories_body->items); + tabs.push_back(Tab{std::move(categories_body), std::move(category_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "4chan") == 0) { + auto boards_page = std::make_unique<FourchanBoardsPage>(this, resources_root); + auto boards_body = create_body(); + boards_page->get_boards(boards_body->items); + tabs.push_back(Tab{std::move(boards_body), std::move(boards_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "file-manager") == 0) { + auto file_manager_page = std::make_unique<FileManagerPage>(this); + if(start_dir && !file_manager_page->set_current_directory(start_dir)) { + fprintf(stderr, "Invalid directory provided with --dir: %s\n", start_dir); + return -3; } + auto file_manager_body = create_body(); + file_manager_page->get_files_in_directory(file_manager_body->items); + tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "youtube") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<YoutubeSearchPage>(this), create_search_bar("Search...", 350)}); + + auto history_body = create_body(); + history_body->draw_thumbnails = true; + youtube_get_watch_history(history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique<HistoryPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + auto recommended_body = create_body(); + recommended_body->draw_thumbnails = true; + fill_recommended_items_from_json(load_recommended_json(), recommended_body->items); + tabs.push_back(Tab{std::move(recommended_body), std::make_unique<RecommendedPage>(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "pornhub") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique<PornhubSearchPage>(this), create_search_bar("Search...", 500)}); } - if(search_placeholder.empty()) - search_placeholder = "Search..."; + if(!tabs.empty()) { + page_loop(std::move(tabs)); + return exit_code; + } - search_bar = std::make_unique<SearchBar>(*font, &plugin_logo, search_placeholder); - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); + if(matrix) { + matrix->use_tor = use_tor; + if(matrix->load_and_verify_cached_session() == PluginResult::OK) { + current_page = PageType::CHAT; + } else { + fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); + current_page = PageType::CHAT_LOGIN; + } - while(window.isOpen()) { - switch(current_page) { - case Page::EXIT: - window.close(); - break; - case Page::SEARCH_SUGGESTION: - body->draw_thumbnails = current_plugin->search_suggestions_has_thumbnails(); - search_suggestion_page(); - body->clear_thumbnails(); - break; - case Page::VIDEO_CONTENT: - body->draw_thumbnails = false; - video_content_page(); - break; - case Page::EPISODE_LIST: - body->draw_thumbnails = false; - episode_list_page(); - body->clear_thumbnails(); - break; - case Page::IMAGES: { - body->draw_thumbnails = false; - window.setKeyRepeatEnabled(false); - window.setFramerateLimit(20); - image_page(); - body->filter_search_fuzzy(""); - if(vsync_set) - window.setFramerateLimit(0); - else - window.setFramerateLimit(monitor_hz); - window.setKeyRepeatEnabled(true); - break; - } - case Page::IMAGES_CONTINUOUS: { - body->draw_thumbnails = false; - window.setKeyRepeatEnabled(false); - image_continuous_page(); - body->filter_search_fuzzy(""); - window.setKeyRepeatEnabled(true); - break; - } - case Page::CONTENT_LIST: { - body->draw_thumbnails = true; - content_list_page(); - body->clear_thumbnails(); - break; - } - case Page::CONTENT_DETAILS: { - body->draw_thumbnails = true; - content_details_page(); - body->clear_thumbnails(); - break; - } - case Page::IMAGE_BOARD_THREAD_LIST: { - body->draw_thumbnails = true; - image_board_thread_list_page(); - body->clear_thumbnails(); - break; - } - case Page::IMAGE_BOARD_THREAD: { - body->draw_thumbnails = true; - image_board_thread_page(); - body->clear_thumbnails(); - break; - } - case Page::CHAT_LOGIN: { - chat_login_page(); - break; - } - case Page::CHAT: { - body->draw_thumbnails = true; - chat_page(); - break; - } - case Page::FILE_MANAGER: { - body->draw_thumbnails = true; - file_manager_page(); - break; + while(window.isOpen()) { + switch(current_page) { + case PageType::CHAT_LOGIN: + chat_login_page(); + break; + case PageType::CHAT: + chat_page(); + break; + default: + window.close(); + break; } } } @@ -597,9 +607,10 @@ namespace QuickMedia { return exit_code; } - void Program::base_event_handler(sf::Event &event, Page previous_page, bool handle_keypress, bool clear_on_escape, bool handle_searchbar) { + void Program::base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_keypress, bool handle_searchbar) { if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -620,13 +631,9 @@ namespace QuickMedia { body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { current_page = previous_page; - if(clear_on_escape) { - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } } } else if(handle_searchbar) { + assert(search_bar); if(event.type == sf::Event::TextEntered) search_bar->onTextEntered(event.text.unicode); search_bar->on_event(event); @@ -707,76 +714,7 @@ namespace QuickMedia { } } - // TODO: Make asynchronous - static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) { - assert(recommended_json.isObject()); - - std::vector<std::pair<std::string, Json::Value>> recommended_items(recommended_json.size()); - /* TODO: Optimize member access */ - for(auto &member_name : recommended_json.getMemberNames()) { - Json::Value recommended_item = recommended_json[member_name]; - if(recommended_item.isObject()) - recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); - } - - /* TODO: Better algorithm for recommendations */ - std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &b) { - Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; - Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; - int64_t a_timestamp = 0; - int64_t b_timestamp = 0; - if(a_timestamp_json.isNumeric()) - a_timestamp = a_timestamp_json.asInt64(); - if(b_timestamp_json.isNumeric()) - b_timestamp = b_timestamp_json.asInt64(); - - Json::Value &a_recommended_count_json = a.second["recommended_count"]; - Json::Value &b_recommended_count_json = b.second["recommended_count"]; - int64_t a_recommended_count = 0; - int64_t b_recommended_count = 0; - if(a_recommended_count_json.isNumeric()) - a_recommended_count = a_recommended_count_json.asInt64(); - if(b_recommended_count_json.isNumeric()) - b_recommended_count = b_recommended_count_json.asInt64(); - - /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ - a_timestamp += (300 * a_recommended_count); - b_timestamp += (300 * b_recommended_count); - - return a_timestamp > b_timestamp; - }); - - for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { - const std::string &recommended_item_id = it->first; - Json::Value &recommended_item = it->second; - - int64_t watched_count = 0; - const Json::Value &watched_count_json = recommended_item["watched_count"]; - if(watched_count_json.isNumeric()) - watched_count = watched_count_json.asInt64(); - - /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ - if(watched_count != 0) - continue; - - const Json::Value &recommended_title_json = recommended_item["title"]; - if(!recommended_title_json.isString()) - continue; - - auto body_item = BodyItem::create(recommended_title_json.asString()); - body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; - body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/hqdefault.jpg"; - body_items.push_back(std::move(body_item)); - - // We dont want more than 150 recommendations - if(body_items.size() == 150) - break; - } - - std::random_shuffle(body_items.begin(), body_items.end()); - } - - static Path get_video_history_filepath(Plugin *plugin) { + static Path get_video_history_filepath(const char *plugin_name) { Path video_history_dir = get_storage_dir().join("history"); if(create_directory_recursive(video_history_dir) != 0) { std::string err_msg = "Failed to create video history directory "; @@ -786,10 +724,10 @@ namespace QuickMedia { } Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin->name).append(".json"); + return video_history_filepath.join(plugin_name).append(".json"); } - static Path get_recommended_filepath(Plugin *plugin) { + static Path get_recommended_filepath(const char *plugin_name) { Path video_history_dir = get_storage_dir().join("recommended"); if(create_directory_recursive(video_history_dir) != 0) { std::string err_msg = "Failed to create recommended directory "; @@ -799,13 +737,13 @@ namespace QuickMedia { } Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin->name).append(".json"); + return video_history_filepath.join(plugin_name).append(".json"); } // This is not cached because we could have multiple instances of QuickMedia running the same plugin! // TODO: Find a way to optimize this - Json::Value Program::load_video_history_json(Plugin *plugin) { - Path video_history_filepath = get_video_history_filepath(plugin); + Json::Value Program::load_video_history_json() { + Path video_history_filepath = get_video_history_filepath(plugin_name); Json::Value json_result; if(!read_file_as_json(video_history_filepath, json_result) || !json_result.isArray()) json_result = Json::Value(Json::arrayValue); @@ -814,66 +752,62 @@ namespace QuickMedia { // This is not cached because we could have multiple instances of QuickMedia running the same plugin! // TODO: Find a way to optimize this - Json::Value Program::load_recommended_json(Plugin *plugin) { - Path recommended_filepath = get_recommended_filepath(plugin); + Json::Value Program::load_recommended_json() { + Path recommended_filepath = get_recommended_filepath(plugin_name); Json::Value json_result; if(!read_file_as_json(recommended_filepath, json_result) || !json_result.isObject()) json_result = Json::Value(Json::objectValue); return json_result; } - void Program::plugin_get_watch_history(Plugin *plugin, BodyItems &history_items) { + void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items) { // TOOD: Make generic, instead of checking for plugin - if(plugin->is_manga()) { - Path content_storage_dir = get_storage_dir().join(plugin->name); - if(create_directory_recursive(content_storage_dir) != 0) { - show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); - exit(1); - } - Path credentials_storage_dir = get_storage_dir().join("credentials"); - if(create_directory_recursive(credentials_storage_dir) != 0) { - show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); - exit(1); - } - // TODO: Make asynchronous - for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin](const std::filesystem::path &filepath) { - // This can happen when QuickMedia crashes/is killed while writing to storage. - // In that case, the storage wont be corrupt but there will be .tmp files. - // TODO: Remove these .tmp files if they exist during startup - if(filepath.extension() == ".tmp") - return true; - - Path fullpath(filepath.c_str()); - Json::Value body; - if(!read_file_as_json(fullpath, body)) { - fprintf(stderr, "Failed to read json file: %s\n", fullpath.data.c_str()); - return true; - } + Path content_storage_dir = get_storage_dir().join(plugin_name); + if(create_directory_recursive(content_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); + exit(1); + } + Path credentials_storage_dir = get_storage_dir().join("credentials"); + if(create_directory_recursive(credentials_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); + exit(1); + } + // TODO: Make asynchronous + for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin_name](const std::filesystem::path &filepath) { + // This can happen when QuickMedia crashes/is killed while writing to storage. + // In that case, the storage wont be corrupt but there will be .tmp files. + // TODO: Remove these .tmp files if they exist during startup + if(filepath.extension() == ".tmp") + return true; - auto filename = filepath.filename(); - const Json::Value &manga_name = body["name"]; - if(!filename.empty() && manga_name.isString()) { - // TODO: Add thumbnail - auto body_item = BodyItem::create(manga_name.asString()); - if(plugin->name == "manganelo") - body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); - else if(plugin->name == "mangadex") - body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); - else if(plugin->name == "mangatown") - body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); - else - fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); - history_items.push_back(std::move(body_item)); - } + Path fullpath(filepath.c_str()); + Json::Value body; + if(!read_file_as_json(fullpath, body) || !body.isObject()) { + fprintf(stderr, "Failed to read json file: %s\n", fullpath.data.c_str()); return true; - }); - return; - } + } - if(plugin->name != "youtube") - return; + auto filename = filepath.filename(); + const Json::Value &manga_name = body["name"]; + if(!filename.empty() && manga_name.isString()) { + // TODO: Add thumbnail + auto body_item = BodyItem::create(manga_name.asString()); + if(strcmp(plugin_name, "manganelo") == 0) + body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "mangadex") == 0) + body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "mangatown") == 0) + body_item->url = "https://mangatown.com/manga/" + base64_decode(filename.string()); + else + fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n"); + history_items.push_back(std::move(body_item)); + } + return true; + }); + } - fill_history_items_from_json(load_video_history_json(plugin), history_items); + void Program::youtube_get_watch_history(BodyItems &history_items) { + fill_history_items_from_json(load_video_history_json(), history_items); } static void get_body_dimensions(const sf::Vector2f &window_size, SearchBar *search_bar, sf::Vector2f &body_pos, sf::Vector2f &body_size, bool has_tabs = false) { @@ -890,7 +824,7 @@ namespace QuickMedia { if(!has_tabs) tab_h = 0.0f; - float search_bottom = search_bar->getBottomWithoutShadow(); + float search_bottom = search_bar ? search_bar->getBottomWithoutShadow() : 0.0f; body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical + tab_h); body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_h); } @@ -922,173 +856,189 @@ namespace QuickMedia { std::unique_ptr<SearchBar> password; }; - struct Tab { - Body *body; - std::unique_ptr<LoginTab> login_tab; - SearchSuggestionTab tab; - sf::Text *text; - }; - - bool Program::on_search_suggestion_submit_text(Body *input_body, Body *output_body) { - if(input_body->no_items_visible()) - return false; - - Page next_page = current_plugin->get_page_after_search(); - bool skip_search = (next_page == Page::VIDEO_CONTENT || next_page == Page::CONTENT_LIST); - // TODO: This shouldn't be done if search_selected_suggestion fails - if(search_selected_suggestion(input_body, output_body, current_plugin, content_title, content_url, skip_search) != SearchResult::OK) { - show_notification("Search", "Search failed!", Urgency::CRITICAL); - return false; - } + bool Program::is_tor_enabled() { + return use_tor; + } - if(next_page == Page::EPISODE_LIST && current_plugin->is_manga()) { - Manga *manga_plugin = static_cast<Manga*>(current_plugin); - if(content_url.empty()) { - show_notification("Manga", "Url is missing for manga!", Urgency::CRITICAL); - return false; - } - - Path content_storage_dir = get_storage_dir().join(current_plugin->name); + std::unique_ptr<Body> Program::create_body() { + return std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get()); + } - std::string manga_id; - if(!manga_plugin->extract_id_from_url(content_url, manga_id)) - return false; + std::unique_ptr<SearchBar> Program::create_search_bar(const std::string &placeholder, int search_delay) { + auto search_bar = std::make_unique<SearchBar>(*font, &plugin_logo, placeholder); + search_bar->text_autosearch_delay = search_delay; + return search_bar; + } - manga_id_base64 = base64_encode(manga_id); - content_storage_file = content_storage_dir.join(manga_id_base64); - content_storage_json.clear(); - content_storage_json["name"] = content_title; - FileType file_type = get_file_type(content_storage_file); - if(file_type == FileType::REGULAR) - read_file_as_json(content_storage_file, content_storage_json); - } else if(next_page == Page::VIDEO_CONTENT) { - watched_videos.clear(); - if(content_url.empty()) - next_page = Page::SEARCH_SUGGESTION; - else { - page_stack.push(Page::SEARCH_SUGGESTION); - } - current_page = next_page; + bool Program::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) { + Path content_storage_dir = get_storage_dir().join(service_name); + manga_id_base64 = base64_encode(manga_id); + content_storage_file = content_storage_dir.join(manga_id_base64); + content_storage_json.clear(); + content_storage_json["name"] = manga_title; + FileType file_type = get_file_type(content_storage_file); + if(file_type == FileType::REGULAR) { + if(read_file_as_json(content_storage_file, content_storage_json) && content_storage_json.isObject()) + return true; return false; - } else if(next_page == Page::CONTENT_LIST) { - content_list_url = content_url; - } else if(next_page == Page::IMAGE_BOARD_THREAD_LIST) { - image_board_thread_list_url = content_url; + } else { + return true; } - current_page = next_page; - return true; } - void Program::search_suggestion_page() { - std::string update_search_text; - bool search_text_updated = false; - bool search_running = false; - bool typing = false; - bool is_fourchan = current_plugin->name == "4chan"; - - std::string autocomplete_text; - bool autocomplete_running = false; - - Body history_body(this, font.get(), bold_font.get(), cjk_font.get()); - std::unique_ptr<Body> recommended_body; - sf::Text all_tab_text("All", *font, tab_text_size); - sf::Text history_tab_text("History", *font, tab_text_size); - sf::Text recommended_tab_text("Recommended", *font, tab_text_size); - sf::Text login_tab_text("Login", *font, tab_text_size); - SearchBar *focused_login_input = nullptr; - - if(current_plugin->name == "youtube") { - recommended_body = std::make_unique<Body>(this, font.get(), bold_font.get(), cjk_font.get()); - recommended_body->draw_thumbnails = true; - fill_recommended_items_from_json(load_recommended_json(current_plugin), recommended_body->items); + void Program::select_file(const std::string &filepath) { + puts(filepath.c_str()); + selected_files.push_back(filepath); + } + + void Program::page_loop(std::vector<Tab> tabs) { + if(tabs.empty()) { + show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL); + return; } - std::vector<Tab> tabs; - int selected_tab = 0; + const Json::Value *json_chapters = &Json::Value::nullSingleton(); + if(content_storage_json.isObject()) { + const Json::Value &chapters_json = content_storage_json["chapters"]; + if(chapters_json.isObject()) + json_chapters = &chapters_json; + } - auto login_submit_callback = [this, &tabs, &selected_tab](const std::string&) -> bool { - if(!tabs[selected_tab].body) { - std::string username = tabs[selected_tab].login_tab->username->get_text(); - std::string password = tabs[selected_tab].login_tab->password->get_text(); - if(current_plugin->name == "4chan") { - std::string response_msg; - PluginResult result = static_cast<Fourchan*>(current_plugin)->login(username, password, response_msg); - if(result == PluginResult::NET_ERR) { - show_notification("4chan", "Login failed!", Urgency::CRITICAL); - } else if(result == PluginResult::ERR) { - std::string desc = "Login failed, reason: "; - if(response_msg.empty()) - desc += "Unknown"; - else - desc += response_msg; - show_notification("4chan", desc, Urgency::CRITICAL); - } else if(result == PluginResult::OK) { - show_notification("4chan", "Successfully logged in!", Urgency::LOW); - selected_tab = 0; - } - } - } - return false; + struct TabAssociatedData { + std::string update_search_text; + bool search_text_updated = false; + bool search_running = false; + bool typing = false; + bool fetching_next_page_running = false; + int fetched_page = 0; + sf::Text search_result_text; + std::future<BodyItems> search_future; + std::future<BodyItems> next_page_future; }; - tabs.push_back(Tab{body, nullptr, SearchSuggestionTab::ALL, &all_tab_text}); - tabs.push_back(Tab{&history_body, nullptr, SearchSuggestionTab::HISTORY, &history_tab_text}); - if(recommended_body) - tabs.push_back(Tab{recommended_body.get(), nullptr, SearchSuggestionTab::RECOMMENDED, &recommended_tab_text}); - if(is_fourchan) { - tabs.push_back(Tab{nullptr, std::make_unique<LoginTab>(*font), SearchSuggestionTab::LOGIN, &login_tab_text}); - focused_login_input = tabs.back().login_tab->username.get(); + std::vector<TabAssociatedData> tab_associated_data; + for(size_t i = 0; i < tabs.size(); ++i) { + TabAssociatedData data; + data.search_result_text = sf::Text("", *font, 30); + tab_associated_data.push_back(std::move(data)); + } - tabs.back().login_tab->username->caret_visible = true; - tabs.back().login_tab->password->caret_visible = false; + //std::string autocomplete_text; + //bool autocomplete_running = false; - tabs.back().login_tab->username->onTextSubmitCallback = login_submit_callback; - tabs.back().login_tab->password->onTextSubmitCallback = login_submit_callback; - } + double gradient_inc = 0.0; + const float gradient_height = 5.0f; + sf::Vertex gradient_points[4]; - plugin_get_watch_history(current_plugin, history_body.items); - if(current_plugin->name == "youtube") - history_body.draw_thumbnails = true; + sf::Text tab_text("", *font, tab_text_size); + int selected_tab = 0; - search_bar->onTextBeginTypingCallback = [&typing]() { - typing = true; - }; + bool loop_running = true; - search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); - search_bar->onAutocompleteRequestCallback = [this, &tabs, &selected_tab, &autocomplete_text](const std::string &text) { - if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) - autocomplete_text = text; - }; + auto submit_handler = [this, &tabs, &selected_tab, &loop_running]() { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(!selected_item) + return; + + std::vector<Tab> new_tabs; + PluginResult submit_result = tabs[selected_tab].page->submit(selected_item->get_title(), selected_item->url, new_tabs); + if(submit_result == PluginResult::OK) { + if(tabs[selected_tab].page->is_single_page()) { + tabs[selected_tab].search_bar->clear(); + if(new_tabs.size() == 1) + tabs[selected_tab].body = std::move(new_tabs[0].body); + else + loop_running = false; + return; + } + + if(new_tabs.empty()) + return; - std::string recommended_filter; + for(Tab &tab : tabs) { + tab.body->clear_cache(); + } - search_bar->onTextUpdateCallback = [&update_search_text, &search_text_updated, this, &tabs, &selected_tab, &typing, &recommended_body, &recommended_filter](const std::string &text) { - if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) { - update_search_text = text; - search_text_updated = true; + if(new_tabs.size() == 1 && new_tabs[0].page->is_manga_images_page()) { + select_episode(selected_item, false); + Body *chapters_body = tabs[selected_tab].body.get(); + chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter + MangaImagesPage *manga_images_page = static_cast<MangaImagesPage*>(new_tabs[0].page.get()); + window.setKeyRepeatEnabled(false); + while(true) { + if(current_page == PageType::IMAGES) { + window.setFramerateLimit(20); + while(current_page == PageType::IMAGES) { + int page_navigation = image_page(manga_images_page, chapters_body); + if(page_navigation == -1) { + // TODO: Make this work if the list is sorted differently than from newest to oldest. + chapters_body->select_next_item(); + select_episode(chapters_body->get_selected(), true); + image_index = 99999; // Start at the page that shows we are at the end of the chapter + manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); + } else if(page_navigation == 1) { + // TODO: Make this work if the list is sorted differently than from newest to oldest. + chapters_body->select_previous_item(); + select_episode(chapters_body->get_selected(), true); + manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); + } + } + if(vsync_set) + window.setFramerateLimit(0); + else + window.setFramerateLimit(monitor_hz); + } else if(current_page == PageType::IMAGES_CONTINUOUS) { + image_continuous_page(manga_images_page); + } else { + break; + } + } + window.setKeyRepeatEnabled(true); + } else if(new_tabs.size() == 1 && new_tabs[0].page->is_image_board_thread_page()) { + current_page = PageType::IMAGE_BOARD_THREAD; + image_board_thread_page(static_cast<ImageBoardThreadPage*>(new_tabs[0].page.get()), new_tabs[0].body.get()); + } else if(new_tabs.size() == 1 && new_tabs[0].page->is_video_page()) { + current_page = PageType::VIDEO_CONTENT; + video_content_page(new_tabs[0].page.get(), selected_item->url, selected_item->get_title()); + } else { + page_loop(std::move(new_tabs)); + } } else { - tabs[selected_tab].body->filter_search_fuzzy(text); - tabs[selected_tab].body->select_first_item(); + // TODO: Show the exact cause of error (get error message from curl). + // TODO: Make asynchronous + show_notification("QuickMedia", std::string("Submit failed for page ") + tabs[selected_tab].page->get_title(), Urgency::CRITICAL); } - if(tabs[selected_tab].body == recommended_body.get()) - recommended_filter = text; - typing = false; }; - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string&) -> bool { - if(current_plugin->name != "dmenu") { - if(typing || tabs[selected_tab].body->no_items_visible()) - return false; - } - return on_search_suggestion_submit_text(tabs[selected_tab].body, body); - }; + for(size_t i = 0; i < tabs.size(); ++i) { + Tab &tab = tabs[i]; + TabAssociatedData &associated_data = tab_associated_data[i]; + if(!tab.search_bar) + continue; - if(current_plugin->get_front_page(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "Failed to get front page", Urgency::CRITICAL); - current_page = Page::EXIT; - return; + // tab.search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); + // tab.search_bar->onAutocompleteRequestCallback = [this, &tabs, &selected_tab, &autocomplete_text](const std::string &text) { + // if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) + // autocomplete_text = text; + // }; + + tab.search_bar->onTextUpdateCallback = [&associated_data, &tabs, i](const std::string &text) { + if(!tabs[i].page->search_is_filter()) { + associated_data.update_search_text = text; + associated_data.search_text_updated = true; + } else { + tabs[i].body->filter_search_fuzzy(text); + tabs[i].body->select_first_item(); + } + associated_data.typing = false; + }; + + tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string&) { + if(associated_data.typing) + return; + submit_handler(); + }; } - body->clamp_selection(); sf::Vector2f body_pos; sf::Vector2f body_size; @@ -1103,149 +1053,247 @@ namespace QuickMedia { sf::RoundedRectangleShape tab_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10); tab_background.setFillColor(tab_selected_color); - while (current_page == Page::SEARCH_SUGGESTION) { + sf::Clock frame_timer; + + while (window.isOpen() && loop_running) { + sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); + while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, true, tabs[selected_tab].body != nullptr); + if (event.type == sf::Event::Closed) { + window.close(); + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + } + + if(tabs[selected_tab].search_bar) { + if(event.type == sf::Event::TextEntered) + tabs[selected_tab].search_bar->onTextEntered(event.text.unicode); + tabs[selected_tab].search_bar->on_event(event); + } + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) redraw = true; else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_next_item(); + if(event.key.code == sf::Keyboard::Down || event.key.code == sf::Keyboard::PageDown || event.key.code == sf::Keyboard::End) { + bool hit_bottom = false; + switch(event.key.code) { + case sf::Keyboard::Down: + hit_bottom = !tabs[selected_tab].body->select_next_item(); + break; + case sf::Keyboard::PageDown: + hit_bottom = !tabs[selected_tab].body->select_next_page(); + break; + case sf::Keyboard::End: + tabs[selected_tab].body->select_last_item(); + hit_bottom = true; + break; + default: + hit_bottom = false; + break; + } + if(hit_bottom && !tab_associated_data[selected_tab].search_running && !tab_associated_data[selected_tab].fetching_next_page_running && tabs[selected_tab].page) { + gradient_inc = 0.0; + tab_associated_data[selected_tab].fetching_next_page_running = true; + int next_page = tab_associated_data[selected_tab].fetched_page + 1; + Page *page = tabs[selected_tab].page.get(); + std::string update_search_text = tab_associated_data[selected_tab].update_search_text; + tab_associated_data[selected_tab].next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() { + BodyItems result_items; + if(page->get_page(update_search_text, next_page, result_items) != PluginResult::OK) + fprintf(stderr, "Failed to get next page (page %d)\n", next_page); + return result_items; + }); + } + } else if(event.key.code == sf::Keyboard::Up) { + tabs[selected_tab].body->select_previous_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::PageDown) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_next_page(); + tabs[selected_tab].body->select_previous_page(); } else if(event.key.code == sf::Keyboard::Home) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_first_item(); - } else if(event.key.code == sf::Keyboard::End) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_last_item(); + tabs[selected_tab].body->select_first_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - exit_code = 1; + goto page_end; } else if(event.key.code == sf::Keyboard::Left) { - if(tabs[selected_tab].body) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); + if(selected_tab > 0) { + tabs[selected_tab].body->clear_cache(); + --selected_tab; + redraw = true; } - selected_tab = std::max(0, selected_tab - 1); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Right) { - if(tabs[selected_tab].body) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); + if(selected_tab < (int)tabs.size() - 1) { + tabs[selected_tab].body->clear_cache(); + ++selected_tab; + redraw = true; } - selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Tab) { - if(tabs[selected_tab].body) search_bar->set_to_autocomplete(); - } - } - - if(!tabs[selected_tab].body) { - if(event.type == sf::Event::TextEntered) - focused_login_input->onTextEntered(event.text.unicode); - focused_login_input->on_event(event); - - if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Tab) { - focused_login_input->caret_visible = false; - if(focused_login_input == tabs[selected_tab].login_tab->username.get()) - focused_login_input = tabs[selected_tab].login_tab->password.get(); - else - focused_login_input = tabs[selected_tab].login_tab->username.get(); - focused_login_input->caret_visible = true; + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete(); + } else if(event.key.code == sf::Keyboard::Enter) { + if(!tabs[selected_tab].search_bar) submit_handler(); + } else if(event.key.code == sf::Keyboard::T && event.key.control) { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { + TrackablePage *trackable_page = static_cast<TrackablePage*>(tabs[selected_tab].page.get()); + TrackResult track_result = trackable_page->track(selected_item->get_title()); + // TODO: Show proper error message when this fails. For example if we are already tracking the manga + if(track_result == TrackResult::OK) { + show_notification("Media tracker", "You are now tracking \"" + trackable_page->content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); + } else { + show_notification("Media tracker", "Failed to track media \"" + trackable_page->content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); + } + } } } } if(redraw) { redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->onWindowResize(window_size); + // TODO: Dont show tabs if there is only one tab + get_body_dimensions(window_size, tabs[selected_tab].search_bar.get(), body_pos, body_size, true); + + gradient_points[0].position.x = 0.0f; + gradient_points[0].position.y = window_size.y - gradient_height; + + gradient_points[1].position.x = window_size.x; + gradient_points[1].position.y = window_size.y - gradient_height; + + gradient_points[2].position.x = window_size.x; + gradient_points[2].position.y = window_size.y; + + gradient_points[3].position.x = 0.0f; + gradient_points[3].position.y = window_size.y; } - if(tabs[selected_tab].body) - search_bar->update(); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update(); - if(search_text_updated && !search_running) { - search_suggestion_future = std::async(std::launch::async, [this, update_search_text]() { - BodyItems result; - if(current_plugin->update_search_suggestions(update_search_text, result) != SuggestionResult::OK) { - show_notification("Search", "Search failed!", Urgency::CRITICAL); + for(size_t i = 0; i < tabs.size(); ++i) { + TabAssociatedData &associated_data = tab_associated_data[i]; + + if(associated_data.fetching_next_page_running && associated_data.next_page_future.valid() && associated_data.next_page_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + BodyItems new_body_items = associated_data.next_page_future.get(); + fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", associated_data.fetched_page + 1, new_body_items.size()); + size_t num_new_messages = new_body_items.size(); + if(num_new_messages > 0) { + tabs[i].body->append_items(std::move(new_body_items)); + associated_data.fetched_page++; } - return result; - }); - update_search_text.clear(); - search_text_updated = false; - search_running = true; - } + associated_data.fetching_next_page_running = false; + } - if(search_running && search_suggestion_future.valid() && search_suggestion_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if(!search_text_updated) { - body->items = search_suggestion_future.get(); - body->select_first_item(); - } else { - search_suggestion_future.get(); + if(associated_data.search_text_updated && !associated_data.search_running && !associated_data.fetching_next_page_running) { + Page *page = tabs[i].page.get(); + std::string update_search_text = associated_data.update_search_text; + associated_data.search_future = std::async(std::launch::async, [update_search_text, page]() { + BodyItems result_items; + if(page->search(update_search_text, result_items) != SearchResult::OK) { + show_notification("QuickMedia", "Search failed!", Urgency::CRITICAL); + } + return result_items; + }); + update_search_text.clear(); + associated_data.search_text_updated = false; + associated_data.search_running = true; + associated_data.search_result_text.setString("Searching..."); + } + + if(associated_data.search_running && associated_data.search_future.valid() && associated_data.search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + if(!associated_data.search_text_updated) { + BodyItems result_items = associated_data.search_future.get(); + tabs[i].body->items = std::move(result_items); + tabs[i].body->select_first_item(); + if(tabs[i].body->items.empty()) + associated_data.search_result_text.setString("No results found"); + else + associated_data.search_result_text.setString(""); + } else { + associated_data.search_future.get(); + } + associated_data.search_running = false; } - search_running = false; } - if(!autocomplete_text.empty() && !autocomplete_running) { - autocomplete_future = std::async(std::launch::async, [this, autocomplete_text]() { - return current_plugin->autocomplete_search(autocomplete_text); - }); - autocomplete_text.clear(); - autocomplete_running = true; - } + // if(!autocomplete_text.empty() && !autocomplete_running) { + // autocomplete_future = std::async(std::launch::async, [this, autocomplete_text]() { + // return current_plugin->autocomplete_search(autocomplete_text); + // }); + // autocomplete_text.clear(); + // autocomplete_running = true; + // } - if(autocomplete_running && autocomplete_future.valid() && autocomplete_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - search_bar->set_autocomplete_text(autocomplete_future.get()); - autocomplete_running = false; - } + // if(autocomplete_running && autocomplete_future.valid() && autocomplete_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + // if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_autocomplete_text(autocomplete_future.get()); + // autocomplete_running = false; + // } window.clear(back_color); - if(tabs[selected_tab].body) - search_bar->draw(window, false); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, false); + { + float shade_extra_height = 0.0f; + if(!tabs[selected_tab].search_bar) + shade_extra_height = 10.0f; + const float width_per_tab = window_size.x / tabs.size(); tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - float tab_vertical_offset = search_bar->getBottomWithoutShadow(); - if(tabs[selected_tab].body) { - tabs[selected_tab].body->draw(window, body_pos, body_size); - } else { - tabs[selected_tab].login_tab->username->draw(window, false); - tabs[selected_tab].login_tab->password->draw(window, false); - tabs[selected_tab].login_tab->password->set_vertical_position(tabs[selected_tab].login_tab->username->getBottomWithoutShadow()); - tab_vertical_offset = tabs[selected_tab].login_tab->username->getBottomWithoutShadow() + tabs[selected_tab].login_tab->password->getBottomWithoutShadow(); - } - const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); + float tab_vertical_offset = tabs[selected_tab].search_bar ? tabs[selected_tab].search_bar->getBottomWithoutShadow() : 0.0f; + tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); + const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f) + shade_extra_height; tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); - tab_shade.setSize(sf::Vector2f(window_size.x, tab_height + 10.0f)); + tab_shade.setSize(sf::Vector2f(window_size.x, shade_extra_height + tab_height + 10.0f)); window.draw(tab_shade); int i = 0; + // TODO: Dont show tabs if there is only one tab for(Tab &tab : tabs) { if(i == selected_tab) { - tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset)); + tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset) + shade_extra_height); window.draw(tab_background); } const float center = (i * width_per_tab) + (width_per_tab * 0.5f); - tab.text->setPosition(std::floor(center - tab.text->getLocalBounds().width * 0.5f), tab_y); - window.draw(*tab.text); + // TODO: Optimize. Only set once for each tab! + tab_text.setString(tab.page->get_title()); + tab_text.setPosition(std::floor(center - tab_text.getLocalBounds().width * 0.5f), tab_y); + window.draw(tab_text); ++i; } } + if(tab_associated_data[selected_tab].fetching_next_page_running) { + double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; + gradient_inc += (frame_time_ms * 0.5); + sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); + + gradient_points[0].color = back_color; + gradient_points[1].color = back_color; + gradient_points[2].color = bottom_color; + gradient_points[3].color = bottom_color; + window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl + } + + if(!tab_associated_data[selected_tab].search_result_text.getString().isEmpty()) { + auto search_result_text_bounds = tab_associated_data[selected_tab].search_result_text.getLocalBounds(); + tab_associated_data[selected_tab].search_result_text.setPosition( + std::floor(body_pos.x + body_size.x * 0.5f - search_result_text_bounds.width * 0.5f), + std::floor(body_pos.y + body_size.y * 0.5f - search_result_text_bounds.height * 0.5f)); + window.draw(tab_associated_data[selected_tab].search_result_text); + } + window.display(); } - search_bar->onTextBeginTypingCallback = nullptr; - search_bar->onAutocompleteRequestCallback = nullptr; + page_end: + // TODO: This is needed, because you cant terminate futures without causing an exception to be thrown and its not safe anyways. + // Need a way to solve this, we dont want to wait for a search to finish when navigating backwards + for(TabAssociatedData &associated_data : tab_associated_data) { + if(associated_data.next_page_future.valid()) + associated_data.next_page_future.get(); + if(associated_data.search_future.valid()) + associated_data.search_future.get(); + } } static bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) { @@ -1326,17 +1374,17 @@ namespace QuickMedia { return true; } - void Program::save_recommendations_from_related_videos() { + void Program::save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const Body *related_media_body) { std::string video_id; - if(!youtube_url_extract_id(content_url, video_id)) { + if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += content_url; + err_msg += video_url; err_msg + ", video wont be saved in recommendations"; show_notification("Video player", err_msg.c_str(), Urgency::LOW); return; } - Json::Value recommended_json = load_recommended_json(current_plugin); + Json::Value recommended_json = load_recommended_json(); time_t time_now = time(NULL); Json::Value &existing_recommended_json = recommended_json[video_id]; @@ -1349,7 +1397,7 @@ namespace QuickMedia { existing_recommended_json["watched_timestamp"] = time_now; } else { Json::Value new_content_object(Json::objectValue); - new_content_object["title"] = content_title; + new_content_object["title"] = video_title; new_content_object["recommended_timestamp"] = time_now; new_content_object["recommended_count"] = 1; new_content_object["watched_count"] = 1; @@ -1381,24 +1429,21 @@ namespace QuickMedia { break; } } else { - fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", content_url.c_str()); + fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", video_url.c_str()); } } - save_json_to_file_atomic(get_recommended_filepath(current_plugin), recommended_json); + save_json_to_file_atomic(get_recommended_filepath(plugin_name), recommended_json); } #define CLEANMASK(mask) ((mask) & (ShiftMask|ControlMask|Mod1Mask|Mod4Mask|Mod5Mask)) - void Program::video_content_page() { - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; - + void Program::video_content_page(Page *page, std::string video_url, std::string video_title) { sf::Clock time_watched_timer; bool added_recommendations = false; bool video_loaded = false; - Page previous_page = pop_page_stack(); + PageType previous_page = pop_page_stack(); std::unique_ptr<VideoPlayer> video_player; std::unique_ptr<sf::RenderWindow> related_media_window; @@ -1408,56 +1453,45 @@ namespace QuickMedia { sf::Text related_videos_text("Related videos", *bold_font, 20); const float related_videos_text_height = related_videos_text.getCharacterSize(); + auto related_media_body = create_body(); + related_media_body->draw_thumbnails = true; + sf::WindowHandle video_player_window = None; - auto on_window_create = [this, &video_player_window, &related_media_window, &related_media_window_size](sf::WindowHandle _video_player_window) mutable { + auto on_window_create = [this, &video_player_window](sf::WindowHandle _video_player_window) mutable { video_player_window = _video_player_window; - - if(!current_plugin->is_image_board()) { - related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; - related_media_window_size.y = window_size.y; - related_media_window = std::make_unique<sf::RenderWindow>(sf::VideoMode(related_media_window_size.x, related_media_window_size.y), "", 0, sf::ContextSettings(0, 0, 0, 3, 3)); - related_media_window->setFramerateLimit(0); - if(!enable_vsync(disp, related_media_window->getSystemHandle())) { - fprintf(stderr, "Failed to enable vsync, fallback to frame limiting\n"); - related_media_window->setFramerateLimit(monitor_hz); - } - related_media_window->setVisible(false); - XReparentWindow(disp, related_media_window->getSystemHandle(), video_player_window, window_size.x - related_media_window_size.x, 0); - } - XSelectInput(disp, video_player_window, KeyPressMask | PointerMotionMask); XSync(disp, False); }; - auto load_video_error_check = [this, &video_player, previous_page, &time_watched_timer, &added_recommendations]() mutable { + auto load_video_error_check = [this, &related_media_body, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &added_recommendations, page]() mutable { time_watched_timer.restart(); added_recommendations = false; - watched_videos.insert(content_url); - VideoPlayer::Error err = video_player->load_video(content_url.c_str(), window.getSystemHandle(), current_plugin->name); + watched_videos.insert(video_url); + VideoPlayer::Error err = video_player->load_video(video_url.c_str(), window.getSystemHandle(), plugin_name); if(err != VideoPlayer::Error::OK) { std::string err_msg = "Failed to play url: "; - err_msg += content_url; + err_msg += video_url; show_notification("Video player", err_msg.c_str(), Urgency::CRITICAL); current_page = previous_page; } else { related_media_body->clear_items(); related_media_body->clear_thumbnails(); - related_media_body->items = current_plugin->get_related_media(content_url); + related_media_body->items = page->get_related_media(video_url); // TODO: Make this also work for other video plugins - if(current_plugin->name != "youtube") + if(strcmp(plugin_name, "youtube") != 0) return; std::string video_id; - if(!youtube_url_extract_id(content_url, video_id)) { + if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += content_url; + err_msg += video_url; err_msg + ", video wont be saved in history"; show_notification("Video player", err_msg.c_str(), Urgency::LOW); return; } - Json::Value video_history_json = load_video_history_json(current_plugin); + Json::Value video_history_json = load_video_history_json(); int existing_index = watch_history_get_item_by_id(video_history_json, video_id.c_str()); if(existing_index != -1) { @@ -1470,18 +1504,18 @@ namespace QuickMedia { Json::Value new_content_object(Json::objectValue); new_content_object["id"] = video_id; - new_content_object["title"] = content_title; + new_content_object["title"] = video_title; new_content_object["timestamp"] = time_now; video_history_json.append(std::move(new_content_object)); - Path video_history_filepath = get_video_history_filepath(current_plugin); + Path video_history_filepath = get_video_history_filepath(plugin_name); save_json_to_file_atomic(video_history_filepath, video_history_json); } }; bool has_video_started = true; - auto video_event_callback = [this, &video_player, &load_video_error_check, previous_page, &has_video_started, &time_watched_timer, &video_loaded](const char *event_name) mutable { + auto video_event_callback = [this, &related_media_body, &video_url, &video_title, &video_player, &load_video_error_check, previous_page, &has_video_started, &time_watched_timer, &video_loaded](const char *event_name) mutable { bool end_of_file = false; if(strcmp(event_name, "pause") == 0) { double time_remaining = 9999.0; @@ -1518,8 +1552,8 @@ namespace QuickMedia { return; } - content_url = std::move(new_video_url); - content_title = std::move(new_video_title); + video_url = std::move(new_video_url); + video_title = std::move(new_video_title); load_video_error_check(); } }; @@ -1540,27 +1574,28 @@ namespace QuickMedia { bool cursor_visible = true; sf::Clock cursor_hide_timer; - bool is_youtube = current_plugin->name == "youtube"; - bool is_pornhub = current_plugin->name == "pornhub"; + bool is_youtube = strcmp(plugin_name, "youtube") == 0; + bool is_pornhub = strcmp(plugin_name, "pornhub") == 0; bool supports_url_timestamp = is_youtube || is_pornhub; - auto save_video_url_to_clipboard = [this, &video_player_window, &video_player, &supports_url_timestamp]() { + auto save_video_url_to_clipboard = [&video_url, &video_player_window, &video_player, &supports_url_timestamp]() { if(!video_player_window) return; if(supports_url_timestamp) { + // TODO: Remove timestamp (&t= or ?t=) from video_url double time_in_file; if(video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK) time_in_file = 0.0; - sf::Clipboard::setString(content_url + "&t=" + std::to_string((int)time_in_file)); + sf::Clipboard::setString(video_url + "&t=" + std::to_string((int)time_in_file)); } else { - sf::Clipboard::setString(content_url); + sf::Clipboard::setString(video_url); } }; - while (current_page == Page::VIDEO_CONTENT) { + while (current_page == PageType::VIDEO_CONTENT) { while (window.pollEvent(event)) { - base_event_handler(event, previous_page, true, false, false); + base_event_handler(event, previous_page, related_media_body.get(), nullptr, true, false); if(event.type == sf::Event::Resized && related_media_window) { related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; related_media_window_size.y = window_size.y; @@ -1605,8 +1640,8 @@ namespace QuickMedia { related_media_window->setVisible(false); has_video_started = false; - content_url = selected_item->url; - content_title = selected_item->get_title(); + video_url = selected_item->url; + video_title = selected_item->get_title(); load_video_error_check(); } else if(event.key.code == sf::Keyboard::C && event.key.control) { save_video_url_to_clipboard(); @@ -1624,7 +1659,21 @@ namespace QuickMedia { current_page = previous_page; } else if(pressed_keysym == XK_f && pressing_ctrl) { window_set_fullscreen(disp, window.getSystemHandle(), WindowFullscreenState::TOGGLE); - } else if(pressed_keysym == XK_r && pressing_ctrl && related_media_window) { + } else if(pressed_keysym == XK_r && pressing_ctrl && strcmp(plugin_name, "4chan") != 0) { + if(!related_media_window) { + related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; + related_media_window_size.y = window_size.y; + related_media_window = std::make_unique<sf::RenderWindow>(sf::VideoMode(related_media_window_size.x, related_media_window_size.y), "", 0, sf::ContextSettings(0, 0, 0, 3, 3)); + related_media_window->setFramerateLimit(0); + if(!enable_vsync(disp, related_media_window->getSystemHandle())) { + fprintf(stderr, "Failed to enable vsync, fallback to frame limiting\n"); + related_media_window->setFramerateLimit(monitor_hz); + } + related_media_window->setVisible(false); + XReparentWindow(disp, related_media_window->getSystemHandle(), video_player_window, window_size.x - related_media_window_size.x, 0); + XSync(disp, False); + } + related_media_window_visible = true; related_media_window->setVisible(related_media_window_visible); if(!cursor_visible) @@ -1663,7 +1712,7 @@ namespace QuickMedia { /* Only save recommendations for the video if we have been watching it for 15 seconds */ if(is_youtube && video_loaded && !added_recommendations && time_watched_timer.getElapsedTime().asSeconds() >= 15) { added_recommendations = true; - save_recommendations_from_related_videos(); + save_recommendations_from_related_videos(video_url, video_title, related_media_body.get()); } if(video_player_window) { @@ -1701,37 +1750,14 @@ namespace QuickMedia { window_size.y = window_size_u.y; } - enum class TrackMediaType { - RSS, - HTML - }; - - const char* track_media_type_string(TrackMediaType media_type) { - switch(media_type) { - case TrackMediaType::RSS: - return "rss"; - case TrackMediaType::HTML: - return "html"; - } - assert(false); - return ""; - } - - static int track_media(TrackMediaType media_type, const std::string &manga_title, const std::string &chapter_title, const std::string &url) { - const char *args[] = { "automedia", "add", track_media_type_string(media_type), url.data(), "--start-after", chapter_title.data(), "--name", manga_title.data(), nullptr }; - return exec_program(args, nullptr, nullptr); - } - void Program::select_episode(BodyItem *item, bool start_from_beginning) { - images_url = item->url; - chapter_title = item->get_title(); image_index = 0; switch(image_view_mode) { case ImageViewMode::SINGLE: - current_page = Page::IMAGES; + current_page = PageType::IMAGES; break; case ImageViewMode::SCROLL: - current_page = Page::IMAGES_CONTINUOUS; + current_page = PageType::IMAGES_CONTINUOUS; break; } @@ -1740,7 +1766,7 @@ namespace QuickMedia { const Json::Value &json_chapters = content_storage_json["chapters"]; if(json_chapters.isObject()) { - const Json::Value &json_chapter = json_chapters[chapter_title]; + const Json::Value &json_chapter = json_chapters[item->get_title()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -1749,199 +1775,14 @@ namespace QuickMedia { } } - Page Program::pop_page_stack() { + // TODO: Remove + PageType Program::pop_page_stack() { if(!page_stack.empty()) { - Page previous_page = page_stack.top(); + PageType previous_page = page_stack.top(); page_stack.pop(); return previous_page; } - return Page::EXIT; - } - - enum class EpisodeListTabType { - CHAPTERS, - CREATOR - }; - - struct EpisodeListTab { - EpisodeListTabType type; - Body *body; - const Creator *creator; - std::future<BodyItems> creator_page_download_future; - sf::Text text; - }; - - void Program::episode_list_page() { - assert(current_plugin->is_manga()); - Manga *manga = static_cast<Manga*>(current_plugin); - - Json::Value *json_chapters = &content_storage_json["chapters"]; - std::vector<EpisodeListTab> tabs; - int selected_tab = 0; - - search_bar->onTextUpdateCallback = [&tabs, &selected_tab](const std::string &text) { - tabs[selected_tab].body->filter_search_fuzzy(text); - tabs[selected_tab].body->select_first_item(); - }; - - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string&) -> bool { - if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - select_episode(selected_item, false); - return true; - } else { - if(on_search_suggestion_submit_text(tabs[selected_tab].body, body)) { - selected_tab = 0; - json_chapters = &content_storage_json["chapters"]; - return true; - } else { - return false; - } - } - }; - - auto download_creator_page = [manga](std::string url) { - BodyItems body_items; - if(manga->get_creators_manga_list(url, body_items) != PluginResult::OK) - show_notification("Manga", "Failed to download authors page", Urgency::CRITICAL); - return body_items; - }; - - EpisodeListTab chapters_tab; - chapters_tab.type = EpisodeListTabType::CHAPTERS; - chapters_tab.body = body; - chapters_tab.creator = nullptr; - chapters_tab.text = sf::Text("Chapters", *font, tab_text_size); - tabs.push_back(std::move(chapters_tab)); - - const std::vector<Creator>& creators = manga->get_creators(); - for(const Creator &creator : creators) { - EpisodeListTab tab; - tab.type = EpisodeListTabType::CREATOR; - tab.body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - tab.body->draw_thumbnails = true; - tab.creator = &creator; - tab.creator_page_download_future = std::async(std::launch::async, download_creator_page, creator.url); - tab.text = sf::Text(creator.name, *font, tab_text_size); - tabs.push_back(std::move(tab)); - } - - const float tab_spacer_height = 0.0f; - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - sf::RectangleShape tab_shade; - tab_shade.setFillColor(sf::Color(33, 38, 44)); - - sf::RoundedRectangleShape tab_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10); - tab_background.setFillColor(tab_selected_color); - - while (current_page == Page::EPISODE_LIST) { - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION, false, true); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::T && event.key.control && tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { - BodyItem *selected_item = body->get_selected(); - if(selected_item) { - if(track_media(TrackMediaType::HTML, content_title, selected_item->get_title(), content_url) == 0) { - show_notification("Media tracker", "You are now tracking \"" + content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); - } else { - show_notification("Media tracker", "Failed to track media \"" + content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); - } - } - } else if(event.key.code == sf::Keyboard::Up) { - tabs[selected_tab].body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - tabs[selected_tab].body->select_next_item(); - } else if(event.key.code == sf::Keyboard::PageUp) { - tabs[selected_tab].body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::PageDown) { - tabs[selected_tab].body->select_next_page(); - } else if(event.key.code == sf::Keyboard::Home) { - tabs[selected_tab].body->select_first_item(); - } else if(event.key.code == sf::Keyboard::End) { - tabs[selected_tab].body->select_last_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } else if(event.key.code == sf::Keyboard::Left) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); - selected_tab = std::max(0, selected_tab - 1); - search_bar->clear(); - } else if(event.key.code == sf::Keyboard::Right) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); - selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); - } - } - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window, false); - - const float width_per_tab = window_size.x / tabs.size(); - tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - - float tab_vertical_offset = search_bar->getBottomWithoutShadow(); - if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) - tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); - else - tabs[selected_tab].body->draw(window, body_pos, body_size); - const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); - - tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); - tab_shade.setSize(sf::Vector2f(window_size.x, tab_height + 10.0f)); - window.draw(tab_shade); - - int i = 0; - for(EpisodeListTab &tab : tabs) { - if(tab.type == EpisodeListTabType::CREATOR - && tab.creator_page_download_future.valid() - && tab.creator_page_download_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) - { - tab.body->items = tab.creator_page_download_future.get(); - tab.body->filter_search_fuzzy(search_bar->get_text()); - tab.body->select_first_item(); - } - - if(i == selected_tab) { - tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset)); - window.draw(tab_background); - } - const float center = (i * width_per_tab) + (width_per_tab * 0.5f); - tab.text.setPosition(std::floor(center - tab.text.getLocalBounds().width * 0.5f), tab_y); - window.draw(tab.text); - ++i; - } - - window.display(); - } - - for(EpisodeListTab &tab : tabs) { - if(tab.type == EpisodeListTabType::CREATOR) - delete tab.body; - } + return PageType::EXIT; } // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image @@ -1980,24 +1821,23 @@ namespace QuickMedia { } } - // TODO: Cancel download when navigating to another non-manga page - void Program::download_chapter_images_if_needed(Manga *image_plugin) { - if(downloading_chapter_url == images_url) + void Program::download_chapter_images_if_needed(MangaImagesPage *images_page) { + if(downloading_chapter_url == images_page->get_url()) return; - downloading_chapter_url = images_url; + downloading_chapter_url = images_page->get_url(); if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish image_download_cancel = true; image_download_future.get(); image_download_cancel = false; } - std::string chapter_url = images_url; Path content_cache_dir_ = content_cache_dir; - image_download_future = std::async(std::launch::async, [chapter_url, image_plugin, content_cache_dir_, this]() { + image_download_future = std::async(std::launch::async, [images_page, content_cache_dir_, this]() { // TODO: Download images in parallel int page = 1; - image_plugin->for_each_page_in_chapter(chapter_url, [content_cache_dir_, &page, this](const std::string &url) { + images_page->for_each_page_in_chapter([content_cache_dir_, &page, images_page, this](const std::string &url) { if(image_download_cancel) return false; @@ -2019,7 +1859,7 @@ namespace QuickMedia { return true; std::vector<CommandArg> extra_args; - if(current_plugin->name == "manganelo") { + if(strcmp(images_page->get_service_name(), "manganelo") == 0) { extra_args = { CommandArg { "-H", "accept: image/webp,image/apng,image/*,*/*;q=0.8" }, CommandArg { "-H", "sec-fetch-site: cross-site" }, @@ -2029,9 +1869,10 @@ namespace QuickMedia { }; } + // TODO: Download directly to file instead. TODO: Move to page std::string image_content; - if(download_to_string(url, image_content, extra_args, current_plugin->use_tor, true) != DownloadResult::OK || image_content.size() <= 255) { - if(current_plugin->name == "manganelo") { + if(download_to_string(url, image_content, extra_args, is_tor_enabled(), true) != DownloadResult::OK || image_content.size() <= 255) { + if(strcmp(images_page->get_service_name(), "manganelo") == 0) { bool try_backup_url = false; std::string new_url = url; if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) { @@ -2042,7 +1883,7 @@ namespace QuickMedia { if(try_backup_url) { image_content.clear(); - if(download_to_string(new_url, image_content, extra_args, current_plugin->use_tor, true) != DownloadResult::OK || image_content.size() <= 255) { + if(download_to_string(new_url, image_content, extra_args, is_tor_enabled(), true) != DownloadResult::OK || image_content.size() <= 255) { show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL); return false; } @@ -2109,40 +1950,36 @@ namespace QuickMedia { }); } - void Program::image_page() { + int Program::image_page(MangaImagesPage *images_page, Body *chapters_body) { + int page_navigation = 0; image_download_cancel = false; - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; sf::Texture image_texture; sf::Sprite image; sf::Text error_message("", *font, 30); error_message.setFillColor(sf::Color::White); - assert(current_plugin->is_manga()); - Manga *image_plugin = static_cast<Manga*>(current_plugin); - std::string image_data; bool download_in_progress = false; - content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; - return; + current_page = pop_page_stack(); + return 0; } int num_images = 0; - if(image_plugin->get_number_of_images(images_url, num_images) != ImageResult::OK) { + if(images_page->get_number_of_images(num_images) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; - return; + current_page = pop_page_stack(); + return 0; } image_index = std::min(image_index, num_images); if(num_images != (int)image_upscale_status.size()) image_upscale_status.resize(num_images); - download_chapter_images_if_needed(image_plugin); + download_chapter_images_if_needed(images_page); if(image_index < num_images) { sf::String error_msg; @@ -2153,14 +1990,15 @@ namespace QuickMedia { download_in_progress = true; error_message.setString(error_msg); } else if(image_index == num_images) { - error_message.setString("End of " + chapter_title); + error_message.setString("End of " + images_page->get_chapter_name()); } + // TODO: Dont do this every time we change page? Json::Value &json_chapters = content_storage_json["chapters"]; Json::Value json_chapter; int latest_read = image_index + 1; if(json_chapters.isObject()) { - json_chapter = json_chapters[chapter_title]; + json_chapter = json_chapters[images_page->get_chapter_name()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -2174,7 +2012,7 @@ namespace QuickMedia { } json_chapter["current"] = std::min(latest_read, num_images); json_chapter["total"] = num_images; - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } @@ -2182,9 +2020,9 @@ namespace QuickMedia { bool error = !error_message.getString().isEmpty(); bool redraw = true; - sf::Text chapter_text(content_title + " | " + chapter_title + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_images), *font, 14); + sf::Text chapter_text(images_page->manga_name + " | " + images_page->get_chapter_name() + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_images), *font, 14); if(image_index == num_images) - chapter_text.setString(content_title + " | " + chapter_title + " | End"); + chapter_text.setString(images_page->manga_name + " | " + images_page->get_chapter_name() + " | End"); chapter_text.setFillColor(sf::Color::White); sf::RectangleShape chapter_text_background; chapter_text_background.setFillColor(sf::Color(0, 0, 0, 150)); @@ -2204,10 +2042,11 @@ namespace QuickMedia { while(window.pollEvent(event)) {} // TODO: Show to user if a certain page is missing (by checking page name (number) and checking if some are skipped) - while (current_page == Page::IMAGES) { + while (current_page == PageType::IMAGES) { while(window.pollEvent(event)) { if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -2221,29 +2060,22 @@ namespace QuickMedia { if(image_index > 0) { --image_index; goto end_of_images_page; - } else if(image_index == 0 && body->get_selected_item() < (int)body->items.size() - 1) { - // TODO: Make this work if the list is sorted differently than from newest to oldest. - body->filter_search_fuzzy(""); - body->select_next_item(); - select_episode(body->items[body->get_selected_item()].get(), true); - image_index = 99999; // Start at the page that shows we are at the end of the chapter + } else if(image_index == 0 && chapters_body->get_selected_item() < (int)chapters_body->items.size() - 1) { + page_navigation = -1; goto end_of_images_page; } } else if(event.key.code == sf::Keyboard::Down) { if(image_index < num_images) { ++image_index; goto end_of_images_page; - } else if(image_index == num_images && body->get_selected_item() > 0) { - // TODO: Make this work if the list is sorted differently than from newest to oldest. - body->filter_search_fuzzy(""); - body->select_previous_item(); - select_episode(body->items[body->get_selected_item()].get(), true); + } else if(image_index == num_images && chapters_body->get_selected_item() > 0) { + page_navigation = 1; goto end_of_images_page; } } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); } else if(event.key.code == sf::Keyboard::I) { - current_page = Page::IMAGES_CONTINUOUS; + current_page = PageType::IMAGES_CONTINUOUS; image_view_mode = ImageViewMode::SCROLL; } else if(event.key.code == sf::Keyboard::F) { fit_image_to_window = !fit_image_to_window; @@ -2317,46 +2149,47 @@ namespace QuickMedia { } end_of_images_page: - if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) { + if(current_page != PageType::IMAGES && current_page != PageType::IMAGES_CONTINUOUS) { image_download_cancel = true; + if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish + image_download_future.get(); + image_download_cancel = false; + } std::unique_lock<std::mutex> lock(image_upscale_mutex); images_to_upscale.clear(); image_upscale_status.clear(); } + return page_navigation; } - void Program::image_continuous_page() { + void Program::image_continuous_page(MangaImagesPage *images_page) { image_download_cancel = false; - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; - - assert(current_plugin->is_manga()); - Manga *image_plugin = static_cast<Manga*>(current_plugin); - content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); return; } int num_images = 0; - if(image_plugin->get_number_of_images(images_url, num_images) != ImageResult::OK) { + if(images_page->get_number_of_images(num_images) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); return; } if(num_images != (int)image_upscale_status.size()) image_upscale_status.resize(num_images); - download_chapter_images_if_needed(image_plugin); + download_chapter_images_if_needed(images_page); Json::Value &json_chapters = content_storage_json["chapters"]; Json::Value json_chapter; int latest_read = 1 + image_index; if(json_chapters.isObject()) { - json_chapter = json_chapters[chapter_title]; + json_chapter = json_chapters[images_page->get_chapter_name()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -2369,27 +2202,27 @@ namespace QuickMedia { json_chapter = Json::Value(Json::objectValue); } - ImageViewer image_viewer(image_plugin, images_url, content_title, chapter_title, image_index, content_cache_dir, font.get()); + ImageViewer image_viewer(images_page, images_page->manga_name, images_page->get_chapter_name(), image_index, content_cache_dir, font.get()); json_chapter["current"] = std::min(latest_read, image_viewer.get_num_pages()); json_chapter["total"] = image_viewer.get_num_pages(); - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } - while(current_page == Page::IMAGES_CONTINUOUS) { + while(current_page == PageType::IMAGES_CONTINUOUS) { window.clear(back_color); ImageViewerAction action = image_viewer.draw(window); switch(action) { case ImageViewerAction::NONE: break; case ImageViewerAction::RETURN: - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); break; case ImageViewerAction::SWITCH_TO_SINGLE_IMAGE_MODE: image_view_mode = ImageViewMode::SINGLE; - current_page = Page::IMAGES; + current_page = PageType::IMAGES; break; } window.display(); @@ -2399,414 +2232,31 @@ namespace QuickMedia { if(focused_page > latest_read) { latest_read = focused_page; json_chapter["current"] = latest_read; - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } } } - if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) { + if(current_page != PageType::IMAGES && current_page != PageType::IMAGES_CONTINUOUS) { image_download_cancel = true; + if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish + image_download_future.get(); + image_download_cancel = false; + } std::unique_lock<std::mutex> lock(image_upscale_mutex); images_to_upscale.clear(); image_upscale_status.clear(); } } - void Program::content_list_page() { - std::string update_search_text; - bool search_text_updated = false; - bool search_running = false; - std::future<BodyItems> search_future; - - if(!current_plugin->content_list_search_is_filter()) - search_bar->text_autosearch_delay = current_plugin->get_content_list_search_delay(); - - body->clear_items(); - body->clear_thumbnails(); - if(current_plugin->get_content_list(content_list_url, body->items) != PluginResult::OK) { - show_notification("Content list", "Failed to get content list for url: " + content_list_url, Urgency::CRITICAL); - current_page = Page::SEARCH_SUGGESTION; - return; - } - - search_bar->onTextUpdateCallback = [this, &update_search_text, &search_text_updated](const std::string &text) { - if(current_plugin->content_list_search_is_filter()) { - body->filter_search_fuzzy(text); - body->select_first_item(); - } else { - update_search_text = text; - search_text_updated = true; - } - }; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - content_episode = selected_item->get_title(); - content_url = selected_item->url; - current_page = Page::CONTENT_DETAILS; - body->clear_items(); - return true; - }; - - int fetched_page = 0; - bool fetching_next_page_running = false; - double gradient_inc = 0; - const float gradient_height = 5.0f; - sf::Vertex gradient_points[4]; - std::future<BodyItems> next_page_future; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - sf::Clock frame_timer; - - while (current_page == Page::CONTENT_LIST) { - sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); - - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION, false); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { - redraw = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Down || event.key.code == sf::Keyboard::PageDown || event.key.code == sf::Keyboard::End) { - bool hit_bottom = false; - switch(event.key.code) { - case sf::Keyboard::Down: - hit_bottom = !body->select_next_item(); - break; - case sf::Keyboard::PageDown: - hit_bottom = !body->select_next_page(); - break; - case sf::Keyboard::End: - body->select_last_item(); - hit_bottom = true; - break; - default: - hit_bottom = false; - break; - } - if(hit_bottom && !search_running && !fetching_next_page_running) { - gradient_inc = 0; - fetching_next_page_running = true; - int next_page = fetched_page + 1; - std::string content_list_url_copy = content_list_url; - std::string update_search_text_copy = update_search_text; - next_page_future = std::async(std::launch::async, [this, content_list_url_copy, update_search_text_copy, next_page]() { - BodyItems result_items; - if(current_plugin->content_list_search_page(content_list_url_copy, update_search_text_copy, next_page, result_items) != SearchResult::OK) - fprintf(stderr, "Failed to get next content list page (page %d)\n", next_page); - return result_items; - }); - } - } else if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::PageUp) { - body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::Home) { - body->select_first_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } - } - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - - gradient_points[0].position.x = 0.0f; - gradient_points[0].position.y = window_size.y - gradient_height; - - gradient_points[1].position.x = window_size.x; - gradient_points[1].position.y = window_size.y - gradient_height; - - gradient_points[2].position.x = window_size.x; - gradient_points[2].position.y = window_size.y; - - gradient_points[3].position.x = 0.0f; - gradient_points[3].position.y = window_size.y; - } - - search_bar->update(); - - if(search_text_updated && !fetching_next_page_running && !search_running) { - std::string search_term = update_search_text; - search_future = std::async(std::launch::async, [this, search_term]() { - BodyItems result; - if(current_plugin->content_list_search(content_list_url, search_term, result) != SearchResult::OK) { - // TODO: Show this? - //show_notification("Search", "Search failed!", Urgency::CRITICAL); - } - return result; - }); - search_text_updated = false; - search_running = true; - } - - if(search_running && search_future.valid() && search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if(!search_text_updated) { - body->items = search_future.get(); - body->select_first_item(); - } else { - search_future.get(); - } - search_running = false; - fetched_page = 0; - } - - if(fetching_next_page_running && next_page_future.valid() && next_page_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - BodyItems new_body_items = next_page_future.get(); - fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", fetched_page + 1, new_body_items.size()); - size_t num_new_messages = new_body_items.size(); - if(num_new_messages > 0) { - body->append_items(std::move(new_body_items)); - fetched_page++; - } - fetching_next_page_running = false; - } - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - if(fetching_next_page_running) { - double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; - gradient_inc += (frame_time_ms * 0.5); - sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); - - gradient_points[0].color = back_color; - gradient_points[1].color = back_color; - gradient_points[2].color = bottom_color; - gradient_points[3].color = bottom_color; - window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl - } - window.display(); - } - - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); - } - - void Program::content_details_page() { - if(current_plugin->get_content_details(content_list_url, content_url, body->items) != PluginResult::OK) { - show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); - // TODO: This will return to an empty content list. - // Each page should have its own @Body so we can return to the last page and still have the data loaded - // however the cached images should be cleared. - current_page = Page::CONTENT_LIST; - return; - } - - search_bar->onTextUpdateCallback = nullptr; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - if(current_plugin->name == "nyaa.si") { - BodyItem *selected_item = body->get_selected(); - if(selected_item && strncmp(selected_item->url.c_str(), "magnet:?", 8) == 0) { - if(!is_program_executable_by_name("xdg-open")) { - show_notification("Nyaa.si", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); - return false; - } - const char *args[] = { "xdg-open", selected_item->url.c_str(), nullptr }; - exec_program_async(args, nullptr); - } - } - return false; - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::CONTENT_DETAILS) { - while (window.pollEvent(event)) { - base_event_handler(event, Page::CONTENT_LIST); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.display(); - } - } - - void Program::file_manager_page() { - selected_files.clear(); - search_bar->clear(); - int prev_autosearch_delay = search_bar->text_autosearch_delay; - search_bar->text_autosearch_delay = file_manager->get_search_delay(); - Page previous_page = pop_page_stack(); - - sf::Text current_dir_text(file_manager->get_current_dir().string(), *bold_font, 18); - - // TODO: Make asynchronous. - // TODO: Automatically go to the parent if this fails (recursively). - body->select_first_item(); - body->items.clear(); - if(file_manager->get_files_in_directory(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "File manager failed to get files in directory: " + file_manager->get_current_dir().string(), Urgency::CRITICAL); - } - - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->reset_selected(); - }; - - search_bar->onTextSubmitCallback = [this, previous_page, ¤t_dir_text](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - if(file_manager->set_child_directory(selected_item->get_title())) { - std::string current_dir_str = file_manager->get_current_dir().string(); - current_dir_text.setString(current_dir_str); - // TODO: Make asynchronous. - // TODO: Automatically go to the parent if this fails (recursively). - body->items.clear(); - if(file_manager->get_files_in_directory(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "File manager failed to get files in directory: " + current_dir_str, Urgency::CRITICAL); - } - body->select_first_item(); - return true; - } else { - std::filesystem::path full_path = file_manager->get_current_dir() / selected_item->get_title(); - selected_files.push_back(full_path.string()); - printf("%s\n", selected_files.back().c_str()); - current_page = previous_page; - return false; - } - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::FILE_MANAGER) { - while (window.pollEvent(event)) { - base_event_handler(event, previous_page); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - const float dir_text_height = std::floor(current_dir_text.getLocalBounds().height + 12.0f); - body_pos.y += dir_text_height; - body_size.y -= dir_text_height; - current_dir_text.setPosition(body_pos.x, body_pos.y - dir_text_height); - } - - search_bar->update(); - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.draw(current_dir_text); - window.display(); - } - - search_bar->text_autosearch_delay = prev_autosearch_delay; - // We want exit code 1 if the file manager was launched and no files were selected, to know when the user didn't select any file(s) - if(selected_files.empty() && current_page == Page::EXIT) - exit_code = 1; - } - - void Program::image_board_thread_list_page() { - assert(current_plugin->is_image_board()); - ImageBoard *image_board = static_cast<ImageBoard*>(current_plugin); - if(image_board->get_threads(image_board_thread_list_url, body->items) != PluginResult::OK) { - show_notification("Content list", "Failed to get threads for url: " + image_board_thread_list_url, Urgency::CRITICAL); - current_page = Page::SEARCH_SUGGESTION; - return; - } - - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->select_first_item(); - }; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - content_episode = selected_item->get_title(); - content_url = selected_item->url; - current_page = Page::IMAGE_BOARD_THREAD; - body->clear_items(); - return true; - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::IMAGE_BOARD_THREAD_LIST) { - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.display(); - } - } - static bool is_url_video(const std::string &url) { return string_ends_with(url, ".webm") || string_ends_with(url, ".mp4") || string_ends_with(url, ".gif"); } - void Program::image_board_thread_page() { - assert(current_plugin->is_image_board()); - // TODO: Support image board other than 4chan. To make this work, the captcha code needs to be changed - // to work with other captcha than google captcha - assert(current_plugin->name == "4chan"); - ImageBoard *image_board = static_cast<ImageBoard*>(current_plugin); - if(image_board->get_thread_comments(image_board_thread_list_url, content_url, body->items) != PluginResult::OK) { - show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); - // TODO: This will return to an empty content list. - // Each page should have its own @Body so we can return to the last page and still have the data loaded - // however the cached images should be cleared. - current_page = Page::IMAGE_BOARD_THREAD_LIST; - return; - } - - const std::string &board = image_board_thread_list_url; - const std::string &thread = content_url; - + void Program::image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body) { // TODO: Instead of using stage here, use different pages for each stage enum class NavigationStage { VIEWING_COMMENTS, @@ -2857,7 +2307,7 @@ namespace QuickMedia { // TODO: Make this work with other sites than 4chan auto request_google_captcha_image = [this, &captcha_texture, &captcha_image_mutex, &navigation_stage, &captcha_sprite, &challenge_description_text](GoogleCaptchaChallengeInfo &challenge_info) { std::string payload_image_data; - DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, current_plugin->use_tor); + DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, is_tor_enabled()); if(download_image_result == DownloadResult::OK) { std::lock_guard<std::mutex> lock(captcha_image_mutex); if(captcha_texture.loadFromMemory(payload_image_data.data(), payload_image_data.size())) { @@ -2893,40 +2343,40 @@ namespace QuickMedia { show_notification("Google captcha", "Failed to get captcha challenge", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } - }, current_plugin->use_tor); + }, is_tor_enabled()); }; Entry comment_input("Press ctrl+m to begin writing a comment...", font.get(), cjk_font.get()); comment_input.draw_background = false; comment_input.set_editable(false); - auto post_comment = [this, &comment_input, &navigation_stage, &image_board, &board, &thread, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { + auto post_comment = [this, &comment_input, &navigation_stage, &thread_page, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { comment_input.set_editable(false); navigation_stage = NavigationStage::POSTING_COMMENT; - PostResult post_result = image_board->post_comment(board, thread, captcha_post_id, comment_to_post); + PostResult post_result = thread_page->post_comment(captcha_post_id, comment_to_post); if(post_result == PostResult::OK) { - show_notification(current_plugin->name, "Comment posted!"); + show_notification("QuickMedia", "Comment posted!"); navigation_stage = NavigationStage::VIEWING_COMMENTS; // TODO: Append posted comment to the thread so the user can see their posted comment. // TODO: Asynchronously update the thread periodically to show new comments. } else if(post_result == PostResult::TRY_AGAIN) { - show_notification(current_plugin->name, "Error while posting, did the captcha expire? Please try again"); + show_notification("QuickMedia", "Error while posting, did the captcha expire? Please try again"); // TODO: Check if the response contains a new captcha instead of requesting a new one manually request_new_google_captcha_challenge(); } else if(post_result == PostResult::BANNED) { - show_notification(current_plugin->name, "Failed to post comment because you are banned", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment because you are banned", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::ERR) { - show_notification(current_plugin->name, "Failed to post comment. Is " + current_plugin->name + " down or is your internet down?", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment. Is " + std::string(plugin_name) + " down or is your internet down?", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else { assert(false && "Unhandled post result"); - show_notification(current_plugin->name, "Failed to post comment. Unknown error", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment. Unknown error", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } }; - comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &image_board](const std::string &text) -> bool { + comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](const std::string &text) -> bool { if(text.empty()) return false; @@ -2937,9 +2387,9 @@ namespace QuickMedia { post_comment(); return true; }); - } else if(image_board->get_pass_id().empty()) { + } else if(thread_page->get_pass_id().empty()) { request_new_google_captcha_challenge(); - } else if(!image_board->get_pass_id().empty()) { + } else if(!thread_page->get_pass_id().empty()) { post_comment_future = std::async(std::launch::async, [&post_comment]() -> bool { post_comment(); return true; @@ -2967,7 +2417,7 @@ namespace QuickMedia { std::stack<int> comment_navigation_stack; std::stack<int> comment_page_scroll_stack; - while (current_page == Page::IMAGE_BOARD_THREAD) { + while (current_page == PageType::IMAGE_BOARD_THREAD) { while (window.pollEvent(event)) { if(navigation_stage == NavigationStage::REPLYING) { comment_input.process_event(event); @@ -2981,8 +2431,9 @@ namespace QuickMedia { } } - if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + if(event.type == sf::Event::Closed) { + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -2994,46 +2445,42 @@ namespace QuickMedia { redraw = true; else if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::VIEWING_COMMENTS) { if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); + thread_body->select_previous_item(); } else if(event.key.code == sf::Keyboard::Down) { - body->select_next_item(); + thread_body->select_next_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - body->select_previous_page(); + thread_body->select_previous_page(); } else if(event.key.code == sf::Keyboard::PageDown) { - body->select_next_page(); + thread_body->select_next_page(); } else if(event.key.code == sf::Keyboard::Home) { - body->select_first_item(); + thread_body->select_first_item(); } else if(event.key.code == sf::Keyboard::End) { - body->select_last_item(); + thread_body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::IMAGE_BOARD_THREAD_LIST; - body->clear_items(); - body->reset_selected(); + current_page = pop_page_stack(); } else if(event.key.code == sf::Keyboard::P) { - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->attached_content_url.empty()) { if(is_url_video(selected_item->attached_content_url)) { - page_stack.push(Page::IMAGE_BOARD_THREAD); - current_page = Page::VIDEO_CONTENT; - std::string prev_content_url = content_url; - content_url = selected_item->attached_content_url; + page_stack.push(PageType::IMAGE_BOARD_THREAD); + current_page = PageType::VIDEO_CONTENT; watched_videos.clear(); - video_content_page(); - content_url = std::move(prev_content_url); + // TODO: Use real title + video_content_page(thread_page, selected_item->attached_content_url, "No title.webm"); redraw = true; } else { if(downloading_image && load_image_future.valid()) load_image_future.get(); downloading_image = true; navigation_stage = NavigationStage::VIEWING_ATTACHED_IMAGE; - load_image_future = std::async(std::launch::async, [this, &image_board]() { + load_image_future = std::async(std::launch::async, [this, &thread_body]() { std::string image_data; - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); if(!selected_item || selected_item->attached_content_url.empty()) { return image_data; } - if(download_to_string_cache(selected_item->attached_content_url, image_data, {}, image_board->use_tor) != DownloadResult::OK) { - show_notification(image_board->name, "Failed to download image: " + selected_item->attached_content_url, Urgency::CRITICAL); + if(download_to_string_cache(selected_item->attached_content_url, image_data, {}, is_tor_enabled()) != DownloadResult::OK) { + show_notification("QuickMedia", "Failed to download image: " + selected_item->attached_content_url, Urgency::CRITICAL); image_data.clear(); } return image_data; @@ -3042,43 +2489,43 @@ namespace QuickMedia { } } - BodyItem *selected_item = body->get_selected(); - if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || body->get_selected_item() != comment_navigation_stack.top()) && !selected_item->replies.empty()) { - for(auto &body_item : body->items) { + BodyItem *selected_item = thread_body->get_selected(); + if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || thread_body->get_selected_item() != comment_navigation_stack.top()) && !selected_item->replies.empty()) { + for(auto &body_item : thread_body->items) { body_item->visible = false; } selected_item->visible = true; for(size_t reply_index : selected_item->replies) { - body->items[reply_index]->visible = true; + thread_body->items[reply_index]->visible = true; } - comment_navigation_stack.push(body->get_selected_item()); - comment_page_scroll_stack.push(body->get_page_scroll()); - body->clamp_selection(); - body->set_page_scroll(0.0f); + comment_navigation_stack.push(thread_body->get_selected_item()); + comment_page_scroll_stack.push(thread_body->get_page_scroll()); + thread_body->clamp_selection(); + thread_body->set_page_scroll(0.0f); } else if(event.key.code == sf::Keyboard::BackSpace && !comment_navigation_stack.empty()) { size_t previous_selected = comment_navigation_stack.top(); float previous_page_scroll = comment_page_scroll_stack.top(); comment_navigation_stack.pop(); comment_page_scroll_stack.pop(); if(comment_navigation_stack.empty()) { - for(auto &body_item : body->items) { + for(auto &body_item : thread_body->items) { body_item->visible = true; } - body->set_selected_item(previous_selected); - body->clamp_selection(); + thread_body->set_selected_item(previous_selected); + thread_body->clamp_selection(); } else { - for(auto &body_item : body->items) { + for(auto &body_item : thread_body->items) { body_item->visible = false; } - body->set_selected_item(previous_selected); - selected_item = body->items[comment_navigation_stack.top()].get(); + thread_body->set_selected_item(previous_selected); + selected_item = thread_body->items[comment_navigation_stack.top()].get(); selected_item->visible = true; for(size_t reply_index : selected_item->replies) { - body->items[reply_index]->visible = true; + thread_body->items[reply_index]->visible = true; } - body->clamp_selection(); + thread_body->clamp_selection(); } - body->set_page_scroll(previous_page_scroll); + thread_body->set_page_scroll(previous_page_scroll); } else if(event.key.code == sf::Keyboard::M && event.key.control && selected_item) { navigation_stage = NavigationStage::REPLYING; comment_input.set_editable(true); @@ -3126,7 +2573,7 @@ namespace QuickMedia { } request_google_captcha_image(challenge_info); } - }, current_plugin->use_tor); + }, is_tor_enabled()); } } @@ -3226,11 +2673,11 @@ namespace QuickMedia { //attached_image_texture->generateMipmap(); attached_image_sprite.setTexture(*attached_image_texture, true); } else { - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); std::string selected_item_attached_url; if(selected_item) selected_item_attached_url = selected_item->attached_content_url; - show_notification(image_board->name, "Failed to load image downloaded from url: " + selected_item->attached_content_url, Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to load image downloaded from url: " + selected_item->attached_content_url, Urgency::CRITICAL); } } @@ -3260,12 +2707,12 @@ namespace QuickMedia { window.draw(comment_input_shade); window.draw(logo_sprite); comment_input.draw(window); - body->draw(window, body_pos, body_size); + thread_body->draw(window, body_pos, body_size); } else if(navigation_stage == NavigationStage::VIEWING_COMMENTS) { window.draw(comment_input_shade); window.draw(logo_sprite); comment_input.draw(window); - body->draw(window, body_pos, body_size); + thread_body->draw(window, body_pos, body_size); } window.display(); } @@ -3279,14 +2726,10 @@ namespace QuickMedia { post_comment_future.get(); if(load_image_future.valid()) load_image_future.get(); - - // Clear post that is still being written. - // TODO: This post should be saved for the thread. Each thread should have its own text edit widget, - // so you dont have to retype a post that was in the middle of being posted when returning. } void Program::chat_login_page() { - assert(current_plugin->name == "matrix"); + assert(strcmp(plugin_name, "matrix") == 0); SearchBar login_input(*font, nullptr, "Username"); SearchBar password_input(*font, nullptr, "Password", true); @@ -3298,23 +2741,22 @@ namespace QuickMedia { SearchBar *inputs[num_inputs] = { &login_input, &password_input, &homeserver_input }; int focused_input = 0; - auto text_submit_callback = [this, inputs, &status_text](const sf::String&) -> bool { - Matrix *matrix = static_cast<Matrix*>(current_plugin); + auto text_submit_callback = [this, inputs, &status_text](const sf::String&) { for(int i = 0; i < num_inputs; ++i) { if(inputs[i]->get_text().empty()) { status_text.setString("All fields need to be filled in"); - return false; + return; } } std::string err_msg; // TODO: Make asynchronous if(matrix->login(inputs[0]->get_text(), inputs[1]->get_text(), inputs[2]->get_text(), err_msg) == PluginResult::OK) { - current_page = Page::CHAT; + current_page = PageType::CHAT; } else { status_text.setString("Failed to login, error: " + err_msg); } - return false; + return; }; for(int i = 0; i < num_inputs; ++i) { @@ -3328,9 +2770,11 @@ namespace QuickMedia { bool redraw = true; sf::Event event; - while (current_page == Page::CHAT_LOGIN) { + auto body = create_body(); + + while (current_page == PageType::CHAT_LOGIN) { while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false, false); + base_event_handler(event, PageType::EXIT, body.get(), nullptr, false, false); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { redraw = true; } else if(event.type == sf::Event::TextEntered) { @@ -3347,8 +2791,7 @@ namespace QuickMedia { if(redraw) { redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); + get_body_dimensions(window_size, nullptr, body_pos, body_size); } window.clear(back_color); @@ -3392,8 +2835,9 @@ namespace QuickMedia { } void Program::chat_page() { - assert(current_plugin->name == "matrix"); - Matrix *matrix = static_cast<Matrix*>(current_plugin); + assert(strcmp(plugin_name, "matrix") == 0); + + auto video_page = std::make_unique<MatrixVideoPage>(this); std::vector<ChatTab> tabs; int selected_tab = 0; @@ -3439,7 +2883,7 @@ namespace QuickMedia { bool is_window_focused = window.hasFocus(); - auto process_new_room_messages = [matrix, &body_items_by_room_id, ¤t_room_id, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { + auto process_new_room_messages = [this, &body_items_by_room_id, ¤t_room_id, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { for(auto &[room, messages] : room_sync_messages) { bool was_mentioned = false; for(auto &message : messages) { @@ -3494,7 +2938,7 @@ namespace QuickMedia { URL_SELECTION }; - Page new_page = Page::CHAT; + PageType new_page = PageType::CHAT; ChatState chat_state = ChatState::NAVIGATING; std::shared_ptr<BodyItem> currently_operating_on_item; @@ -3506,7 +2950,7 @@ namespace QuickMedia { chat_input.draw_background = false; chat_input.set_editable(false); - chat_input.on_submit_callback = [matrix, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { + chat_input.on_submit_callback = [this, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { if(tabs[selected_tab].type == ChatTabType::MESSAGES) { if(text.empty()) return false; @@ -3514,12 +2958,12 @@ namespace QuickMedia { if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') { std::string command = strip(text); if(command == "/upload") { - new_page = Page::FILE_MANAGER; + new_page = PageType::FILE_MANAGER; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(command == "/logout") { - new_page = Page::CHAT_LOGIN; + new_page = PageType::CHAT_LOGIN; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; @@ -3622,7 +3066,7 @@ namespace QuickMedia { auto room_avatar_thumbnail_data = std::make_shared<ThumbnailData>(); AsyncImageLoader async_image_loader; - auto typing_async_func = [matrix](bool new_state, std::string room_id) { + auto typing_async_func = [this](bool new_state, std::string room_id) { if(new_state) { matrix->on_start_typing(room_id); } else { @@ -3650,17 +3094,17 @@ namespace QuickMedia { std::future<void> set_read_marker_future; bool setting_read_marker = false; - auto launch_url = [this, &redraw](const std::string &url) mutable { + auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable { if(url.empty()) return; std::string video_id; if(youtube_url_extract_id(url, video_id)) { - page_stack.push(Page::CHAT); + page_stack.push(PageType::CHAT); watched_videos.clear(); - content_url = url; - current_page = Page::VIDEO_CONTENT; - video_content_page(); + current_page = PageType::VIDEO_CONTENT; + // TODO: Add title + video_content_page(video_page.get(), url, "No title"); redraw = true; } else { if(!is_program_executable_by_name("xdg-open")) { @@ -3710,10 +3154,10 @@ namespace QuickMedia { return result; }; - while (current_page == Page::CHAT) { + while (current_page == PageType::CHAT) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false, false); + base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); if(event.type == sf::Event::GainedFocus) { is_window_focused = true; redraw = true; @@ -3744,7 +3188,6 @@ namespace QuickMedia { fetching_previous_messages_running = true; previous_messages_future_room_id = current_room_id; previous_messages_future = std::async(std::launch::async, [this, &previous_messages_future_room_id]() { - Matrix *matrix = static_cast<Matrix*>(current_plugin); BodyItems result_items; if(matrix->get_previous_room_messages(previous_messages_future_room_id, result_items) != PluginResult::OK) fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", previous_messages_future_room_id.c_str()); @@ -3758,9 +3201,7 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::End) { tabs[selected_tab].body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - body->clear_items(); - body->reset_selected(); + current_page = PageType::EXIT; } else if(event.key.code == sf::Keyboard::Left && synced) { tabs[selected_tab].body->clear_thumbnails(); selected_tab = std::max(0, selected_tab - 1); @@ -3836,11 +3277,11 @@ namespace QuickMedia { if(!selected->url.empty()) { const char *content_type = link_get_content_type(selected->url); if(content_type && (strcmp(content_type, "audio") == 0 || strcmp(content_type, "video") == 0 || strcmp(content_type, "image") == 0)) { - page_stack.push(Page::CHAT); + page_stack.push(PageType::CHAT); watched_videos.clear(); - content_url = selected->url; - current_page = Page::VIDEO_CONTENT; - video_content_page(); + current_page = PageType::VIDEO_CONTENT; + // TODO: Add title + video_content_page(video_page.get(), selected->url, "No title"); redraw = true; continue; } @@ -3959,15 +3400,19 @@ namespace QuickMedia { } switch(new_page) { - case Page::FILE_MANAGER: { - new_page = Page::CHAT; - if(!file_manager) { - file_manager = new FileManager(); - file_manager->set_current_directory(get_home_dir().data); - } - page_stack.push(Page::CHAT); - current_page = Page::FILE_MANAGER; - file_manager_page(); + case PageType::FILE_MANAGER: { + new_page = PageType::CHAT; + + auto file_manager_page = std::make_unique<FileManagerPage>(this); + file_manager_page->set_current_directory(get_home_dir().data); + auto file_manager_body = create_body(); + file_manager_page->get_files_in_directory(file_manager_body->items); + std::vector<Tab> tabs; + tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + selected_files.clear(); + page_loop(std::move(tabs)); + if(selected_files.empty()) { fprintf(stderr, "No files selected!\n"); } else { @@ -3982,8 +3427,8 @@ namespace QuickMedia { redraw = true; break; } - case Page::CHAT_LOGIN: { - new_page = Page::CHAT; + case PageType::CHAT_LOGIN: { + new_page = PageType::CHAT; matrix->logout(); tabs[MESSAGES_TAB_INDEX].body->clear_thumbnails(); // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. @@ -3991,9 +3436,9 @@ namespace QuickMedia { // and one of them is /sync, which has a timeout of 30 seconds. That timeout has to be killed somehow. //delete current_plugin; //current_plugin = new Matrix(); - current_page = Page::CHAT_LOGIN; + current_page = PageType::CHAT_LOGIN; chat_login_page(); - if(current_page == Page::CHAT) + if(current_page == PageType::CHAT) chat_page(); exit(0); break; @@ -4091,8 +3536,6 @@ namespace QuickMedia { sync_timer.restart(); sync_future_room_id = current_room_id; sync_future = std::async(std::launch::async, [this, &sync_future_room_id, synced]() { - Matrix *matrix = static_cast<Matrix*>(current_plugin); - SyncFutureResult result; if(matrix->sync(result.room_sync_messages) == PluginResult::OK) { fprintf(stderr, "Synced matrix\n"); @@ -4100,7 +3543,7 @@ namespace QuickMedia { if(!synced) { if(matrix->get_joined_rooms(result.rooms_body_items) != PluginResult::OK) { show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL); - current_page = Page::EXIT; + current_page = PageType::EXIT; return result; } } @@ -4311,7 +3754,7 @@ namespace QuickMedia { current_room_body_data->last_read_message_timestamp = message->timestamp; // TODO: What if the message is no longer valid? setting_read_marker = true; - set_read_marker_future = std::async(std::launch::async, [matrix, current_room_id, message]() mutable { + set_read_marker_future = std::async(std::launch::async, [this, current_room_id, message]() mutable { if(matrix->set_read_marker(current_room_id, message) != PluginResult::OK) { fprintf(stderr, "Warning: failed to set read marker to %s\n", message->event_id.c_str()); } diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index f489779..382b06a 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -32,6 +32,7 @@ namespace QuickMedia { draw_logo(false), needs_update(true), input_masked(input_masked), + typing(false), vertical_pos(0.0f) { text.setFillColor(text_placeholder_color); @@ -97,6 +98,7 @@ namespace QuickMedia { u8_str->clear(); if(onTextUpdateCallback) onTextUpdateCallback(*u8_str); + typing = false; } else if(updated_autocomplete && elapsed_time >= autocomplete_search_delay) { updated_autocomplete = false; if(!show_placeholder && onAutocompleteRequestCallback) { @@ -160,24 +162,23 @@ namespace QuickMedia { } else { clear_autocomplete_if_text_not_substring(); } - if(!updated_search && onTextBeginTypingCallback) - onTextBeginTypingCallback(); + if(!updated_search) { + typing = true; + if(onTextBeginTypingCallback) + onTextBeginTypingCallback(); + } updated_search = true; updated_autocomplete = true; time_since_search_update.restart(); } } else if(codepoint == 13) { // Return - bool clear_search = true; if(onTextSubmitCallback) { auto u8 = text.getString().toUtf8(); std::string *u8_str = (std::string*)&u8; if(show_placeholder) u8_str->clear(); - clear_search = onTextSubmitCallback(*u8_str); + onTextSubmitCallback(*u8_str); } - - if(clear_search) - clear(); } else if(codepoint > 31) { // Non-control character append_text(sf::String(codepoint)); } else if(codepoint == '\n') @@ -211,8 +212,11 @@ namespace QuickMedia { text.setString(str); clear_autocomplete_if_text_not_substring(); - if(!updated_search && onTextBeginTypingCallback) - onTextBeginTypingCallback(); + if(!updated_search) { + typing = true; + if(onTextBeginTypingCallback) + onTextBeginTypingCallback(); + } updated_search = true; updated_autocomplete = true; time_since_search_update.restart(); @@ -235,8 +239,11 @@ namespace QuickMedia { text.setFillColor(sf::Color::White); } text.setString(autocomplete_str); - if(!updated_search && onTextBeginTypingCallback) - onTextBeginTypingCallback(); + if(!updated_search) { + typing = true; + if(onTextBeginTypingCallback) + onTextBeginTypingCallback(); + } updated_search = true; updated_autocomplete = true; time_since_search_update.restart(); diff --git a/src/Storage.cpp b/src/Storage.cpp index cd34b56..c9dfb17 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1,9 +1,11 @@ #include "../include/Storage.hpp" #include "../include/env.hpp" +#include "../include/StringUtils.hpp" #include <stdio.h> #include <assert.h> #include <json/reader.h> #include <json/writer.h> +#include <unordered_set> #if OS_FAMILY == OS_FAMILY_POSIX #include <pwd.h> @@ -223,4 +225,23 @@ namespace QuickMedia { return true; } + + bool is_program_executable_by_name(const char *name) { + // TODO: Implement for Windows. Windows also uses semicolon instead of colon as a separator + char *env = getenv("PATH"); + std::unordered_set<std::string> paths; + string_split(env, ':', [&paths](const char *str, size_t size) { + paths.insert(std::string(str, size)); + return true; + }); + + for(const std::string &path_str : paths) { + Path path(path_str); + path.join(name); + if(get_file_type(path) == FileType::REGULAR) + return true; + } + + return false; + } }
\ No newline at end of file diff --git a/src/plugins/Dmenu.cpp b/src/plugins/Dmenu.cpp deleted file mode 100644 index 9a8b5b8..0000000 --- a/src/plugins/Dmenu.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "../../plugins/Dmenu.hpp" -#include <iostream> - -namespace QuickMedia { - Dmenu::Dmenu() : Plugin("dmenu") { - std::string line; - while(std::getline(std::cin, line)) { - stdin_data.push_back(std::move(line)); - } - } - - PluginResult Dmenu::get_front_page(BodyItems &result_items) { - for(const std::string &line_data : stdin_data) { - result_items.push_back(BodyItem::create(line_data)); - } - return PluginResult::OK; - } - - SearchResult Dmenu::search(const std::string &text, BodyItems&) { - std::cout << text << std::endl; - return SearchResult::OK; - } -}
\ No newline at end of file diff --git a/src/plugins/FileManager.cpp b/src/plugins/FileManager.cpp index ccaf2c1..5fac79c 100644 --- a/src/plugins/FileManager.cpp +++ b/src/plugins/FileManager.cpp @@ -1,12 +1,8 @@ #include "../../plugins/FileManager.hpp" #include "../../include/ImageUtils.hpp" -#include <filesystem> +#include "../../include/QuickMedia.hpp" namespace QuickMedia { - FileManager::FileManager() : Plugin("file-manager"), current_dir("/") { - - } - // Returns empty string if no extension static const char* get_ext(const std::filesystem::path &path) { const char *path_c = path.c_str(); @@ -26,7 +22,45 @@ namespace QuickMedia { } } - PluginResult FileManager::get_files_in_directory(BodyItems &result_items) { + PluginResult FileManagerPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)url; + + std::filesystem::path new_path; + if(title == "..") + new_path = current_dir.parent_path(); + else + new_path = current_dir / title; + + if(std::filesystem::is_regular_file(new_path)) { + program->select_file(new_path); + return PluginResult::OK; + } + + if(!std::filesystem::is_directory(new_path)) + return PluginResult::ERR; + + current_dir = std::move(new_path); + + BodyItems result_items; + PluginResult result = get_files_in_directory(result_items); + if(result != PluginResult::OK) + return result; + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), nullptr, nullptr}); + return PluginResult::OK; + } + + bool FileManagerPage::set_current_directory(const std::string &path) { + if(!std::filesystem::is_directory(path)) + return false; + current_dir = path; + return true; + } + + PluginResult FileManagerPage::get_files_in_directory(BodyItems &result_items) { std::vector<std::filesystem::directory_entry> paths; try { for(auto &p : std::filesystem::directory_iterator(current_dir)) { @@ -58,33 +92,4 @@ namespace QuickMedia { return PluginResult::OK; } - - bool FileManager::set_current_directory(const std::string &path) { - if(!std::filesystem::is_directory(path)) - return false; - current_dir = path; - return true; - } - - bool FileManager::set_child_directory(const std::string &filename) { - if(filename == "..") { - std::filesystem::path new_path = current_dir.parent_path(); - if(std::filesystem::is_directory(new_path)) { - current_dir = std::move(new_path); - return true; - } - return false; - } else { - std::filesystem::path new_path = current_dir / filename; - if(std::filesystem::is_directory(new_path)) { - current_dir = std::move(new_path); - return true; - } - return false; - } - } - - const std::filesystem::path& FileManager::get_current_dir() const { - return current_dir; - } }
\ No newline at end of file diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 1cecc2b..1d3681a 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Fourchan.hpp" #include "../../include/DataView.hpp" #include "../../include/Storage.hpp" +#include "../../include/StringUtils.hpp" #include <json/reader.h> #include <string.h> #include <tidy.h> @@ -11,40 +12,9 @@ static const std::string fourchan_url = "https://a.4cdn.org/"; static const std::string fourchan_image_url = "https://i.4cdn.org/"; -namespace QuickMedia { - Fourchan::Fourchan(const std::string &resources_root) : ImageBoard("4chan"), resources_root(resources_root) { - thread_list_update_thread = std::thread([this]() { - BodyItems new_thread_list_items; - while(running) { - new_thread_list_items.clear(); - auto start_time = std::chrono::steady_clock::now(); - - std::string board_url = get_board_url(); - if(!board_url.empty()) { - PluginResult plugin_result = get_threads_internal(board_url, new_thread_list_items); - if(plugin_result == PluginResult::OK) - set_board_thread_list(std::move(new_thread_list_items)); - } - - auto time_passed = std::chrono::steady_clock::now() - start_time; - if(time_passed < std::chrono::seconds(15)) { - auto time_to_sleep = std::chrono::seconds(15) - time_passed; - std::unique_lock<std::mutex> lock(thread_list_cache_mutex); - thread_list_update_cv.wait_for(lock, time_to_sleep); - } - } - }); - } - - Fourchan::~Fourchan() { - running = false; - { - std::unique_lock<std::mutex> lock(thread_list_cache_mutex); - thread_list_update_cv.notify_one(); - } - thread_list_update_thread.join(); - } +static const char *SERVICE_NAME = "4chan"; +namespace QuickMedia { // Returns empty string on failure to read cookie static std::string get_pass_id_from_cookies_file(const Path &cookies_filepath) { std::string file_content; @@ -63,53 +33,6 @@ namespace QuickMedia { return strip(file_content.substr(pass_id_index, line_end - pass_id_index)); } - PluginResult Fourchan::get_front_page(BodyItems &result_items) { - if(pass_id.empty()) { - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { - fprintf(stderr, "Failed to get 4chan cookies filepath\n"); - } else { - pass_id = get_pass_id_from_cookies_file(cookies_filepath); - } - } - - std::string server_response; - if(file_get_content(resources_root + "boards.json", server_response) != 0) { - fprintf(stderr, "failed to read boards.json\n"); - return PluginResult::ERR; - } - - Json::Value json_root; - Json::CharReaderBuilder json_builder; - std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) { - fprintf(stderr, "4chan front page json error: %s\n", json_errors.c_str()); - return PluginResult::ERR; - } - - if(!json_root.isObject()) - return PluginResult::ERR; - - const Json::Value &boards = json_root["boards"]; - if(boards.isArray()) { - for(const Json::Value &board : boards) { - const Json::Value &board_id = board["board"]; // /g/, /a/, /b/ etc - const Json::Value &board_title = board["title"]; - const Json::Value &board_description = board["meta_description"]; - if(board_id.isString() && board_title.isString() && board_description.isString()) { - std::string board_description_str = board_description.asString(); - html_unescape_sequences(board_description_str); - auto body_item = BodyItem::create("/" + board_id.asString() + "/ " + board_title.asString()); - body_item->url = board_id.asString(); - result_items.emplace_back(std::move(body_item)); - } - } - } - - return PluginResult::OK; - } - struct CommentPiece { enum class Type { TEXT, @@ -210,270 +133,74 @@ namespace QuickMedia { tidyRelease(doc); } - PluginResult Fourchan::get_threads_internal(const std::string &url, BodyItems &result_items) { + PluginResult FourchanBoardsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { Json::Value json_root; DownloadResult result = download_json(json_root, fourchan_url + url + "/catalog.json", {}, true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - if(json_root.isArray()) { - for(const Json::Value &page_data : json_root) { - if(!page_data.isObject()) - continue; - - const Json::Value &threads = page_data["threads"]; - if(!threads.isArray()) - continue; - - for(const Json::Value &thread : threads) { - if(!thread.isObject()) - continue; - - const Json::Value &sub = thread["sub"]; - const char *sub_begin = ""; - const char *sub_end = sub_begin; - sub.getString(&sub_begin, &sub_end); - - const Json::Value &com = thread["com"]; - const char *comment_begin = ""; - const char *comment_end = comment_begin; - com.getString(&comment_begin, &comment_end); - - const Json::Value &thread_num = thread["no"]; - if(!thread_num.isNumeric()) - continue; - - std::string title_text; - extract_comment_pieces(sub_begin, sub_end - sub_begin, - [&title_text](const CommentPiece &cp) { - switch(cp.type) { - case CommentPiece::Type::TEXT: - title_text.append(cp.text.data, cp.text.size); - break; - case CommentPiece::Type::QUOTE: - title_text += '>'; - title_text.append(cp.text.data, cp.text.size); - //comment_text += '\n'; - break; - case CommentPiece::Type::QUOTELINK: { - title_text.append(cp.text.data, cp.text.size); - break; - } - case CommentPiece::Type::LINE_CONTINUE: { - if(!title_text.empty() && title_text.back() == '\n') { - title_text.pop_back(); - } - break; - } - } - } - ); - if(!title_text.empty() && title_text.back() == '\n') - title_text.back() = ' '; - html_unescape_sequences(title_text); - - std::string comment_text; - extract_comment_pieces(comment_begin, comment_end - comment_begin, - [&comment_text](const CommentPiece &cp) { - switch(cp.type) { - case CommentPiece::Type::TEXT: - comment_text.append(cp.text.data, cp.text.size); - break; - case CommentPiece::Type::QUOTE: - comment_text += '>'; - comment_text.append(cp.text.data, cp.text.size); - //comment_text += '\n'; - break; - case CommentPiece::Type::QUOTELINK: { - comment_text.append(cp.text.data, cp.text.size); - break; - } - case CommentPiece::Type::LINE_CONTINUE: { - if(!comment_text.empty() && comment_text.back() == '\n') { - comment_text.pop_back(); - } - break; - } - } - } - ); - html_unescape_sequences(comment_text); - // TODO: Do the same when wrapping is implemented - // TODO: Remove this - int num_lines = 0; - for(size_t i = 0; i < comment_text.size(); ++i) { - if(comment_text[i] == '\n') { - ++num_lines; - if(num_lines == 6) { - comment_text = comment_text.substr(0, i) + " (...)"; - break; - } - } - } - auto body_item = BodyItem::create(std::move(comment_text)); - body_item->set_author(std::move(title_text)); - body_item->url = std::to_string(thread_num.asInt64()); - - const Json::Value &ext = thread["ext"]; - const Json::Value &tim = thread["tim"]; - if(tim.isNumeric() && ext.isString()) { - std::string ext_str = ext.asString(); - if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg" || ext_str == ".webm" || ext_str == ".mp4" || ext_str == ".gif") { - } else { - fprintf(stderr, "TODO: Support file extension: %s\n", ext_str.c_str()); - } - // "s" means small, that's the url 4chan uses for thumbnails. - // thumbnails always has .jpg extension even if they are gifs or webm. - body_item->thumbnail_url = fourchan_image_url + url + "/" + std::to_string(tim.asInt64()) + "s.jpg"; - } - - result_items.emplace_back(std::move(body_item)); - } - } - } - - return PluginResult::OK; - } - - void Fourchan::set_board_url(const std::string &new_url) { - { - std::lock_guard<std::mutex> lock(board_url_mutex); - if(current_board_url == new_url) - return; - current_board_url = new_url; - } - - std::lock_guard<std::mutex> thread_list_lock(thread_list_cache_mutex); - thread_list_update_cv.notify_one(); - thread_list_cached = false; - } - - std::string Fourchan::get_board_url() { - std::lock_guard<std::mutex> lock(board_url_mutex); - return current_board_url; - } - - void Fourchan::set_board_thread_list(BodyItems body_items) { - { - std::lock_guard<std::mutex> lock(board_list_mutex); - cached_thread_list_items.clear(); - for(auto &body_item : body_items) { - cached_thread_list_items.push_back(std::move(body_item)); - } - } - - std::unique_lock<std::mutex> thread_list_cache_lock(thread_list_cache_mutex); - if(!thread_list_cached) { - thread_list_cached = true; - thread_list_cached_cv.notify_one(); - } - } - - BodyItems Fourchan::get_board_thread_list() { - std::lock_guard<std::mutex> lock(board_list_mutex); - BodyItems body_items; - for(auto &cached_body_item : cached_thread_list_items) { - body_items.push_back(std::make_shared<BodyItem>(*cached_body_item)); - } - return body_items; - } - - PluginResult Fourchan::get_threads(const std::string &url, BodyItems &result_items) { - set_board_url(url); - - std::unique_lock<std::mutex> lock(thread_list_cache_mutex); - if(!thread_list_cached) { - if(thread_list_cached_cv.wait_for(lock, std::chrono::seconds(10)) == std::cv_status::timeout) - return PluginResult::NET_ERR; - } - - result_items = get_board_thread_list(); - return PluginResult::OK; - } - - // TODO: Merge with get_threads_internal - PluginResult Fourchan::get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) { - cached_media_urls.clear(); - - Json::Value json_root; - DownloadResult result = download_json(json_root, fourchan_url + list_url + "/thread/" + url + ".json", {}, true); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isObject()) + if(!json_root.isArray()) return PluginResult::ERR; - std::unordered_map<int64_t, size_t> comment_by_postno; + BodyItems result_items; - const Json::Value &posts = json_root["posts"]; - if(posts.isArray()) { - for(const Json::Value &post : posts) { - if(!post.isObject()) - continue; + for(const Json::Value &page_data : json_root) { + if(!page_data.isObject()) + continue; - const Json::Value &post_num = post["no"]; - if(!post_num.isNumeric()) - continue; - - int64_t post_num_int = post_num.asInt64(); - comment_by_postno[post_num_int] = result_items.size(); - result_items.push_back(BodyItem::create("")); - result_items.back()->post_number = std::to_string(post_num_int); - } - } + const Json::Value &threads = page_data["threads"]; + if(!threads.isArray()) + continue; - size_t body_item_index = 0; - if(posts.isArray()) { - for(const Json::Value &post : posts) { - if(!post.isObject()) + for(const Json::Value &thread : threads) { + if(!thread.isObject()) continue; - const Json::Value &sub = post["sub"]; + const Json::Value &sub = thread["sub"]; const char *sub_begin = ""; const char *sub_end = sub_begin; sub.getString(&sub_begin, &sub_end); - const Json::Value &com = post["com"]; + const Json::Value &com = thread["com"]; const char *comment_begin = ""; const char *comment_end = comment_begin; com.getString(&comment_begin, &comment_end); - const Json::Value &post_num = post["no"]; - if(!post_num.isNumeric()) + const Json::Value &thread_num = thread["no"]; + if(!thread_num.isNumeric()) continue; - const Json::Value &author = post["name"]; - std::string author_str = "Anonymous"; - if(author.isString()) - author_str = author.asString(); - - std::string comment_text; + std::string title_text; extract_comment_pieces(sub_begin, sub_end - sub_begin, - [&comment_text](const CommentPiece &cp) { + [&title_text](const CommentPiece &cp) { switch(cp.type) { case CommentPiece::Type::TEXT: - comment_text.append(cp.text.data, cp.text.size); + title_text.append(cp.text.data, cp.text.size); break; case CommentPiece::Type::QUOTE: - comment_text += '>'; - comment_text.append(cp.text.data, cp.text.size); + title_text += '>'; + title_text.append(cp.text.data, cp.text.size); //comment_text += '\n'; break; case CommentPiece::Type::QUOTELINK: { - comment_text.append(cp.text.data, cp.text.size); + title_text.append(cp.text.data, cp.text.size); break; } case CommentPiece::Type::LINE_CONTINUE: { - if(!comment_text.empty() && comment_text.back() == '\n') { - comment_text.pop_back(); + if(!title_text.empty() && title_text.back() == '\n') { + title_text.pop_back(); } break; } } } ); - if(!comment_text.empty()) - comment_text += '\n'; + if(!title_text.empty() && title_text.back() == '\n') + title_text.back() = ' '; + html_unescape_sequences(title_text); + + std::string comment_text; extract_comment_pieces(comment_begin, comment_end - comment_begin, - [&comment_text, &comment_by_postno, &result_items, body_item_index](const CommentPiece &cp) { + [&comment_text](const CommentPiece &cp) { switch(cp.type) { case CommentPiece::Type::TEXT: comment_text.append(cp.text.data, cp.text.size); @@ -485,13 +212,6 @@ namespace QuickMedia { break; case CommentPiece::Type::QUOTELINK: { comment_text.append(cp.text.data, cp.text.size); - auto it = comment_by_postno.find(cp.quote_postnumber); - if(it == comment_by_postno.end()) { - // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) - comment_text += "(dead)"; - } else { - result_items[it->second]->replies.push_back(body_item_index); - } break; } case CommentPiece::Type::LINE_CONTINUE: { @@ -503,15 +223,25 @@ namespace QuickMedia { } } ); - if(!comment_text.empty() && comment_text.back() == '\n') - comment_text.back() = ' '; html_unescape_sequences(comment_text); - BodyItem *body_item = result_items[body_item_index].get(); - body_item->set_title(std::move(comment_text)); - body_item->set_author(std::move(author_str)); + // TODO: Do the same when wrapping is implemented + // TODO: Remove this + int num_lines = 0; + for(size_t i = 0; i < comment_text.size(); ++i) { + if(comment_text[i] == '\n') { + ++num_lines; + if(num_lines == 6) { + comment_text = comment_text.substr(0, i) + " (...)"; + break; + } + } + } + auto body_item = BodyItem::create(std::move(comment_text)); + body_item->set_author(std::move(title_text)); + body_item->url = std::to_string(thread_num.asInt64()); - const Json::Value &ext = post["ext"]; - const Json::Value &tim = post["tim"]; + const Json::Value &ext = thread["ext"]; + const Json::Value &tim = thread["tim"]; if(tim.isNumeric() && ext.isString()) { std::string ext_str = ext.asString(); if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg" || ext_str == ".webm" || ext_str == ".mp4" || ext_str == ".gif") { @@ -520,76 +250,210 @@ namespace QuickMedia { } // "s" means small, that's the url 4chan uses for thumbnails. // thumbnails always has .jpg extension even if they are gifs or webm. - std::string tim_str = std::to_string(tim.asInt64()); - body_item->thumbnail_url = fourchan_image_url + list_url + "/" + tim_str + "s.jpg"; - body_item->attached_content_url = fourchan_image_url + list_url + "/" + tim_str + ext_str; - cached_media_urls.push_back(body_item->attached_content_url); + body_item->thumbnail_url = fourchan_image_url + url + "/" + std::to_string(tim.asInt64()) + "s.jpg"; } - ++body_item_index; + result_items.push_back(std::move(body_item)); } } + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique<FourchanThreadListPage>(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); return PluginResult::OK; } - PostResult Fourchan::post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) { - std::string url = "https://sys.4chan.org/" + board + "/post"; + void FourchanBoardsPage::get_boards(BodyItems &result_items) { + std::string server_response; + if(file_get_content(resources_root + "boards.json", server_response) != 0) { + fprintf(stderr, "failed to read boards.json\n"); + return; + } - std::vector<CommandArg> additional_args = { - CommandArg{"-H", "Referer: https://boards.4chan.org/"}, - CommandArg{"-H", "Origin: https://boards.4chan.org"}, - CommandArg{"-F", "resto=" + thread}, - CommandArg{"-F", "com=" + comment}, - CommandArg{"-F", "mode=regist"} - }; + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) { + fprintf(stderr, "4chan front page json error: %s\n", json_errors.c_str()); + return; + } - if(pass_id.empty()) { - additional_args.push_back(CommandArg{"-F", "g-recaptcha-response=" + captcha_id}); - } else { - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { - fprintf(stderr, "Failed to get 4chan cookies filepath\n"); - return PostResult::ERR; - } else { - additional_args.push_back(CommandArg{"-c", cookies_filepath.data}); - additional_args.push_back(CommandArg{"-b", cookies_filepath.data}); + if(!json_root.isObject()) + return; + + const Json::Value &boards = json_root["boards"]; + if(!boards.isArray()) + return; + + for(const Json::Value &board : boards) { + const Json::Value &board_id = board["board"]; // /g/, /a/, /b/ etc + const Json::Value &board_title = board["title"]; + const Json::Value &board_description = board["meta_description"]; + if(board_id.isString() && board_title.isString() && board_description.isString()) { + std::string board_description_str = board_description.asString(); + html_unescape_sequences(board_description_str); + auto body_item = BodyItem::create("/" + board_id.asString() + "/ " + board_title.asString()); + body_item->url = board_id.asString(); + result_items.push_back(std::move(body_item)); } } - - std::string response; - if(download_to_string(url, response, additional_args, use_tor, true) != DownloadResult::OK) - return PostResult::ERR; - - if(response.find("successful") != std::string::npos) - return PostResult::OK; - if(response.find("banned") != std::string::npos) - return PostResult::BANNED; - if(response.find("try again") != std::string::npos || response.find("No valid captcha") != std::string::npos) - return PostResult::TRY_AGAIN; - return PostResult::ERR; } - BodyItems Fourchan::get_related_media(const std::string &url) { - BodyItems body_items; - auto it = std::find(cached_media_urls.begin(), cached_media_urls.end(), url); - if(it == cached_media_urls.end()) - return body_items; - - ++it; - for(; it != cached_media_urls.end(); ++it) { - auto body_item = BodyItem::create(""); - body_item->url = *it; - body_items.push_back(std::move(body_item)); + // TODO: Merge with get_threads_internal + PluginResult FourchanThreadListPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + cached_media_urls.clear(); + + Json::Value json_root; + DownloadResult result = download_json(json_root, fourchan_url + board_id + "/thread/" + url + ".json", {}, true); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + + if(!json_root.isObject()) + return PluginResult::ERR; + + BodyItems result_items; + std::unordered_map<int64_t, size_t> comment_by_postno; + + const Json::Value &posts = json_root["posts"]; + if(!posts.isArray()) + return PluginResult::OK; + + for(const Json::Value &post : posts) { + if(!post.isObject()) + continue; + + const Json::Value &post_num = post["no"]; + if(!post_num.isNumeric()) + continue; + + int64_t post_num_int = post_num.asInt64(); + comment_by_postno[post_num_int] = result_items.size(); + result_items.push_back(BodyItem::create("")); + result_items.back()->post_number = std::to_string(post_num_int); } - return body_items; + + size_t body_item_index = 0; + for(const Json::Value &post : posts) { + if(!post.isObject()) + continue; + + const Json::Value &sub = post["sub"]; + const char *sub_begin = ""; + const char *sub_end = sub_begin; + sub.getString(&sub_begin, &sub_end); + + const Json::Value &com = post["com"]; + const char *comment_begin = ""; + const char *comment_end = comment_begin; + com.getString(&comment_begin, &comment_end); + + const Json::Value &post_num = post["no"]; + if(!post_num.isNumeric()) + continue; + + const Json::Value &author = post["name"]; + std::string author_str = "Anonymous"; + if(author.isString()) + author_str = author.asString(); + + std::string comment_text; + extract_comment_pieces(sub_begin, sub_end - sub_begin, + [&comment_text](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + break; + } + case CommentPiece::Type::LINE_CONTINUE: { + if(!comment_text.empty() && comment_text.back() == '\n') { + comment_text.pop_back(); + } + break; + } + } + } + ); + if(!comment_text.empty()) + comment_text += '\n'; + extract_comment_pieces(comment_begin, comment_end - comment_begin, + [&comment_text, &comment_by_postno, &result_items, body_item_index](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + auto it = comment_by_postno.find(cp.quote_postnumber); + if(it == comment_by_postno.end()) { + // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) + comment_text += "(dead)"; + } else { + result_items[it->second]->replies.push_back(body_item_index); + } + break; + } + case CommentPiece::Type::LINE_CONTINUE: { + if(!comment_text.empty() && comment_text.back() == '\n') { + comment_text.pop_back(); + } + break; + } + } + } + ); + if(!comment_text.empty() && comment_text.back() == '\n') + comment_text.back() = ' '; + html_unescape_sequences(comment_text); + BodyItem *body_item = result_items[body_item_index].get(); + body_item->set_title(std::move(comment_text)); + body_item->set_author(std::move(author_str)); + + const Json::Value &ext = post["ext"]; + const Json::Value &tim = post["tim"]; + if(tim.isNumeric() && ext.isString()) { + std::string ext_str = ext.asString(); + if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg" || ext_str == ".webm" || ext_str == ".mp4" || ext_str == ".gif") { + } else { + fprintf(stderr, "TODO: Support file extension: %s\n", ext_str.c_str()); + } + // "s" means small, that's the url 4chan uses for thumbnails. + // thumbnails always has .jpg extension even if they are gifs or webm. + std::string tim_str = std::to_string(tim.asInt64()); + body_item->thumbnail_url = fourchan_image_url + board_id + "/" + tim_str + "s.jpg"; + body_item->attached_content_url = fourchan_image_url + board_id + "/" + tim_str + ext_str; + cached_media_urls.push_back(body_item->attached_content_url); + } + + ++body_item_index; + } + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique<FourchanThreadPage>(program, board_id, url, std::move(cached_media_urls)), nullptr}); + return PluginResult::OK; } - PluginResult Fourchan::login(const std::string &token, const std::string &pin, std::string &response_msg) { + PluginResult FourchanThreadPage::login(const std::string &token, const std::string &pin, std::string &response_msg) { response_msg.clear(); Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { + if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { fprintf(stderr, "Failed to get 4chan cookies filepath\n"); return PluginResult::ERR; } @@ -626,7 +490,52 @@ namespace QuickMedia { } } - const std::string& Fourchan::get_pass_id() const { + PostResult FourchanThreadPage::post_comment(const std::string &captcha_id, const std::string &comment) { + std::string url = "https://sys.4chan.org/" + board_id + "/post"; + + std::vector<CommandArg> additional_args = { + CommandArg{"-H", "Referer: https://boards.4chan.org/"}, + CommandArg{"-H", "Origin: https://boards.4chan.org"}, + CommandArg{"-F", "resto=" + thread_id}, + CommandArg{"-F", "com=" + comment}, + CommandArg{"-F", "mode=regist"} + }; + + if(pass_id.empty()) { + additional_args.push_back(CommandArg{"-F", "g-recaptcha-response=" + captcha_id}); + } else { + Path cookies_filepath; + if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { + fprintf(stderr, "Failed to get 4chan cookies filepath\n"); + return PostResult::ERR; + } else { + additional_args.push_back(CommandArg{"-c", cookies_filepath.data}); + additional_args.push_back(CommandArg{"-b", cookies_filepath.data}); + } + } + + std::string response; + if(download_to_string(url, response, additional_args, is_tor_enabled(), true) != DownloadResult::OK) + return PostResult::ERR; + + if(response.find("successful") != std::string::npos) + return PostResult::OK; + if(response.find("banned") != std::string::npos) + return PostResult::BANNED; + if(response.find("try again") != std::string::npos || response.find("No valid captcha") != std::string::npos) + return PostResult::TRY_AGAIN; + return PostResult::ERR; + } + + const std::string& FourchanThreadPage::get_pass_id() { + if(pass_id.empty()) { + Path cookies_filepath; + if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { + fprintf(stderr, "Failed to get 4chan cookies filepath\n"); + } else { + pass_id = get_pass_id_from_cookies_file(cookies_filepath); + } + } return pass_id; } }
\ No newline at end of file diff --git a/src/plugins/ImageBoard.cpp b/src/plugins/ImageBoard.cpp new file mode 100644 index 0000000..ac05f80 --- /dev/null +++ b/src/plugins/ImageBoard.cpp @@ -0,0 +1,30 @@ +#include "../../plugins/ImageBoard.hpp" + +namespace QuickMedia { + BodyItems ImageBoardThreadPage::get_related_media(const std::string &url) { + BodyItems body_items; + auto it = std::find(cached_media_urls.begin(), cached_media_urls.end(), url); + if(it == cached_media_urls.end()) + return body_items; + + ++it; + for(; it != cached_media_urls.end(); ++it) { + auto body_item = BodyItem::create(""); + body_item->url = *it; + body_items.push_back(std::move(body_item)); + } + return body_items; + } + + PluginResult ImageBoardThreadPage::login(const std::string &token, const std::string &pin, std::string &response_msg) { + (void)token; + (void)pin; + response_msg = "Login is not supported on this image board"; + return PluginResult::ERR; + } + + const std::string& ImageBoardThreadPage::get_pass_id() { + static std::string empty_str; + return empty_str; + } +}
\ No newline at end of file diff --git a/src/plugins/Manga.cpp b/src/plugins/Manga.cpp index 6ad11ab..70a1664 100644 --- a/src/plugins/Manga.cpp +++ b/src/plugins/Manga.cpp @@ -1,7 +1,12 @@ #include "../../plugins/Manga.hpp" +#include "../../include/Program.h" namespace QuickMedia { - const std::vector<Creator>& Manga::get_creators() const { - return creators; + TrackResult MangaChaptersPage::track(const std::string &str) { + const char *args[] = { "automedia", "add", "html", content_url.data(), "--start-after", str.data(), "--name", content_title.data(), nullptr }; + if(exec_program(args, nullptr, nullptr) == 0) + return TrackResult::OK; + else + return TrackResult::ERR; } }
\ No newline at end of file diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index 9808654..be2d342 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Mangadex.hpp" #include "../../include/Storage.hpp" #include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include <quickmedia/HtmlSearch.h> #include <json/reader.h> @@ -27,30 +28,96 @@ namespace QuickMedia { return url.substr(find_index + 9); } + static bool get_cookie_filepath(std::string &cookie_filepath) { + Path cookie_path = get_storage_dir().join("cookies"); + if(create_directory_recursive(cookie_path) != 0) { + show_notification("Storage", "Failed to create directory: " + cookie_path.data, Urgency::CRITICAL); + return false; + } + cookie_filepath = cookie_path.join("mangadex.txt").data; + return true; + } + struct BodyItemChapterContext { BodyItems *body_items; int prev_chapter_number; bool *is_last_page; }; - SearchResult Mangadex::search(const std::string &url, BodyItems &result_items) { + // TODO: Implement pagination (go to next page and get all results as well) + SearchResult MangadexSearchPage::search(const std::string &str, BodyItems &result_items) { + std::string rememberme_token; + if(!get_rememberme_token(rememberme_token)) + return SearchResult::ERR; + + std::string url = "https://mangadex.org/search?title="; + url += url_param_encode(str); + CommandArg cookie_arg = { "-H", "cookie: mangadex_rememberme_token=" + rememberme_token }; + + std::string website_data; + if(download_to_string(url, website_data, {std::move(cookie_arg)}, is_tor_enabled(), true) != DownloadResult::OK) + return SearchResult::NET_ERR; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); + if(result != 0) + goto cleanup; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//a", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *title = quickmedia_html_node_get_attribute_value(node, "title"); + if(title && href && strncmp(href, "/title/", 7) == 0) { + auto item = BodyItem::create(strip(title)); + item->url = mangadex_url + href; + item_data->push_back(std::move(item)); + } + }, &result_items); + + if(result != 0) + goto cleanup; + + BodyItemImageContext body_item_image_context; + body_item_image_context.body_items = &result_items; + body_item_image_context.index = 0; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//img", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItemImageContext*)userdata; + const char *src = quickmedia_html_node_get_attribute_value(node, "src"); + if(src && strncmp(src, "/images/manga/", 14) == 0 && item_data->index < item_data->body_items->size()) { + (*item_data->body_items)[item_data->index]->thumbnail_url = mangadex_url + src; + item_data->index++; + } + }, &body_item_image_context); + + if(result != 0) + goto cleanup; + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? SearchResult::OK : SearchResult::ERR; + } + + PluginResult MangadexSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { std::string manga_id = title_url_extract_manga_id(url); std::string request_url = "https://mangadex.org/api/?id=" + manga_id + "&type=manga"; Json::Value json_root; DownloadResult result = download_json(json_root, request_url, {}, true); - if(result != DownloadResult::OK) return download_result_to_search_result(result); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); if(!json_root.isObject()) - return SearchResult::ERR; + return PluginResult::ERR; Json::Value &status_json = json_root["status"]; if(!status_json.isString() || status_json.asString() != "OK") - return SearchResult::ERR; + return PluginResult::ERR; Json::Value &chapter_json = json_root["chapter"]; if(!chapter_json.isObject()) - return SearchResult::ERR; + return PluginResult::ERR; std::vector<std::pair<std::string, Json::Value>> chapters(chapter_json.size()); /* TODO: Optimize member access */ @@ -74,6 +141,8 @@ namespace QuickMedia { time_t time_now = time(NULL); + auto body = create_body(); + /* TODO: Pointer */ std::string prev_chapter_number; for(auto it = chapters.begin(); it != chapters.end(); ++it) { @@ -106,22 +175,17 @@ namespace QuickMedia { auto item = BodyItem::create(std::move(chapter_name)); item->url = std::move(chapter_url); - result_items.push_back(std::move(item)); + body->items.push_back(std::move(item)); } - return SearchResult::OK; - } - static bool get_cookie_filepath(std::string &cookie_filepath) { - Path cookie_path = get_storage_dir().join("cookies"); - if(create_directory_recursive(cookie_path) != 0) { - show_notification("Storage", "Failed to create directory: " + cookie_path.data, Urgency::CRITICAL); - return false; - } - cookie_filepath = cookie_path.join("mangadex.txt").data; - return true; + result_tabs.push_back(Tab{std::move(body), std::make_unique<MangadexChaptersPage>(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + if(load_manga_content_storage("mangadex", title, manga_id)) + return PluginResult::OK; + return PluginResult::ERR; } - bool Mangadex::get_rememberme_token(std::string &rememberme_token_output) { + bool MangadexSearchPage::get_rememberme_token(std::string &rememberme_token_output) { if(rememberme_token) { rememberme_token_output = rememberme_token.value(); return true; @@ -153,93 +217,34 @@ namespace QuickMedia { return true; } - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; - - // TODO: Implement pagination (go to next page and get all results as well) - SuggestionResult Mangadex::update_search_suggestions(const std::string &text, BodyItems &result_items) { - std::string rememberme_token; - if(!get_rememberme_token(rememberme_token)) - return SuggestionResult::ERR; - - std::string url = "https://mangadex.org/search?title="; - url += url_param_encode(text); - CommandArg cookie_arg = { "-H", "cookie: mangadex_rememberme_token=" + rememberme_token }; - - std::string website_data; - if(download_to_string(url, website_data, {std::move(cookie_arg)}, use_tor, true) != DownloadResult::OK) - return SuggestionResult::NET_ERR; - - QuickMediaHtmlSearch html_search; - int result = quickmedia_html_search_init(&html_search, website_data.c_str()); - if(result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *title = quickmedia_html_node_get_attribute_value(node, "title"); - if(title && href && strncmp(href, "/title/", 7) == 0) { - auto item = BodyItem::create(strip(title)); - item->url = mangadex_url + href; - item_data->push_back(std::move(item)); - } - }, &result_items); - - if(result != 0) - goto cleanup; - - BodyItemImageContext body_item_image_context; - body_item_image_context.body_items = &result_items; - body_item_image_context.index = 0; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//img", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItemImageContext*)userdata; - const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src && strncmp(src, "/images/manga/", 14) == 0 && item_data->index < item_data->body_items->size()) { - (*item_data->body_items)[item_data->index]->thumbnail_url = mangadex_url + src; - item_data->index++; - } - }, &body_item_image_context); - - if(result != 0) - goto cleanup; - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR; + PluginResult MangadexChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique<MangadexImagesPage>(program, content_title, title, url), nullptr}); + return PluginResult::OK; } - ImageResult Mangadex::get_number_of_images(const std::string &url, int &num_images) { - std::lock_guard<std::mutex> lock(image_urls_mutex); + ImageResult MangadexImagesPage::get_number_of_images(int &num_images) { num_images = 0; ImageResult image_result = get_image_urls_for_chapter(url); if(image_result != ImageResult::OK) return image_result; - num_images = last_chapter_image_urls.size(); + num_images = chapter_image_urls.size(); return ImageResult::OK; } - bool Mangadex::save_mangadex_cookies(const std::string &url, const std::string &cookie_filepath) { + bool MangadexImagesPage::save_mangadex_cookies(const std::string &url, const std::string &cookie_filepath) { CommandArg cookie_arg = { "-c", cookie_filepath }; std::string server_response; - if(download_to_string(url, server_response, {std::move(cookie_arg)}, use_tor, true) != DownloadResult::OK) + if(download_to_string(url, server_response, {std::move(cookie_arg)}, is_tor_enabled(), true) != DownloadResult::OK) return false; return true; } - ImageResult Mangadex::get_image_urls_for_chapter(const std::string &url) { - if(url == last_chapter_url) + ImageResult MangadexImagesPage::get_image_urls_for_chapter(const std::string &url) { + if(!chapter_image_urls.empty()) return ImageResult::OK; - last_chapter_image_urls.clear(); - std::string cookie_filepath; if(!get_cookie_filepath(cookie_filepath)) return ImageResult::ERR; @@ -281,38 +286,28 @@ namespace QuickMedia { continue; std::string image_url = server + chapter_hash_str + "/" + image_name.asCString(); - last_chapter_image_urls.push_back(std::move(image_url)); + chapter_image_urls.push_back(std::move(image_url)); } } - last_chapter_url = url; - if(last_chapter_image_urls.empty()) { - last_chapter_url.clear(); + if(chapter_image_urls.empty()) return ImageResult::ERR; - } return ImageResult::OK; } - ImageResult Mangadex::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) { + ImageResult MangadexImagesPage::for_each_page_in_chapter(PageCallback callback) { std::vector<std::string> image_urls; - { - std::lock_guard<std::mutex> lock(image_urls_mutex); - ImageResult image_result = get_image_urls_for_chapter(chapter_url); - if(image_result != ImageResult::OK) - return image_result; + ImageResult image_result = get_image_urls_for_chapter(url); + if(image_result != ImageResult::OK) + return image_result; - image_urls = last_chapter_image_urls; - } + image_urls = chapter_image_urls; for(const std::string &url : image_urls) { if(!callback(url)) break; } - return ImageResult::OK; - } - bool Mangadex::extract_id_from_url(const std::string &url, std::string &manga_id) { - manga_id = title_url_extract_manga_id(url); - return true; + return ImageResult::OK; } } diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index 52b9ebd..e96bc65 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -1,56 +1,10 @@ #include "../../plugins/Manganelo.hpp" #include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include <quickmedia/HtmlSearch.h> namespace QuickMedia { - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; - - SearchResult Manganelo::search(const std::string &url, BodyItems &result_items) { - creators.clear(); - - std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return SearchResult::NET_ERR; - - QuickMediaHtmlSearch html_search; - int result = quickmedia_html_search_init(&html_search, website_data.c_str()); - if(result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='row-content-chapter']//a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text) { - auto item = BodyItem::create(strip(text)); - item->url = href; - item_data->push_back(std::move(item)); - } - }, &result_items); - - quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", - [](QuickMediaHtmlNode *node, void *userdata) { - std::vector<Creator> *creators = (std::vector<Creator>*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text && strstr(href, "/author/story/")) { - Creator creator; - creator.name = strip(text); - creator.url = href; - creators->push_back(std::move(creator)); - } - }, &creators); - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result == 0 ? SearchResult::OK : SearchResult::ERR; - } - - // Returns true if changed + // Returns true if modified static bool remove_html_span(std::string &str) { size_t open_tag_start = str.find("<span"); if(open_tag_start == std::string::npos) @@ -70,103 +24,103 @@ namespace QuickMedia { return true; } - SuggestionResult Manganelo::update_search_suggestions(const std::string &text, BodyItems &result_items) { + SearchResult ManganeloSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://manganelo.com/getstorysearchjson"; std::string search_term = "searchword="; - search_term += url_param_encode(text); + search_term += url_param_encode(str); CommandArg data_arg = { "--data", std::move(search_term) }; Json::Value json_root; DownloadResult result = download_json(json_root, url, {data_arg}, true); - if(result != DownloadResult::OK) return download_result_to_suggestion_result(result); - - if(json_root.isArray()) { - for(const Json::Value &child : json_root) { - if(child.isObject()) { - Json::Value name = child.get("name", ""); - Json::Value nameunsigned = child.get("nameunsigned", ""); - if(name.isString() && name.asCString()[0] != '\0' && nameunsigned.isString() && nameunsigned.asCString()[0] != '\0') { - std::string name_str = name.asString(); - while(remove_html_span(name_str)) {} - auto item = BodyItem::create(strip(name_str)); - item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); - Json::Value image = child.get("image", ""); - if(image.isString() && image.asCString()[0] != '\0') - item->thumbnail_url = image.asString(); - result_items.push_back(std::move(item)); - } - } + if(result != DownloadResult::OK) return download_result_to_search_result(result); + + if(json_root.isNull()) + return SearchResult::OK; + + if(!json_root.isArray()) + return SearchResult::ERR; + + for(const Json::Value &child : json_root) { + if(!child.isObject()) + continue; + + Json::Value name = child.get("name", ""); + Json::Value nameunsigned = child.get("nameunsigned", ""); + if(name.isString() && name.asCString()[0] != '\0' && nameunsigned.isString() && nameunsigned.asCString()[0] != '\0') { + std::string name_str = name.asString(); + while(remove_html_span(name_str)) {} + auto item = BodyItem::create(strip(name_str)); + item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); + Json::Value image = child.get("image", ""); + if(image.isString() && image.asCString()[0] != '\0') + item->thumbnail_url = image.asString(); + result_items.push_back(std::move(item)); } } - return SuggestionResult::OK; - } - ImageResult Manganelo::get_number_of_images(const std::string &url, int &num_images) { - std::lock_guard<std::mutex> lock(image_urls_mutex); - num_images = 0; - ImageResult image_result = get_image_urls_for_chapter(url); - if(image_result != ImageResult::OK) - return image_result; - - num_images = last_chapter_image_urls.size(); - return ImageResult::OK; + return SearchResult::OK; } - ImageResult Manganelo::get_image_urls_for_chapter(const std::string &url) { - if(url == last_chapter_url) - return ImageResult::OK; - - last_chapter_image_urls.clear(); + PluginResult ManganeloSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + BodyItems chapters_items; + std::vector<Creator> creators; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return ImageResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='container-chapter-reader']/img", + result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='row-content-chapter']//a", [](QuickMediaHtmlNode *node, void *userdata) { - auto *urls = (std::vector<std::string>*)userdata; - const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src) { - std::string image_url = src; - urls->emplace_back(std::move(image_url)); + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *text = quickmedia_html_node_get_text(node); + if(href && text) { + auto item = BodyItem::create(strip(text)); + item->url = href; + item_data->push_back(std::move(item)); + } + }, &chapters_items); + + quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", + [](QuickMediaHtmlNode *node, void *userdata) { + std::vector<Creator> *creators = (std::vector<Creator>*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *text = quickmedia_html_node_get_text(node); + if(href && text && strstr(href, "/author/story/")) { + Creator creator; + creator.name = strip(text); + creator.url = href; + creators->push_back(std::move(creator)); } - }, &last_chapter_image_urls); + }, &creators); cleanup: quickmedia_html_search_deinit(&html_search); - if(result == 0) - last_chapter_url = url; - if(last_chapter_image_urls.empty()) { - last_chapter_url.clear(); - return ImageResult::ERR; - } - return result == 0 ? ImageResult::OK : ImageResult::ERR; - } + if(result != 0) + return PluginResult::ERR; - ImageResult Manganelo::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) { - std::vector<std::string> image_urls; - { - std::lock_guard<std::mutex> lock(image_urls_mutex); - ImageResult image_result = get_image_urls_for_chapter(chapter_url); - if(image_result != ImageResult::OK) - return image_result; + auto chapters_body = create_body(); + chapters_body->items = std::move(chapters_items); + result_tabs.push_back(Tab{std::move(chapters_body), std::make_unique<ManganeloChaptersPage>(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); - image_urls = last_chapter_image_urls; + for(Creator &creator : creators) { + result_tabs.push_back(Tab{create_body(), std::make_unique<ManganeloCreatorPage>(program, std::move(creator)), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } - for(const std::string &url : image_urls) { - if(!callback(url)) - break; + std::string manga_id; + if(extract_id_from_url(url, manga_id)) { + if(load_manga_content_storage("manganelo", title, manga_id)) + return PluginResult::OK; } - return ImageResult::OK; + return PluginResult::ERR; } - - bool Manganelo::extract_id_from_url(const std::string &url, std::string &manga_id) { + + bool ManganeloSearchPage::extract_id_from_url(const std::string &url, std::string &manga_id) const { bool manganelo_website = false; if(url.find("mangakakalot") != std::string::npos || url.find("manganelo") != std::string::npos) manganelo_website = true; @@ -177,7 +131,7 @@ namespace QuickMedia { std::string err_msg = "Url "; err_msg += url; err_msg += " doesn't contain manga id"; - show_notification("Manga", err_msg, Urgency::CRITICAL); + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } @@ -186,7 +140,7 @@ namespace QuickMedia { std::string err_msg = "Url "; err_msg += url; err_msg += " doesn't contain manga id"; - show_notification("Manga", err_msg, Urgency::CRITICAL); + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } return true; @@ -194,56 +148,81 @@ namespace QuickMedia { std::string err_msg = "Unexpected url "; err_msg += url; err_msg += " is not manganelo or mangakakalot"; - show_notification("Manga", err_msg, Urgency::CRITICAL); + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } } - PluginResult Manganelo::get_creators_manga_list(const std::string &url, BodyItems &result_items) { + PluginResult ManganeloChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique<ManganeloImagesPage>(program, content_title, title, url), nullptr}); + return PluginResult::OK; + } + + PluginResult ManganeloCreatorPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + (void)url; + (void)result_tabs; + // TODO: Implement + return PluginResult::ERR; + } + + ImageResult ManganeloImagesPage::get_number_of_images(int &num_images) { + num_images = 0; + ImageResult image_result = get_image_urls_for_chapter(url); + if(image_result != ImageResult::OK) + return image_result; + + num_images = chapter_image_urls.size(); + return ImageResult::OK; + } + + ImageResult ManganeloImagesPage::for_each_page_in_chapter(PageCallback callback) { + std::vector<std::string> image_urls; + ImageResult image_result = get_image_urls_for_chapter(url); + if(image_result != ImageResult::OK) + return image_result; + + image_urls = chapter_image_urls; + + for(const std::string &url : image_urls) { + if(!callback(url)) + break; + } + + return ImageResult::OK; + } + + ImageResult ManganeloImagesPage::get_image_urls_for_chapter(const std::string &url) { + if(!chapter_image_urls.empty()) + return ImageResult::OK; + std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return PluginResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return ImageResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *title = quickmedia_html_node_get_attribute_value(node, "title"); - if(href && title && strstr(href, "/manga/")) { - auto body_item = BodyItem::create(title); - body_item->url = href; - item_data->push_back(std::move(body_item)); - } - }, &result_items); - - if(result != 0) - goto cleanup; - - BodyItemImageContext body_item_image_context; - body_item_image_context.body_items = &result_items; - body_item_image_context.index = 0; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']//img", + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='container-chapter-reader']/img", [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItemImageContext*)userdata; + auto *urls = (std::vector<std::string>*)userdata; const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src && item_data->index < item_data->body_items->size()) { - (*item_data->body_items)[item_data->index]->thumbnail_url = src; - item_data->index++; + if(src) { + std::string image_url = src; + urls->push_back(std::move(image_url)); } - }, &body_item_image_context); + }, &chapter_image_urls); cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0) { - result_items.clear(); - return PluginResult::ERR; + chapter_image_urls.clear(); + return ImageResult::ERR; } - return PluginResult::OK; + if(chapter_image_urls.empty()) + return ImageResult::ERR; + return ImageResult::OK; } }
\ No newline at end of file diff --git a/src/plugins/Mangatown.cpp b/src/plugins/Mangatown.cpp index 400d1ef..5d6f97f 100644 --- a/src/plugins/Mangatown.cpp +++ b/src/plugins/Mangatown.cpp @@ -1,59 +1,26 @@ #include "../../plugins/Mangatown.hpp" #include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include <quickmedia/HtmlSearch.h> static const std::string mangatown_url = "https://www.mangatown.com"; namespace QuickMedia { - SearchResult Mangatown::search(const std::string &url, BodyItems &result_items) { - std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return SearchResult::NET_ERR; - - QuickMediaHtmlSearch html_search; - int result = quickmedia_html_search_init(&html_search, website_data.c_str()); - if(result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='chapter_list']//a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text && strncmp(href, "/manga/", 7) == 0) { - auto item = BodyItem::create(strip(text)); - item->url = mangatown_url + href; - item_data->push_back(std::move(item)); - } - }, &result_items); - - cleanup: - quickmedia_html_search_deinit(&html_search); - - int chapter_num = result_items.size(); - for(auto &body_item : result_items) { - body_item->set_title("Ch. " + std::to_string(chapter_num)); - chapter_num--; - } - - return result == 0 ? SearchResult::OK : SearchResult::ERR; + static bool is_number_with_zero_fill(const char *str) { + while(*str == '0') { ++str; } + return atoi(str) != 0; } - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; - - SuggestionResult Mangatown::update_search_suggestions(const std::string &text, BodyItems &result_items) { + SearchResult MangatownSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://www.mangatown.com/search?name="; - url += url_param_encode(text); + url += url_param_encode(str); std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return SuggestionResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return SearchResult::NET_ERR; if(website_data.empty()) - return SuggestionResult::OK; + return SearchResult::OK; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); @@ -88,77 +55,101 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - return SuggestionResult::OK; + return SearchResult::OK; } - static bool is_number_with_zero_fill(const char *str) { - while(*str == '0') { ++str; } - return atoi(str) != 0; - } - - ImageResult Mangatown::get_number_of_images(const std::string &url, int &num_images) { - std::lock_guard<std::mutex> lock(image_urls_mutex); - - num_images = last_num_pages; - if(url == last_chapter_url_num_images) - return ImageResult::OK; - - last_num_pages = 0; + PluginResult MangatownSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + BodyItems chapters_items; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return ImageResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='page_select']//option", + result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='chapter_list']//a", [](QuickMediaHtmlNode *node, void *userdata) { - int *last_num_pages = (int*)userdata; - const char *value = quickmedia_html_node_get_attribute_value(node, "value"); + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); - if(value && strncmp(value, "/manga/", 7) == 0) { - if(is_number_with_zero_fill(text)) { - (*last_num_pages)++; - } + if(href && text && strncmp(href, "/manga/", 7) == 0) { + auto item = BodyItem::create(strip(text)); + item->url = mangatown_url + href; + item_data->push_back(std::move(item)); } - }, &last_num_pages); - - last_num_pages /= 2; - num_images = last_num_pages; + }, &chapters_items); cleanup: quickmedia_html_search_deinit(&html_search); + if(result != 0) + return PluginResult::ERR; - if(result == 0) - last_chapter_url_num_images = url; - if(last_num_pages == 0) { - last_chapter_url_num_images.clear(); - return ImageResult::ERR; + int chapter_num = chapters_items.size(); + for(auto &body_item : chapters_items) { + body_item->set_title("Ch. " + std::to_string(chapter_num)); + chapter_num--; } - return result == 0 ? ImageResult::OK : ImageResult::ERR; + + auto body = create_body(); + body->items = std::move(chapters_items); + result_tabs.push_back(Tab{std::move(body), std::make_unique<MangatownChaptersPage>(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + std::string manga_id; + if(extract_id_from_url(url, manga_id)) { + if(load_manga_content_storage("mangatown", title, manga_id)) + return PluginResult::OK; + } + return PluginResult::ERR; + } + + bool MangatownSearchPage::extract_id_from_url(const std::string &url, std::string &manga_id) const { + size_t start_index = url.find("/manga/"); + if(start_index == std::string::npos) + return false; + + start_index += 7; + size_t end_index = url.find("/", start_index); + if(end_index == std::string::npos) { + manga_id = url.substr(start_index); + return true; + } + + manga_id = url.substr(start_index, end_index - start_index); + return true; } - ImageResult Mangatown::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) { - int num_pages; - ImageResult image_result = get_number_of_images(chapter_url, num_pages); + PluginResult MangatownChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique<MangatownImagesPage>(program, content_title, title, url), nullptr}); + return PluginResult::OK; + } + + ImageResult MangatownImagesPage::get_number_of_images(int &num_images) { + num_images = 0; + ImageResult image_result = get_image_urls_for_chapter(url); if(image_result != ImageResult::OK) return image_result; - int result = 0; - int page_index = 1; + num_images = chapter_image_urls.size(); + return ImageResult::OK; + } + + ImageResult MangatownImagesPage::for_each_page_in_chapter(PageCallback callback) { + int num_pages; + ImageResult image_result = get_number_of_images(num_pages); + if(image_result != ImageResult::OK) + return image_result; - while(true) { + for(const std::string &full_url : chapter_image_urls) { std::string image_src; std::string website_data; - std::string full_url = chapter_url + std::to_string(page_index++) + ".html"; - if(download_to_string_cache(full_url, website_data, {}, use_tor, true) != DownloadResult::OK) + if(download_to_string_cache(full_url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) break; QuickMediaHtmlSearch html_search; - result = quickmedia_html_search_init(&html_search, website_data.c_str()); + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; @@ -190,19 +181,46 @@ namespace QuickMedia { return ImageResult::OK; } - bool Mangatown::extract_id_from_url(const std::string &url, std::string &manga_id) { - size_t start_index = url.find("/manga/"); - if(start_index == std::string::npos) - return false; - - start_index += 7; - size_t end_index = url.find("/", start_index); - if(end_index == std::string::npos) { - manga_id = url.substr(start_index); - return true; + ImageResult MangatownImagesPage::get_image_urls_for_chapter(const std::string &url) { + if(!chapter_image_urls.empty()) + return ImageResult::OK; + + int num_pages = 0; + + std::string website_data; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return ImageResult::NET_ERR; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); + if(result != 0) + goto cleanup; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='page_select']//option", + [](QuickMediaHtmlNode *node, void *userdata) { + int *last_num_pages = (int*)userdata; + const char *value = quickmedia_html_node_get_attribute_value(node, "value"); + const char *text = quickmedia_html_node_get_text(node); + if(value && strncmp(value, "/manga/", 7) == 0) { + if(is_number_with_zero_fill(text)) { + (*last_num_pages)++; + } + } + }, &num_pages); + + num_pages /= 2; + + cleanup: + quickmedia_html_search_deinit(&html_search); + if(result != 0) { + chapter_image_urls.clear(); + return ImageResult::ERR; } - - manga_id = url.substr(start_index, end_index - start_index); - return true; + if(num_pages == 0) + return ImageResult::ERR; + for(int i = 0; i < num_pages; ++i) { + chapter_image_urls.push_back(url + std::to_string(1 + i) + ".html"); + } + return ImageResult::OK; } }
\ No newline at end of file diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 2107812..dec4a68 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1,5 +1,6 @@ #include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" +#include "../../include/StringUtils.hpp" #include <json/reader.h> #include <json/writer.h> #include <fcntl.h> @@ -18,6 +19,8 @@ // TODO: Verify if this class really is thread-safe (for example room data fields, user fields, message fields; etc that are updated in /sync) +static const char* SERVICE_NAME = "matrix"; + namespace QuickMedia { std::shared_ptr<UserInfo> RoomData::get_user_by_id(const std::string &user_id) { std::lock_guard<std::mutex> lock(room_mutex); @@ -99,10 +102,6 @@ namespace QuickMedia { return messages; } - Matrix::Matrix() : Plugin("matrix") { - - } - PluginResult Matrix::sync(RoomSyncMessages &room_messages) { std::vector<CommandArg> additional_args = { { "-H", "Authorization: Bearer " + access_token }, @@ -819,10 +818,6 @@ namespace QuickMedia { return PluginResult::OK; } - SearchResult Matrix::search(const std::string&, BodyItems&) { - return SearchResult::OK; - } - static bool generate_random_characters(char *buffer, int buffer_size) { int fd = open("/dev/urandom", O_RDONLY); if(fd == -1) { @@ -1443,7 +1438,7 @@ namespace QuickMedia { // TODO: Handle well_known field. The spec says clients SHOULD handle it if its provided - Path session_path = get_storage_dir().join(name); + Path session_path = get_storage_dir().join(SERVICE_NAME); if(create_directory_recursive(session_path) == 0) { session_path.join("session.json"); if(!save_json_to_file_atomic(session_path, json_root)) { @@ -1457,7 +1452,7 @@ namespace QuickMedia { } PluginResult Matrix::logout() { - Path session_path = get_storage_dir().join(name).join("session.json"); + Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); remove(session_path.data.c_str()); std::vector<CommandArg> additional_args = { @@ -1530,7 +1525,7 @@ namespace QuickMedia { } PluginResult Matrix::load_and_verify_cached_session() { - Path session_path = get_storage_dir().join(name).join("session.json"); + Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); std::string session_json_content; if(file_get_content(session_path, session_json_content) != 0) { fprintf(stderr, "Info: failed to read matrix session from %s. Either its missing or we failed to read the file\n", session_path.data.c_str()); @@ -1721,4 +1716,28 @@ namespace QuickMedia { std::lock_guard<std::mutex> lock(room_data_mutex); room_data_by_id.insert(std::make_pair(room->id, room)); } + + DownloadResult Matrix::download_json(Json::Value &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent, std::string *err_msg) const { + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { + if(err_msg) + *err_msg = server_response; + return DownloadResult::NET_ERR; + } + + if(server_response.empty()) + return DownloadResult::OK; + + Json::CharReaderBuilder json_builder; + std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &result, &json_errors)) { + fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); + if(err_msg) + *err_msg = std::move(json_errors); + return DownloadResult::ERR; + } + + return DownloadResult::OK; + } }
\ No newline at end of file diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp index e29b2b0..8d0679e 100644 --- a/src/plugins/NyaaSi.cpp +++ b/src/plugins/NyaaSi.cpp @@ -1,5 +1,8 @@ #include "../../plugins/NyaaSi.hpp" #include "../../include/Program.h" +#include "../../include/Storage.hpp" +#include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include <quickmedia/HtmlSearch.h> namespace QuickMedia { @@ -29,60 +32,19 @@ namespace QuickMedia { return true; } - NyaaSi::NyaaSi() : Plugin("nyaa.si") { - - } - - NyaaSi::~NyaaSi() { - - } - static std::shared_ptr<BodyItem> create_front_page_item(const std::string &title, const std::string &category) { auto body_item = BodyItem::create(title); body_item->url = category; return body_item; } - PluginResult NyaaSi::get_front_page(BodyItems &result_items) { - result_items.push_back(create_front_page_item("All categories", "0_0")); - result_items.push_back(create_front_page_item("Anime", "1_0")); - result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); - result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); - result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); - result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); - result_items.push_back(create_front_page_item("Audio", "2_0")); - result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); - result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); - result_items.push_back(create_front_page_item("Literature", "3_0")); - result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); - result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); - result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); - result_items.push_back(create_front_page_item("Live Action", "4_0")); - result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); - result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); - result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); - result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); - result_items.push_back(create_front_page_item("Pictures", "5_0")); - result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); - result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); - result_items.push_back(create_front_page_item("Software", "6_0")); - result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); - result_items.push_back(create_front_page_item(" Software - Games", "6_2")); - return PluginResult::OK; - } - - - SearchResult NyaaSi::content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) { - return search_page(list_url, text, 1, result_items); - } - - SearchResult NyaaSi::content_list_search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { - return search_page(list_url, text, 1 + page, result_items); + static PluginResult search_result_to_plugin_result(SearchResult search_result) { + return (PluginResult)search_result; } // TODO: Also show the number of comments for each torrent. TODO: Optimize? // TODO: Show each field as seperate columns instead of seperating by | - SearchResult NyaaSi::search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { + static SearchResult search_page(const std::string &list_url, const std::string &text, int page, bool use_tor, BodyItems &result_items) { std::string full_url = "https://nyaa.si/?c=" + list_url + "&f=0&p=" + std::to_string(page) + "&q="; full_url += url_param_encode(text); @@ -217,34 +179,64 @@ namespace QuickMedia { return SearchResult::OK; } - static PluginResult search_result_to_plugin_result(SearchResult search_result) { - switch(search_result) { - case SearchResult::OK: return PluginResult::OK; - case SearchResult::ERR: return PluginResult::ERR; - case SearchResult::NET_ERR: return PluginResult::NET_ERR; - } - return PluginResult::ERR; + PluginResult NyaaSiCategoryPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + BodyItems result_items; + SearchResult search_result = search_page(url, "", 1, is_tor_enabled(), result_items); + if(search_result != SearchResult::OK) return search_result_to_plugin_result(search_result); + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique<NyaaSiSearchPage>(program, strip(title), url), create_search_bar("Search...", 200)}); + return PluginResult::OK; } - PluginResult NyaaSi::get_content_list(const std::string &url, BodyItems &result_items) { - return search_result_to_plugin_result(search_page(url, "", 1, result_items)); + void NyaaSiCategoryPage::get_categories(BodyItems &result_items) { + result_items.push_back(create_front_page_item("All categories", "0_0")); + result_items.push_back(create_front_page_item("Anime", "1_0")); + result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); + result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); + result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); + result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); + result_items.push_back(create_front_page_item("Audio", "2_0")); + result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); + result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); + result_items.push_back(create_front_page_item("Literature", "3_0")); + result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); + result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); + result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); + result_items.push_back(create_front_page_item("Live Action", "4_0")); + result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); + result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); + result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); + result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); + result_items.push_back(create_front_page_item("Pictures", "5_0")); + result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); + result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); + result_items.push_back(create_front_page_item("Software", "6_0")); + result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); + result_items.push_back(create_front_page_item(" Software - Games", "6_2")); } - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; + SearchResult NyaaSiSearchPage::search(const std::string &str, BodyItems &result_items) { + return search_page(category_id, str, 1, is_tor_enabled(), result_items); + } - PluginResult NyaaSi::get_content_details(const std::string&, const std::string &url, BodyItems &result_items) { + PluginResult NyaaSiSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { + return search_result_to_plugin_result(search_page(category_id, str, 1 + page, is_tor_enabled(), result_items)); + } + + PluginResult NyaaSiSearchPage::submit(const std::string&, const std::string &url, std::vector<Tab> &result_tabs) { size_t comments_start_index; std::string title; + BodyItems result_items; auto torrent_item = BodyItem::create("Download magnet"); std::string magnet_url; std::string description; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; @@ -377,9 +369,26 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - if(result != 0) { - result_items.clear(); + if(result != 0) return PluginResult::ERR; + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique<NyaaSiTorrentPage>(program), nullptr}); + return PluginResult::OK; + } + + PluginResult NyaaSiTorrentPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + (void)result_tabs; + if(strncmp(url.c_str(), "magnet:?", 8) == 0) { + if(!is_program_executable_by_name("xdg-open")) { + show_notification("Nyaa.si", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); + return PluginResult::ERR; + } + const char *args[] = { "xdg-open", url.c_str(), nullptr }; + exec_program_async(args, nullptr); } return PluginResult::OK; } diff --git a/src/plugins/Page.cpp b/src/plugins/Page.cpp new file mode 100644 index 0000000..48efeff --- /dev/null +++ b/src/plugins/Page.cpp @@ -0,0 +1,50 @@ +#include "../../plugins/Page.hpp" +#include "../../include/QuickMedia.hpp" +#include <json/reader.h> + +namespace QuickMedia { + BodyItems Page::get_related_media(const std::string &url) { + (void)url; + return {}; + } + + DownloadResult Page::download_json(Json::Value &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent, std::string *err_msg) { + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), is_tor_enabled(), use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { + if(err_msg) + *err_msg = server_response; + return DownloadResult::NET_ERR; + } + + if(server_response.empty()) + return DownloadResult::OK; + + Json::CharReaderBuilder json_builder; + std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &result, &json_errors)) { + fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); + if(err_msg) + *err_msg = std::move(json_errors); + return DownloadResult::ERR; + } + + return DownloadResult::OK; + } + + bool Page::is_tor_enabled() { + return program->is_tor_enabled(); + } + + std::unique_ptr<Body> Page::create_body() { + return program->create_body(); + } + + std::unique_ptr<SearchBar> Page::create_search_bar(const std::string &placeholder_text, int search_delay) { + return program->create_search_bar(placeholder_text, search_delay); + } + + bool Page::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) { + return program->load_manga_content_storage(service_name, manga_title, manga_id); + } +}
\ No newline at end of file diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index ac60187..3f76b4c 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -1,34 +1,10 @@ #include "../../plugins/Plugin.hpp" +#include "../../include/StringUtils.hpp" #include <sstream> #include <iomanip> #include <array> -#include <json/reader.h> namespace QuickMedia { - SearchResult Plugin::search(const std::string &text, BodyItems &result_items) { - (void)text; - (void)result_items; - return SearchResult::OK; - } - - SuggestionResult Plugin::update_search_suggestions(const std::string &text, BodyItems &result_items) { - (void)text; - (void)result_items; - return SuggestionResult::OK; - } - - SearchResult Plugin::content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) { - (void)list_url; - (void)text; - (void)result_items; - return SearchResult::OK; - } - - BodyItems Plugin::get_related_media(const std::string &url) { - (void)url; - return {}; - } - struct HtmlEscapeSequence { char unescape_char; std::string escape_sequence; @@ -69,7 +45,7 @@ namespace QuickMedia { } } - std::string Plugin::url_param_encode(const std::string ¶m) const { + std::string url_param_encode(const std::string ¶m) { std::ostringstream result; result.fill('0'); result << std::hex; @@ -86,24 +62,8 @@ namespace QuickMedia { return result.str(); } - DownloadResult Plugin::download_json(Json::Value &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent, std::string *err_msg) const { - std::string server_response; - if(download_to_string(url, server_response, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { - if(err_msg) - *err_msg = server_response; - return DownloadResult::NET_ERR; - } - - Json::CharReaderBuilder json_builder; - std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &result, &json_errors)) { - fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); - if(err_msg) - *err_msg = std::move(json_errors); - return DownloadResult::ERR; - } - - return DownloadResult::OK; - } + SuggestionResult download_result_to_suggestion_result(DownloadResult download_result) { return (SuggestionResult)download_result; } + PluginResult download_result_to_plugin_result(DownloadResult download_result) { return (PluginResult)download_result; } + SearchResult download_result_to_search_result(DownloadResult download_result) { return (SearchResult)download_result; } + ImageResult download_result_to_image_result(DownloadResult download_result) { return (ImageResult)download_result; } }
\ No newline at end of file diff --git a/src/plugins/Pornhub.cpp b/src/plugins/Pornhub.cpp index 77d5594..afdd8fc 100644 --- a/src/plugins/Pornhub.cpp +++ b/src/plugins/Pornhub.cpp @@ -1,4 +1,5 @@ #include "../../plugins/Pornhub.hpp" +#include "../../include/StringUtils.hpp" #include <quickmedia/HtmlSearch.h> #include <string.h> @@ -11,14 +12,13 @@ namespace QuickMedia { return strstr(str, substr); } - // TODO: Speed this up by using string.find instead of parsing html - SuggestionResult Pornhub::update_search_suggestions(const std::string &text, BodyItems &result_items) { + SearchResult PornhubSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://www.pornhub.com/video/search?search="; - url += url_param_encode(text); + url += url_param_encode(str); std::string website_data; - if(download_to_string(url, website_data, {}, use_tor) != DownloadResult::OK) - return SuggestionResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled()) != DownloadResult::OK) + return SearchResult::NET_ERR; struct ItemData { BodyItems *result_items; @@ -65,14 +65,21 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR; + return result == 0 ? SearchResult::OK : SearchResult::ERR; } - BodyItems Pornhub::get_related_media(const std::string &url) { + PluginResult PornhubSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + (void)url; + result_tabs.push_back(Tab{create_body(), std::make_unique<PornhubVideoPage>(program), nullptr}); + return PluginResult::OK; + } + + BodyItems PornhubVideoPage::get_related_media(const std::string &url) { BodyItems result_items; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor) != DownloadResult::OK) + if(download_to_string(url, website_data, {}, is_tor_enabled()) != DownloadResult::OK) return result_items; struct ItemData { diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 7e1fc63..40b296d 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -1,22 +1,9 @@ #include "../../plugins/Youtube.hpp" #include "../../include/Storage.hpp" -#include <json/reader.h> -#include <json/writer.h> #include <string.h> #include <unordered_set> namespace QuickMedia { - static void iterate_suggestion_result(const Json::Value &value, std::vector<std::string> &result_items, int &iterate_count) { - ++iterate_count; - if(value.isArray()) { - for(const Json::Value &child : value) { - iterate_suggestion_result(child, result_items, iterate_count); - } - } else if(value.isString() && iterate_count > 2) { - result_items.push_back(value.asString()); - } - } - static std::shared_ptr<BodyItem> parse_content_video_renderer(const Json::Value &content_item_json, std::unordered_set<std::string> &added_videos) { if(!content_item_json.isObject()) return nullptr; @@ -85,135 +72,6 @@ namespace QuickMedia { return body_item; } - Youtube::Youtube() : Plugin("youtube") { - - } - - PluginResult Youtube::get_front_page(BodyItems &result_items) { - bool disabled = true; - if(disabled) - return PluginResult::OK; - - std::string url = "https://youtube.com/"; - - std::vector<CommandArg> additional_args = { - { "-H", "x-spf-referer: " + url }, - { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, - { "-H", "referer: " + url } - }; - - //std::vector<CommandArg> cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); - - Json::Value json_root; - DownloadResult result = download_json(json_root, url + "?pbj=1", std::move(additional_args), true); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isArray()) - return PluginResult::ERR; - - std::unordered_set<std::string> added_videos; - - for(const Json::Value &json_item : json_root) { - if(!json_item.isObject()) - continue; - - const Json::Value &response_json = json_item["response"]; - if(!response_json.isObject()) - continue; - - const Json::Value &contents_json = response_json["contents"]; - if(!contents_json.isObject()) - continue; - - const Json::Value &tcbrr_json = contents_json["twoColumnBrowseResultsRenderer"]; - if(!tcbrr_json.isObject()) - continue; - - const Json::Value &tabs_json = tcbrr_json["tabs"]; - if(!tabs_json.isArray()) - continue; - - for(const Json::Value &tab_item_json : tabs_json) { - if(!tab_item_json.isObject()) - continue; - - const Json::Value &tab_renderer_json = tab_item_json["tabRenderer"]; - if(!tab_renderer_json.isObject()) - continue; - - const Json::Value &content_json = tab_renderer_json["content"]; - if(!content_json.isObject()) - continue; - - const Json::Value &rich_grid_renderer = content_json["richGridRenderer"]; - if(!rich_grid_renderer.isObject()) - continue; - - const Json::Value &contents2_json = rich_grid_renderer["contents"]; - if(!contents2_json.isArray()) - continue; - - for(const Json::Value &contents_item : contents2_json) { - const Json::Value &rich_item_renderer_json = contents_item["richItemRenderer"]; - if(!rich_item_renderer_json.isObject()) - continue; - - const Json::Value &rich_item_contents = rich_item_renderer_json["content"]; - std::shared_ptr<BodyItem> body_item = parse_content_video_renderer(rich_item_contents, added_videos); - if(body_item) - result_items.push_back(std::move(body_item)); - } - } - } - - return PluginResult::OK; - } - - std::string Youtube::autocomplete_search(const std::string &query) { - // Return the last result if the query is a substring of the autocomplete result - if(last_autocomplete_result.size() >= query.size() && memcmp(query.data(), last_autocomplete_result.data(), query.size()) == 0) - return last_autocomplete_result; - - std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gs_rn=64&gs_ri=youtube&ds=yt&cp=7&gs_id=x&q="; - url += url_param_encode(query); - - std::string server_response; - if(download_to_string(url, server_response, {}, use_tor, true) != DownloadResult::OK) - return query; - - size_t json_start = server_response.find_first_of('('); - if(json_start == std::string::npos) - return query; - ++json_start; - - size_t json_end = server_response.find_last_of(')'); - if(json_end == std::string::npos) - return query; - - if(json_end == 0 || json_start >= json_end) - return query; - - Json::Value json_root; - Json::CharReaderBuilder json_builder; - std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(&server_response[json_start], &server_response[json_end], &json_root, &json_errors)) { - fprintf(stderr, "Youtube autocomplete search json error: %s\n", json_errors.c_str()); - return query; - } - - int iterate_count = 0; - std::vector<std::string> result_items; - iterate_suggestion_result(json_root, result_items, iterate_count); - if(result_items.empty()) - return query; - - last_autocomplete_result = result_items[0]; - return result_items[0]; - } - // Returns empty string if continuation token can't be found static std::string item_section_renderer_get_continuation_token(const Json::Value &item_section_renderer_json) { const Json::Value &continuations_json = item_section_renderer_json["continuations"]; @@ -277,9 +135,77 @@ namespace QuickMedia { } } - SuggestionResult Youtube::update_search_suggestions(const std::string &text, BodyItems &result_items) { + static std::string remove_index_from_playlist_url(const std::string &url) { + std::string result = url; + size_t index = result.rfind("&index="); + if(index == std::string::npos) + return result; + return result.substr(0, index); + } + + static std::shared_ptr<BodyItem> parse_compact_video_renderer_json(const Json::Value &item_json, std::unordered_set<std::string> &added_videos) { + const Json::Value &compact_video_renderer_json = item_json["compactVideoRenderer"]; + if(!compact_video_renderer_json.isObject()) + return nullptr; + + const Json::Value &video_id_json = compact_video_renderer_json["videoId"]; + if(!video_id_json.isString()) + return nullptr; + + std::string video_id_str = video_id_json.asString(); + if(added_videos.find(video_id_str) != added_videos.end()) + return nullptr; + + std::string thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/hqdefault.jpg"; + + const char *date = nullptr; + const Json::Value &published_time_text_json = compact_video_renderer_json["publishedTimeText"]; + if(published_time_text_json.isObject()) { + const Json::Value &text_json = published_time_text_json["simpleText"]; + if(text_json.isString()) + date = text_json.asCString(); + } + + const char *length = nullptr; + const Json::Value &length_text_json = compact_video_renderer_json["lengthText"]; + if(length_text_json.isObject()) { + const Json::Value &text_json = length_text_json["simpleText"]; + if(text_json.isString()) + length = text_json.asCString(); + } + + const char *title = nullptr; + const Json::Value &title_json = compact_video_renderer_json["title"]; + if(title_json.isObject()) { + const Json::Value &simple_text_json = title_json["simpleText"]; + if(simple_text_json.isString()) { + title = simple_text_json.asCString(); + } + } + + if(!title) + return nullptr; + + auto body_item = BodyItem::create(title); + /* TODO: Make date a different color */ + std::string date_str; + if(date) + date_str += date; + if(length) { + if(!date_str.empty()) + date_str += '\n'; + date_str += length; + } + body_item->set_description(std::move(date_str)); + body_item->url = "https://www.youtube.com/watch?v=" + video_id_str; + body_item->thumbnail_url = std::move(thumbnail_url); + added_videos.insert(video_id_str); + return body_item; + } + + SearchResult YoutubeSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://youtube.com/results?search_query="; - url += url_param_encode(text); + url += url_param_encode(str); std::vector<CommandArg> additional_args = { { "-H", "x-spf-referer: " + url }, @@ -292,11 +218,11 @@ namespace QuickMedia { //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult result = download_json(json_root, url + "?pbj=1", std::move(additional_args), true); - if(result != DownloadResult::OK) return download_result_to_suggestion_result(result); + DownloadResult result = download_json(json_root, url + "&pbj=1", std::move(additional_args), true); + if(result != DownloadResult::OK) return download_result_to_search_result(result); if(!json_root.isArray()) - return SuggestionResult::ERR; + return SearchResult::ERR; std::string continuation_token; std::unordered_set<std::string> added_videos; /* The input contains duplicates, filter them out! */ @@ -345,10 +271,17 @@ namespace QuickMedia { if(!continuation_token.empty()) search_suggestions_get_continuation(url, continuation_token, result_items); - return SuggestionResult::OK; + return SearchResult::OK; + } + + PluginResult YoutubeSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + (void)url; + result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeVideoPage>(program), nullptr}); + return PluginResult::OK; } - void Youtube::search_suggestions_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items) { + void YoutubeSearchPage::search_suggestions_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items) { std::string next_url = url + "&pbj=1&ctoken=" + continuation_token; std::vector<CommandArg> additional_args = { @@ -393,93 +326,9 @@ namespace QuickMedia { } } - std::vector<CommandArg> Youtube::get_cookies() const { - if(use_tor) - return {}; - - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { - fprintf(stderr, "Warning: Failed to create youtube cookies file\n"); - return {}; - } - - return { - CommandArg{ "-b", cookies_filepath.data }, - CommandArg{ "-c", cookies_filepath.data } - }; - } - - static std::string remove_index_from_playlist_url(const std::string &url) { - std::string result = url; - size_t index = result.rfind("&index="); - if(index == std::string::npos) - return result; - return result.substr(0, index); - } - - static std::shared_ptr<BodyItem> parse_compact_video_renderer_json(const Json::Value &item_json, std::unordered_set<std::string> &added_videos) { - const Json::Value &compact_video_renderer_json = item_json["compactVideoRenderer"]; - if(!compact_video_renderer_json.isObject()) - return nullptr; - - const Json::Value &video_id_json = compact_video_renderer_json["videoId"]; - if(!video_id_json.isString()) - return nullptr; - - std::string video_id_str = video_id_json.asString(); - if(added_videos.find(video_id_str) != added_videos.end()) - return nullptr; - - std::string thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/hqdefault.jpg"; - - const char *date = nullptr; - const Json::Value &published_time_text_json = compact_video_renderer_json["publishedTimeText"]; - if(published_time_text_json.isObject()) { - const Json::Value &text_json = published_time_text_json["simpleText"]; - if(text_json.isString()) - date = text_json.asCString(); - } - - const char *length = nullptr; - const Json::Value &length_text_json = compact_video_renderer_json["lengthText"]; - if(length_text_json.isObject()) { - const Json::Value &text_json = length_text_json["simpleText"]; - if(text_json.isString()) - length = text_json.asCString(); - } - - const char *title = nullptr; - const Json::Value &title_json = compact_video_renderer_json["title"]; - if(title_json.isObject()) { - const Json::Value &simple_text_json = title_json["simpleText"]; - if(simple_text_json.isString()) { - title = simple_text_json.asCString(); - } - } - - if(!title) - return nullptr; - - auto body_item = BodyItem::create(title); - /* TODO: Make date a different color */ - std::string date_str; - if(date) - date_str += date; - if(length) { - if(!date_str.empty()) - date_str += '\n'; - date_str += length; - } - body_item->set_description(std::move(date_str)); - body_item->url = "https://www.youtube.com/watch?v=" + video_id_str; - body_item->thumbnail_url = std::move(thumbnail_url); - added_videos.insert(video_id_str); - return body_item; - } - // TODO: Make this faster by using string search instead of parsing html. // TODO: If the result is a play - BodyItems Youtube::get_related_media(const std::string &url) { + BodyItems YoutubeVideoPage::get_related_media(const std::string &url) { BodyItems result_items; std::string modified_url = remove_index_from_playlist_url(url); |