aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--TODO3
-rw-r--r--include/Body.hpp1
-rw-r--r--include/QuickMedia.hpp1
-rw-r--r--include/StringUtils.hpp1
-rw-r--r--plugins/MangaGeneric.hpp1
-rw-r--r--plugins/Mangadex.hpp1
-rw-r--r--plugins/Manganelo.hpp1
-rw-r--r--plugins/Page.hpp18
-rw-r--r--src/Body.cpp1
-rw-r--r--src/QuickMedia.cpp165
-rw-r--r--src/Storage.cpp3
-rw-r--r--src/StringUtils.cpp24
-rw-r--r--src/plugins/Page.cpp42
-rw-r--r--src/plugins/Youtube.cpp24
15 files changed, 225 insertions, 63 deletions
diff --git a/README.md b/README.md
index 27bddb1..c951a8a 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,8 @@ Type text and then wait and QuickMedia will automatically search.\
`F5`: Reload the video/music.
### Youtube channel controls
`Ctrl+T`: Subscribe/Unsubscribe from the channel.
+### Manga search page controls
+`Ctrl+B`: Bookmark the selected manga. If the manga is already bookmarked then its removed from bookmarks.
### Manga page view controls
`Up`/`Ctrl+K`: Go to the next page (or chapter if the current page is the last one).\
`Down`/`Ctrl+J`: Go to the previous page (or chapter if the current page is the first one).\
diff --git a/TODO b/TODO
index b241b45..3707290 100644
--- a/TODO
+++ b/TODO
@@ -191,4 +191,5 @@ Renable throttle detection after fixing it (it doesn't detect throttling well an
Show who deleted a message in matrix.
Sync should replace all messages in the room (except for the selected room?) to reduce ram usage when in many rooms and when quickmedia has been running for a long time doing sync.
Fix notifications not being marked as read correctly (they remain red!).
-Show youtube annotations. \ No newline at end of file
+Show youtube annotations.
+Show indicator in body item if it has been bookmarked (to prevent accidental removal of an item that has already been bookmarked before). \ No newline at end of file
diff --git a/include/Body.hpp b/include/Body.hpp
index 23439ec..e1e6fef 100644
--- a/include/Body.hpp
+++ b/include/Body.hpp
@@ -81,6 +81,7 @@ namespace QuickMedia {
int find_item_index(std::function<bool(std::shared_ptr<BodyItem>&)> callback);
// Return true to remove the current item.
// Returns true if the item was found.
+ // Note: only removes the first item found.
bool erase_item(std::function<bool(std::shared_ptr<BodyItem>&)> callback);
std::shared_ptr<BodyItem> get_item_by_index(size_t index);
BodyItemList get_items();
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index cb3f5ef..55d7870 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -110,6 +110,7 @@ namespace QuickMedia {
void idle_active_handler();
void update_idle_state();
bool show_info_page(BodyItem *body_item, bool include_reverse_image_search);
+ bool toggle_bookmark(BodyItem *body_item, const char *bookmark_name);
void page_loop_render(sf::RenderWindow &window, std::vector<Tab> &tabs, int selected_tab, TabAssociatedData &tab_associated_data, const Json::Value *json_chapters, Tabs &ui_tabs);
using PageLoopSubmitHandler = std::function<void(const std::vector<Tab> &new_tabs)>;
// Returns false if the page loop was escaped by user navigation (pressing escape) or if there was an error at startup
diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp
index acc7305..6554ea7 100644
--- a/include/StringUtils.hpp
+++ b/include/StringUtils.hpp
@@ -23,4 +23,5 @@ namespace QuickMedia {
bool strncase_equals(const char *str1, const char *str2, size_t length);
bool strcase_equals(const char *str1, const char *str2);
bool to_num(const char *str, size_t size, int &num);
+ std::string seconds_to_relative_time_str(time_t seconds);
} \ No newline at end of file
diff --git a/plugins/MangaGeneric.hpp b/plugins/MangaGeneric.hpp
index 2f65762..4b4f55c 100644
--- a/plugins/MangaGeneric.hpp
+++ b/plugins/MangaGeneric.hpp
@@ -113,6 +113,7 @@ namespace QuickMedia {
PluginResult get_page(const std::string &url, bool is_post, const std::vector<MangaFormDataStr> &form_data, BodyItems &result_items);
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ const char* get_bookmark_name() const override { return service_name; }
MangaGenericSearchPage& search_handler(const char *search_template, int page_start);
MangaGenericSearchPage& search_post_handler(const char *url, std::vector<MangaFormData> form_data, SearchQueryJsonHandler result_handler);
diff --git a/plugins/Mangadex.hpp b/plugins/Mangadex.hpp
index cd81c44..1bc4b21 100644
--- a/plugins/Mangadex.hpp
+++ b/plugins/Mangadex.hpp
@@ -16,6 +16,7 @@ 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 std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ const char* get_bookmark_name() const override { return "mangadex"; }
ChapterImageUrls chapter_image_urls;
private:
diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp
index 62fd9bf..70526ff 100644
--- a/plugins/Manganelo.hpp
+++ b/plugins/Manganelo.hpp
@@ -10,6 +10,7 @@ namespace QuickMedia {
bool search_is_filter() override { return false; }
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ const char* get_bookmark_name() const override { return "manganelo"; }
};
class ManganeloChaptersPage : public MangaChaptersPage {
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index e60c752..39d76d6 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -59,6 +59,9 @@ namespace QuickMedia {
// Mutually exclusive with |get_type| when |get_type| is not PageTypez::REGULAR
virtual bool is_single_page() const { return false; }
virtual bool is_trackable() const { return false; }
+ // Return nullptr if bookmark is not supported by this page
+ virtual const char* get_bookmark_name() const { return nullptr; }
+ virtual bool is_bookmark_page() const { return false; }
virtual bool is_lazy_fetch_page() const { return false; }
// Note: If submit is done without any selection, then the search term is sent as the |title| and |url|. Submit will only be sent if the input text is not empty or if an item is selected
virtual bool allow_submit_no_selection() const { return false; }
@@ -76,7 +79,7 @@ namespace QuickMedia {
Program *program;
std::shared_ptr<BodyItem> submit_body_item; // TODO: Remove this
- bool needs_refresh = false; // Set to true to refresh the page
+ bool needs_refresh = false; // Set to true to refresh the page. Note: only works for search pages and lazy fetch pages
};
enum class TrackResult {
@@ -153,4 +156,17 @@ namespace QuickMedia {
protected:
std::string url;
};
+
+ class BookmarksPage : public LazyFetchPage {
+ public:
+ BookmarksPage(Program *program, Page *redirect_page) : LazyFetchPage(program), redirect_page(redirect_page) {}
+ const char* get_title() const override { return "Bookmarks"; }
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ bool reload_on_page_change() override { return true; }
+ const char* get_bookmark_name() const override { return redirect_page->get_bookmark_name(); }
+ bool is_bookmark_page() const override { return true; }
+ private:
+ Page *redirect_page;
+ };
} \ No newline at end of file
diff --git a/src/Body.cpp b/src/Body.cpp
index f9a3717..10f0d86 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -378,6 +378,7 @@ namespace QuickMedia {
for(auto it = items.begin(), end = items.end(); it != end; ++it) {
if(callback(*it)) {
items.erase(it);
+ clamp_selection();
return true;
}
}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 1bc260c..acc1770 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -293,6 +293,7 @@ namespace QuickMedia {
return PluginResult::OK;
}
bool reload_on_page_change() override { return true; }
+ const char* get_bookmark_name() const override { return search_page->get_bookmark_name(); }
private:
Page *search_page;
HistoryType history_type;
@@ -1118,51 +1119,81 @@ namespace QuickMedia {
});
tabs.push_back(Tab{std::move(pipe_body), std::make_unique<PipePage>(this, "Select plugin to launch"), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "manganelo") == 0) {
- tabs.push_back(Tab{create_body(false, true), std::make_unique<ManganeloSearchPage>(this), create_search_bar("Search...", 400)});
+ auto search_page = std::make_unique<ManganeloSearchPage>(this);
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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, "manganelos") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "http://manganelos.com/");
add_manganelos_handlers(search_page.get());
- tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 400)});
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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, "mangatown") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://www.mangatown.com/");
add_mangatown_handlers(search_page.get());
- tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 400)});
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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, "mangakatana") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://mangakatana.com/", false);
add_mangakatana_handlers(search_page.get());
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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, "mangadex") == 0) {
- tabs.push_back(Tab{create_body(), std::make_unique<MangadexSearchPage>(this), create_search_bar("Search...", 400)});
- upgrade_legacy_mangadex_ids(this, tabs.back().page.get());
+ auto search_page = std::make_unique<MangadexSearchPage>(this);
+ upgrade_legacy_mangadex_ids(this, search_page.get());
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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, "readm") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://readm.org/");
add_readm_handlers(search_page.get());
- tabs.push_back(Tab{create_body(false, true), std::move(search_page), create_search_bar("Search...", 400)});
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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, "onimanga") == 0) {
auto search_page = std::make_unique<MangaGenericSearchPage>(this, plugin_name, "https://onimanga.com/");
add_onimanga_handlers(search_page.get());
+
+ tabs.push_back(Tab{create_body(), std::make_unique<BookmarksPage>(this, search_page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
tabs.push_back(Tab{create_body(), std::move(search_page), create_search_bar("Search...", 400)});
auto history_page = std::make_unique<HistoryPage>(this, tabs.front().page.get(), HistoryType::MANGA);
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());
@@ -1349,35 +1380,9 @@ namespace QuickMedia {
LOGIN
};
- // Returns relative time as a string (approximation)
- static std::string seconds_to_relative_time_str(time_t seconds) {
- seconds = std::max(0L, seconds);
-
- time_t minutes = seconds / 60;
- time_t hours = minutes / 60;
- time_t days = hours / 24;
- time_t months = days / 30;
- time_t years = days / 365;
-
- if(years >= 1)
- return std::to_string(years) + " year" + (years == 1 ? "" : "s") + " ago";
- else if(months >= 1)
- return std::to_string(months) + " month" + (months == 1 ? "" : "s") + " ago";
- else if(days >= 1)
- return std::to_string(days) + " day" + (days == 1 ? "" : "s") + " ago";
- else if(hours >= 1)
- return std::to_string(hours) + " hour" + (hours == 1 ? "" : "s") + " ago";
- else if(minutes >= 1)
- return std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s") + " ago";
- else
- return std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s") + " ago";
- }
-
static void fill_history_items_from_json(const Json::Value &history_json, BodyItems &history_items) {
assert(history_json.isArray());
- BodyItems body_items;
-
time_t time_now = time(NULL);
for(const Json::Value &item : history_json) {
if(!item.isObject())
@@ -1403,12 +1408,10 @@ namespace QuickMedia {
body_item->set_description("Watched " + seconds_to_relative_time_str(time_now - timestamp.asInt64()));
body_item->set_description_color(get_current_theme().faded_text_color);
body_item->thumbnail_size = sf::Vector2i(192, 108);
- body_items.push_back(std::move(body_item));
+ history_items.push_back(std::move(body_item));
}
- for(auto it = body_items.rbegin(), end = body_items.rend(); it != end; ++it) {
- history_items.push_back(std::move(*it));
- }
+ std::reverse(history_items.begin(), history_items.end());
}
static Path get_video_history_filepath(const char *plugin_name) {
@@ -1626,6 +1629,81 @@ namespace QuickMedia {
return true;
}
+ // Returns -1 if not found
+ int bookmark_find_item(Json::Value &root, const std::string &title, const std::string &author, const std::string &url) {
+ if(!root.isArray())
+ return false;
+
+ int index = -1;
+ for(const Json::Value &item_json : root) {
+ ++index;
+ if(!item_json.isObject())
+ continue;
+
+ const Json::Value &title_json = item_json["title"];
+ const Json::Value &author_json = item_json["author"];
+ const Json::Value &url_json = item_json["url"];
+ if((!title.empty() && title_json.isString() && strcmp(title.c_str(), title_json.asCString()) == 0)
+ || (!author.empty() && author_json.isString() && strcmp(author.c_str(), author_json.asCString()) == 0)
+ || (!url.empty() && url_json.isString() && strcmp(url.c_str(), url_json.asCString()) == 0))
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ bool Program::toggle_bookmark(BodyItem *body_item, const char *bookmark_name) {
+ assert(bookmark_name);
+ Path bookmark_path = get_storage_dir().join("bookmarks");
+ if(create_directory_recursive(bookmark_path) != 0) {
+ show_notification("QuickMedia", "Failed to update bookmark", Urgency::CRITICAL);
+ return false;
+ }
+
+ bookmark_path.join(bookmark_name);
+ Json::Value json_root;
+ if(!read_file_as_json(bookmark_path, json_root) || !json_root.isArray())
+ json_root = Json::Value(Json::arrayValue);
+
+ const int existing_index = bookmark_find_item(json_root, body_item->get_title(), body_item->get_author(), body_item->url);
+ if(existing_index != -1) {
+ Json::Value removed;
+ json_root.removeIndex(existing_index, &removed);
+ if(!save_json_to_file_atomic(bookmark_path, json_root)) {
+ show_notification("QuickMedia", "Failed to update bookmark", Urgency::CRITICAL);
+ return false;
+ }
+
+ std::string bookmark_title = body_item->get_title();
+ if(bookmark_title.empty()) bookmark_title = body_item->get_author();
+ show_notification("QuickMedia", "Removed " + bookmark_title + " from bookmarks");
+ removed = true;
+ return true;
+ }
+
+ Json::Value new_item(Json::objectValue);
+ if(!body_item->get_title().empty())
+ new_item["title"] = body_item->get_title();
+ if(!body_item->get_author().empty())
+ new_item["author"] = body_item->get_author();
+ if(!body_item->url.empty())
+ new_item["url"] = body_item->url;
+ new_item["timestamp"] = (int64_t)time(nullptr);
+
+ json_root.append(std::move(new_item));
+ if(!save_json_to_file_atomic(bookmark_path, json_root)) {
+ show_notification("QuickMedia", "Failed to update bookmark", Urgency::CRITICAL);
+ return false;
+ }
+
+ std::string bookmark_title = body_item->get_title();
+ if(bookmark_title.empty()) bookmark_title = body_item->get_author();
+ show_notification("QuickMedia", "Added " + bookmark_title + " to bookmarks");
+ return true;
+ }
+
void Program::page_loop_render(sf::RenderWindow &window, std::vector<Tab> &tabs, int selected_tab, TabAssociatedData &tab_associated_data, const Json::Value *json_chapters, Tabs &ui_tabs) {
if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, window_size, true);
@@ -2053,6 +2131,19 @@ namespace QuickMedia {
return trackable_page->track(selected_item->get_title()) == TrackResult::OK;
});
}
+ } else if(event.key.code == sf::Keyboard::B && event.key.control) {
+ BodyItem *selected_item = tabs[selected_tab].body->get_selected();
+ if(selected_item) {
+ const char *bookmark_name = tabs[selected_tab].page->get_bookmark_name();
+ if(bookmark_name) {
+ if(toggle_bookmark(selected_item, bookmark_name)) {
+ for(Tab &tab : tabs) {
+ if(tab.page && tab.page->is_bookmark_page())
+ tab.page->needs_refresh = true;
+ }
+ }
+ }
+ }
} else if(event.key.code == sf::Keyboard::C && event.key.control) {
BodyItem *selected_item = tabs[selected_tab].body->get_selected();
if(selected_item) {
@@ -7422,7 +7513,7 @@ namespace QuickMedia {
redraw = true;
} else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Tab) {
file_name_entry.set_editable(!file_name_entry.is_editable());
- search_bar->set_editable(!file_name_entry.is_editable());
+ search_bar->set_editable(!search_bar->is_editable());
} else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Enter && event.key.control) {
std::string save_path = save_file();
if(!save_path.empty())
diff --git a/src/Storage.cpp b/src/Storage.cpp
index 4baca49..1b5435f 100644
--- a/src/Storage.cpp
+++ b/src/Storage.cpp
@@ -333,6 +333,9 @@ namespace QuickMedia {
bool is_program_executable_by_name(const char *name) {
// TODO: Implement for Windows. Windows also uses semicolon instead of colon as a separator
char *env = getenv("PATH");
+ if(!env)
+ return false;
+
std::unordered_set<std::string> paths;
string_split(env, ':', [&paths](const char *str, size_t size) {
if(size > 0)
diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp
index 8a3a0ef..6a8d3f2 100644
--- a/src/StringUtils.cpp
+++ b/src/StringUtils.cpp
@@ -170,4 +170,28 @@ namespace QuickMedia {
return true;
}
+
+ // Returns relative time as a string (approximation)
+ std::string seconds_to_relative_time_str(time_t seconds) {
+ seconds = std::max(0L, seconds);
+
+ time_t minutes = seconds / 60;
+ time_t hours = minutes / 60;
+ time_t days = hours / 24;
+ time_t months = days / 30;
+ time_t years = days / 365;
+
+ if(years >= 1)
+ return std::to_string(years) + " year" + (years == 1 ? "" : "s") + " ago";
+ else if(months >= 1)
+ return std::to_string(months) + " month" + (months == 1 ? "" : "s") + " ago";
+ else if(days >= 1)
+ return std::to_string(days) + " day" + (days == 1 ? "" : "s") + " ago";
+ else if(hours >= 1)
+ return std::to_string(hours) + " hour" + (hours == 1 ? "" : "s") + " ago";
+ else if(minutes >= 1)
+ return std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s") + " ago";
+ else
+ return std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s") + " ago";
+ }
} \ No newline at end of file
diff --git a/src/plugins/Page.cpp b/src/plugins/Page.cpp
index 8605d82..5c64183 100644
--- a/src/plugins/Page.cpp
+++ b/src/plugins/Page.cpp
@@ -1,4 +1,6 @@
#include "../../plugins/Page.hpp"
+#include "../../include/StringUtils.hpp"
+#include "../../include/Theme.hpp"
#include "../../include/QuickMedia.hpp"
#include <json/reader.h>
@@ -40,4 +42,44 @@ namespace QuickMedia {
bool Page::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) {
return program->load_manga_content_storage(service_name, manga_title, manga_id);
}
+
+ PluginResult BookmarksPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) {
+ return redirect_page->submit(title, url, result_tabs);
+ }
+
+ PluginResult BookmarksPage::lazy_fetch(BodyItems &result_items) {
+ const char *bookmark_name = redirect_page->get_bookmark_name();
+ if(!bookmark_name)
+ return PluginResult::ERR;
+
+ Path bookmark_path = get_storage_dir().join("bookmarks").join(bookmark_name);
+ Json::Value json_root;
+ if(!read_file_as_json(bookmark_path, json_root) || !json_root.isArray())
+ return PluginResult::OK;
+
+ const time_t time_now = time(nullptr);
+ for(const Json::Value &item_json : json_root) {
+ if(!item_json.isObject())
+ continue;
+
+ const Json::Value &title_json = item_json["title"];
+ const Json::Value &author_json = item_json["author"];
+ const Json::Value &url_json = item_json["url"];
+ const Json::Value &timestamp_json = item_json["timestamp"];
+
+ auto body_item = BodyItem::create(title_json.isString() ? title_json.asString() : "");
+ if(author_json.isString())
+ body_item->set_author(author_json.asString());
+ if(url_json.isString())
+ body_item->url = url_json.asString();
+ if(timestamp_json.isInt64()) {
+ body_item->set_description("Bookmarked " + seconds_to_relative_time_str(time_now - timestamp_json.asInt64()));
+ body_item->set_description_color(get_current_theme().faded_text_color);
+ }
+ result_items.push_back(std::move(body_item));
+ }
+
+ std::reverse(result_items.begin(), result_items.end());
+ return PluginResult::OK;
+ }
} \ No newline at end of file
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index b490e97..6c080c5 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -1660,30 +1660,6 @@ namespace QuickMedia {
return self->size == sub_len && memcmp(self->data, sub, sub_len) == 0;
}
- // Returns relative time as a string (approximation)
- static std::string seconds_to_relative_time_str(time_t seconds) {
- seconds = std::max(0L, seconds);
-
- time_t minutes = seconds / 60;
- time_t hours = minutes / 60;
- time_t days = hours / 24;
- time_t months = days / 30;
- time_t years = days / 365;
-
- if(years >= 1)
- return std::to_string(years) + " year" + (years == 1 ? "" : "s") + " ago";
- else if(months >= 1)
- return std::to_string(months) + " month" + (months == 1 ? "" : "s") + " ago";
- else if(days >= 1)
- return std::to_string(days) + " day" + (days == 1 ? "" : "s") + " ago";
- else if(hours >= 1)
- return std::to_string(hours) + " hour" + (hours == 1 ? "" : "s") + " ago";
- else if(minutes >= 1)
- return std::to_string(minutes) + " minute" + (minutes == 1 ? "" : "s") + " ago";
- else
- return std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s") + " ago";
- }
-
PluginResult YoutubeSubscriptionsPage::lazy_fetch(BodyItems &result_items) {
Path subscriptions_path = get_storage_dir().join("subscriptions").join("youtube.txt");
std::string subscriptions_str;