aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2019-08-06 03:12:16 +0200
committerdec05eba <dec05eba@protonmail.com>2019-08-06 03:12:40 +0200
commit58481b46a2c64fda4f506e15ee94dd97f527d552 (patch)
tree337809ed5aadece3cc6a3746aa78a24b390472ca /src
parent7ce2139650012d4c571c7e7600924853ab7032bb (diff)
Save and show progress in manga and return to last page"
Diffstat (limited to 'src')
-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
7 files changed, 262 insertions, 17 deletions
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));
}