#include "../include/Body.hpp" #include "../include/QuickMedia.hpp" #include "../include/Scale.hpp" #include "../include/base64_url.hpp" #include "../include/ImageUtils.hpp" #include "../plugins/Plugin.hpp" #include #include #include #include #include const sf::Color front_color(43, 45, 47); const sf::Color back_color(33, 35, 37); namespace QuickMedia { BodyItem::BodyItem(std::string _title): visible(true), dirty(false), dirty_description(false), thumbnail_is_local(false), background_color(front_color) { set_title(std::move(_title)); } BodyItem::BodyItem(const BodyItem &other) { title = other.title; description = other.description; url = other.url; thumbnail_url = other.thumbnail_url; attached_content_url = other.attached_content_url; author = other.author; visible = other.visible; dirty = other.dirty; dirty_description = other.dirty_description; thumbnail_is_local = other.thumbnail_is_local; if(other.title_text) title_text = std::make_unique(*other.title_text); else title_text = nullptr; if(other.description_text) description_text = std::make_unique(*other.description_text); else description_text = nullptr; replies = other.replies; post_number = other.post_number; } Body::Body(Program *program, sf::Font *font, sf::Font *bold_font) : font(font), bold_font(bold_font), progress_text("", *font, 14), author_text("", *bold_font, 16), replies_text("", *font, 14), draw_thumbnails(false), wrap_around(false), program(program), loading_thumbnail(false), selected_item(0) { progress_text.setFillColor(sf::Color::White); author_text.setFillColor(sf::Color::White); replies_text.setFillColor(sf::Color(129, 162, 190)); thumbnail_resize_target_size.x = 200; thumbnail_resize_target_size.y = 119; thumbnail_fallback_size.x = 50.0f; thumbnail_fallback_size.y = 100.0f; } bool Body::select_previous_item() { if(items.empty()) return false; int new_selected_item = selected_item; int num_items = (int)items.size(); for(int i = 0; i < num_items; ++i) { if(new_selected_item - 1 < 0) { if(wrap_around) new_selected_item = num_items - 1; else { new_selected_item = selected_item; break; } } else { --new_selected_item; } if(items[new_selected_item]->visible) break; } if(selected_item == new_selected_item) return false; selected_item = new_selected_item; return true; } bool Body::select_next_item() { if(items.empty()) return false; int new_selected_item = selected_item; int num_items = (int)items.size(); for(int i = 0; i < num_items; ++i) { if(new_selected_item + 1 == num_items) { if(wrap_around) { new_selected_item = 0; } else { new_selected_item = selected_item; break; } } else { ++new_selected_item; } if(items[new_selected_item]->visible) break; } if(selected_item == new_selected_item) return false; selected_item = new_selected_item; return true; } void Body::set_selected_item(int item) { assert(item >= 0 && item < (int)items.size()); selected_item = item; } void Body::select_first_item() { selected_item = 0; clamp_selection(); } void Body::reset_selected() { for(size_t i = 0; i < items.size(); ++i) { if(items[i]->visible) { selected_item = i; return; } } selected_item = 0; } void Body::clear_items() { items.clear(); selected_item = 0; } void Body::append_items(BodyItems new_items) { for(auto &body_item : new_items) { items.push_back(std::move(body_item)); } } void Body::clear_thumbnails() { item_thumbnail_textures.clear(); } BodyItem* Body::get_selected() const { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return nullptr; return items[selected_item].get(); } void Body::clamp_selection() { int num_items = (int)items.size(); if(items.empty()) return; if(selected_item < 0) selected_item = 0; else if(selected_item >= num_items) selected_item = num_items - 1; for(int i = selected_item; i >= 0; --i) { if(items[i]->visible) { selected_item = i; return; } } for(int i = selected_item; i < num_items; ++i) { if(items[i]->visible) { selected_item = i; return; } } } static sf::Vector2f to_vec2f(const sf::Vector2u &vec) { return sf::Vector2f(vec.x, vec.y); } static sf::Vector2f to_vec2f(const sf::Vector2i &vec) { return sf::Vector2f(vec.x, vec.y); } static sf::Vector2u to_vec2u(const sf::Vector2f &vec) { return sf::Vector2u(vec.x, vec.y); } static void copy_resize(const sf::Image &source, sf::Image &destination, sf::Vector2u destination_size) { const sf::Vector2u source_size = source.getSize(); if(source_size.x == 0 || source_size.y == 0 || destination_size.x == 0 || destination_size.y == 0) return; //float width_ratio = (float)source_size.x / (float)destination_size.x; //float height_ratio = (float)source_size.y / (float)destination_size.y; const sf::Uint8 *source_pixels = source.getPixelsPtr(); // TODO: Remove this somehow. Right now we need to allocate this and also allocate the same array in the destination image sf::Uint32 *destination_pixels = new sf::Uint32[destination_size.x * destination_size.y]; sf::Uint32 *destination_pixel = destination_pixels; for(unsigned int y = 0; y < destination_size.y; ++y) { for(unsigned int x = 0; x < destination_size.x; ++x) { int scaled_x = ((float)x / (float)destination_size.x) * source_size.x; int scaled_y = ((float)y / (float)destination_size.y) * source_size.y; //float scaled_x = x * width_ratio; //float scaled_y = y * height_ratio; //sf::Uint32 *source_pixel = (sf::Uint32*)(source_pixels + (int)(scaled_x + scaled_y * source_size.x) * 4); sf::Uint32 *source_pixel = (sf::Uint32*)(source_pixels + (scaled_x + scaled_y * source_size.x) * 4); *destination_pixel = *source_pixel; ++destination_pixel; } } destination.create(destination_size.x, destination_size.y, (sf::Uint8*)destination_pixels); delete []destination_pixels; } static bool save_image_as_thumbnail_atomic(const sf::Image &image, const Path &thumbnail_path, const char *ext) { Path tmp_path = thumbnail_path; tmp_path.append(".tmp"); const char *thumbnail_path_ext = thumbnail_path.ext(); if(is_image_ext(ext)) tmp_path.append(ext); else if(is_image_ext(thumbnail_path_ext)) tmp_path.append(thumbnail_path_ext); else tmp_path.append(".png"); return image.saveToFile(tmp_path.data) && (rename(tmp_path.data.c_str(), thumbnail_path.data.c_str()) == 0); } // Returns empty string if no extension static const char* get_ext(const std::string &path) { size_t index = path.rfind('.'); if(index == std::string::npos) return ""; return path.c_str() + index; } // TODO: Do not load thumbnails for images larger than 30mb std::shared_ptr Body::load_thumbnail_from_url(const std::string &url, bool local, sf::Vector2i thumbnail_resize_target_size) { auto result = std::make_shared(); result->setSmooth(true); assert(!loading_thumbnail); loading_thumbnail = true; thumbnail_load_thread = std::thread([this, result, url, local, thumbnail_resize_target_size]() { // TODO: Use sha256 instead of base64_url encoding Path thumbnail_path = get_cache_dir().join("thumbnails").join(base64_url::encode(url)); std::string texture_data; if(file_get_content(thumbnail_path, texture_data) == 0) { fprintf(stderr, "Loaded %s from thumbnail cache\n", url.c_str()); result->loadFromMemory(texture_data.data(), texture_data.size()); loading_thumbnail = false; return; } else { if(local) { if(file_get_content(url, texture_data) != 0) { loading_thumbnail = false; return; } } else { if(download_to_string_cache(url, texture_data, {}, program->get_current_plugin()->use_tor, true) != DownloadResult::OK) { loading_thumbnail = false; return; } } } if(thumbnail_resize_target_size.x != 0 && thumbnail_resize_target_size.y != 0) { auto image = std::make_unique(); // TODO: Load from file instead? decreases ram usage and we save to file above anyways if(image->loadFromMemory(texture_data.data(), texture_data.size())) { texture_data.resize(0); sf::Vector2u new_image_size = to_vec2u(clamp_to_size(to_vec2f(image->getSize()), to_vec2f(thumbnail_resize_target_size))); if(new_image_size.x < image->getSize().x || new_image_size.y < image->getSize().y) { sf::Image destination_image; copy_resize(*image, destination_image, new_image_size); if(save_image_as_thumbnail_atomic(destination_image, thumbnail_path, get_ext(url))) { image.reset(); result->loadFromImage(destination_image); } else { result->loadFromImage(*image); } loading_thumbnail = false; return; } else { result->loadFromImage(*image); loading_thumbnail = false; return; } } } result->loadFromMemory(texture_data.data(), texture_data.size()); loading_thumbnail = false; }); thumbnail_load_thread.detach(); 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: Use a render target for the whole body so all images can be put into one. // TODO: Load thumbnails with more than one thread. // TODO: Show chapters (rows) that have been read differently to make it easier to see what hasn't been read yet. void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size, const Json::Value &content_progress) { sf::Vector2f scissor_pos = pos; sf::Vector2f scissor_size = size; size.x = std::max(0.0f, size.x - 5); float image_max_height = 100.0f; const float spacing_y = 15.0f; const float padding_x = 10.0f; const float image_padding_x = 5.0f; const float padding_y = 5.0f; const float start_y = pos.y; sf::RectangleShape image_fallback(thumbnail_fallback_size); image_fallback.setFillColor(sf::Color::White); if(thumbnail_resize_target_size.x != 0 && thumbnail_resize_target_size.y != 0) { image_max_height = thumbnail_resize_target_size.y; } sf::Sprite image; sf::RectangleShape item_background; item_background.setFillColor(front_color); //item_background.setOutlineThickness(1.0f); //item_background.setOutlineColor(sf::Color(13, 15, 17)); sf::RectangleShape item_background_shadow; item_background_shadow.setFillColor(sf::Color(23, 25, 27)); sf::RectangleShape selected_border; selected_border.setFillColor(sf::Color(0, 85, 119)); int num_items = items.size(); if(num_items == 0) return; for(auto &thumbnail_it : item_thumbnail_textures) { thumbnail_it.second.referenced = false; } // TODO: Change font size. Currently it doesn't work because it glitches out. Why does that happen?? for(auto &body_item : items) { if(body_item->dirty) { body_item->dirty = false; if(body_item->title_text) body_item->title_text->setString(body_item->get_title()); else body_item->title_text = std::make_unique(body_item->get_title(), font, 16, size.x - 50 - image_padding_x * 2.0f); body_item->title_text->updateGeometry(); } if(body_item->dirty_description) { body_item->dirty_description = true; if(body_item->description_text) body_item->description_text->setString(body_item->get_description()); else body_item->description_text = std::make_unique(body_item->get_description(), font, 14, size.x - 50 - image_padding_x * 2.0f); body_item->description_text->updateGeometry(); } } // Find the starting row that can be drawn to make selected row visible as well int first_visible_item = selected_item; assert(first_visible_item >= 0 && first_visible_item < (int)items.size()); float visible_height = 0.0f; for(; first_visible_item >= 0; --first_visible_item) { auto &item = items[first_visible_item]; if(item->visible) { float item_height = 0.0f; if(!item->get_title().empty()) { item_height += item->title_text->getHeight(); } if(!item->author.empty()) { item_height += author_text.getCharacterSize() + 2.0f; } if(item->description_text) { item_height += item->description_text->getHeight(); } if(draw_thumbnails && !item->thumbnail_url.empty()) { auto &item_thumbnail = item_thumbnail_textures[item->thumbnail_url]; item_thumbnail.referenced = false; float image_height = image_fallback.getSize().y; if(item_thumbnail.texture && item_thumbnail.texture->getNativeHandle() != 0) { auto image_size = item_thumbnail.texture->getSize(); image_height = std::min(image_max_height, (float)image_size.y); } item_height = std::max(item_height, image_height); } item_height += spacing_y + padding_y * 2.0f; visible_height += item_height; if(visible_height >= size.y) { --first_visible_item; //pos.y += (size.y - (visible_height - item_height)); pos.y -= (visible_height - size.y); break; } } } sf::Vector2u window_size = window.getSize(); glEnable(GL_SCISSOR_TEST); glScissor(scissor_pos.x, (int)window_size.y - (int)scissor_pos.y - (int)scissor_size.y, scissor_size.x, scissor_size.y); for(int i = first_visible_item + 1; i < num_items; ++i) { const auto &item = items[i]; if(pos.y >= start_y + size.y) break; if(!item->visible) continue; // TODO: Instead of generating a new hash everytime to access textures, cache the hash of the thumbnail url // Intentionally create the item with the key item->thumbnail_url if it doesn't exist item_thumbnail_textures[item->thumbnail_url].referenced = true; auto &item_thumbnail = item_thumbnail_textures[item->thumbnail_url]; float item_height = 0.0f; if(!item->get_title().empty()) { item_height += item->title_text->getHeight(); } if(!item->author.empty()) { item_height += author_text.getCharacterSize() + 2.0f; } if(item->description_text) { item_height += item->description_text->getHeight(); } if(draw_thumbnails && !item->thumbnail_url.empty()) { float image_height = image_fallback.getSize().y; if(item_thumbnail.loaded && item_thumbnail.texture && item_thumbnail.texture->getNativeHandle() != 0) { auto image_size = item_thumbnail.texture->getSize(); image_height = std::min(image_max_height, (float)image_size.y); } item_height = std::max(item_height, image_height); } item_height += (padding_y * 2.0f); if(draw_thumbnails) { if(!item->thumbnail_url.empty() && !loading_thumbnail && !item_thumbnail.loaded && !item_thumbnail.texture) { item_thumbnail.loaded = true; item_thumbnail.texture = load_thumbnail_from_url(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size); } } sf::Vector2f item_pos = pos; if(i == selected_item) { //selected_border.setPosition(pos); //selected_border.setSize(sf::Vector2f(selected_border_width, item_height)); //window.draw(selected_border); //item_pos.x += selected_border_width; item_background.setFillColor(sf::Color(0, 85, 119)); } else { item_background.setFillColor(item->background_color); } item_pos.x = std::floor(item_pos.x); item_pos.y = std::floor(item_pos.y); item_background_shadow.setPosition(item_pos + sf::Vector2f(size.x, 0.0f) + sf::Vector2f(0.0, 5.0f)); item_background_shadow.setSize(sf::Vector2f(5.0f, item_height)); window.draw(item_background_shadow); item_background_shadow.setPosition(item_pos + sf::Vector2f(0.0f, item_height) + sf::Vector2f(5.0, 0.0f)); item_background_shadow.setSize(sf::Vector2f(size.x - 5.0f, 5.0f)); window.draw(item_background_shadow); item_background.setPosition(item_pos); item_background.setSize(sf::Vector2f(size.x, item_height)); window.draw(item_background); float text_offset_x = padding_x; if(draw_thumbnails) { // TODO: Verify if this is safe. The thumbnail is being modified in another thread // and it might not be fully finished before the native handle is set? if(item_thumbnail.texture && item_thumbnail.texture->getNativeHandle() != 0) { image.setTexture(*item_thumbnail.texture, true); auto image_size = image.getTexture()->getSize(); auto height_ratio = std::min(image_max_height, (float)image_size.y) / image_size.y; auto scale = image.getScale(); auto image_scale_ratio = scale.x / scale.y; const float width_ratio = height_ratio * image_scale_ratio; image.setScale(width_ratio, height_ratio); image.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image); text_offset_x += image_padding_x + width_ratio * image_size.x; } else if(!item->thumbnail_url.empty()) { image_fallback.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image_fallback); text_offset_x += image_padding_x + image_fallback.getSize().x; } } if(!item->author.empty()) { // TODO: Remove this call, should not be called every frame author_text.setString(item->author); author_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); window.draw(author_text); sf::Vector2f replies_text_pos = author_text.getPosition(); replies_text_pos.x += author_text.getLocalBounds().width + 5.0f; for(size_t reply_index : item->replies) { BodyItem *reply_item = items[reply_index].get(); replies_text.setString(">>" + reply_item->post_number); replies_text.setPosition(replies_text_pos); window.draw(replies_text); replies_text_pos.x += replies_text.getLocalBounds().width + 5.0f; } item_pos.y += author_text.getCharacterSize() + 2.0f; } //title_text.setString(item->title); //title_text.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y)); //window.draw(title_text); if(!item->get_title().empty()) { item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 4.0f)); item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x * 2.0f); item->title_text->draw(window); } if(!item->get_description().empty()) { float height_offset = 0.0f; if(!item->get_title().empty()) { height_offset = item->title_text->getHeight(); } item->description_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 4.0f + height_offset)); item->description_text->setMaxWidth(size.x - text_offset_x - image_padding_x * 2.0f); item->description_text->draw(window); } // TODO: Do the same for non-manga content. // TODO: Cache this instead of hash access every item every frame. const Json::Value &item_progress = content_progress[item->get_title()]; if(item_progress.isObject()) { 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("Page: ") + 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 - padding_x), std::floor(item_pos.y + padding_y)); window.draw(progress_text); } } pos.y += item_height + spacing_y; } glDisable(GL_SCISSOR_TEST); for(auto it = item_thumbnail_textures.begin(); it != item_thumbnail_textures.end();) { if(!it->second.referenced) it = item_thumbnail_textures.erase(it); else ++it; } } //static bool Body::string_find_case_insensitive(const std::string &str, const std::string &substr) { auto it = std::search(str.begin(), str.end(), substr.begin(), substr.end(), [](char c1, char c2) { return std::toupper(c1) == std::toupper(c2); }); return it != str.end(); } void Body::filter_search_fuzzy(const std::string &text) { if(text.empty()) { for(auto &item : items) { item->visible = true; } return; } for(auto &item : items) { item->visible = string_find_case_insensitive(item->get_title(), text); if(!item->visible && !item->get_description().empty()) item->visible = string_find_case_insensitive(item->get_description(), text); } } bool Body::no_items_visible() const { for(auto &item : items) { if(item->visible) return false; } return true; } }