From a8e0846a7c111a8d5b5cf8592ecb9b9bbd15ce26 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 22 Sep 2020 22:46:29 +0200 Subject: Initial file manager implementation, with thumbnail caching --- README.md | 5 +- include/Body.hpp | 9 ++- include/ImageUtils.hpp | 1 + include/Page.hpp | 3 +- include/Path.hpp | 8 +++ include/QuickMedia.hpp | 2 + plugins/Dmenu.hpp | 2 +- plugins/FileManager.hpp | 22 +++++++ src/Body.cpp | 142 ++++++++++++++++++++++++++++++++++++++++---- src/ImageUtils.cpp | 4 ++ src/QuickMedia.cpp | 135 +++++++++++++++++++++++++++++++++++++++-- src/plugins/FileManager.cpp | 59 ++++++++++++++++++ src/plugins/Matrix.cpp | 7 ++- 13 files changed, 375 insertions(+), 24 deletions(-) create mode 100644 plugins/FileManager.hpp create mode 100644 src/plugins/FileManager.cpp diff --git a/README.md b/README.md index b52b382..7494159 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # QuickMedia Native clients of websites with fast access to what you want to see, **with TOR support**. See [old video demo with manga](https://lbry.tv/quickmedia_manga-2019-08-05_21.20.46/7).\ -Currently supported websites: `youtube`, `nyaa.si`, `manganelo`, `mangatown`, `mangadex`, `4chan` and _others_.\ +Currently supported websites: `youtube`, `nyaa.si`, `manganelo`, `mangatown`, `mangadex`, `4chan`, `matrix` and _others_.\ **Note:** Manganelo doesn't work when used with TOR.\ **Note:** Posting comments on 4chan doesn't work when used with TOR. However browsing works.\ **Note:** TOR system service needs to be running (`systemctl start tor.service`).\ **Note:** Image pages that were downloaded without --upscale-images and are cached wont get upscaled when running with `--upscale-images`.\ +**Note:** Matrix and file-manager is early in progress, not very usable yet.\ Config data, including manga progress is stored under `$HOME/.config/quickmedia`.\ Cache is stored under `$HOME/.cache/quickmedia`. ## Usage ``` usage: QuickMedia [--tor] [--use-system-mpv-config] [-p placeholder-text] OPTIONS: - plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube, nyaa.si or dmenu + plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, youtube, nyaa.si, matrix, file-manager or dmenu --tor Use tor. Disabled by default --use-system-mpv-config Use system mpv config instead of no config. Disabled by default --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default diff --git a/include/Body.hpp b/include/Body.hpp index 0bea3c2..4e30684 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -42,11 +42,12 @@ namespace QuickMedia { // TODO: Use a list of strings instead, not all plugins need all of these fields std::string url; std::string thumbnail_url; - std::string attached_content_url; + std::string attached_content_url; // TODO: Remove and use |url| instead std::string author; bool visible; bool dirty; bool dirty_description; + bool thumbnail_is_local; std::unique_ptr title_text; std::unique_ptr description_text; // Used by image boards for example. The elements are indices to other body items @@ -103,13 +104,17 @@ namespace QuickMedia { std::thread thumbnail_load_thread; bool draw_thumbnails; bool wrap_around; + // Set to {0, 0} to disable resizing + sf::Vector2i thumbnail_resize_target_size; + sf::Vector2f thumbnail_fallback_size; private: struct ThumbnailData { bool referenced; std::shared_ptr texture; + bool loaded = false; }; Program *program; - std::shared_ptr load_thumbnail_from_url(const std::string &url); + std::shared_ptr load_thumbnail_from_url(const std::string &url, bool local, sf::Vector2i thumbnail_resize_target_size); std::unordered_map item_thumbnail_textures; bool loading_thumbnail; int selected_item; diff --git a/include/ImageUtils.hpp b/include/ImageUtils.hpp index f0670d6..58eb197 100644 --- a/include/ImageUtils.hpp +++ b/include/ImageUtils.hpp @@ -5,4 +5,5 @@ namespace QuickMedia { // Works with jpg, png and gif files bool image_get_resolution(const Path &path, int *width, int *height); + bool is_image_ext(const char *ext); } \ No newline at end of file diff --git a/include/Page.hpp b/include/Page.hpp index 5bd8e0d..68c2470 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -14,6 +14,7 @@ namespace QuickMedia { IMAGE_BOARD_THREAD_LIST, IMAGE_BOARD_THREAD, CHAT_LOGIN, - CHAT + CHAT, + FILE_MANAGER }; } \ No newline at end of file diff --git a/include/Path.hpp b/include/Path.hpp index bd978bc..bdc31c1 100644 --- a/include/Path.hpp +++ b/include/Path.hpp @@ -26,6 +26,14 @@ namespace QuickMedia { return *this; } + // Returns empty string if no extension + const char* ext() const { + size_t index = data.rfind('.'); + if(index == std::string::npos) + return ""; + return data.c_str() + index; + } + std::string data; }; } \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 485aeca..6afdfce 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -54,6 +54,7 @@ namespace QuickMedia { void image_board_thread_page(); void chat_login_page(); void chat_page(); + void file_manager_page(); bool on_search_suggestion_submit_text(Body *input_body, Body *output_body); @@ -120,5 +121,6 @@ namespace QuickMedia { // TODO: Save this to config file when switching modes ImageViewMode image_view_mode = ImageViewMode::SINGLE; Body *related_media_body; + std::vector selected_files; }; } \ No newline at end of file diff --git a/plugins/Dmenu.hpp b/plugins/Dmenu.hpp index 84614cc..32fdad1 100644 --- a/plugins/Dmenu.hpp +++ b/plugins/Dmenu.hpp @@ -9,7 +9,7 @@ namespace QuickMedia { bool search_is_filter() override { return true; } bool search_suggestions_has_thumbnails() const override { return false; } bool search_results_has_thumbnails() const override { return false; } - int get_search_delay() const override { return 0; } + int get_search_delay() const override { return 50; } bool search_suggestion_is_search() const override { return true; } Page get_page_after_search() const override { return Page::EXIT; } PluginResult get_front_page(BodyItems &result_items) override; diff --git a/plugins/FileManager.hpp b/plugins/FileManager.hpp new file mode 100644 index 0000000..d5d7088 --- /dev/null +++ b/plugins/FileManager.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Plugin.hpp" +#include + +namespace QuickMedia { + class FileManager : public Plugin { + public: + FileManager(); + PluginResult get_files_in_directory(BodyItems &result_items); + bool set_current_directory(const std::string &path); + bool set_child_directory(const std::string &filename); + const std::filesystem::path& get_current_dir() const; + + bool search_suggestions_has_thumbnails() const override { return true; } + bool search_results_has_thumbnails() const override { return true; } + int get_search_delay() const override { return 50; } + Page get_page_after_search() const override { return Page::FILE_MANAGER; } + private: + std::filesystem::path current_dir; + }; +} \ No newline at end of file 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 #include @@ -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(*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 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 Body::load_thumbnail_from_url(const std::string &url, bool local, sf::Vector2i thumbnail_resize_target_size) { auto result = std::make_shared(); 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(); + // 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 [--tor] [--use-system-mpv-config] [-p placeholder-text]\n"); + fprintf(stderr, "usage: QuickMedia [--tor] [--use-system-mpv-config] [--dir ] [-p ]\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(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(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, ¤t_dir_text](const std::string&) -> bool { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return false; + + FileManager *file_manager = static_cast(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(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 + +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(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(""); 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)); -- cgit v1.2.3