aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--include/Body.hpp3
-rw-r--r--include/Path.hpp26
-rw-r--r--include/QuickMedia.hpp6
-rw-r--r--include/Storage.hpp18
-rw-r--r--include/env.hpp59
-rw-r--r--plugins/Plugin.hpp1
-rw-r--r--project.conf3
-rw-r--r--src/Body.cpp38
-rw-r--r--src/QuickMedia.cpp101
-rw-r--r--src/SearchBar.cpp2
-rw-r--r--src/Storage.cpp109
-rw-r--r--src/plugins/Manganelo.cpp4
-rw-r--r--src/plugins/Plugin.cpp23
-rw-r--r--src/plugins/Youtube.cpp2
15 files changed, 378 insertions, 19 deletions
diff --git a/README.md b/README.md
index 9937445..0ff3427 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ youtube-dl needs to be installed to play videos from youtube.
# TODO
Fix x11 freeze that sometimes happens when playing video.
If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions.
-Keep track of content that has been viewed so the user can return to where they were last.
+Give user the option to start where they left off or from the start.
For manga, view the next chapter when reaching the end of a chapter.
Make network requests asynchronous to not freeze gui when navigating. Also have loading animation.
Retain search text when navigating back. \ No newline at end of file
diff --git a/include/Body.hpp b/include/Body.hpp
index e09017d..e854e76 100644
--- a/include/Body.hpp
+++ b/include/Body.hpp
@@ -4,6 +4,7 @@
#include <SFML/Graphics/Text.hpp>
#include <SFML/Graphics/Texture.hpp>
#include <SFML/Graphics/RenderWindow.hpp>
+#include <json/value.h>
#include <thread>
namespace QuickMedia {
@@ -35,6 +36,7 @@ namespace QuickMedia {
void clamp_selection();
void draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size);
+ void draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress);
static bool string_find_case_insensitive(const std::string &str, const std::string &substr);
// TODO: Make this actually fuzzy... Right now it's just a case insensitive string find.
@@ -42,6 +44,7 @@ namespace QuickMedia {
void filter_search_fuzzy(const std::string &text);
sf::Text title_text;
+ sf::Text progress_text;
int selected_item;
std::vector<std::unique_ptr<BodyItem>> items;
std::vector<std::shared_ptr<sf::Texture>> item_thumbnail_textures;
diff --git a/include/Path.hpp b/include/Path.hpp
new file mode 100644
index 0000000..351fd5d
--- /dev/null
+++ b/include/Path.hpp
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <string>
+
+namespace QuickMedia {
+ class Path {
+ public:
+ Path() = default;
+ ~Path() = default;
+ Path(const Path &other) = default;
+ Path& operator=(const Path &other) = default;
+ Path(const char *path) : data(path) {}
+ Path(const std::string &path) : data(path) {}
+ Path(Path &&other) {
+ data = std::move(other.data);
+ }
+
+ Path& join(const Path &other) {
+ data += "/";
+ data += other.data;
+ return *this;
+ }
+
+ std::string data;
+ };
+} \ No newline at end of file
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index e5f4f2d..48a2d8a 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -2,10 +2,12 @@
#include "SearchBar.hpp"
#include "Page.hpp"
+#include "Storage.hpp"
#include <vector>
#include <memory>
#include <SFML/Graphics/Font.hpp>
#include <SFML/Graphics/RenderWindow.hpp>
+#include <json/value.h>
namespace QuickMedia {
class Body;
@@ -34,6 +36,10 @@ namespace QuickMedia {
// TODO: Combine these
std::string video_url;
std::string images_url;
+ std::string content_title;
+ std::string chapter_title;
int image_index;
+ Path content_storage_file;
+ Json::Value content_storage_json;
};
} \ No newline at end of file
diff --git a/include/Storage.hpp b/include/Storage.hpp
new file mode 100644
index 0000000..bd4283c
--- /dev/null
+++ b/include/Storage.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Path.hpp"
+
+namespace QuickMedia {
+ enum class FileType {
+ FILE_NOT_FOUND,
+ REGULAR,
+ DIRECTORY
+ };
+
+ Path get_home_dir();
+ Path get_storage_dir();
+ int create_directory_recursive(const Path &path);
+ FileType get_file_type(const Path &path);
+ int file_get_content(const Path &path, std::string &result);
+ int file_overwrite(const Path &path, const std::string &data);
+} \ No newline at end of file
diff --git a/include/env.hpp b/include/env.hpp
new file mode 100644
index 0000000..b842ff3
--- /dev/null
+++ b/include/env.hpp
@@ -0,0 +1,59 @@
+#pragma once
+
+#define OS_FAMILY_WINDOWS 0
+#define OS_FAMILY_POSIX 1
+
+#define OS_TYPE_WINDOWS 0
+#define OS_TYPE_LINUX 1
+
+#if defined(_WIN32) || defined(_WIN64)
+ #if defined(_WIN64)
+ #define SYS_ENV_64BIT
+ #else
+ #define SYS_ENV_32BIT
+ #endif
+ #define OS_FAMILY OS_FAMILY_WINDOWS
+ #define OS_TYPE OS_TYPE_WINDOWS
+
+ #ifndef UNICODE
+ #define UNICODE
+ #endif
+
+ #ifndef _UNICODE
+ #define _UNICODE
+ #endif
+
+ #ifndef WIN32_LEAN_AND_MEAN
+ #define WIN32_LEAN_AND_MEAN
+ #endif
+
+ #include <windows.h>
+#endif
+
+#if defined(__linux__) || defined(__unix__) || defined(__APPLE__) || defined(_POSIX_VERSION)
+ #define OS_FAMILY OS_FAMILY_POSIX
+#endif
+
+#if defined(__linux__) || defined(__CYGWIN__)
+ #define OS_TYPE OS_TYPE_LINUX
+#endif
+
+#if defined(__GNUC__)
+ #if defined(__x86_64__) || defined(__pc64__)
+ #define SYS_ENV_64BIT
+ #else
+ #define SYS_ENV_32BIT
+ #endif
+#endif
+
+#if !defined(SYS_ENV_32BIT) && !defined(SYS_ENV_64BIT)
+ #error "System is not detected as either 32-bit or 64-bit"
+#endif
+
+#if !defined(OS_FAMILY)
+ #error "System not supported. Only Windows and Posix systems supported right now"
+#endif
+
+#if !defined(OS_TYPE)
+ #error "System not supported. Only Windows and linux systems supported right now"
+#endif
diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp
index 2d6005e..6d5a986 100644
--- a/plugins/Plugin.hpp
+++ b/plugins/Plugin.hpp
@@ -38,6 +38,7 @@ namespace QuickMedia {
};
DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector<CommandArg> &additional_args = {});
+ std::string strip(const std::string &str);
class Plugin {
public:
diff --git a/project.conf b/project.conf
index d54e94b..7e4e124 100644
--- a/project.conf
+++ b/project.conf
@@ -9,4 +9,5 @@ sfml-graphics = "2"
mpv = "1.25.0"
gl = ">=17.3"
x11 = "1.6.5"
-jsoncpp = "1.5" \ No newline at end of file
+jsoncpp = "1.5"
+cppcodec-1 = "0.1" \ No newline at end of file
diff --git a/src/Body.cpp b/src/Body.cpp
index d391e87..68acaad 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -9,8 +9,14 @@ const sf::Color front_color(43, 45, 47);
const sf::Color back_color(33, 35, 37);
namespace QuickMedia {
- Body::Body(sf::Font &font) : title_text("", font, 14), selected_item(0), loading_thumbnail(false) {
+ Body::Body(sf::Font &font) :
+ title_text("", font, 14),
+ progress_text("", font, 14),
+ selected_item(0),
+ loading_thumbnail(false)
+ {
title_text.setFillColor(sf::Color::White);
+ progress_text.setFillColor(sf::Color::White);
}
void Body::select_previous_item() {
@@ -98,11 +104,19 @@ namespace QuickMedia {
return result;
}
+ void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) {
+ Json::Value empty_object(Json::objectValue);
+ draw(window, pos, size, empty_object);
+ }
+
// TODO: Skip drawing the rows that are outside the window.
// TODO: Use a render target for the whole body so all images can be put into one.
// TODO: Only load images once they are visible on the screen.
// TODO: Load thumbnails with more than one thread.
- void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) {
+ // TODO: Show chapters (rows) that have been read differently to make it easier to see what
+ // needs hasn't been read yet.
+ void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) {
+ assert(content_progress.isObject());
const float font_height = title_text.getCharacterSize() + 8.0f;
const float image_height = 100.0f;
@@ -113,16 +127,19 @@ namespace QuickMedia {
sf::RectangleShape item_background;
item_background.setFillColor(front_color);
- item_background.setOutlineThickness(1.0f);
- item_background.setOutlineColor(sf::Color(63, 65, 67));
+ //item_background.setOutlineThickness(1.0f);
+ //item_background.setOutlineColor(sf::Color(63, 65, 67));
sf::RectangleShape selected_border;
selected_border.setFillColor(sf::Color::Red);
const float selected_border_width = 5.0f;
int num_items = items.size();
- if((int)item_thumbnail_textures.size() != num_items)
+ if((int)item_thumbnail_textures.size() != num_items) {
+ // First unload all textures, then prepare to load new textures
+ item_thumbnail_textures.resize(0);
item_thumbnail_textures.resize(num_items);
+ }
for(int i = 0; i < num_items; ++i) {
const auto &item = items[i];
@@ -182,6 +199,17 @@ namespace QuickMedia {
title_text.setPosition(std::floor(item_pos.x + text_offset_x + 10.0f), std::floor(item_pos.y));
window.draw(title_text);
+ // TODO: Do the same for non-manga content
+ const Json::Value &item_progress = content_progress[item->title];
+ const Json::Value &current_json = item_progress["current"];
+ const Json::Value &total_json = item_progress["total"];
+ if(current_json.isNumeric() && total_json.isNumeric()) {
+ progress_text.setString(std::string("Progress: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt()));
+ auto bounds = progress_text.getLocalBounds();
+ progress_text.setPosition(std::floor(item_pos.x + size.x - bounds.width - 10.0f), std::floor(item_pos.y));
+ window.draw(progress_text);
+ }
+
pos.y += row_height + 10.0f;
}
}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 3285eb9..3cfbc08 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -2,10 +2,13 @@
#include "../plugins/Manganelo.hpp"
#include "../plugins/Youtube.hpp"
#include "../include/VideoPlayer.hpp"
+#include <cppcodec/base64_rfc4648.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/Graphics/Text.hpp>
#include <SFML/Window/Event.hpp>
+#include <json/reader.h>
+#include <json/writer.h>
#include <assert.h>
#include <cmath>
@@ -37,15 +40,15 @@ namespace QuickMedia {
delete current_plugin;
}
- static SearchResult search_selected_suggestion(Body *body, Plugin *plugin, Page &next_page) {
+ static SearchResult search_selected_suggestion(Body *body, Plugin *plugin, Page &next_page, std::string &selected_title) {
BodyItem *selected_item = body->get_selected();
if(!selected_item)
return SearchResult::ERR;
- std::string selected_item_title = selected_item->title;
+ selected_title = selected_item->title;
std::string selected_item_url = selected_item->url;
body->clear_items();
- SearchResult search_result = plugin->search(!selected_item_url.empty() ? selected_item_url : selected_item_title, body->items, next_page);
+ SearchResult search_result = plugin->search(!selected_item_url.empty() ? selected_item_url : selected_title, body->items, next_page);
body->reset_selected();
return search_result;
}
@@ -77,9 +80,12 @@ namespace QuickMedia {
case Page::EPISODE_LIST:
episode_list_page();
break;
- case Page::IMAGES:
+ case Page::IMAGES: {
+ window.setKeyRepeatEnabled(false);
image_page();
+ window.setKeyRepeatEnabled(true);
break;
+ }
default:
return;
}
@@ -110,6 +116,31 @@ namespace QuickMedia {
}
}
+ static std::string base64_encode(const std::string &data) {
+ return cppcodec::base64_rfc4648::encode(data);
+ }
+
+ static bool get_manga_storage_json(const Path &storage_path, Json::Value &result) {
+ std::string file_content;
+ if(file_get_content(storage_path, file_content) != 0)
+ return -1;
+
+ Json::CharReaderBuilder json_builder;
+ std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
+ std::string json_errors;
+ if(json_reader->parse(&file_content.front(), &file_content.back(), &result, &json_errors)) {
+ fprintf(stderr, "Failed to read json, error: %s\n", json_errors.c_str());
+ return -1;
+ }
+
+ return 0;
+ }
+
+ static bool save_manga_progress_json(const Path &path, const Json::Value &json) {
+ Json::StreamWriterBuilder json_builder;
+ return file_overwrite(path, Json::writeString(json_builder, json)) == 0;
+ }
+
void Program::search_suggestion_page() {
search_bar->onTextUpdateCallback = [this](const std::string &text) {
update_search_suggestions(text, body, current_plugin);
@@ -117,8 +148,24 @@ namespace QuickMedia {
search_bar->onTextSubmitCallback = [this](const std::string &text) {
Page next_page;
- if(search_selected_suggestion(body, current_plugin, next_page) == SearchResult::OK)
+ if(search_selected_suggestion(body, current_plugin, next_page, content_title) == SearchResult::OK) {
+ if(next_page == Page::EPISODE_LIST) {
+ Path content_storage_dir = get_storage_dir().join("manga");
+ if(create_directory_recursive(content_storage_dir) != 0) {
+ // TODO: Show this to the user
+ fprintf(stderr, "Failed to create directory: %s\n", content_storage_dir.data.c_str());
+ return;
+ }
+
+ content_storage_file = content_storage_dir.join(base64_encode(content_title));
+ content_storage_json.clear();
+ content_storage_json["name"] = content_title;
+ FileType file_type = get_file_type(content_storage_file);
+ if(file_type == FileType::REGULAR)
+ get_manga_storage_json(content_storage_file, content_storage_json);
+ }
current_page = next_page;
+ }
};
sf::Vector2f body_pos;
@@ -263,9 +310,23 @@ namespace QuickMedia {
images_url = selected_item->url;
image_index = 0;
+ chapter_title = selected_item->title;
current_page = Page::IMAGES;
+
+ const Json::Value &json_chapters = content_storage_json["chapters"];
+ if(json_chapters.isObject()) {
+ const Json::Value &json_chapter = json_chapters[chapter_title];
+ if(json_chapter.isObject()) {
+ const Json::Value &current = json_chapter["current"];
+ if(current.isNumeric())
+ image_index = current.asInt() - 1;
+ }
+ }
+
};
+ const Json::Value &json_chapters = content_storage_json["chapters"];
+
sf::Vector2f body_pos;
sf::Vector2f body_size;
bool resized = true;
@@ -297,7 +358,7 @@ namespace QuickMedia {
search_bar->update();
window.clear(back_color);
- body->draw(window, body_pos, body_size);
+ body->draw(window, body_pos, body_size, json_chapters);
search_bar->draw(window);
window.display();
}
@@ -361,13 +422,35 @@ namespace QuickMedia {
}
image_data.resize(0);
+ int num_images = 0;
+ image_plugin->get_number_of_images(images_url, num_images);
+
+ Json::Value &json_chapters = content_storage_json["chapters"];
+ Json::Value json_chapter;
+ int latest_read = image_index + 1;
+ if(json_chapters.isObject()) {
+ json_chapter = json_chapters[chapter_title];
+ if(json_chapter.isObject()) {
+ const Json::Value &current = json_chapter["current"];
+ if(current.isNumeric())
+ latest_read = std::max(latest_read, current.asInt());
+ }
+ } else {
+ json_chapters = Json::Value(Json::objectValue);
+ json_chapter = Json::Value(Json::objectValue);
+ }
+ json_chapter["current"] = latest_read;
+ json_chapter["total"] = num_images;
+ json_chapters[chapter_title] = json_chapter;
+ if(!save_manga_progress_json(content_storage_file, content_storage_json)) {
+ // TODO: Show this to the user
+ fprintf(stderr, "Failed to save manga progress!\n");
+ }
+
bool error = !error_message.getString().isEmpty();
bool resized = true;
sf::Event event;
- int num_images = 0;
- image_plugin->get_number_of_images(images_url, num_images);
-
sf::Text chapter_text(std::string("Page ") + std::to_string(image_index + 1) + "/" + std::to_string(num_images), font, 14);
if(image_index == num_images)
chapter_text.setString("End");
diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp
index 15777e5..82ade2f 100644
--- a/src/SearchBar.cpp
+++ b/src/SearchBar.cpp
@@ -20,6 +20,8 @@ namespace QuickMedia {
text.setFillColor(text_placeholder_color);
background.setFillColor(front_color);
background.setPosition(padding_horizontal, padding_vertical);
+ //background.setOutlineThickness(1.0f);
+ //background.setOutlineColor(sf::Color(63, 65, 67));
}
void SearchBar::draw(sf::RenderWindow &window) {
diff --git a/src/Storage.cpp b/src/Storage.cpp
new file mode 100644
index 0000000..80d5d70
--- /dev/null
+++ b/src/Storage.cpp
@@ -0,0 +1,109 @@
+#include "../include/Storage.hpp"
+#include "../include/env.hpp"
+#include <stdio.h>
+
+#if OS_FAMILY == OS_FAMILY_POSIX
+#include <pwd.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#endif
+
+static int makedir(const char *path) {
+ return mkdir(path, S_IRWXU);
+}
+
+namespace QuickMedia {
+ Path get_home_dir()
+ {
+ #if OS_FAMILY == OS_FAMILY_POSIX
+ const char *homeDir = getenv("HOME");
+ if(!homeDir)
+ {
+ passwd *pw = getpwuid(getuid());
+ homeDir = pw->pw_dir;
+ }
+ return homeDir;
+ #elif OS_FAMILY == OS_FAMILY_WINDOWS
+ BOOL ret;
+ HANDLE hToken;
+ std::wstring homeDir;
+ DWORD homeDirLen = MAX_PATH;
+ homeDir.resize(homeDirLen);
+
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ, &hToken))
+ throw std::runtime_error("Failed to open process token");
+
+ if (!GetUserProfileDirectory(hToken, &homeDir[0], &homeDirLen))
+ {
+ CloseHandle(hToken);
+ throw std::runtime_error("Failed to get home directory");
+ }
+
+ CloseHandle(hToken);
+ homeDir.resize(wcslen(homeDir.c_str()));
+ return boost::filesystem::path(homeDir);
+ #endif
+ }
+
+ Path get_storage_dir() {
+ return get_home_dir().join(".local").join("share").join("quickmedia");
+ }
+
+ int create_directory_recursive(const Path &path) {
+ size_t index = 0;
+ while(true) {
+ index = path.data.find('/', index);
+
+ // Skips first '/' on unix-like systems
+ if(index == 0) {
+ ++index;
+ continue;
+ }
+
+ std::string path_component = path.data.substr(0, index);
+ int err = makedir(path_component.c_str());
+
+ if(err == -1 && errno != EEXIST)
+ return err;
+
+ if(index == std::string::npos)
+ break;
+ else
+ ++index;
+ }
+ return 0;
+ }
+
+ FileType get_file_type(const Path &path) {
+ struct stat file_stat;
+ if(stat(path.data.c_str(), &file_stat) == 0)
+ return S_ISREG(file_stat.st_mode) ? FileType::REGULAR : FileType::DIRECTORY;
+ return FileType::FILE_NOT_FOUND;
+ }
+
+ int file_get_content(const Path &path, std::string &result) {
+ FILE *file = fopen(path.data.c_str(), "rb");
+ if(!file)
+ return -errno;
+
+ fseek(file, 0, SEEK_END);
+ size_t file_size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+
+ result.resize(file_size);
+ fread(&result[0], 1, file_size, file);
+
+ fclose(file);
+ return 0;
+ }
+
+ int file_overwrite(const Path &path, const std::string &data) {
+ FILE *file = fopen(path.data.c_str(), "wb");
+ if(!file)
+ return -errno;
+
+ fwrite(data.data(), 1, data.size(), file);
+ fclose(file);
+ return 0;
+ }
+} \ No newline at end of file
diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp
index e8d24dc..1989a37 100644
--- a/src/plugins/Manganelo.cpp
+++ b/src/plugins/Manganelo.cpp
@@ -21,7 +21,7 @@ namespace QuickMedia {
const char *href = quickmedia_html_node_get_attribute_value(node, "href");
const char *text = quickmedia_html_node_get_text(node);
if(href && text) {
- auto item = std::make_unique<BodyItem>(text);
+ auto item = std::make_unique<BodyItem>(strip(text));
item->url = href;
item_data->push_back(std::move(item));
}
@@ -83,7 +83,7 @@ namespace QuickMedia {
std::string name_str = name.asString();
while(remove_html_span(name_str)) {}
if(name_str != text) {
- auto item = std::make_unique<BodyItem>(name_str);
+ auto item = std::make_unique<BodyItem>(strip(name_str));
item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString());
Json::Value image = child.get("image", "");
if(image.isString() && image.asCString()[0] != '\0')
diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp
index d87ac34..2367cb3 100644
--- a/src/plugins/Plugin.cpp
+++ b/src/plugins/Plugin.cpp
@@ -33,6 +33,29 @@ namespace QuickMedia {
return DownloadResult::OK;
}
+ static bool is_whitespace(char c) {
+ return c == ' ' || c == '\n' || c == '\t' || c == '\v';
+ }
+
+ std::string strip(const std::string &str) {
+ if(str.empty())
+ return str;
+
+ int start = 0;
+ for(; start < (int)str.size(); ++start) {
+ if(!is_whitespace(str[start]))
+ break;
+ }
+
+ int end = str.size() - 1;
+ for(; end >= start; --end) {
+ if(!is_whitespace(str[end]))
+ break;
+ }
+
+ return str.substr(start, end - start + 1);
+ }
+
std::string Plugin::url_param_encode(const std::string &param) const {
std::ostringstream result;
result.fill('0');
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index 6cc4ac6..4ee3933 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -29,7 +29,7 @@ namespace QuickMedia {
const char *title = quickmedia_html_node_get_attribute_value(node, "title");
// Checking for watch?v helps skipping ads
if(href && title && begins_with(href, "/watch?v=")) {
- auto item = std::make_unique<BodyItem>(title);
+ auto item = std::make_unique<BodyItem>(strip(title));
item->url = std::string("https://www.youtube.com") + href;
result_items->push_back(std::move(item));
}