From 07f80da8f9c46c228c272e95ab88a3e098c899d9 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 30 Jun 2020 02:25:10 +0200 Subject: Add infinite image scroll mode --- README.md | 10 +- include/ImageViewer.hpp | 63 ++++++++++++ include/Page.hpp | 1 + include/QuickMedia.hpp | 1 + src/ImageViewer.cpp | 249 ++++++++++++++++++++++++++++++++++++++++++++++++ src/QuickMedia.cpp | 68 ++++++++++++- 6 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 include/ImageViewer.hpp create mode 100644 src/ImageViewer.cpp diff --git a/README.md b/README.md index 438b65d..e24c663 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,13 @@ QuickMedia youtube --tor echo -e "hello\nworld" | QuickMedia dmenu ``` ## Controls -Press `arrow up` and `arrow down` to navigate the menu and also to go to the previous/next image when viewing manga.\ +Press `arrow up` and `arrow down` to navigate the menu and also to scroll to the previous/next image when viewing manga. Alternatively you can use the mouse scroll to scroll to the previous/next manga.\ Press `Enter` (aka `Return`) to select the item.\ Press `ESC` to go back to the previous menu.\ Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed and accessible in PATH environment variable.\ Press `Backspace` to return to the preview item when reading replies in image board threads.\ -Press `R` to paste the post number of the selected post into the post field (image boards). +Press `R` to paste the post number of the selected post into the post field (image boards).\ Press `Ctrl + C` to begin writing a post to a thread (image boards).\ Press `1 to 9` or `Numpad 1 to 9` to select google captcha image when posting a comment on 4chan.\ Press `P` to preview the attached item of the selected row in full screen view. Only works for image boards when browsing a thread. @@ -55,7 +55,6 @@ See project.conf \[dependencies]. # TODO If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions.\ Give user the option to start where they left off or from the start or from the start.\ -For manga, view the next chapter when reaching the end of a chapter.\ Search is asynchronous, but download of image also needs to be asynchronous, also add loading animation.\ Retain search text when navigating back.\ Disable ytdl_hook subtitles. If a video has subtitles for many languages, then it will stall video playback for several seconds @@ -75,4 +74,7 @@ Wrap text that is too long.\ Add greentext support for quotes.\ Add support for special formatting for posts by admins on imageboards.\ For image boards, track (You)'s and show notification when somebody replies to your post.\ -In image boards when viewing videos, automatically play the next one after the current one has finished playing (just like the youtube plugin does). +In image boards when viewing videos, automatically play the next one after the current one has finished playing (just like the youtube plugin does).\ +When viewing images the image download start from page 1 to the end page. The image download should start from the viewing page.\ +Go to next chapter when reaching the end of the chapter in image endless mode.\ +Allow to switch between 1 page and endless mode for images. diff --git a/include/ImageViewer.hpp b/include/ImageViewer.hpp new file mode 100644 index 0000000..fb1e5e3 --- /dev/null +++ b/include/ImageViewer.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "Path.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QuickMedia { + class Manga; + + struct ImageData { + sf::Texture texture; + sf::Sprite sprite; + bool failed_to_load_image; + bool visible_on_screen; + }; + + struct PageSize { + sf::Vector2 size; + bool loaded; + }; + + class ImageViewer { + public: + ImageViewer(Manga *manga, const std::string &images_url, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font); + bool draw(sf::RenderWindow &window); + // Returns page as 1 indexed + int get_focused_page() const; + int get_num_pages() const { return num_pages; } + private: + bool render_page(sf::RenderWindow &window, int page, double offset_y); + sf::Vector2 get_page_size(int page); + private: + int current_page; + int num_pages; + int page_center; + + std::string chapter_title; + Path chapter_cache_dir; + + double scroll = 0.0; + double scroll_speed = 0.0; + double min_page_center_dist; + int page_closest_to_center; + int focused_page; + int prev_focused_page = -1; + + sf::Font *font; + sf::Clock frame_timer; + sf::Text page_text; + + std::vector> image_data; + std::vector page_size; + + sf::Vector2 window_size; + bool window_size_set = false; + }; +} \ No newline at end of file diff --git a/include/Page.hpp b/include/Page.hpp index b615c8c..dc4051c 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -8,6 +8,7 @@ namespace QuickMedia { VIDEO_CONTENT, EPISODE_LIST, IMAGES, + IMAGES_CONTINUOUS, CONTENT_LIST, CONTENT_DETAILS, IMAGE_BOARD_THREAD_LIST, diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 2e14b09..387a5a5 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -33,6 +33,7 @@ namespace QuickMedia { void video_content_page(); void episode_list_page(); void image_page(); + void image_continuous_page(); void content_list_page(); void content_details_page(); void image_board_thread_list_page(); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp new file mode 100644 index 0000000..fcf9519 --- /dev/null +++ b/src/ImageViewer.cpp @@ -0,0 +1,249 @@ +#include "../include/ImageViewer.hpp" +#include "../include/Notification.hpp" +#include "../include/Storage.hpp" +#include "../plugins/Manga.hpp" +#include +#include +#include + +namespace QuickMedia { + ImageViewer::ImageViewer(Manga *manga, const std::string &images_url, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font) : + current_page(current_page), + num_pages(0), + chapter_title(chapter_title), + chapter_cache_dir(chapter_cache_dir), + focused_page(current_page), + font(font), + page_text("", *font, 14) + { + if(manga->get_number_of_images(images_url, num_pages) != ImageResult::OK) { + show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); + return; + } + current_page = std::min(current_page, num_pages); + image_data.resize(num_pages); + page_size.resize(num_pages); + for(int i = 0; i < num_pages; ++i) { + image_data[i] = nullptr; + page_size[i].loaded = false; + } + page_text.setFillColor(sf::Color::White); + } + + bool ImageViewer::render_page(sf::RenderWindow &window, int page, double offset_y) { + if(page < 0 || page >= (int)image_data.size()) + return false; + + const sf::Vector2 image_size = get_page_size(page); + std::unique_ptr &page_image_data = image_data[page]; + sf::Vector2 render_pos(std::floor(window_size.x * 0.5 - image_size.x * 0.5), std::floor(- image_size.y * 0.5 + scroll + offset_y)); + if(render_pos.y + image_size.y <= 0.0 || render_pos.y >= window_size.y) { + if(page_image_data) + page_image_data->visible_on_screen = false; + return true; + } + + double center_dist = std::abs(window_size.y * 0.5 - (render_pos.y + image_size.y * 0.5)); + if(center_dist < min_page_center_dist) { + min_page_center_dist = center_dist; + page_closest_to_center = page; + } + + if(page_image_data) { + if(page_image_data->failed_to_load_image) { + sf::Text error_message("Failed to load image for page " + std::to_string(1 + page), *font, 30); + auto text_bounds = error_message.getLocalBounds(); + error_message.setFillColor(sf::Color::Black); + sf::Vector2 render_pos_text(std::floor(window_size.x * 0.5 - text_bounds.width * 0.5), std::floor(- text_bounds.height * 0.5 + scroll + offset_y)); + + sf::RectangleShape background(sf::Vector2f(image_size.x, image_size.y)); + background.setFillColor(sf::Color::White); + background.setPosition(render_pos.x, render_pos.y); + window.draw(background); + + error_message.setPosition(render_pos_text.x, render_pos_text.y); + window.draw(error_message); + } else { + page_image_data->sprite.setPosition(render_pos.x, render_pos.y); + window.draw(page_image_data->sprite); + } + } else { + std::string page_str = std::to_string(1 + page); + + sf::Text error_message("Downloading page " + page_str, *font, 30); + auto text_bounds = error_message.getLocalBounds(); + error_message.setFillColor(sf::Color::Black); + sf::Vector2 render_pos_text(std::floor(window_size.x * 0.5 - text_bounds.width * 0.5), std::floor(- text_bounds.height * 0.5 + scroll + offset_y)); + + sf::RectangleShape background(sf::Vector2f(image_size.x, image_size.y)); + background.setFillColor(sf::Color::White); + background.setPosition(render_pos.x, render_pos.y); + window.draw(background); + + error_message.setPosition(render_pos_text.x, render_pos_text.y); + window.draw(error_message); + + Path image_path = chapter_cache_dir; + image_path.join(page_str); + + // TODO: Make image loading asynchronous + Path image_finished_path(image_path.data + ".finished"); + if(get_file_type(image_finished_path) != FileType::FILE_NOT_FOUND && get_file_type(image_path) == FileType::REGULAR) { + fprintf(stderr, "ImageViewer: Load page %d\n", 1 + page); + page_image_data = std::make_unique(); + page_image_data->visible_on_screen = true; + std::string image_data; + if(file_get_content(image_path, image_data) == 0) { + if(page_image_data->texture.loadFromMemory(image_data.data(), image_data.size())) { + page_image_data->texture.setSmooth(true); + page_image_data->sprite.setTexture(page_image_data->texture, true); + //image_texture.generateMipmap(); + page_image_data->failed_to_load_image = false; + page_size[page].size = get_page_size(page); + page_size[page].loaded = true; + } else { + page_image_data->failed_to_load_image = true; + } + } else { + show_notification("Manga", "Failed to load image for page " + page_str + ". Image filepath: " + image_path.data, Urgency::CRITICAL); + page_image_data->failed_to_load_image = true; + } + } + + } + + return true; + } + + bool ImageViewer::draw(sf::RenderWindow &window) { + const double frame_delta = frame_timer.restart().asSeconds(); + const double scroll_speed_key_input = 450.0; + const double scroll_speed_mouse_wheel = 450.0; + const double scroll_deaccel = 0.93; + + if(!window_size_set) { + auto window_size_i = window.getSize(); + window_size.x = window_size_i.x; + window_size.y = window_size_i.y; + window_size_set = true; + } + + // TODO: Only redraw when scrolling and when image has finished downloading + sf::Event event; + while(window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + //current_page = Page::EXIT; + window.close(); + return false; + } 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)); + //redraw = true; + } else if(event.type == sf::Event::GainedFocus) { + //redraw = true; + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + scroll_speed += scroll_speed_key_input * frame_delta; + } else if(event.key.code == sf::Keyboard::Down) { + scroll_speed -= scroll_speed_key_input * frame_delta; + } else if(event.key.code == sf::Keyboard::Escape) { + return false; + } + } else if(event.type == sf::Event::MouseWheelScrolled && event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel) { + scroll_speed += scroll_speed_mouse_wheel * event.mouseWheelScroll.delta * frame_delta; + } + } + + scroll += scroll_speed; + scroll *= scroll_deaccel; + if(std::abs(scroll) < 0.1) + scroll = 0.0; + + min_page_center_dist = 9999999.0; + page_closest_to_center = -1; + + const sf::Vector2 selected_page_size = get_page_size(current_page); + render_page(window, current_page, window_size.y*0.5); + //if(!focused_page_rendered) + // return; + + // Render previous pages + double page_offset = window_size.y*0.5 - selected_page_size.y*0.5; + int page = current_page - 1; + while(true) { + const sf::Vector2 image_size = get_page_size(page); + page_offset -= image_size.y*0.5; + if(!render_page(window, page, page_offset)) + break; + --page; + page_offset -= image_size.y*0.5; + } + + // Render next pages + page_offset = window_size.y*0.5 + selected_page_size.y*0.5; + page = current_page + 1; + while(true) { + const sf::Vector2 image_size = get_page_size(page); + page_offset += image_size.y*0.5; + if(!render_page(window, page, page_offset)) + break; + ++page; + page_offset += image_size.y*0.5; + } + + if(page_closest_to_center != -1) { + focused_page = page_closest_to_center; + } + + if(focused_page != prev_focused_page) { + prev_focused_page = focused_page; + page_text.setString(chapter_title + " | Page " + std::to_string(1 + focused_page) + "/" + std::to_string(num_pages)); + } + + const float font_height = page_text.getCharacterSize() + 8.0f; + const float background_height = font_height + 6.0f; + + sf::RectangleShape page_text_background(sf::Vector2f(window_size.x, background_height)); + page_text_background.setFillColor(sf::Color(0, 0, 0, 150)); + page_text_background.setPosition(0.0f, window_size.y - background_height); + window.draw(page_text_background); + + auto page_text_bounds = page_text.getLocalBounds(); + page_text.setPosition(std::floor(window_size.x * 0.5f - page_text_bounds.width * 0.5f), std::floor(window_size.y - background_height * 0.5f - font_height * 0.5f)); + window.draw(page_text); + + // Free pages that are not visible on the screen + int i = 0; + for(auto &page_data : image_data) { + if(page_data && !page_data->visible_on_screen) { + fprintf(stderr, "ImageViewer: Unload page %d\n", 1 + i); + page_data.reset(); + } + ++i; + } + + return true; + } + + int ImageViewer::get_focused_page() const { + return 1 + focused_page; + } + + sf::Vector2 ImageViewer::get_page_size(int page) { + const sf::Vector2 no_image_page_size(720.0, 1280.0); + + if(page < 0 || page >= (int)image_data.size()) + return no_image_page_size; + + if(page_size[page].loaded) + return page_size[page].size; + + if(!image_data[page]) + return no_image_page_size; + + sf::Vector2u texture_size = image_data[page]->texture.getSize(); + return sf::Vector2(texture_size.x, texture_size.y); + } +} \ No newline at end of file diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 881f2d6..9b57b0b 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -12,6 +12,7 @@ #include "../include/StringUtils.hpp" #include "../include/GoogleCaptcha.hpp" #include "../include/Notification.hpp" +#include "../include/ImageViewer.hpp" #include #include @@ -234,6 +235,11 @@ namespace QuickMedia { window.setKeyRepeatEnabled(true); break; } + case Page::IMAGES_CONTINUOUS: { + body->draw_thumbnails = false; + image_continuous_page(); + break; + } case Page::CONTENT_LIST: { body->draw_thumbnails = true; content_list_page(); @@ -796,7 +802,7 @@ namespace QuickMedia { images_url = item->url; chapter_title = item->title; image_index = 0; - current_page = Page::IMAGES; + current_page = Page::IMAGES_CONTINUOUS; if(start_from_beginning) return; @@ -1181,6 +1187,66 @@ namespace QuickMedia { } } + void Program::image_continuous_page() { + search_bar->onTextUpdateCallback = nullptr; + search_bar->onTextSubmitCallback = nullptr; + + assert(current_plugin->is_manga()); + Manga *image_plugin = static_cast(current_plugin); + + content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + 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; + } + download_chapter_images_if_needed(image_plugin); + + 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]; + if(json_chapter.isObject()) { + const Json::Value ¤t = json_chapter["current"]; + if(current.isNumeric()) + latest_read = std::max(latest_read, current.asInt()); + } else { + json_chapter = Json::Value(Json::objectValue); + } + } else { + json_chapters = Json::Value(Json::objectValue); + json_chapter = Json::Value(Json::objectValue); + } + + ImageViewer image_viewer(image_plugin, images_url, chapter_title, image_index, content_cache_dir, &font); + + 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; + if(!save_manga_progress_json(content_storage_file, content_storage_json)) { + show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); + } + + while(current_page == Page::IMAGES_CONTINUOUS) { + window.clear(back_color); + if(!image_viewer.draw(window)) + current_page = Page::EPISODE_LIST; + window.display(); + + int focused_page = image_viewer.get_focused_page(); + if(focused_page > latest_read) { + latest_read = focused_page; + image_index = latest_read - 1; + json_chapter["current"] = latest_read; + json_chapters[chapter_title] = json_chapter; + if(!save_manga_progress_json(content_storage_file, content_storage_json)) { + show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); + } + } + } + } + void Program::content_list_page() { 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); -- cgit v1.2.3