aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-06-30 02:25:10 +0200
committerdec05eba <dec05eba@protonmail.com>2020-06-30 16:55:20 +0200
commit07f80da8f9c46c228c272e95ab88a3e098c899d9 (patch)
treebef7b74826c1845911f0afcbf8611a2e8ad75e15
parent82e66059dc09087b625e25027922a9e3c3ccc6cd (diff)
Add infinite image scroll mode
-rw-r--r--README.md10
-rw-r--r--include/ImageViewer.hpp63
-rw-r--r--include/Page.hpp1
-rw-r--r--include/QuickMedia.hpp1
-rw-r--r--src/ImageViewer.cpp249
-rw-r--r--src/QuickMedia.cpp68
6 files changed, 387 insertions, 5 deletions
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 <string>
+#include <vector>
+#include <SFML/Graphics/RenderWindow.hpp>
+#include <SFML/Graphics/Texture.hpp>
+#include <SFML/Graphics/Sprite.hpp>
+#include <SFML/Graphics/Font.hpp>
+#include <SFML/Graphics/Text.hpp>
+#include <SFML/System/Clock.hpp>
+
+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<double> 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<double> 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<std::unique_ptr<ImageData>> image_data;
+ std::vector<PageSize> page_size;
+
+ sf::Vector2<double> 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 <cmath>
+#include <SFML/Window/Event.hpp>
+#include <SFML/Graphics/RectangleShape.hpp>
+
+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<double> image_size = get_page_size(page);
+ std::unique_ptr<ImageData> &page_image_data = image_data[page];
+ sf::Vector2<double> 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<double> 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<double> 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<ImageData>();
+ 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<double> 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<double> 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<double> 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<double> ImageViewer::get_page_size(int page) {
+ const sf::Vector2<double> 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<double>(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 <cppcodec/base64_rfc4648.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
@@ -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<Manga*>(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 &current = 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);