aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--TODO3
-rw-r--r--example-config.json3
-rw-r--r--include/Config.hpp1
-rw-r--r--include/Path.hpp10
-rw-r--r--include/QuickMedia.hpp2
-rw-r--r--include/Storage.hpp8
-rw-r--r--include/StringUtils.hpp1
-rw-r--r--plugins/LocalManga.hpp61
-rw-r--r--plugins/Manga.hpp2
-rw-r--r--plugins/MangaCombined.hpp5
-rw-r--r--plugins/Page.hpp3
-rw-r--r--src/AsyncImageLoader.cpp2
-rw-r--r--src/Body.cpp23
-rw-r--r--src/Config.cpp14
-rw-r--r--src/QuickMedia.cpp63
-rw-r--r--src/Storage.cpp41
-rw-r--r--src/StringUtils.cpp34
-rw-r--r--src/Theme.cpp9
-rw-r--r--src/plugins/LocalManga.cpp195
-rw-r--r--src/plugins/MangaCombined.cpp15
-rw-r--r--src/plugins/Page.cpp1
22 files changed, 448 insertions, 51 deletions
diff --git a/README.md b/README.md
index 7b0f8e8..00d4d2e 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,12 @@
# QuickMedia
A rofi inspired native client for web services.
Currently supported web services: `youtube`, `peertube`, `lbry`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `anilist` and _others_.
+QuickMedia also supports reading local manga (add "local_manga_directory" config to ~/.config/quickmedia/config.json).
## Usage
```
usage: quickmedia [plugin] [--dir <directory>] [-e <window>] [youtube-url]
OPTIONS:
- plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager or stdin
+ plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager or stdin
--no-video Only play audio when playing a video. Disabled by default
--upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default
--upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default
diff --git a/TODO b/TODO
index 03d1d25..620e88f 100644
--- a/TODO
+++ b/TODO
@@ -213,4 +213,5 @@ Very large resolutions, such as 7680x2160 (id 272) for video https://www.youtube
Use std::move(string) for all places where text.set_string is called.
Use reference for text.get_string() in all places.
Cleanup font sizes that are not visible (or not used for a while). Also do the same for character ranges font texture.
-Show video upload date in video description on youtube. \ No newline at end of file
+Show video upload date in video description on youtube.
+Add latest read chapter name to manga progress, to easily see from manga list page how we have progressed in the manga. \ No newline at end of file
diff --git a/example-config.json b/example-config.json
index 72cfffe..0d76348 100644
--- a/example-config.json
+++ b/example-config.json
@@ -26,5 +26,6 @@
"theme": "default",
"scale": 1.0,
"font_scale": 1.0,
- "spacing_scale": 1.0
+ "spacing_scale": 1.0,
+ "local_manga_directory": ""
} \ No newline at end of file
diff --git a/include/Config.hpp b/include/Config.hpp
index 1a5ef10..8430e73 100644
--- a/include/Config.hpp
+++ b/include/Config.hpp
@@ -46,6 +46,7 @@ namespace QuickMedia {
float scale = 1.0f;
float font_scale = 1.0f;
float spacing_scale = 1.0f;
+ std::string local_manga_directory;
};
const Config& get_config();
diff --git a/include/Path.hpp b/include/Path.hpp
index fe49265..bb08cc6 100644
--- a/include/Path.hpp
+++ b/include/Path.hpp
@@ -21,6 +21,7 @@ namespace QuickMedia {
return *this;
}
+ // Includes extension
const char* filename() const {
size_t index = data.rfind('/');
if(index == std::string::npos)
@@ -28,6 +29,15 @@ namespace QuickMedia {
return data.c_str() + index + 1;
}
+ std::string filename_no_ext() const {
+ const char *name = filename();
+ const char *extension = ext();
+ if(extension[0] == '\0')
+ return name;
+ else
+ return data.substr(name - data.data(), extension - name);
+ }
+
// Returns empty string if no extension
const char* ext() const {
size_t slash_index = data.rfind('/');
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index ce63fbb..27c6bb2 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -96,7 +96,7 @@ namespace QuickMedia {
TaskResult run_task_with_loading_screen(std::function<bool()> callback);
const char* get_plugin_name() const;
- void manga_get_watch_history(const char *plugin_name, BodyItems &history_items);
+ void manga_get_watch_history(const char *plugin_name, BodyItems &history_items, bool local_thumbnail);
void youtube_get_watch_history(BodyItems &history_items);
Json::Value load_history_json();
diff --git a/include/Storage.hpp b/include/Storage.hpp
index 1bf44a9..1e38906 100644
--- a/include/Storage.hpp
+++ b/include/Storage.hpp
@@ -18,6 +18,11 @@ namespace QuickMedia {
DIRECTORY
};
+ enum FileSortDirection {
+ ASC,
+ DESC
+ };
+
Path get_home_dir();
Path get_storage_dir();
Path get_cache_dir();
@@ -30,7 +35,8 @@ namespace QuickMedia {
int file_overwrite(const Path &path, const std::string &data);
int file_overwrite_atomic(const Path &path, const std::string &data);
void for_files_in_dir(const Path &path, FileIteratorCallback callback);
- void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback);
+ void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir = FileSortDirection::ASC);
+ void for_files_in_dir_sort_name(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir = FileSortDirection::ASC);
bool read_file_as_json(const Path &filepath, Json::Value &result);
bool save_json_to_file_atomic(const Path &path, const Json::Value &json);
diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp
index d31713d..fb16d7a 100644
--- a/include/StringUtils.hpp
+++ b/include/StringUtils.hpp
@@ -20,6 +20,7 @@ namespace QuickMedia {
bool string_starts_with(const std::string &str, const char *sub);
bool string_ends_with(const std::string &str, const std::string &ends_with_str);
size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len);
+ bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr);
char to_upper(char c);
bool strncase_equals(const char *str1, const char *str2, size_t length);
bool strcase_equals(const char *str1, const char *str2);
diff --git a/plugins/LocalManga.hpp b/plugins/LocalManga.hpp
new file mode 100644
index 0000000..27ee89d
--- /dev/null
+++ b/plugins/LocalManga.hpp
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "Manga.hpp"
+
+namespace QuickMedia {
+ struct LocalMangaPage {
+ Path path;
+ int number;
+ };
+
+ struct LocalMangaChapter {
+ std::string name;
+ std::vector<LocalMangaPage> pages;
+ time_t modified_time_seconds;
+ };
+
+ struct LocalManga {
+ std::string name;
+ std::vector<LocalMangaChapter> chapters;
+ time_t modified_time_seconds;
+ };
+
+ class LocalMangaSearchPage : public LazyFetchPage {
+ public:
+ LocalMangaSearchPage(Program *program, bool standalone) : LazyFetchPage(program), standalone(standalone) {}
+ const char* get_title() const override { return "Search"; }
+ bool search_is_filter() override { return standalone; }
+ SearchResult search(const std::string &str, BodyItems &result_items) override;
+ PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ const char* get_bookmark_name() const override { return "local-manga"; }
+ private:
+ std::vector<LocalManga> manga_list;
+ bool standalone;
+ };
+
+ class LocalMangaChaptersPage : public MangaChaptersPage {
+ public:
+ LocalMangaChaptersPage(Program *program, std::string manga_name, std::string manga_url, const std::string &thumbnail_url) : MangaChaptersPage(program, std::move(manga_name), std::move(manga_url), thumbnail_url) {}
+ PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ const char* get_bookmark_name() const override { return "local-manga"; }
+ protected:
+ bool extract_id_from_url(const std::string &url, std::string &manga_id) const override;
+ const char* get_service_name() const override { return "local-manga"; }
+ };
+
+ class LocalMangaImagesPage : public MangaImagesPage {
+ public:
+ LocalMangaImagesPage(Program *program, std::string manga_name, std::string chapter_name, std::string url, std::string thumbnail_url, std::vector<LocalMangaPage> pages) :
+ MangaImagesPage(program, std::move(manga_name), std::move(chapter_name), std::move(url), std::move(thumbnail_url)), pages(std::move(pages)) {}
+ ImageResult get_number_of_images(int &num_images) override;
+ ImageResult for_each_page_in_chapter(PageCallback callback) override;
+ const char* get_service_name() const override { return "local-manga"; }
+ const char* get_website_url() const override { return "localhost"; }
+ bool is_local() override { return true; }
+ private:
+ ImageResult get_image_urls_for_chapter(const std::string &url);
+ private:
+ std::vector<LocalMangaPage> pages;
+ };
+} \ No newline at end of file
diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp
index 7b67e13..bc6b415 100644
--- a/plugins/Manga.hpp
+++ b/plugins/Manga.hpp
@@ -46,6 +46,8 @@ namespace QuickMedia {
virtual const char* get_website_url() const = 0;
+ virtual bool is_local() { return false; }
+
const std::string manga_name;
const std::string thumbnail_url;
protected:
diff --git a/plugins/MangaCombined.hpp b/plugins/MangaCombined.hpp
index 1348b1b..670055f 100644
--- a/plugins/MangaCombined.hpp
+++ b/plugins/MangaCombined.hpp
@@ -10,11 +10,12 @@ namespace QuickMedia {
std::unique_ptr<Page> page;
std::string title;
std::string service_name;
+ bool local_manga = false;
};
using MangaCombinedSearchThread = std::pair<MangaPlugin*, AsyncTask<BodyItems>>;
- class MangaCombinedSearchPage : public Page {
+ class MangaCombinedSearchPage : public LazyFetchPage {
public:
MangaCombinedSearchPage(Program *program, std::vector<MangaPlugin> search_pages);
const char* get_title() const override { return "Search"; }
@@ -22,6 +23,8 @@ namespace QuickMedia {
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ bool lazy_fetch_is_loader() override { return true; }
void cancel_operation() override;
private:
std::vector<MangaPlugin> search_pages;
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index 0e904c2..a803725 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -176,7 +176,7 @@ namespace QuickMedia {
class BookmarksPage : public LazyFetchPage {
public:
- BookmarksPage(Program *program, Page *redirect_page) : LazyFetchPage(program), redirect_page(redirect_page) {}
+ BookmarksPage(Program *program, Page *redirect_page, bool local_thumbnail = false) : LazyFetchPage(program), redirect_page(redirect_page), local_thumbnail(local_thumbnail) {}
const char* get_title() const override { return "Bookmarks"; }
PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override;
PluginResult lazy_fetch(BodyItems &result_items) override;
@@ -185,5 +185,6 @@ namespace QuickMedia {
bool is_bookmark_page() const override { return true; }
private:
Page *redirect_page;
+ bool local_thumbnail;
};
} \ No newline at end of file
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index 741ff01..ddcb604 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -79,7 +79,7 @@ namespace QuickMedia {
} else if(symlink_if_no_resize) {
int res = symlink(thumbnail_path.data.c_str(), result_path_tmp.data.c_str());
if(res == -1 && errno != EEXIST) {
- fprintf(stderr, "Failed to symlink %s to %s\n", thumbnail_path_resized.data.c_str(), thumbnail_path.data.c_str());
+ fprintf(stderr, "Failed to symlink %s to %s\n", result_path_tmp.data.c_str(), thumbnail_path.data.c_str());
_exit(1);
}
} else {
diff --git a/src/Body.cpp b/src/Body.cpp
index dbc13af..4681410 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -1713,29 +1713,6 @@ namespace QuickMedia {
return item_height;
}
- // TODO: Support utf-8 case insensitive find
- static bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr) {
- if(substr.empty()) return true;
-
- size_t str_index = 0;
- bool full_match = true;
-
- string_split(substr, ' ', [&str, &str_index, &full_match](const char *str_part, size_t size) {
- if(size == 0) return true;
-
- size_t found_index = str_find_case_insensitive(str, str_index, str_part, size);
- if(found_index == std::string::npos) {
- full_match = false;
- return false;
- }
-
- str_index = found_index + size;
- return true;
- });
-
- return full_match;
- }
-
void Body::filter_search_fuzzy(const std::string &text) {
current_filter = text;
diff --git a/src/Config.cpp b/src/Config.cpp
index 159836f..7f71f21 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -22,8 +22,11 @@ namespace QuickMedia {
}
char *dpi = XGetDefault(display, "Xft", "dpi");
- if(dpi)
+ if(dpi) {
xft_dpi = strtol(dpi, nullptr, 10);
+ if(xft_dpi == 0)
+ xft_dpi = XFT_DPI_DEFAULT;
+ }
XCloseDisplay(display);
return xft_dpi;
@@ -37,12 +40,13 @@ namespace QuickMedia {
if(gdk_scale) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
scale = atof(gdk_scale);
- if(scale < 0.0001f)
- scale = 1.0f;
} else {
scale = (float)xrdb_get_dpi() / (float)XFT_DPI_DEFAULT;
}
+ if(scale < 0.0001f)
+ scale = 1.0f;
+
scale_set = true;
return scale;
}
@@ -148,6 +152,10 @@ namespace QuickMedia {
const Json::Value &spacing_scale = json_root["spacing_scale"];
if(spacing_scale.isNumeric())
config->spacing_scale = spacing_scale.asFloat();
+
+ const Json::Value &local_manga_directory_json = json_root["local_manga_directory"];
+ if(local_manga_directory_json.isString())
+ config->local_manga_directory = local_manga_directory_json.asString();
}
const Config& get_config() {
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 1f7629e..cfe4e15 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -1,6 +1,7 @@
#include "../include/QuickMedia.hpp"
#include "../plugins/Manganelo.hpp"
#include "../plugins/Mangadex.hpp"
+#include "../plugins/LocalManga.hpp"
#include "../plugins/MangaGeneric.hpp"
#include "../plugins/MangaCombined.hpp"
#include "../plugins/MediaGeneric.hpp"
@@ -79,6 +80,7 @@ static const std::pair<const char*, const char*> valid_plugins[] = {
std::make_pair("mangadex", "mangadex_logo.png"),
std::make_pair("onimanga", nullptr),
std::make_pair("readm", "readm_logo.png"),
+ std::make_pair("local-manga", nullptr),
std::make_pair("manga", nullptr),
std::make_pair("youtube", "yt_logo_rgb_dark_small.png"),
std::make_pair("peertube", "peertube_logo.png"),
@@ -200,8 +202,8 @@ namespace QuickMedia {
class HistoryPage : public LazyFetchPage {
public:
- HistoryPage(Program *program, Page *search_page, HistoryType history_type) :
- LazyFetchPage(program), search_page(search_page), history_type(history_type) {}
+ HistoryPage(Program *program, Page *search_page, HistoryType history_type, bool local_thumbnail = false) :
+ LazyFetchPage(program), search_page(search_page), history_type(history_type), local_thumbnail(local_thumbnail) {}
const char* get_title() const override { return "History"; }
PluginResult submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) override {
return search_page->submit(args, result_tabs);
@@ -212,7 +214,7 @@ namespace QuickMedia {
program->youtube_get_watch_history(result_items);
break;
case HistoryType::MANGA:
- program->manga_get_watch_history(program->get_plugin_name(), result_items);
+ program->manga_get_watch_history(program->get_plugin_name(), result_items, local_thumbnail);
break;
}
return PluginResult::OK;
@@ -222,6 +224,7 @@ namespace QuickMedia {
private:
Page *search_page;
HistoryType history_type;
+ bool local_thumbnail;
};
using OptionsPageHandler = std::function<void()>;
@@ -279,7 +282,7 @@ namespace QuickMedia {
static void usage() {
fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--dir <directory>] [-e <window>] [youtube-url]\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
+ fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n");
fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n");
fprintf(stderr, " --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n");
@@ -296,7 +299,15 @@ namespace QuickMedia {
}
static bool is_manga_plugin(const char *plugin_name) {
- return strcmp(plugin_name, "manga") == 0 || strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "manganelos") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangakatana") == 0 || strcmp(plugin_name, "mangadex") == 0 || strcmp(plugin_name, "readm") == 0 || strcmp(plugin_name, "onimanga") == 0;
+ return strcmp(plugin_name, "manga") == 0
+ || strcmp(plugin_name, "manganelo") == 0
+ || strcmp(plugin_name, "manganelos") == 0
+ || strcmp(plugin_name, "mangatown") == 0
+ || strcmp(plugin_name, "mangakatana") == 0
+ || strcmp(plugin_name, "mangadex") == 0
+ || strcmp(plugin_name, "readm") == 0
+ || strcmp(plugin_name, "onimanga") == 0
+ || strcmp(plugin_name, "local-manga") == 0;
}
static std::shared_ptr<BodyItem> create_launcher_body_item(const char *title, const char *plugin_name, const std::string &thumbnail_url) {
@@ -617,6 +628,10 @@ namespace QuickMedia {
resources_root = program_path + "../../../";
}
+ // Initialize config and theme early to prevent possible race condition on initialize
+ get_config();
+ get_theme();
+
set_resource_loader_root_path(resources_root.c_str());
set_use_system_fonts(get_config().use_system_fonts);
init_body_themes();
@@ -1041,6 +1056,7 @@ namespace QuickMedia {
create_launcher_body_item("AniList", "anilist", resources_root + "images/anilist_logo.png"),
create_launcher_body_item("Hot Examples", "hotexamples", ""),
create_launcher_body_item("Lbry", "lbry", resources_root + "icons/lbry_launcher.png"),
+ create_launcher_body_item("Local manga", "local-manga", ""),
create_launcher_body_item("Manga (all)", "manga", ""),
create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png"),
create_launcher_body_item("Mangakatana", "mangakatana", resources_root + "icons/mangakatana_launcher.png"),
@@ -1134,6 +1150,16 @@ namespace QuickMedia {
tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
start_tab_index = 1;
+ } else if(strcmp(plugin_name, "local-manga") == 0) {
+ auto search_page = std::make_unique<LocalMangaSearchPage>(this, true);
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get(), true), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+
+ auto history_page = std::make_unique<HistoryPage>(this, tabs.back().page.get(), HistoryType::MANGA, true);
+ tabs.push_back(Tab{create_body(), std::move(history_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+
+ start_tab_index = 1;
} else if(strcmp(plugin_name, "manga") == 0) {
auto mangadex = std::make_unique<MangadexSearchPage>(this);
upgrade_legacy_mangadex_ids(this, mangadex.get());
@@ -1149,6 +1175,7 @@ namespace QuickMedia {
add_onimanga_handlers(onimanga.get());
auto readm = std::make_unique<MangaGenericSearchPage>(this, "readm", "https://readm.org/");
add_readm_handlers(readm.get());
+ auto local_manga = std::make_unique<LocalMangaSearchPage>(this, false);
// TODO: Use async task pool
std::vector<MangaPlugin> pages;
@@ -1160,6 +1187,9 @@ namespace QuickMedia {
pages.push_back({std::move(readm), "Readm", "readm"});
pages.push_back({std::move(mangadex), "Mangadex", "mangadex"});
+ if(!get_config().local_manga_directory.empty())
+ pages.push_back({std::move(local_manga), "Local manga", "local-manga", true});
+
tabs.push_back(Tab{create_body(), std::make_unique<MangaCombinedSearchPage>(this, std::move(pages)), create_search_bar("Search...", 400)});
} else if(strcmp(plugin_name, "nyaa.si") == 0) {
auto categories_nyaa_si_body = create_body();
@@ -1408,7 +1438,7 @@ namespace QuickMedia {
window.set_clipboard(str);
}
- void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items) {
+ void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items, bool local_thumbnail) {
// TOOD: Make generic, instead of checking for plugin
Path content_storage_dir = get_storage_dir().join(plugin_name);
if(create_directory_recursive(content_storage_dir) != 0) {
@@ -1430,7 +1460,7 @@ namespace QuickMedia {
// TODO: Remove this once manga history file has been in use for a few months and is filled with history
time_t now = time(NULL);
- for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin_name, &manga_id_to_thumbnail_url_map, now](const Path &filepath) {
+ for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin_name, &manga_id_to_thumbnail_url_map, now, local_thumbnail](const Path &filepath) {
// This can happen when QuickMedia crashes/is killed while writing to storage.
// In that case, the storage wont be corrupt but there will be .tmp files.
// TODO: Remove these .tmp files if they exist during startup
@@ -1464,6 +1494,7 @@ namespace QuickMedia {
if(thumbnail_it != manga_id_to_thumbnail_url_map.end()) {
body_item->thumbnail_url = thumbnail_it->second;
body_item->thumbnail_size = {101, 141};
+ body_item->thumbnail_is_local = local_thumbnail;
}
if(strcmp(plugin_name, "manganelo") == 0)
@@ -1480,6 +1511,8 @@ namespace QuickMedia {
body_item->url = "https://onimanga.com/" + manga_id;
else if(strcmp(plugin_name, "readm") == 0)
body_item->url = "https://readm.org/manga/" + manga_id;
+ else if(strcmp(plugin_name, "local-manga") == 0)
+ body_item->url = manga_id;
else
fprintf(stderr, "Error: Not implemented: filename to manga chapter list\n");
@@ -3434,10 +3467,18 @@ namespace QuickMedia {
Path image_filepath_tmp(image_filepath.data + ".tmpz");
// TODO: Move to page
- size_t file_size = 0;
- if(download_to_file(url, image_filepath_tmp.data, extra_args, true, cloudflare_bypass) != DownloadResult::OK || (is_manganelo && file_get_size(image_filepath_tmp, &file_size) == 0 && file_size < 255)) {
- if(!image_download_cancel) show_notification("QuickMedia", "Failed to download image: " + url, Urgency::CRITICAL);
- return true;
+ if(images_page->is_local()) {
+ int res = symlink(url.c_str(), image_filepath_tmp.data.c_str());
+ if(res == -1 && errno != EEXIST) {
+ show_notification("QuickMedia", "Failed to symlink " + image_filepath_tmp.data + " to " + url);
+ return true;
+ }
+ } else {
+ size_t file_size = 0;
+ if(download_to_file(url, image_filepath_tmp.data, extra_args, true, cloudflare_bypass) != DownloadResult::OK || (is_manganelo && file_get_size(image_filepath_tmp, &file_size) == 0 && file_size < 255)) {
+ if(!image_download_cancel) show_notification("QuickMedia", "Failed to download image: " + url, Urgency::CRITICAL);
+ return true;
+ }
}
bool rename_immediately = true;
diff --git a/src/Storage.cpp b/src/Storage.cpp
index 2754bc8..a0f20e4 100644
--- a/src/Storage.cpp
+++ b/src/Storage.cpp
@@ -256,7 +256,7 @@ namespace QuickMedia {
}
}
- void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback) {
+ void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir) {
std::vector<std::filesystem::directory_entry> paths;
try {
for(auto &p : std::filesystem::directory_iterator(path.data)) {
@@ -267,9 +267,42 @@ namespace QuickMedia {
return;
}
- std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
- return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min());
- });
+ if(sort_dir == FileSortDirection::ASC) {
+ std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
+ return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min());
+ });
+ } else {
+ std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
+ return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) < file_get_filetime_or(path2, std::filesystem::file_time_type::min());
+ });
+ }
+
+ for(auto &p : paths) {
+ if(!callback(p.path().string()))
+ break;
+ }
+ }
+
+ void for_files_in_dir_sort_name(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir) {
+ std::vector<std::filesystem::directory_entry> paths;
+ try {
+ for(auto &p : std::filesystem::directory_iterator(path.data)) {
+ paths.push_back(p);
+ }
+ } catch(const std::filesystem::filesystem_error &err) {
+ fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what());
+ return;
+ }
+
+ if(sort_dir == FileSortDirection::ASC) {
+ std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
+ return path1.path().filename() < path2.path().filename();
+ });
+ } else {
+ std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
+ return path1.path().filename() > path2.path().filename();
+ });
+ }
for(auto &p : paths) {
if(!callback(p.path().string()))
diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp
index 81ea1eb..927c6e1 100644
--- a/src/StringUtils.cpp
+++ b/src/StringUtils.cpp
@@ -122,15 +122,43 @@ namespace QuickMedia {
}
size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len) {
+ if(substr_len == 0)
+ return 0;
+
auto it = std::search(str.begin() + start_index, str.end(), substr, substr + substr_len,
[](char c1, char c2) {
return to_upper(c1) == to_upper(c2);
});
+
if(it == str.end())
return std::string::npos;
+
return it - str.begin();
}
+ // TODO: Support utf-8 case insensitive find
+ bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr) {
+ if(substr.empty()) return true;
+
+ size_t str_index = 0;
+ bool full_match = true;
+
+ string_split(substr, ' ', [&str, &str_index, &full_match](const char *str_part, size_t size) {
+ if(size == 0) return true;
+
+ size_t found_index = str_find_case_insensitive(str, str_index, str_part, size);
+ if(found_index == std::string::npos) {
+ full_match = false;
+ return false;
+ }
+
+ str_index = found_index + size;
+ return true;
+ });
+
+ return full_match;
+ }
+
char to_upper(char c) {
if(c >= 'a' && c <= 'z')
return c - 32;
@@ -173,6 +201,9 @@ namespace QuickMedia {
}
bool to_num(const char *str, size_t size, int &num) {
+ if(size == 0)
+ return false;
+
size_t i = 0;
const bool is_negative = size > 0 && str[0] == '-';
if(is_negative)
@@ -193,6 +224,9 @@ namespace QuickMedia {
}
bool to_num_hex(const char *str, size_t size, int &num) {
+ if(size == 0)
+ return false;
+
size_t i = 0;
const bool is_negative = size > 0 && str[0] == '-';
if(is_negative)
diff --git a/src/Theme.cpp b/src/Theme.cpp
index 36c8ff7..8320054 100644
--- a/src/Theme.cpp
+++ b/src/Theme.cpp
@@ -34,6 +34,9 @@ namespace QuickMedia {
static void parse_hex_set_color(const Json::Value &json_obj, const char *field_name, mgl::Color &color) {
const Json::Value &json_val = json_obj[field_name];
+ if(json_val.isNull())
+ return;
+
if(!json_val.isString()) {
fprintf(stderr, "Warning: theme variable \"%s\" does not exists or is not a string\n", field_name);
return;
@@ -69,10 +72,14 @@ namespace QuickMedia {
static void get_bool_value(const Json::Value &json_obj, const char *field_name, bool &val) {
const Json::Value &json_val = json_obj[field_name];
+ if(json_val.isNull())
+ return;
+
if(!json_val.isBool()) {
- fprintf(stderr, "Warning: theme variable \"%s\" does not exists or is not a boolean\n", field_name);
+ fprintf(stderr, "Warning: theme variable \"%s\" is not a boolean\n", field_name);
return;
}
+
val = json_val.asBool();
}
diff --git a/src/plugins/LocalManga.cpp b/src/plugins/LocalManga.cpp
new file mode 100644
index 0000000..06b5cf0
--- /dev/null
+++ b/src/plugins/LocalManga.cpp
@@ -0,0 +1,195 @@
+#include "../../plugins/LocalManga.hpp"
+#include "../../include/Notification.hpp"
+#include "../../include/Config.hpp"
+#include "../../include/Theme.hpp"
+#include "../../include/StringUtils.hpp"
+#include "../../include/Storage.hpp"
+
+namespace QuickMedia {
+ // Pages are sorted from 1.png to n.png
+ static std::vector<LocalMangaPage> get_images_in_manga(const Path &directory) {
+ std::vector<LocalMangaPage> page_list;
+ for_files_in_dir(directory, [&page_list](const Path &filepath) -> bool {
+ if(get_file_type(filepath) != FileType::REGULAR)
+ return true;
+
+ std::string filname_no_ext = filepath.filename_no_ext();
+ int page_number = 0;
+ if(!to_num(filname_no_ext.c_str(), filname_no_ext.size(), page_number) || filepath.ext()[0] == '\0')
+ return true;
+
+ LocalMangaPage local_manga_page;
+ local_manga_page.path = filepath;
+ local_manga_page.number = page_number;
+ page_list.push_back(std::move(local_manga_page));
+ return true;
+ });
+
+ std::sort(page_list.begin(), page_list.end(), [](const LocalMangaPage &manga_page1, const LocalMangaPage &manga_page2) {
+ return manga_page1.number < manga_page2.number;
+ });
+ return page_list;
+ }
+
+ static std::vector<LocalMangaChapter> get_chapters_in_manga(const Path &directory) {
+ std::vector<LocalMangaChapter> chapter_list;
+ for_files_in_dir_sort_last_modified(directory, [&chapter_list](const Path &filepath) -> bool {
+ if(get_file_type(filepath) != FileType::DIRECTORY)
+ return true;
+
+ LocalMangaChapter local_manga_chapter;
+ local_manga_chapter.name = filepath.filename();
+ local_manga_chapter.pages = get_images_in_manga(filepath);
+ if(local_manga_chapter.pages.empty() || !file_get_last_modified_time_seconds(filepath.data.c_str(), &local_manga_chapter.modified_time_seconds))
+ return true;
+
+ chapter_list.push_back(std::move(local_manga_chapter));
+ return true;
+ });
+ return chapter_list;
+ }
+
+ static std::vector<LocalManga> get_manga_in_directory(const Path &directory) {
+ std::vector<LocalManga> manga_list;
+ for_files_in_dir_sort_last_modified(directory, [&manga_list](const Path &filepath) -> bool {
+ if(get_file_type(filepath) != FileType::DIRECTORY)
+ return true;
+
+ LocalManga local_manga;
+ local_manga.name = filepath.filename();
+ local_manga.chapters = get_chapters_in_manga(filepath);
+ if(local_manga.chapters.empty() || !file_get_last_modified_time_seconds(filepath.data.c_str(), &local_manga.modified_time_seconds))
+ return true;
+
+ manga_list.push_back(std::move(local_manga));
+ return true;
+ });
+ return manga_list;
+ }
+
+ static std::shared_ptr<BodyItem> local_manga_to_body_item(const LocalManga &local_manga, time_t time_now) {
+ auto body_item = BodyItem::create(local_manga.name);
+ body_item->url = local_manga.name;
+ body_item->set_description("Latest chapter: " + local_manga.chapters.front().name + "\nUpdated " + seconds_to_relative_time_str(time_now - local_manga.modified_time_seconds));
+ body_item->set_description_color(get_theme().faded_text_color);
+ body_item->thumbnail_url = local_manga.chapters.back().pages.front().path.data;
+ body_item->thumbnail_is_local = true;
+ body_item->thumbnail_size = {101, 141};
+ return body_item;
+ }
+
+ SearchResult LocalMangaSearchPage::search(const std::string &str, BodyItems &result_items) {
+ time_t time_now = time(nullptr);
+ for(const LocalManga &local_manga : manga_list) {
+ if(string_find_fuzzy_case_insensitive(local_manga.name, str))
+ result_items.push_back(local_manga_to_body_item(local_manga, time_now));
+ }
+ return SearchResult::OK;
+ }
+
+ PluginResult LocalMangaSearchPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
+ if(get_config().local_manga_directory.empty()) {
+ show_notification("QuickMedia", "local_manga_directory config is not set", Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+
+ if(get_file_type(get_config().local_manga_directory) != FileType::DIRECTORY) {
+ show_notification("QuickMedia", "local_manga_directory config is not set to a valid directory", Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+
+ Path manga_url = Path(get_config().local_manga_directory).join(args.url);
+ std::vector<LocalMangaChapter> chapters = get_chapters_in_manga(manga_url);
+
+ const time_t time_now = time(nullptr);
+ BodyItems chapters_items;
+
+ for(const LocalMangaChapter &local_manga_chapter : chapters) {
+ auto body_item = BodyItem::create(local_manga_chapter.name);
+ body_item->url = local_manga_chapter.name;
+ body_item->set_description("Updated " + seconds_to_relative_time_str(time_now - local_manga_chapter.modified_time_seconds));
+ body_item->set_description_color(get_theme().faded_text_color);
+ chapters_items.push_back(std::move(body_item));
+ }
+
+ auto chapters_body = create_body();
+ chapters_body->set_items(std::move(chapters_items));
+
+ auto chapters_page = std::make_unique<LocalMangaChaptersPage>(program, args.title, args.url, args.thumbnail_url);
+ result_tabs.push_back(Tab{std::move(chapters_body), std::move(chapters_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ return PluginResult::OK;
+ }
+
+ PluginResult LocalMangaSearchPage::lazy_fetch(BodyItems &result_items) {
+ manga_list.clear();
+
+ if(get_config().local_manga_directory.empty()) {
+ show_notification("QuickMedia", "local_manga_directory config is not set", Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+
+ if(get_file_type(get_config().local_manga_directory) != FileType::DIRECTORY) {
+ show_notification("QuickMedia", "local_manga_directory config is not set to a valid directory", Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+
+ manga_list = get_manga_in_directory(get_config().local_manga_directory);
+
+ const time_t time_now = time(nullptr);
+ for(const LocalManga &local_manga : manga_list) {
+ result_items.push_back(local_manga_to_body_item(local_manga, time_now));
+ }
+
+ return PluginResult::OK;
+ }
+
+ PluginResult LocalMangaChaptersPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
+ if(get_config().local_manga_directory.empty()) {
+ show_notification("QuickMedia", "local_manga_directory config is not set", Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+
+ if(get_file_type(get_config().local_manga_directory) != FileType::DIRECTORY) {
+ show_notification("QuickMedia", "local_manga_directory config is not set to a valid directory", Urgency::CRITICAL);
+ return PluginResult::OK;
+ }
+
+ Path chapter_url = Path(get_config().local_manga_directory).join(content_url).join(args.url);
+ std::vector<LocalMangaPage> pages = get_images_in_manga(chapter_url);
+ result_tabs.push_back(Tab{nullptr, std::make_unique<LocalMangaImagesPage>(program, content_title, args.title, args.url, thumbnail_url, std::move(pages)), nullptr});
+ return PluginResult::OK;
+ }
+
+ bool LocalMangaChaptersPage::extract_id_from_url(const std::string &url, std::string &manga_id) const {
+ manga_id = url;
+ return true;
+ }
+
+ ImageResult LocalMangaImagesPage::get_number_of_images(int &num_images) {
+ num_images = 0;
+ ImageResult image_result = get_image_urls_for_chapter(url);
+ if(image_result != ImageResult::OK) return image_result;
+ num_images = chapter_image_urls.size();
+ return ImageResult::OK;
+ }
+
+ ImageResult LocalMangaImagesPage::for_each_page_in_chapter(PageCallback callback) {
+ ImageResult image_result = get_image_urls_for_chapter(url);
+ if(image_result != ImageResult::OK) return image_result;
+ for(const std::string &url : chapter_image_urls) {
+ if(!callback(url))
+ break;
+ }
+ return ImageResult::OK;
+ }
+
+ ImageResult LocalMangaImagesPage::get_image_urls_for_chapter(const std::string&) {
+ if(!chapter_image_urls.empty())
+ return ImageResult::OK;
+
+ for(const LocalMangaPage &local_manga_page : pages) {
+ chapter_image_urls.push_back(local_manga_page.path.data);
+ }
+ return ImageResult::OK;
+ }
+} \ No newline at end of file
diff --git a/src/plugins/MangaCombined.cpp b/src/plugins/MangaCombined.cpp
index bc4043f..fca5705 100644
--- a/src/plugins/MangaCombined.cpp
+++ b/src/plugins/MangaCombined.cpp
@@ -4,7 +4,7 @@ namespace QuickMedia {
static const int SEARCH_TIMEOUT_MILLISECONDS = 5000;
MangaCombinedSearchPage::MangaCombinedSearchPage(Program *program, std::vector<MangaPlugin> search_pages) :
- Page(program), search_pages(std::move(search_pages))
+ LazyFetchPage(program), search_pages(std::move(search_pages))
{
}
@@ -125,6 +125,19 @@ namespace QuickMedia {
return page->submit(args, result_tabs);
}
+ PluginResult MangaCombinedSearchPage::lazy_fetch(BodyItems&) {
+ for(MangaPlugin &manga_plugin : search_pages) {
+ if(manga_plugin.local_manga) {
+ LazyFetchPage *lazy_fetch_page = dynamic_cast<LazyFetchPage*>(manga_plugin.page.get());
+ if(lazy_fetch_page) {
+ BodyItems dummy_body_items;
+ lazy_fetch_page->lazy_fetch(dummy_body_items);
+ }
+ }
+ }
+ return PluginResult::OK;
+ }
+
void MangaCombinedSearchPage::cancel_operation() {
for(auto &search_thread : search_threads) {
search_thread.second.cancel();
diff --git a/src/plugins/Page.cpp b/src/plugins/Page.cpp
index 26c795c..c2e8060 100644
--- a/src/plugins/Page.cpp
+++ b/src/plugins/Page.cpp
@@ -124,6 +124,7 @@ namespace QuickMedia {
if(thumbnail_url_json.isString()) {
body_item->thumbnail_url = thumbnail_url_json.asString();
body_item->thumbnail_size = {101, 141};
+ body_item->thumbnail_is_local = local_thumbnail;
}
if(timestamp_json.isInt64()) {