aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--include/Body.hpp9
-rw-r--r--include/ImageUtils.hpp1
-rw-r--r--include/Page.hpp3
-rw-r--r--include/Path.hpp8
-rw-r--r--include/QuickMedia.hpp2
-rw-r--r--plugins/Dmenu.hpp2
-rw-r--r--plugins/FileManager.hpp22
-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
13 files changed, 375 insertions, 24 deletions
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 <plugin> [--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<Text> title_text;
std::unique_ptr<Text> 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<sf::Texture> texture;
+ bool loaded = false;
};
Program *program;
- std::shared_ptr<sf::Texture> load_thumbnail_from_url(const std::string &url);
+ std::shared_ptr<sf::Texture> load_thumbnail_from_url(const std::string &url, bool local, sf::Vector2i thumbnail_resize_target_size);
std::unordered_map<std::string, ThumbnailData> 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<std::string> 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 <filesystem>
+
+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 <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));