aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-09-22 22:46:29 +0200
committerdec05eba <dec05eba@protonmail.com>2020-09-22 23:23:13 +0200
commita8e0846a7c111a8d5b5cf8592ecb9b9bbd15ce26 (patch)
tree0ada7bf9bcb31fffd698e261d8ecfc0c85f1d2de /src
parenta29f310b8ad0b088860fe05a5499bccef963a503 (diff)
Initial file manager implementation, with thumbnail caching
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp142
-rw-r--r--src/ImageUtils.cpp4
-rw-r--r--src/QuickMedia.cpp135
-rw-r--r--src/plugins/FileManager.cpp59
-rw-r--r--src/plugins/Matrix.cpp7
5 files changed, 329 insertions, 18 deletions
diff --git a/src/Body.cpp b/src/Body.cpp
index 20911de..ae60da2 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -1,5 +1,8 @@
#include "../include/Body.hpp"
#include "../include/QuickMedia.hpp"
+#include "../include/Scale.hpp"
+#include "../include/base64_url.hpp"
+#include "../include/ImageUtils.hpp"
#include "../plugins/Plugin.hpp"
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/Graphics/Sprite.hpp>
@@ -11,7 +14,7 @@ const sf::Color front_color(43, 45, 47);
const sf::Color back_color(33, 35, 37);
namespace QuickMedia {
- BodyItem::BodyItem(std::string _title): visible(true), dirty(false), dirty_description(false), background_color(front_color) {
+ BodyItem::BodyItem(std::string _title): visible(true), dirty(false), dirty_description(false), thumbnail_is_local(false), background_color(front_color) {
set_title(std::move(_title));
}
@@ -25,6 +28,7 @@ namespace QuickMedia {
visible = other.visible;
dirty = other.dirty;
dirty_description = other.dirty_description;
+ thumbnail_is_local = other.thumbnail_is_local;
if(other.title_text)
title_text = std::make_unique<Text>(*other.title_text);
else
@@ -52,6 +56,10 @@ namespace QuickMedia {
progress_text.setFillColor(sf::Color::White);
author_text.setFillColor(sf::Color::White);
replies_text.setFillColor(sf::Color(129, 162, 190));
+ thumbnail_resize_target_size.x = 200;
+ thumbnail_resize_target_size.y = 119;
+ thumbnail_fallback_size.x = 50.0f;
+ thumbnail_fallback_size.y = 100.0f;
}
bool Body::select_previous_item() {
@@ -176,18 +184,121 @@ namespace QuickMedia {
}
}
- std::shared_ptr<sf::Texture> Body::load_thumbnail_from_url(const std::string &url) {
+ static sf::Vector2f to_vec2f(const sf::Vector2u &vec) {
+ return sf::Vector2f(vec.x, vec.y);
+ }
+
+ static sf::Vector2f to_vec2f(const sf::Vector2i &vec) {
+ return sf::Vector2f(vec.x, vec.y);
+ }
+
+ static sf::Vector2u to_vec2u(const sf::Vector2f &vec) {
+ return sf::Vector2u(vec.x, vec.y);
+ }
+
+ static void copy_resize(const sf::Image &source, sf::Image &destination, sf::Vector2u destination_size) {
+ const sf::Vector2u source_size = source.getSize();
+ if(source_size.x == 0 || source_size.y == 0 || destination_size.x == 0 || destination_size.y == 0)
+ return;
+
+ //float width_ratio = (float)source_size.x / (float)destination_size.x;
+ //float height_ratio = (float)source_size.y / (float)destination_size.y;
+
+ const sf::Uint8 *source_pixels = source.getPixelsPtr();
+ // TODO: Remove this somehow. Right now we need to allocate this and also allocate the same array in the destination image
+ sf::Uint32 *destination_pixels = new sf::Uint32[destination_size.x * destination_size.y];
+ sf::Uint32 *destination_pixel = destination_pixels;
+ for(unsigned int y = 0; y < destination_size.y; ++y) {
+ for(unsigned int x = 0; x < destination_size.x; ++x) {
+ int scaled_x = ((float)x / (float)destination_size.x) * source_size.x;
+ int scaled_y = ((float)y / (float)destination_size.y) * source_size.y;
+ //float scaled_x = x * width_ratio;
+ //float scaled_y = y * height_ratio;
+
+ //sf::Uint32 *source_pixel = (sf::Uint32*)(source_pixels + (int)(scaled_x + scaled_y * source_size.x) * 4);
+ sf::Uint32 *source_pixel = (sf::Uint32*)(source_pixels + (scaled_x + scaled_y * source_size.x) * 4);
+ *destination_pixel = *source_pixel;
+ ++destination_pixel;
+ }
+ }
+ destination.create(destination_size.x, destination_size.y, (sf::Uint8*)destination_pixels);
+ delete []destination_pixels;
+ }
+
+ static bool save_image_as_thumbnail_atomic(const sf::Image &image, const Path &thumbnail_path, const char *ext) {
+ Path tmp_path = thumbnail_path;
+ tmp_path.append(".tmp");
+ const char *thumbnail_path_ext = thumbnail_path.ext();
+ if(is_image_ext(ext))
+ tmp_path.append(ext);
+ else if(is_image_ext(thumbnail_path_ext))
+ tmp_path.append(thumbnail_path_ext);
+ else
+ tmp_path.append(".png");
+ return image.saveToFile(tmp_path.data) && (rename(tmp_path.data.c_str(), thumbnail_path.data.c_str()) == 0);
+ }
+
+ // Returns empty string if no extension
+ static const char* get_ext(const std::string &path) {
+ size_t index = path.rfind('.');
+ if(index == std::string::npos)
+ return "";
+ return path.c_str() + index;
+ }
+
+ // TODO: Do not load thumbnails for images larger than 30mb
+ std::shared_ptr<sf::Texture> Body::load_thumbnail_from_url(const std::string &url, bool local, sf::Vector2i thumbnail_resize_target_size) {
auto result = std::make_shared<sf::Texture>();
result->setSmooth(true);
assert(!loading_thumbnail);
loading_thumbnail = true;
- thumbnail_load_thread = std::thread([this, result, url]() {
+ thumbnail_load_thread = std::thread([this, result, url, local, thumbnail_resize_target_size]() {
+ // TODO: Use sha256 instead of base64_url encoding
+ Path thumbnail_path = get_cache_dir().join("thumbnails").join(base64_url::encode(url));
+
std::string texture_data;
- if(download_to_string_cache(url, texture_data, {}, program->get_current_plugin()->use_tor, true) == DownloadResult::OK) {
- if(result->loadFromMemory(texture_data.data(), texture_data.size())) {
- //result->generateMipmap();
+ if(file_get_content(thumbnail_path, texture_data) == 0) {
+ fprintf(stderr, "Loaded %s from thumbnail cache\n", url.c_str());
+ result->loadFromMemory(texture_data.data(), texture_data.size());
+ loading_thumbnail = false;
+ return;
+ } else {
+ if(local) {
+ if(file_get_content(url, texture_data) != 0) {
+ loading_thumbnail = false;
+ return;
+ }
+ } else {
+ if(download_to_string_cache(url, texture_data, {}, program->get_current_plugin()->use_tor, true) != DownloadResult::OK) {
+ loading_thumbnail = false;
+ return;
+ }
}
}
+
+ if(thumbnail_resize_target_size.x != 0 && thumbnail_resize_target_size.y != 0) {
+ auto image = std::make_unique<sf::Image>();
+ // TODO: Load from file instead? decreases ram usage and we save to file above anyways
+ if(image->loadFromMemory(texture_data.data(), texture_data.size())) {
+ texture_data.resize(0);
+ sf::Vector2u new_image_size = to_vec2u(clamp_to_size(to_vec2f(image->getSize()), to_vec2f(thumbnail_resize_target_size)));
+ if(new_image_size.x < image->getSize().x || new_image_size.y < image->getSize().y) {
+ sf::Image destination_image;
+ copy_resize(*image, destination_image, new_image_size);
+ image.reset();
+ if(save_image_as_thumbnail_atomic(destination_image, thumbnail_path, get_ext(url)))
+ result->loadFromImage(destination_image);
+ loading_thumbnail = false;
+ return;
+ } else {
+ result->loadFromImage(*image);
+ loading_thumbnail = false;
+ return;
+ }
+ }
+ }
+
+ result->loadFromMemory(texture_data.data(), texture_data.size());
loading_thumbnail = false;
});
thumbnail_load_thread.detach();
@@ -207,16 +318,20 @@ namespace QuickMedia {
sf::Vector2f scissor_size = size;
size.x = std::max(0.0f, size.x - 5);
- const float image_max_height = 100.0f;
+ float image_max_height = 100.0f;
const float spacing_y = 15.0f;
const float padding_x = 10.0f;
const float image_padding_x = 5.0f;
const float padding_y = 5.0f;
const float start_y = pos.y;
- sf::RectangleShape image_fallback(sf::Vector2f(50, image_max_height));
+ sf::RectangleShape image_fallback(thumbnail_fallback_size);
image_fallback.setFillColor(sf::Color::White);
+ if(thumbnail_resize_target_size.x != 0 && thumbnail_resize_target_size.y != 0) {
+ image_max_height = thumbnail_resize_target_size.y;
+ }
+
sf::Sprite image;
sf::RectangleShape item_background;
@@ -278,7 +393,7 @@ namespace QuickMedia {
if(draw_thumbnails && !item->thumbnail_url.empty()) {
auto &item_thumbnail = item_thumbnail_textures[item->thumbnail_url];
item_thumbnail.referenced = false;
- float image_height = image_max_height;
+ float image_height = image_fallback.getSize().y;
if(item_thumbnail.texture && item_thumbnail.texture->getNativeHandle() != 0) {
auto image_size = item_thumbnail.texture->getSize();
image_height = std::min(image_max_height, (float)image_size.y);
@@ -325,8 +440,8 @@ namespace QuickMedia {
item_height += item->description_text->getHeight();
}
if(draw_thumbnails && !item->thumbnail_url.empty()) {
- float image_height = image_max_height;
- if(item_thumbnail.texture && item_thumbnail.texture->getNativeHandle() != 0) {
+ float image_height = image_fallback.getSize().y;
+ if(item_thumbnail.loaded && item_thumbnail.texture && item_thumbnail.texture->getNativeHandle() != 0) {
auto image_size = item_thumbnail.texture->getSize();
image_height = std::min(image_max_height, (float)image_size.y);
}
@@ -335,8 +450,9 @@ namespace QuickMedia {
item_height += (padding_y * 2.0f);
if(draw_thumbnails) {
- if(!item->thumbnail_url.empty() && !loading_thumbnail && !item_thumbnail.texture) {
- item_thumbnail.texture = load_thumbnail_from_url(item->thumbnail_url);
+ if(!item->thumbnail_url.empty() && !loading_thumbnail && !item_thumbnail.loaded && !item_thumbnail.texture) {
+ item_thumbnail.loaded = true;
+ item_thumbnail.texture = load_thumbnail_from_url(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size);
}
}
diff --git a/src/ImageUtils.cpp b/src/ImageUtils.cpp
index ea1841b..5f493d1 100644
--- a/src/ImageUtils.cpp
+++ b/src/ImageUtils.cpp
@@ -115,4 +115,8 @@ namespace QuickMedia {
return true;
return false;
}
+
+ bool is_image_ext(const char *ext) {
+ return strcasecmp(ext, ".jpg") == 0 || strcasecmp(ext, ".jpeg") == 0 || strcasecmp(ext, ".png") == 0 || strcasecmp(ext, ".gif") == 0;
+ }
} \ No newline at end of file
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index f57b674..e7a310d 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -8,6 +8,7 @@
#include "../plugins/Dmenu.hpp"
#include "../plugins/NyaaSi.hpp"
#include "../plugins/Matrix.hpp"
+#include "../plugins/FileManager.hpp"
#include "../include/Scale.hpp"
#include "../include/Program.h"
#include "../include/VideoPlayer.hpp"
@@ -198,6 +199,10 @@ namespace QuickMedia {
window.setFramerateLimit(monitor_hz);
}
fprintf(stderr, "Monitor hz: %d\n", monitor_hz);
+
+ if(create_directory_recursive(get_cache_dir().join("thumbnails")) != 0) {
+ fprintf(stderr, "Failed to create thumbnails directory\n");
+ }
}
Program::~Program() {
@@ -236,12 +241,13 @@ namespace QuickMedia {
}
static void usage() {
- fprintf(stderr, "usage: QuickMedia <plugin> [--tor] [--use-system-mpv-config] [-p placeholder-text]\n");
+ fprintf(stderr, "usage: QuickMedia <plugin> [--tor] [--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 or dmenu\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, " --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, " --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");
@@ -277,6 +283,7 @@ namespace QuickMedia {
current_plugin = nullptr;
std::string plugin_logo_path;
std::string search_placeholder;
+ const char *start_dir = nullptr;
for(int i = 1; i < argc; ++i) {
if(!current_plugin) {
@@ -301,11 +308,13 @@ namespace QuickMedia {
} else if(strcmp(argv[i], "nyaa.si") == 0) {
current_plugin = new NyaaSi();
plugin_logo_path = resources_root + "images/nyaa_si_logo.png";
- } else if(strcmp(argv[i], "dmenu") == 0) {
- current_plugin = new Dmenu();
} else if(strcmp(argv[i], "matrix") == 0) {
current_plugin = 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();
@@ -319,6 +328,11 @@ namespace QuickMedia {
use_system_mpv_config = true;
} else if(strcmp(argv[i], "--upscale-images") == 0) {
upscale_images = true;
+ } else if(strcmp(argv[i], "--dir") == 0) {
+ if(i < argc - 1) {
+ start_dir = argv[i + 1];
+ ++i;
+ }
} else if(strcmp(argv[i], "-p") == 0) {
if(i < argc - 1) {
search_placeholder = argv[i + 1];
@@ -331,12 +345,35 @@ namespace QuickMedia {
}
}
+ if(!current_plugin) {
+ 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;
+ } 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;
@@ -492,6 +529,11 @@ namespace QuickMedia {
chat_page();
break;
}
+ case Page::FILE_MANAGER: {
+ body->draw_thumbnails = true;
+ file_manager_page();
+ break;
+ }
}
}
@@ -2444,6 +2486,91 @@ namespace QuickMedia {
}
}
+ void Program::file_manager_page() {
+ selected_files.clear();
+ int prev_autosearch_delay = search_bar->text_autosearch_delay;
+ search_bar->text_autosearch_delay = current_plugin->get_search_delay();
+ Page previous_page = pop_page_stack();
+
+ assert(current_plugin->name == "file-manager");
+ FileManager *file_manager = static_cast<FileManager*>(current_plugin);
+
+ 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).
+ 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);
+ }
+
+ // TODO: Have an option for the search bar to be multi-line.
+ search_bar->onTextUpdateCallback = [this](const sf::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;
+
+ FileManager *file_manager = static_cast<FileManager*>(current_plugin);
+ 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->reset_selected();
+ 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);
+ body->draw(window, body_pos, body_size);
+ window.draw(current_dir_text);
+ search_bar->draw(window);
+ 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(1);
+ }
+
void Program::image_board_thread_list_page() {
assert(current_plugin->is_image_board());
ImageBoard *image_board = static_cast<ImageBoard*>(current_plugin);
diff --git a/src/plugins/FileManager.cpp b/src/plugins/FileManager.cpp
new file mode 100644
index 0000000..fc6205c
--- /dev/null
+++ b/src/plugins/FileManager.cpp
@@ -0,0 +1,59 @@
+#include "../../plugins/FileManager.hpp"
+#include "../../include/ImageUtils.hpp"
+#include <filesystem>
+
+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();
+ int len = strlen(path_c);
+ for(int i = len - 1; i >= 0; --i) {
+ if(path_c[i] == '.')
+ return path_c + i;
+ }
+ return "";
+ }
+
+ PluginResult FileManager::get_files_in_directory(BodyItems &result_items) {
+ try {
+ for(auto &p : std::filesystem::directory_iterator(current_dir)) {
+ auto body_item = std::make_unique<BodyItem>(p.path().filename().string());
+ if(p.is_regular_file()) {
+ if(is_image_ext(get_ext(p.path()))) {
+ body_item->thumbnail_is_local = true;
+ body_item->thumbnail_url = p.path().string();
+ }
+ }
+ result_items.push_back(std::move(body_item));
+ }
+ return PluginResult::OK;
+ } catch(const std::filesystem::filesystem_error &err) {
+ fprintf(stderr, "Failed to list files in directory %s, error: %s\n", current_dir.c_str(), err.what());
+ return PluginResult::ERR;
+ }
+ }
+
+ 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) {
+ 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/Matrix.cpp b/src/plugins/Matrix.cpp
index 8c6d07d..ff854c5 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -191,7 +191,12 @@ namespace QuickMedia {
auto body_item = std::make_unique<BodyItem>("");
body_item->author = user_info.display_name;
body_item->set_description(it->body);
- body_item->thumbnail_url = user_info.avatar_url;
+ if(!it->thumbnail_url.empty())
+ body_item->thumbnail_url = it->thumbnail_url;
+ else if(!it->url.empty())
+ body_item->thumbnail_url = it->url;
+ else
+ body_item->thumbnail_url = user_info.avatar_url;
// TODO: Show image thumbnail inline instead of url to image
body_item->url = it->url;
result_items.push_back(std::move(body_item));