From 58481b46a2c64fda4f506e15ee94dd97f527d552 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 6 Aug 2019 03:12:16 +0200 Subject: Save and show progress in manga and return to last page" --- src/Body.cpp | 38 +++++++++++++--- src/QuickMedia.cpp | 101 ++++++++++++++++++++++++++++++++++++++---- src/SearchBar.cpp | 2 + src/Storage.cpp | 109 ++++++++++++++++++++++++++++++++++++++++++++++ src/plugins/Manganelo.cpp | 4 +- src/plugins/Plugin.cpp | 23 ++++++++++ src/plugins/Youtube.cpp | 2 +- 7 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 src/Storage.cpp (limited to 'src') 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 ¤t_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 #include #include #include +#include +#include #include #include @@ -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_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 ¤t = 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 ¤t = 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 + +#if OS_FAMILY == OS_FAMILY_POSIX +#include +#include +#include +#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(text); + auto item = std::make_unique(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(name_str); + auto item = std::make_unique(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 ¶m) 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(title); + auto item = std::make_unique(strip(title)); item->url = std::string("https://www.youtube.com") + href; result_items->push_back(std::move(item)); } -- cgit v1.2.3