aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-10-11 21:35:37 +0200
committerdec05eba <dec05eba@protonmail.com>2020-10-13 13:13:01 +0200
commit77ed51898157d99112be7550471ec06e32344c9e (patch)
tree0645274d0f13b4fa6940d4054f74a070611a8ef0 /src
parentda89ec98fb34757f0c46dc8cb2dd87ae78d317ce (diff)
Refactor plugin into seperate pages
TODO: Readd 4chan login page, manganelo creators page, autocomplete
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp18
-rw-r--r--src/DownloadUtils.cpp2
-rw-r--r--src/ImageViewer.cpp4
-rw-r--r--src/QuickMedia.cpp2223
-rw-r--r--src/SearchBar.cpp29
-rw-r--r--src/Storage.cpp21
-rw-r--r--src/plugins/Dmenu.cpp23
-rw-r--r--src/plugins/FileManager.cpp75
-rw-r--r--src/plugins/Fourchan.cpp649
-rw-r--r--src/plugins/ImageBoard.cpp30
-rw-r--r--src/plugins/Manga.cpp9
-rw-r--r--src/plugins/Mangadex.cpp201
-rw-r--r--src/plugins/Manganelo.cpp277
-rw-r--r--src/plugins/Mangatown.cpp212
-rw-r--r--src/plugins/Matrix.cpp41
-rw-r--r--src/plugins/NyaaSi.cpp131
-rw-r--r--src/plugins/Page.cpp50
-rw-r--r--src/plugins/Plugin.cpp52
-rw-r--r--src/plugins/Pornhub.cpp23
-rw-r--r--src/plugins/Youtube.cpp317
20 files changed, 1839 insertions, 2548 deletions
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 &current = 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 &current = 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 &current = 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, &current_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, &current_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, &current_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, &current_room_id, &new_page, &chat_state, &currently_operating_on_item](const std::string &text) mutable {
+ chat_input.on_submit_callback = [this, &chat_input, &tabs, &selected_tab, &current_room_id, &new_page, &chat_state, &currently_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 &param) const {
+ std::string url_param_encode(const std::string &param) {
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);