|
|
@@ -0,0 +1,623 @@
|
|
|
+#include "ReaderView.hpp"
|
|
|
+#include <tinyxml2.h>
|
|
|
+#include <glibmm/miscutils.h>
|
|
|
+#include <iostream>
|
|
|
+#include <algorithm>
|
|
|
+#include <regex>
|
|
|
+#include <sstream>
|
|
|
+#include <map>
|
|
|
+
|
|
|
+// Helper to convert path to URI for Poppler
|
|
|
+static std::string to_uri(const std::string& path) {
|
|
|
+ return Glib::filename_to_uri(path);
|
|
|
+}
|
|
|
+
|
|
|
+static std::string lower_ext(const std::string& p) {
|
|
|
+ auto dot = p.find_last_of('.');
|
|
|
+ std::string ext = (dot == std::string::npos) ? "" : p.substr(dot + 1);
|
|
|
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
|
+ return ext;
|
|
|
+}
|
|
|
+
|
|
|
+// Trim whitespace from string
|
|
|
+static std::string trim(const std::string& s) {
|
|
|
+ auto start = s.find_first_not_of(" \t\n\r");
|
|
|
+ if (start == std::string::npos) return "";
|
|
|
+ auto end = s.find_last_not_of(" \t\n\r");
|
|
|
+ return s.substr(start, end - start + 1);
|
|
|
+}
|
|
|
+
|
|
|
+// Collapse multiple whitespace into single space
|
|
|
+static std::string collapse_whitespace(const std::string& s) {
|
|
|
+ std::string result;
|
|
|
+ result.reserve(s.size());
|
|
|
+ bool in_space = false;
|
|
|
+ for (char c : s) {
|
|
|
+ if (std::isspace(static_cast<unsigned char>(c))) {
|
|
|
+ if (!in_space) {
|
|
|
+ result += ' ';
|
|
|
+ in_space = true;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ result += c;
|
|
|
+ in_space = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+ReaderView::ReaderView(DatabaseManager& db) :
|
|
|
+ Gtk::Box(Gtk::Orientation::VERTICAL),
|
|
|
+ m_db(db) {
|
|
|
+ build_ui();
|
|
|
+}
|
|
|
+
|
|
|
+ReaderView::~ReaderView() {
|
|
|
+ close();
|
|
|
+}
|
|
|
+
|
|
|
+// EPUB helper: read a file from the EPUB zip archive
|
|
|
+std::string ReaderView::epub_read_file(const std::string& path) {
|
|
|
+ if (!m_epub_zip) return "";
|
|
|
+
|
|
|
+ zip_stat_t st{};
|
|
|
+ if (zip_stat(m_epub_zip, path.c_str(), 0, &st) != 0) {
|
|
|
+ std::cerr << "ReaderView: Failed to stat " << path << " in EPUB\n";
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ zip_file* zf = zip_fopen(m_epub_zip, path.c_str(), 0);
|
|
|
+ if (!zf) {
|
|
|
+ std::cerr << "ReaderView: Failed to open " << path << " in EPUB\n";
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string content(static_cast<std::size_t>(st.size), '\0');
|
|
|
+ zip_fread(zf, content.data(), st.size);
|
|
|
+ zip_fclose(zf);
|
|
|
+ return content;
|
|
|
+}
|
|
|
+
|
|
|
+// Extract text recursively from XML element
|
|
|
+static void extract_text_recursive(tinyxml2::XMLElement* elem, std::ostringstream& out, bool& need_space) {
|
|
|
+ if (!elem) return;
|
|
|
+
|
|
|
+ const char* name = elem->Name();
|
|
|
+
|
|
|
+ // Block elements that should have line breaks
|
|
|
+ bool is_block = (strcmp(name, "p") == 0 || strcmp(name, "div") == 0 ||
|
|
|
+ strcmp(name, "h1") == 0 || strcmp(name, "h2") == 0 ||
|
|
|
+ strcmp(name, "h3") == 0 || strcmp(name, "h4") == 0 ||
|
|
|
+ strcmp(name, "h5") == 0 || strcmp(name, "h6") == 0 ||
|
|
|
+ strcmp(name, "li") == 0 || strcmp(name, "br") == 0 ||
|
|
|
+ strcmp(name, "tr") == 0 || strcmp(name, "blockquote") == 0);
|
|
|
+
|
|
|
+ if (is_block && out.tellp() > 0) {
|
|
|
+ out << "\n\n";
|
|
|
+ need_space = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Process children
|
|
|
+ for (auto* node = elem->FirstChild(); node; node = node->NextSibling()) {
|
|
|
+ if (auto* text = node->ToText()) {
|
|
|
+ std::string t = trim(text->Value());
|
|
|
+ if (!t.empty()) {
|
|
|
+ if (need_space) out << " ";
|
|
|
+ out << t;
|
|
|
+ need_space = true;
|
|
|
+ }
|
|
|
+ } else if (auto* child = node->ToElement()) {
|
|
|
+ extract_text_recursive(child, out, need_space);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (is_block) {
|
|
|
+ out << "\n";
|
|
|
+ need_space = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+std::string ReaderView::extract_text_from_html(const std::string& html) {
|
|
|
+ tinyxml2::XMLDocument doc;
|
|
|
+ // Try parsing as XML first (XHTML)
|
|
|
+ if (doc.Parse(html.c_str(), html.size()) != tinyxml2::XML_SUCCESS) {
|
|
|
+ // Fallback: strip HTML tags with regex
|
|
|
+ std::string text = std::regex_replace(html, std::regex("<[^>]*>"), " ");
|
|
|
+ text = collapse_whitespace(text);
|
|
|
+ return trim(text);
|
|
|
+ }
|
|
|
+
|
|
|
+ std::ostringstream out;
|
|
|
+ bool need_space = false;
|
|
|
+
|
|
|
+ // Find body element
|
|
|
+ auto* root = doc.RootElement();
|
|
|
+ if (!root) return "";
|
|
|
+
|
|
|
+ tinyxml2::XMLElement* body = nullptr;
|
|
|
+ if (strcmp(root->Name(), "html") == 0) {
|
|
|
+ body = root->FirstChildElement("body");
|
|
|
+ } else if (strcmp(root->Name(), "body") == 0) {
|
|
|
+ body = root;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (body) {
|
|
|
+ extract_text_recursive(body, out, need_space);
|
|
|
+ } else {
|
|
|
+ // No body, try extracting from root
|
|
|
+ extract_text_recursive(root, out, need_space);
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string result = out.str();
|
|
|
+ // Clean up excessive newlines
|
|
|
+ result = std::regex_replace(result, std::regex("\n{3,}"), "\n\n");
|
|
|
+ return trim(result);
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::parse_epub_spine() {
|
|
|
+ m_epub_chapters.clear();
|
|
|
+
|
|
|
+ // Read container.xml to find OPF
|
|
|
+ std::string container = epub_read_file("META-INF/container.xml");
|
|
|
+ if (container.empty()) {
|
|
|
+ std::cerr << "ReaderView: Cannot read container.xml\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ tinyxml2::XMLDocument containerDoc;
|
|
|
+ if (containerDoc.Parse(container.c_str(), container.size()) != tinyxml2::XML_SUCCESS) {
|
|
|
+ std::cerr << "ReaderView: Cannot parse container.xml\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto* rootfiles = containerDoc.FirstChildElement("container");
|
|
|
+ if (rootfiles) rootfiles = rootfiles->FirstChildElement("rootfiles");
|
|
|
+ auto* rootfile = rootfiles ? rootfiles->FirstChildElement("rootfile") : nullptr;
|
|
|
+ if (!rootfile) {
|
|
|
+ std::cerr << "ReaderView: No rootfile in container.xml\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* opfPath = rootfile->Attribute("full-path");
|
|
|
+ if (!opfPath) {
|
|
|
+ std::cerr << "ReaderView: No full-path in rootfile\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get root directory for relative paths
|
|
|
+ std::string opfPathStr = opfPath;
|
|
|
+ auto slash = opfPathStr.find_last_of('/');
|
|
|
+ m_epub_root_dir = (slash == std::string::npos) ? "" : opfPathStr.substr(0, slash + 1);
|
|
|
+
|
|
|
+ // Read and parse OPF
|
|
|
+ std::string opfContent = epub_read_file(opfPath);
|
|
|
+ if (opfContent.empty()) {
|
|
|
+ std::cerr << "ReaderView: Cannot read OPF file\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ tinyxml2::XMLDocument opfDoc;
|
|
|
+ if (opfDoc.Parse(opfContent.c_str(), opfContent.size()) != tinyxml2::XML_SUCCESS) {
|
|
|
+ std::cerr << "ReaderView: Cannot parse OPF file\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto* package = opfDoc.FirstChildElement("package");
|
|
|
+ if (!package) {
|
|
|
+ std::cerr << "ReaderView: No package element in OPF\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Build manifest map (id -> href)
|
|
|
+ std::map<std::string, std::string> manifest;
|
|
|
+ auto* manifestElem = package->FirstChildElement("manifest");
|
|
|
+ if (manifestElem) {
|
|
|
+ for (auto* item = manifestElem->FirstChildElement("item"); item; item = item->NextSiblingElement("item")) {
|
|
|
+ const char* id = item->Attribute("id");
|
|
|
+ const char* href = item->Attribute("href");
|
|
|
+ if (id && href) {
|
|
|
+ manifest[id] = href;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse spine to get reading order
|
|
|
+ auto* spine = package->FirstChildElement("spine");
|
|
|
+ if (!spine) {
|
|
|
+ std::cerr << "ReaderView: No spine in OPF\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int chapterNum = 1;
|
|
|
+ for (auto* itemref = spine->FirstChildElement("itemref"); itemref; itemref = itemref->NextSiblingElement("itemref")) {
|
|
|
+ const char* idref = itemref->Attribute("idref");
|
|
|
+ if (!idref) continue;
|
|
|
+
|
|
|
+ auto it = manifest.find(idref);
|
|
|
+ if (it == manifest.end()) continue;
|
|
|
+
|
|
|
+ EpubChapter chapter;
|
|
|
+ chapter.href = m_epub_root_dir + it->second;
|
|
|
+ chapter.title = "Chapter " + std::to_string(chapterNum++);
|
|
|
+ // Content loaded lazily when chapter is viewed
|
|
|
+ m_epub_chapters.push_back(std::move(chapter));
|
|
|
+ }
|
|
|
+
|
|
|
+ std::cout << "ReaderView: Found " << m_epub_chapters.size() << " chapters in EPUB\n";
|
|
|
+}
|
|
|
+
|
|
|
+bool ReaderView::open_epub(const std::string& path) {
|
|
|
+ int err = 0;
|
|
|
+ m_epub_zip = zip_open(path.c_str(), ZIP_RDONLY, &err);
|
|
|
+ if (!m_epub_zip) {
|
|
|
+ std::cerr << "ReaderView: Cannot open EPUB as ZIP (error " << err << ")\n";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ parse_epub_spine();
|
|
|
+
|
|
|
+ if (m_epub_chapters.empty()) {
|
|
|
+ zip_close(m_epub_zip);
|
|
|
+ m_epub_zip = nullptr;
|
|
|
+ std::cerr << "ReaderView: EPUB has no readable chapters\n";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ m_is_epub = true;
|
|
|
+ m_total_pages = static_cast<int>(m_epub_chapters.size());
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::close_epub() {
|
|
|
+ if (m_epub_zip) {
|
|
|
+ zip_close(m_epub_zip);
|
|
|
+ m_epub_zip = nullptr;
|
|
|
+ }
|
|
|
+ m_epub_chapters.clear();
|
|
|
+ m_epub_root_dir.clear();
|
|
|
+ m_current_chapter_text.clear();
|
|
|
+ m_is_epub = false;
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::build_ui() {
|
|
|
+ // Toolbar
|
|
|
+ m_toolbar.set_margin_top(6);
|
|
|
+ m_toolbar.set_margin_bottom(6);
|
|
|
+ m_toolbar.set_margin_start(12);
|
|
|
+ m_toolbar.set_margin_end(12);
|
|
|
+
|
|
|
+ m_close_button.set_icon_name("go-previous-symbolic");
|
|
|
+ m_close_button.set_tooltip_text("Close reader and return to library");
|
|
|
+ m_close_button.signal_clicked().connect([this]() {
|
|
|
+ save_progress();
|
|
|
+ m_signal_close_requested.emit();
|
|
|
+ });
|
|
|
+ m_toolbar.append(m_close_button);
|
|
|
+
|
|
|
+ // Spacer
|
|
|
+ auto& spacer1 = *Gtk::make_managed<Gtk::Box>();
|
|
|
+ spacer1.set_hexpand(true);
|
|
|
+ m_toolbar.append(spacer1);
|
|
|
+
|
|
|
+ m_prev_button.set_icon_name("go-previous-symbolic");
|
|
|
+ m_prev_button.set_tooltip_text("Previous page");
|
|
|
+ m_prev_button.signal_clicked().connect(sigc::mem_fun(*this, &ReaderView::prev_page));
|
|
|
+ m_toolbar.append(m_prev_button);
|
|
|
+
|
|
|
+ m_nav_label.set_margin_start(12);
|
|
|
+ m_nav_label.set_margin_end(12);
|
|
|
+ m_toolbar.append(m_nav_label);
|
|
|
+
|
|
|
+ m_next_button.set_icon_name("go-next-symbolic");
|
|
|
+ m_next_button.set_tooltip_text("Next page");
|
|
|
+ m_next_button.signal_clicked().connect(sigc::mem_fun(*this, &ReaderView::next_page));
|
|
|
+ m_toolbar.append(m_next_button);
|
|
|
+
|
|
|
+ // Spacer
|
|
|
+ auto& spacer2 = *Gtk::make_managed<Gtk::Box>();
|
|
|
+ spacer2.set_hexpand(true);
|
|
|
+ m_toolbar.append(spacer2);
|
|
|
+
|
|
|
+ append(m_toolbar);
|
|
|
+
|
|
|
+ // Loading indicator (hidden by default)
|
|
|
+ m_spinner.set_spinning(false);
|
|
|
+ m_loading_label.set_text("Loading...");
|
|
|
+
|
|
|
+ // Drawing area for page rendering
|
|
|
+ m_canvas.set_draw_func(sigc::mem_fun(*this, &ReaderView::on_draw));
|
|
|
+ m_canvas.set_hexpand(true);
|
|
|
+ m_canvas.set_vexpand(true);
|
|
|
+ m_canvas.set_content_width(600);
|
|
|
+ m_canvas.set_content_height(800);
|
|
|
+
|
|
|
+ m_scroll.set_child(m_canvas);
|
|
|
+ m_scroll.set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
|
|
|
+ m_scroll.set_vexpand(true);
|
|
|
+ m_scroll.set_hexpand(true);
|
|
|
+ append(m_scroll);
|
|
|
+
|
|
|
+ update_nav_label();
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::update_nav_label() {
|
|
|
+ if (m_total_pages > 0) {
|
|
|
+ m_nav_label.set_text(std::to_string(m_current_page + 1) + " / " + std::to_string(m_total_pages));
|
|
|
+ m_prev_button.set_sensitive(m_current_page > 0);
|
|
|
+ m_next_button.set_sensitive(m_current_page < m_total_pages - 1);
|
|
|
+ } else {
|
|
|
+ m_nav_label.set_text("No document");
|
|
|
+ m_prev_button.set_sensitive(false);
|
|
|
+ m_next_button.set_sensitive(false);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool ReaderView::open(const Book& book) {
|
|
|
+ close();
|
|
|
+
|
|
|
+ m_file_path = book.filePath();
|
|
|
+ m_book_id = book.id();
|
|
|
+
|
|
|
+ if (m_file_path.empty()) {
|
|
|
+ std::cerr << "ReaderView: Book has no file path\n";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string ext = lower_ext(m_file_path);
|
|
|
+
|
|
|
+ if (ext == "pdf") {
|
|
|
+ // Open PDF with Poppler
|
|
|
+ GError* gerr = nullptr;
|
|
|
+ m_pdf_doc = poppler_document_new_from_file(to_uri(m_file_path).c_str(), nullptr, &gerr);
|
|
|
+ if (!m_pdf_doc) {
|
|
|
+ std::string msg = "Failed to open PDF";
|
|
|
+ if (gerr) {
|
|
|
+ msg = gerr->message ? gerr->message : msg;
|
|
|
+ g_error_free(gerr);
|
|
|
+ }
|
|
|
+ std::cerr << "ReaderView: " << msg << "\n";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ m_total_pages = poppler_document_get_n_pages(m_pdf_doc);
|
|
|
+ m_is_epub = false;
|
|
|
+ m_is_open = true;
|
|
|
+
|
|
|
+ // Load saved progress
|
|
|
+ std::string progress_str = m_db.get_setting("reading_progress_" + m_book_id, "0");
|
|
|
+ try {
|
|
|
+ m_current_page = std::stoi(progress_str);
|
|
|
+ if (m_current_page < 0 || m_current_page >= m_total_pages) {
|
|
|
+ m_current_page = 0;
|
|
|
+ }
|
|
|
+ } catch (...) {
|
|
|
+ m_current_page = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ render_current_page();
|
|
|
+ update_nav_label();
|
|
|
+ return true;
|
|
|
+ } else if (ext == "epub") {
|
|
|
+ if (!open_epub(m_file_path)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ m_is_open = true;
|
|
|
+
|
|
|
+ // Load saved progress
|
|
|
+ std::string progress_str = m_db.get_setting("reading_progress_" + m_book_id, "0");
|
|
|
+ try {
|
|
|
+ m_current_page = std::stoi(progress_str);
|
|
|
+ if (m_current_page < 0 || m_current_page >= m_total_pages) {
|
|
|
+ m_current_page = 0;
|
|
|
+ }
|
|
|
+ } catch (...) {
|
|
|
+ m_current_page = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ render_current_page();
|
|
|
+ update_nav_label();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::cerr << "ReaderView: Unsupported format: " << ext << "\n";
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::close() {
|
|
|
+ if (m_is_open) {
|
|
|
+ save_progress();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (m_pdf_doc) {
|
|
|
+ g_object_unref(m_pdf_doc);
|
|
|
+ m_pdf_doc = nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ close_epub();
|
|
|
+
|
|
|
+ m_page_surface.reset();
|
|
|
+ m_is_open = false;
|
|
|
+ m_current_page = 0;
|
|
|
+ m_total_pages = 0;
|
|
|
+ m_book_id.clear();
|
|
|
+ m_file_path.clear();
|
|
|
+ m_page_width = 0;
|
|
|
+ m_page_height = 0;
|
|
|
+
|
|
|
+ update_nav_label();
|
|
|
+ m_canvas.queue_draw();
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::go_to_page(int page) {
|
|
|
+ if (!m_is_open || m_total_pages == 0) return;
|
|
|
+
|
|
|
+ page = std::clamp(page, 0, m_total_pages - 1);
|
|
|
+ if (page == m_current_page) return;
|
|
|
+
|
|
|
+ m_current_page = page;
|
|
|
+ render_current_page();
|
|
|
+ update_nav_label();
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::next_page() {
|
|
|
+ go_to_page(m_current_page + 1);
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::prev_page() {
|
|
|
+ go_to_page(m_current_page - 1);
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::render_current_page() {
|
|
|
+ if (!m_is_open) return;
|
|
|
+
|
|
|
+ if (m_is_epub) {
|
|
|
+ render_epub_page();
|
|
|
+ } else {
|
|
|
+ render_pdf_page();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::render_pdf_page() {
|
|
|
+ if (!m_pdf_doc) return;
|
|
|
+
|
|
|
+ PopplerPage* page = poppler_document_get_page(m_pdf_doc, m_current_page);
|
|
|
+ if (!page) {
|
|
|
+ std::cerr << "ReaderView: Failed to get page " << m_current_page << "\n";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ double wpt = 0, hpt = 0;
|
|
|
+ poppler_page_get_size(page, &wpt, &hpt);
|
|
|
+
|
|
|
+ // Scale to fit a reasonable screen size while maintaining quality
|
|
|
+ // Use 1.5x scale for good readability
|
|
|
+ const double scale = 1.5;
|
|
|
+ int width = std::max(1, static_cast<int>(wpt * scale));
|
|
|
+ int height = std::max(1, static_cast<int>(hpt * scale));
|
|
|
+
|
|
|
+ // Create Cairo surface
|
|
|
+ m_page_surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, width, height);
|
|
|
+ auto cr = Cairo::Context::create(m_page_surface);
|
|
|
+
|
|
|
+ // White background
|
|
|
+ cr->set_source_rgb(1.0, 1.0, 1.0);
|
|
|
+ cr->paint();
|
|
|
+
|
|
|
+ // Render page
|
|
|
+ cr->scale(scale, scale);
|
|
|
+ poppler_page_render(page, cr->cobj());
|
|
|
+
|
|
|
+ m_page_surface->flush();
|
|
|
+ m_page_width = width;
|
|
|
+ m_page_height = height;
|
|
|
+
|
|
|
+ // Update canvas size
|
|
|
+ m_canvas.set_content_width(width);
|
|
|
+ m_canvas.set_content_height(height);
|
|
|
+ m_canvas.queue_draw();
|
|
|
+
|
|
|
+ g_object_unref(page);
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::render_epub_page() {
|
|
|
+ if (m_current_page < 0 || m_current_page >= static_cast<int>(m_epub_chapters.size())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto& chapter = m_epub_chapters[m_current_page];
|
|
|
+
|
|
|
+ // Load chapter content if not already loaded
|
|
|
+ if (chapter.content.empty()) {
|
|
|
+ std::string html = epub_read_file(chapter.href);
|
|
|
+ if (!html.empty()) {
|
|
|
+ chapter.content = extract_text_from_html(html);
|
|
|
+ }
|
|
|
+ if (chapter.content.empty()) {
|
|
|
+ chapter.content = "(No content)";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ m_current_chapter_text = chapter.content;
|
|
|
+
|
|
|
+ // Render text using Pango
|
|
|
+ const int page_width = 700;
|
|
|
+ const int margin = 40;
|
|
|
+ const int text_width = page_width - 2 * margin;
|
|
|
+
|
|
|
+ // Create a temporary surface to measure text height
|
|
|
+ auto temp_surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, 1, 1);
|
|
|
+ auto temp_cr = Cairo::Context::create(temp_surface);
|
|
|
+
|
|
|
+ // Create Pango layout
|
|
|
+ auto layout = Pango::Layout::create(temp_cr);
|
|
|
+ layout->set_width(text_width * PANGO_SCALE);
|
|
|
+ layout->set_wrap(Pango::WrapMode::WORD_CHAR);
|
|
|
+
|
|
|
+ // Set font
|
|
|
+ Pango::FontDescription font("Sans 14");
|
|
|
+ layout->set_font_description(font);
|
|
|
+
|
|
|
+ // Set text
|
|
|
+ layout->set_text(m_current_chapter_text);
|
|
|
+
|
|
|
+ // Get layout dimensions
|
|
|
+ int text_w, text_h;
|
|
|
+ layout->get_pixel_size(text_w, text_h);
|
|
|
+
|
|
|
+ // Calculate total height with margins
|
|
|
+ int page_height = std::max(600, text_h + 2 * margin);
|
|
|
+
|
|
|
+ // Create actual surface
|
|
|
+ m_page_surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, page_width, page_height);
|
|
|
+ auto cr = Cairo::Context::create(m_page_surface);
|
|
|
+
|
|
|
+ // White background
|
|
|
+ cr->set_source_rgb(1.0, 1.0, 1.0);
|
|
|
+ cr->paint();
|
|
|
+
|
|
|
+ // Draw text
|
|
|
+ cr->set_source_rgb(0.1, 0.1, 0.1);
|
|
|
+ cr->move_to(margin, margin);
|
|
|
+
|
|
|
+ // Recreate layout with actual context
|
|
|
+ auto render_layout = Pango::Layout::create(cr);
|
|
|
+ render_layout->set_width(text_width * PANGO_SCALE);
|
|
|
+ render_layout->set_wrap(Pango::WrapMode::WORD_CHAR);
|
|
|
+ render_layout->set_font_description(font);
|
|
|
+ render_layout->set_text(m_current_chapter_text);
|
|
|
+
|
|
|
+ render_layout->show_in_cairo_context(cr);
|
|
|
+
|
|
|
+ m_page_surface->flush();
|
|
|
+ m_page_width = page_width;
|
|
|
+ m_page_height = page_height;
|
|
|
+
|
|
|
+ // Update canvas size
|
|
|
+ m_canvas.set_content_width(page_width);
|
|
|
+ m_canvas.set_content_height(page_height);
|
|
|
+ m_canvas.queue_draw();
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
|
|
+ // Gray background
|
|
|
+ cr->set_source_rgb(0.9, 0.9, 0.9);
|
|
|
+ cr->paint();
|
|
|
+
|
|
|
+ if (!m_page_surface) return;
|
|
|
+
|
|
|
+ // Center the page
|
|
|
+ double x = (width - m_page_width) / 2.0;
|
|
|
+ double y = (height - m_page_height) / 2.0;
|
|
|
+ if (x < 0) x = 0;
|
|
|
+ if (y < 0) y = 0;
|
|
|
+
|
|
|
+ cr->set_source(m_page_surface, x, y);
|
|
|
+ cr->paint();
|
|
|
+}
|
|
|
+
|
|
|
+void ReaderView::save_progress() {
|
|
|
+ if (m_book_id.empty()) return;
|
|
|
+
|
|
|
+ m_db.set_setting("reading_progress_" + m_book_id, std::to_string(m_current_page));
|
|
|
+}
|