#include "../include/Body.hpp" #include "../include/QuickMedia.hpp" #include "../include/Scale.hpp" #include "../include/FontLoader.hpp" #include "../plugins/Plugin.hpp" #include #include #include #include static const sf::Color front_color(32, 36, 42); static const sf::Color back_color(33, 35, 37); static const float spacing_y = 15.0f; static const float padding_x = 10.0f; static const float image_padding_x = 5.0f; static const float padding_y = 5.0f; static const float embedded_item_padding_y = 0.0f; static const double thumbnail_fade_duration_sec = 0.1; namespace QuickMedia { BodyItem::BodyItem(std::string _title) : visible(true), dirty(false), dirty_description(false), dirty_author(false), dirty_timestamp(false), thumbnail_is_local(false), userdata(nullptr), last_drawn_time(0.0), timestamp(0), title_color(sf::Color::White), author_color(sf::Color::White), description_color(sf::Color::White) { if(!_title.empty()) set_title(std::move(_title)); } BodyItem& BodyItem::operator=(BodyItem &other) { url = other.url; thumbnail_url = other.thumbnail_url; attached_content_url = other.attached_content_url; visible = other.visible; dirty = other.dirty; dirty_description = other.dirty_description; dirty_author = other.dirty_author; dirty_timestamp = other.dirty_timestamp; 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; if(other.author_text) author_text = std::make_unique(*other.author_text); else author_text = nullptr; if(other.timestamp_text) timestamp_text = std::make_unique(*other.timestamp_text); else timestamp_text = nullptr; replies = other.replies; post_number = other.post_number; userdata = other.userdata; last_drawn_time = other.last_drawn_time; embedded_item_status = other.embedded_item_status; embedded_item = other.embedded_item; thumbnail_mask_type = other.thumbnail_mask_type; thumbnail_size = other.thumbnail_size; title = other.title; description = other.description; author = other.author; timestamp = other.timestamp; title_color = other.title_color; author_color = other.author_color; description_color = other.description_color; return *this; } Body::Body(Program *program, sf::Texture &loading_icon_texture) : progress_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), 14), replies_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), 14), embedded_item_load_text("", *FontLoader::get_font(FontLoader::FontType::LATIN), 14), draw_thumbnails(true), wrap_around(false), line_separator_color(sf::Color(32, 37, 43, 255)), body_item_render_callback(nullptr), thumbnail_mask_shader(nullptr), program(program), selected_item(0), prev_selected_item(0), page_scroll(0.0f), item_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10), loading_icon(loading_icon_texture), num_visible_items(0), last_item_fully_visible(true), last_fully_visible_item(-1) { progress_text.setFillColor(sf::Color::White); replies_text.setFillColor(sf::Color(129, 162, 190)); thumbnail_max_size.x = 250; thumbnail_max_size.y = 141; item_background.setFillColor(sf::Color(55, 60, 68)); sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); } // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_previous_page() { for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_previous_item()) return false; } return true; } // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_next_page() { for(int i = 0; i < num_visible_items - 1; ++i) { if(!select_next_item()) return false; } return true; } 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, bool reset_prev_selected_item) { //assert(item >= 0 && item < (int)items.size()); selected_item = item; if(reset_prev_selected_item) prev_selected_item = selected_item; clamp_selection(); //page_scroll = 0.0f; } int Body::get_index_by_body_item(BodyItem *body_item) { for(int i = 0; i < (int)items.size(); ++i) { if(items[i].get() == body_item) return i; } return -1; } void Body::select_first_item() { selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; clamp_selection(); } void Body::select_last_item() { selected_item = std::max(0, (int)items.size() - 1); //prev_selected_item = selected_item; //page_scroll = 0.0f; clamp_selection(); } void Body::clear_items() { items.clear(); selected_item = 0; prev_selected_item = selected_item; page_scroll = 0.0f; } void Body::prepend_items(BodyItems new_items) { items.insert(items.begin(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); } void Body::append_items(BodyItems new_items) { items.insert(items.end(), std::make_move_iterator(new_items.begin()), std::make_move_iterator(new_items.end())); } // TODO: Binary search and use hint to start search from start or end (for example when adding "previous" items or "next" items) void Body::insert_item_by_timestamp(std::shared_ptr body_item) { for(size_t i = 0; i < items.size(); ++i) { if(body_item->get_timestamp() < items[i]->get_timestamp()) { items.insert(items.begin() + i, std::move(body_item)); return; } } items.push_back(std::move(body_item)); } // TODO: Optimize by resizing |items| before insert void Body::insert_items_by_timestamps(BodyItems new_items) { for(auto &new_item : new_items) { insert_item_by_timestamp(std::move(new_item)); } } void Body::clear_cache() { clear_text_cache(); clear_thumbnails(); } void Body::clear_text_cache() { for(auto &body_item : items) { clear_body_item_cache(body_item.get()); if(body_item->embedded_item) clear_body_item_cache(body_item->embedded_item.get()); } } 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(); } std::shared_ptr Body::get_selected_shared() { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return nullptr; return items[selected_item]; } BodyItem* Body::get_last_fully_visible_item() { if(last_fully_visible_item < 0 || last_fully_visible_item >= (int)items.size() || !items[last_fully_visible_item]->visible) return nullptr; return items[last_fully_visible_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 + 1; i < num_items; ++i) { if(items[i]->visible) { selected_item = i; return; } } } void Body::draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) { draw(window, pos, size, Json::Value::nullSingleton()); } // 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. 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; const float start_y = pos.y; elapsed_time_sec = draw_timer.getElapsedTime().asSeconds(); //item_background.setFillColor(front_color); //item_background.setOutlineThickness(1.0f); //item_background.setOutlineColor(sf::Color(13, 15, 17)); item_separator.setFillColor(line_separator_color); num_visible_items = 0; last_item_fully_visible = true; last_fully_visible_item = -1; int num_items = items.size(); if(num_items == 0 || size.y <= 0.0f) { item_thumbnail_textures.clear(); for(auto &body_item : items) { clear_body_item_cache(body_item.get()); if(body_item->embedded_item) clear_body_item_cache(body_item->embedded_item.get()); } return; } for(auto &thumbnail_it : item_thumbnail_textures) { thumbnail_it.second->referenced = false; } if(prev_selected_item < 0 || prev_selected_item >= (int)items.size()) { prev_selected_item = selected_item; } // TODO: Optimize this, especially when scrolling to top/bottom. // TODO: If the thumbnail fallback size is larger than the real thumbnail size then the scroll will be incorrect when scrolling down // because first we scroll by a larger value and then the thumbnail size changes when the thumbnail has finished loading // and the selected item will no longer be exactly at the bottom of the window, and we will see parts of then next item. // To fix this, we should detect when the selected item is at the bottom of the screen and the thumbnail changes from image fallback to the real thumbnail // and add the difference in size to scroll, if the real thumbnail size is smaller than the image fallback. int selected_item_diff = selected_item - prev_selected_item; int selected_int_diff_abs = std::abs(selected_item_diff); if(selected_item_diff > 0) { int num_items_scrolled = 0; int i = prev_selected_item; while(num_items_scrolled < selected_int_diff_abs && i < num_items) { if(items[i]->visible) { page_scroll += (get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50) + spacing_y); } ++num_items_scrolled; ++i; } prev_selected_item = selected_item; } else if(selected_item_diff < 0) { int num_items_scrolled = 0; int i = prev_selected_item - 1; while(num_items_scrolled < selected_int_diff_abs && i >= 0) { if(items[i]->visible) { page_scroll -= (get_item_height(items[i].get(), size.x, selected_int_diff_abs < 50) + spacing_y); } ++num_items_scrolled; --i; } prev_selected_item = selected_item; } float selected_item_height = get_item_height(items[selected_item].get(), size.x) + spacing_y; if(page_scroll > size.y - selected_item_height) { page_scroll = size.y - selected_item_height; } else if(page_scroll < 0.0f) { page_scroll = 0.0f; } pos.y += page_scroll; sf::Vector2u window_size = window.getSize(); sf::Vector2f prev_pos = pos; for(int i = selected_item - 1; i >= 0; --i) { auto &item = items[i]; // TODO: Find a better solution? if(!item->visible) continue; item->last_drawn_time = elapsed_time_sec; float item_height = get_item_height(item.get(), size.x); prev_pos.y -= (item_height + spacing_y); if(prev_pos.y + item_height + spacing_y <= start_y) break; // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason 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); draw_item(window, item.get(), prev_pos, size, item_height, i, content_progress); glDisable(GL_SCISSOR_TEST); ++num_visible_items; } sf::Vector2f after_pos = pos; for(int i = selected_item; i < num_items; ++i) { auto &item = items[i]; // TODO: Find a better solution? if(!item->visible) continue; if(after_pos.y - start_y >= size.y) { last_item_fully_visible = false; break; } item->last_drawn_time = elapsed_time_sec; float item_height = get_item_height(item.get(), size.x); // This is needed here rather than above the loop, since update_dirty_text cant be called inside scissor because it corrupts the text for some reason 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); draw_item(window, item.get(), after_pos, size, item_height, i, content_progress); glDisable(GL_SCISSOR_TEST); after_pos.y += item_height + spacing_y; ++num_visible_items; if(after_pos.y - start_y > size.y) last_item_fully_visible = false; else last_fully_visible_item = i; } if(last_fully_visible_item == -1) last_fully_visible_item = selected_item; for(auto it = item_thumbnail_textures.begin(); it != item_thumbnail_textures.end();) { if(!it->second->referenced) it = item_thumbnail_textures.erase(it); else ++it; } // TODO: Only do this for items that are not visible, do not loop all items. // TODO: Improve performance! right now it can use up to 5-7% cpu with a lot of items! for(auto &body_item : items) { if(elapsed_time_sec - body_item->last_drawn_time >= 1.5) clear_body_item_cache(body_item.get()); // The embedded item might or might not refer to another item in |items|, so we have to make sure we also check it if(body_item->embedded_item && elapsed_time_sec - body_item->embedded_item->last_drawn_time >= 1.5) clear_body_item_cache(body_item->embedded_item.get()); } } void Body::update_dirty_state(BodyItem *body_item, float width) { if(body_item->dirty) { body_item->dirty = false; // TODO: Find a way to optimize fromUtf8 sf::String str = sf::String::fromUtf8(body_item->get_title().data(), body_item->get_title().data() + body_item->get_title().size()); if(body_item->title_text) body_item->title_text->setString(std::move(str)); else body_item->title_text = std::make_unique(std::move(str), false, 16, width - 50 - image_padding_x * 2.0f); body_item->title_text->setFillColor(body_item->get_title_color()); body_item->title_text->updateGeometry(); } if(body_item->dirty_description) { body_item->dirty_description = false; sf::String str = sf::String::fromUtf8(body_item->get_description().data(), body_item->get_description().data() + body_item->get_description().size()); if(body_item->description_text) body_item->description_text->setString(std::move(str)); else body_item->description_text = std::make_unique(std::move(str), false, 14, width - 50 - image_padding_x * 2.0f); body_item->description_text->setFillColor(body_item->get_description_color()); body_item->description_text->updateGeometry(); } if(body_item->dirty_author) { body_item->dirty_author = false; sf::String str = sf::String::fromUtf8(body_item->get_author().data(), body_item->get_author().data() + body_item->get_author().size()); if(body_item->author_text) body_item->author_text->setString(std::move(str)); else body_item->author_text = std::make_unique(std::move(str), true, 14, width - 50 - image_padding_x * 2.0f); body_item->author_text->setFillColor(body_item->get_author_color()); body_item->author_text->updateGeometry(); } if(body_item->dirty_timestamp) { body_item->dirty_timestamp = false; //time_t time_now = time(NULL); //struct tm *now_tm = localtime(&time_now); time_t message_timestamp = body_item->get_timestamp() / 1000; struct tm *message_tm = localtime(&message_timestamp); //bool is_same_year = message_tm->tm_year == now_tm->tm_year; char time_str[128] = {0}; /* if(is_same_year) strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S", message_tm); else strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M:%S %Y", message_tm); */ strftime(time_str, sizeof(time_str) - 1, "%a %b %d %H:%M", message_tm); if(body_item->timestamp_text) body_item->timestamp_text->setString(time_str); else body_item->timestamp_text = std::make_unique(time_str, *FontLoader::get_font(FontLoader::FontType::LATIN), 10); body_item->timestamp_text->setFillColor(sf::Color(185, 190, 198, 100)); } } void Body::clear_body_item_cache(BodyItem *body_item) { if(body_item->title_text) { body_item->title_text.reset(); body_item->dirty = true; } if(body_item->description_text) { body_item->description_text.reset(); body_item->dirty_description = true; } if(body_item->author_text) { body_item->author_text.reset(); body_item->dirty_author = true; } if(body_item->timestamp_text) { body_item->timestamp_text.reset(); body_item->dirty_timestamp = true; } } sf::Vector2i Body::get_item_thumbnail_size(BodyItem *item) const { sf::Vector2i content_size; if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = clamp_to_size(item->thumbnail_size, thumbnail_max_size); else content_size = thumbnail_max_size; return content_size; } static sf::Vector2f to_vec2f(const sf::Vector2i &vec) { return sf::Vector2f(vec.x, vec.y); } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, sf::Vector2f pos, sf::Vector2f size, bool include_embedded_item) { update_dirty_state(item, size.x); item->last_drawn_time = draw_timer.getElapsedTime().asMilliseconds(); sf::Vector2u window_size = window.getSize(); glEnable(GL_SCISSOR_TEST); glScissor(pos.x, (int)window_size.y - (int)pos.y - (int)size.y, size.x, size.y); draw_item(window, item, pos, size, size.y + spacing_y, -1, Json::Value::nullSingleton(), include_embedded_item); glDisable(GL_SCISSOR_TEST); } // TODO: Better message? maybe fallback to the reply message, or message status (such as message redacted) static const char* embedded_item_status_to_string(FetchStatus embedded_item_status) { switch(embedded_item_status) { case FetchStatus::NONE: return ""; case FetchStatus::LOADING: return "Loading message..."; case FetchStatus::FINISHED_LOADING: return "Finished loading message..."; case FetchStatus::FAILED_TO_LOAD: return "Failed to load message!"; } return ""; } void Body::draw_item(sf::RenderWindow &window, BodyItem *item, const sf::Vector2f &pos, const sf::Vector2f &size, const float item_height, const int item_index, const Json::Value &content_progress, bool include_embedded_item) { // TODO: Instead of generating a new hash everytime to access textures, cache the hash of the thumbnail url std::shared_ptr item_thumbnail; if(draw_thumbnails && !item->thumbnail_url.empty()) { auto item_thumbnail_it = item_thumbnail_textures.find(item->thumbnail_url); if(item_thumbnail_it == item_thumbnail_textures.end()) { item_thumbnail = std::make_shared(); item_thumbnail_textures.insert(std::make_pair(item->thumbnail_url, item_thumbnail)); } else { item_thumbnail = item_thumbnail_it->second; } item_thumbnail->referenced = true; } if(body_item_render_callback) body_item_render_callback(item); sf::Vector2f item_pos; item_pos.x = std::floor(pos.x); item_pos.y = std::floor(pos.y); item_separator.setSize(sf::Vector2f(std::max(0.0f, size.x - 20.0f), 1.0f)); item_separator.setPosition(item_pos + sf::Vector2f(10.0f, std::floor(item_height + spacing_y * 0.5f))); window.draw(item_separator); if(item_index == selected_item) { 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 && item_thumbnail) { double elapsed_time_thumbnail = 0.0; if(item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE) elapsed_time_thumbnail = item_thumbnail->texture_applied_time.getElapsedTime().asSeconds(); //thumbnail_fade_duration_sec bool only_show_thumbnail = false; double thumbnail_fade_progress = elapsed_time_thumbnail / thumbnail_fade_duration_sec; if(thumbnail_fade_progress > 1.0) { thumbnail_fade_progress = 1.0; only_show_thumbnail = true; } bool has_thumbnail_texture = false; // TODO: Verify if this is safe. The thumbnail is being modified in another thread if(item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) { image.setTexture(item_thumbnail->texture, true); auto image_size = image.getTexture()->getSize(); sf::Vector2f image_size_f(image_size.x, image_size.y); sf::Vector2f content_size = to_vec2f(get_item_thumbnail_size(item)); auto new_image_size = clamp_to_size(image_size_f, content_size); auto image_scale = get_ratio(image_size_f, new_image_size); image.setColor(sf::Color(255, 255, 255, thumbnail_fade_progress * 255)); image.setScale(image_scale); image.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) window.draw(image, thumbnail_mask_shader); else window.draw(image); text_offset_x += image_padding_x + new_image_size.x; has_thumbnail_texture = true; // We want the next image fallback to have the same size as the successful image rendering, because its likely the image fallback will have the same size (for example thumbnails on youtube) //image_fallback.setSize(sf::Vector2f(width_ratio * image_size.x, height_ratio * image_size.y)); } else if(!item->thumbnail_url.empty() && !only_show_thumbnail) { sf::Vector2f content_size = to_vec2f(get_item_thumbnail_size(item)); sf::Uint8 fallback_fade_alpha = (1.0 - thumbnail_fade_progress) * 255; sf::Color fallback_color(52, 58, 70, fallback_fade_alpha); if(thumbnail_mask_shader && item->thumbnail_mask_type == ThumbnailMaskType::CIRCLE) { // TODO: Use the mask shader instead, but a vertex shader is also needed for that to pass the vertex coordinates since // shapes dont have texture coordinates. // TODO: Cache circle shape sf::CircleShape circle_shape(content_size.x * 0.5f); circle_shape.setFillColor(fallback_color); circle_shape.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(circle_shape); } else { image_fallback.setSize(content_size); image_fallback.setFillColor(fallback_color); image_fallback.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y)); window.draw(image_fallback); } sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); auto new_loading_icon_size = clamp_to_size(loading_icon_size, content_size); loading_icon.setPosition(item_pos + sf::Vector2f(image_padding_x, padding_y) + (content_size * 0.5f)); loading_icon.setScale(get_ratio(loading_icon_size, new_loading_icon_size)); loading_icon.setRotation(elapsed_time_sec * 400.0); loading_icon.setColor(sf::Color(255, 255, 255, fallback_fade_alpha)); window.draw(loading_icon); if(!has_thumbnail_texture) text_offset_x += image_padding_x + content_size.x; } } const float timestamp_text_y = std::floor(item_pos.y + padding_y - 6.0f); if(item->author_text) { item->author_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f)); item->author_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->author_text->draw(window); sf::Vector2f replies_text_pos = item->author_text->getPosition() + sf::Vector2f(0.0f, 5.0f); replies_text_pos.x += item->author_text->getWidth() + 5.0f; replies_text.setPosition(replies_text_pos); sf::String replies_text_str; for(size_t reply_index : item->replies) { BodyItem *reply_item = items[reply_index].get(); replies_text_str += " >>"; replies_text_str += reply_item->post_number; } replies_text.setString(std::move(replies_text_str)); window.draw(replies_text); item_pos.y += item->author_text->getHeight() - 2.0f; } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { const float border_width = 4.0f; const float embedded_item_width = std::floor(size.x - text_offset_x - border_width - padding_x); float embedded_item_height = item->embedded_item ? get_item_height(item->embedded_item.get(), embedded_item_width, true, false) : (embedded_item_load_text.getLocalBounds().height + embedded_item_padding_y * 2.0f); sf::RectangleShape border_left(sf::Vector2f(border_width, std::floor(embedded_item_height))); border_left.setFillColor(sf::Color::White); border_left.setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + 4.0f)); window.draw(border_left); if(item->embedded_item) { sf::Vector2f embedded_item_pos(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_padding_y + 4.0f)); sf::Vector2f embedded_item_size(embedded_item_width, embedded_item_height); draw_item(window, item->embedded_item.get(), embedded_item_pos, embedded_item_size, false); } else { embedded_item_load_text.setString(embedded_item_status_to_string(item->embedded_item_status)); embedded_item_load_text.setPosition(std::floor(item_pos.x + text_offset_x + border_width + padding_x), std::floor(item_pos.y + embedded_item_height * 0.5f - embedded_item_load_text.getLocalBounds().height * 0.5f + 4.0f)); window.draw(embedded_item_load_text); } item_pos.y += embedded_item_height + 4.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->title_text) { item->title_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f)); item->title_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->title_text->draw(window); item_pos.y += item->title_text->getHeight() - 2.0f; } if(item->description_text) { float height_offset = 0.0f; item->description_text->setPosition(std::floor(item_pos.x + text_offset_x), std::floor(item_pos.y + padding_y - 6.0f + height_offset)); item->description_text->setMaxWidth(size.x - text_offset_x - image_padding_x); item->description_text->draw(window); item_pos.y += item->description_text->getHeight() - 2.0f; } if(item->timestamp_text) { item->timestamp_text->setPosition(std::floor(item_pos.x + size.x - item->timestamp_text->getLocalBounds().width - padding_x), timestamp_text_y + 8.0f); window.draw(*item->timestamp_text); } if(!content_progress.isObject()) return; // 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), timestamp_text_y + 6.0f); window.draw(progress_text); } } } float Body::get_item_height(BodyItem *item, float width, bool load_texture, bool include_embedded_item) { if(load_texture) update_dirty_state(item, width); float item_height = 0.0f; if(item->title_text) { item_height += item->title_text->getHeight() - 2.0f; } if(item->author_text) { item_height += item->author_text->getHeight() - 2.0f; } if(include_embedded_item && item->embedded_item_status != FetchStatus::NONE) { if(item->embedded_item) item_height += (get_item_height(item->embedded_item.get(), width, load_texture, false) + 4.0f + embedded_item_padding_y * 2.0f); else item_height += (embedded_item_load_text.getLocalBounds().height + 4.0f + embedded_item_padding_y * 2.0f); } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; } if(draw_thumbnails && !item->thumbnail_url.empty()) { sf::Vector2i content_size = get_item_thumbnail_size(item); float image_height = content_size.y; std::shared_ptr item_thumbnail; auto item_thumbnail_it = item_thumbnail_textures.find(item->thumbnail_url); if(item_thumbnail_it == item_thumbnail_textures.end()) { if(load_texture) { item_thumbnail = std::make_shared(); item_thumbnail_textures.insert(std::make_pair(item->thumbnail_url, item_thumbnail)); } } else { item_thumbnail = item_thumbnail_it->second; } if(load_texture && item_thumbnail) { item_thumbnail->referenced = true; if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED) async_image_loader.load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, content_size, program->is_tor_enabled(), item_thumbnail); if(item_thumbnail->loading_state == LoadingState::FINISHED_LOADING && item_thumbnail->image->getSize().x > 0 && item_thumbnail->image->getSize().y > 0) { if(!item_thumbnail->texture.loadFromImage(*item_thumbnail->image)) fprintf(stderr, "Warning: failed to load texture from image: %s\n", item->thumbnail_url.c_str()); item_thumbnail->texture.setSmooth(true); //item_thumbnail->texture.generateMipmap(); item_thumbnail->image.reset(); item_thumbnail->loading_state = LoadingState::APPLIED_TO_TEXTURE; item_thumbnail->texture_applied_time.restart(); } } if(item_thumbnail && item_thumbnail->loading_state == LoadingState::APPLIED_TO_TEXTURE && item_thumbnail->texture.getNativeHandle() != 0) { auto image_size = item_thumbnail->texture.getSize(); sf::Vector2f image_size_f(image_size.x, image_size.y); auto new_image_size = clamp_to_size(image_size_f, to_vec2f(content_size)); image_height = new_image_size.y; } item_height = std::max(item_height, image_height); } return item_height + padding_y * 2.0f; } float Body::get_spacing_y() const { return spacing_y; } // TODO: Support utf-8 case insensitive find //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) { filter_search_fuzzy_item(text, item.get()); } select_first_item(); } void Body::filter_search_fuzzy_item(const std::string &text, BodyItem *body_item) { body_item->visible = string_find_case_insensitive(body_item->get_title(), text); if(!body_item->visible && !body_item->get_description().empty()) body_item->visible = string_find_case_insensitive(body_item->get_description(), text); if(!body_item->visible && !body_item->get_author().empty()) body_item->visible = string_find_case_insensitive(body_item->get_author(), text); } bool Body::no_items_visible() const { for(auto &item : items) { if(item->visible) return false; } return true; } bool Body::is_selected_item_last_visible_item() const { if(selected_item < 0 || selected_item >= (int)items.size() || !items[selected_item]->visible) return false; for(int i = selected_item + 1; i < (int)items.size(); ++i) { if(items[i]->visible) return false; } return true; } }