feat: epub bookmarks (#1337)
Co-authored-by: vedi0boy <nate@origin8publishing.com> Co-authored-by: Uri Tauber <uritaube@gmail.com>
This commit is contained in:
co-authored by
vedi0boy
Uri Tauber
parent
bbb3e06eb1
commit
36a3a0cc3a
@@ -329,6 +329,43 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
|
||||
return page;
|
||||
}
|
||||
|
||||
std::string Section::getTextFromSectionFile() {
|
||||
std::string fullText;
|
||||
auto p = this->loadPageFromSectionFile();
|
||||
if (p) {
|
||||
for (const auto& el : p->elements) {
|
||||
if (el->getTag() == TAG_PageLine) {
|
||||
const auto& line = static_cast<const PageLine&>(*el);
|
||||
if (line.getBlock()) {
|
||||
const auto& words = line.getBlock()->getWords();
|
||||
for (const auto& w : words) {
|
||||
if (!fullText.empty()) fullText += " ";
|
||||
fullText += w;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fullText;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getCachedPageCount() const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
if (fileSize < HEADER_SIZE) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
|
||||
@@ -40,10 +40,14 @@ class Section {
|
||||
uint8_t imageRendering, bool focusReadingEnabled,
|
||||
const std::function<void()>& popupFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
std::string getTextFromSectionFile();
|
||||
|
||||
// Look up the page number for an anchor id from the section cache file.
|
||||
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
||||
|
||||
// Get the page count from the section cache file without fully loading it.
|
||||
std::optional<uint16_t> getCachedPageCount() const;
|
||||
|
||||
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
||||
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||
|
||||
|
||||
@@ -182,6 +182,8 @@ STR_DOWNLOADING: "Downloading..."
|
||||
STR_DOWNLOAD_FAILED: "Download failed"
|
||||
STR_ERROR_MSG: "Error:"
|
||||
STR_UNNAMED: "Unnamed"
|
||||
STR_HOLD_CONFIRM_TO_DELETE: "Hold Confirm to Delete"
|
||||
STR_BOOKMARK_INSTRUCTIONS: "Hold Confirm from the reader to create a bookmark."
|
||||
STR_NO_SERVER_URL: "No server URL configured"
|
||||
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||
@@ -259,6 +261,8 @@ STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
||||
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||
STR_BOOKMARKS: "Bookmarks"
|
||||
STR_BOOKMARK_ADDED: "Bookmark added."
|
||||
STR_OPDS_BROWSER: "OPDS Browser"
|
||||
STR_SEARCH: "Search"
|
||||
STR_COVER_CUSTOM: "Cover + Custom"
|
||||
@@ -288,6 +292,7 @@ STR_GO_HOME_BUTTON: "Go Home"
|
||||
STR_SYNC_PROGRESS: "Sync Progress"
|
||||
STR_DELETE_CACHE: "Delete Book Cache"
|
||||
STR_DELETE: "Delete"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Delete this bookmark?"
|
||||
STR_DISPLAY_QR: "Show page as QR"
|
||||
STR_CHAPTER_PREFIX: "Chapter: "
|
||||
STR_PAGES_SEPARATOR: " pages | "
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ProgressMapper.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -7,6 +8,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "ChapterXPathResolver.h"
|
||||
#include "Epub/Section.h"
|
||||
#include "Epub/htmlEntities.h"
|
||||
#include "Utf8.h"
|
||||
|
||||
@@ -506,8 +508,9 @@ bool streamSpine(const std::shared_ptr<Epub>& epub, int spineIndex, ParagraphStr
|
||||
}
|
||||
} // namespace
|
||||
|
||||
KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos) {
|
||||
KOReaderPosition result;
|
||||
SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub>& epub,
|
||||
const CrossPointPosition& pos) {
|
||||
SavedProgressPosition result;
|
||||
float intra =
|
||||
(pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f;
|
||||
result.percentage = epub->calculateProgress(pos.spineIndex, intra);
|
||||
@@ -520,13 +523,14 @@ KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, c
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = generateXPath(epub, pos.spineIndex, intra);
|
||||
}
|
||||
LOG_DBG("PM", "-> KO: spine=%d page=%d/%d %.2f%% %s", pos.spineIndex, pos.pageNumber, pos.totalPages,
|
||||
LOG_DBG("PM", "-> Progress: spine=%d page=%d/%d %.2f%% %s", pos.spineIndex, pos.pageNumber, pos.totalPages,
|
||||
result.percentage * 100, result.xpath.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
|
||||
int currentSpineIndex, int totalPagesInCurrentSpine) {
|
||||
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& koPos,
|
||||
GfxRenderer& renderer, int currentSpineIndex,
|
||||
int totalPagesInCurrentSpine, int fallbackTotalPages) {
|
||||
CrossPointPosition result{};
|
||||
const size_t bookSize = epub->getBookSize();
|
||||
if (bookSize == 0) return result;
|
||||
@@ -556,7 +560,6 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.spineIndex >= spineCount) return result;
|
||||
|
||||
const size_t prevCum = (result.spineIndex > 0) ? epub->getCumulativeSpineItemSize(result.spineIndex - 1) : 0;
|
||||
const size_t spineSize = epub->getCumulativeSpineItemSize(result.spineIndex) - prevCum;
|
||||
@@ -570,7 +573,17 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
||||
result.totalPages = std::max(
|
||||
1, static_cast<int>(totalPagesInCurrentSpine * static_cast<float>(spineSize) / static_cast<float>(cs)));
|
||||
}
|
||||
if (spineSize == 0 || result.totalPages == 0) return result;
|
||||
|
||||
if (result.totalPages <= 0) {
|
||||
Section tempSection(epub, result.spineIndex, renderer);
|
||||
if (auto cachedCount = tempSection.getCachedPageCount()) {
|
||||
result.totalPages = *cachedCount;
|
||||
} else if (fallbackTotalPages > 0) {
|
||||
result.totalPages = fallbackTotalPages;
|
||||
} else {
|
||||
result.totalPages = 1; // Prevent division by zero and give a fallback
|
||||
}
|
||||
}
|
||||
|
||||
float intra = 0.0f;
|
||||
if (useAncestry) {
|
||||
@@ -613,8 +626,60 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epu
|
||||
|
||||
result.pageNumber = std::max(
|
||||
0, std::min(static_cast<int>(intra * static_cast<float>(result.totalPages - 1) + 0.5f), result.totalPages - 1));
|
||||
LOG_DBG("PM", "<- KO: %.2f%% %s -> spine=%d page=%d/%d", koPos.percentage * 100, koPos.xpath.c_str(),
|
||||
LOG_DBG("PM", "<- Progress: %.2f%% %s -> spine=%d page=%d/%d", koPos.percentage * 100, koPos.xpath.c_str(),
|
||||
result.spineIndex, result.pageNumber, result.totalPages);
|
||||
|
||||
// Refine page using section cache LUTs: li index, anchor, or paragraph index.
|
||||
if (result.hasLiIndex || result.xpathAnchorId[0] != '\0' || result.hasParagraphIndex) {
|
||||
Section tempSection(epub, result.spineIndex, renderer);
|
||||
bool refined = false;
|
||||
if (result.hasLiIndex) {
|
||||
const auto liPage = tempSection.getPageForListItemIndex(result.liIndex);
|
||||
if (liPage.has_value()) {
|
||||
LOG_DBG("PM", "Li index %u -> page %d (was %d)", result.liIndex, *liPage, result.pageNumber);
|
||||
result.pageNumber = *liPage;
|
||||
refined = true;
|
||||
} else {
|
||||
LOG_DBG("PM", "Li index %u not found in section LUT", result.liIndex);
|
||||
}
|
||||
}
|
||||
if (!refined && result.xpathAnchorId[0] != '\0') {
|
||||
const auto anchorPage = tempSection.getPageForAnchor(std::string(result.xpathAnchorId));
|
||||
if (anchorPage.has_value()) {
|
||||
LOG_DBG("PM", "Anchor '%s' -> page %d (was %d)", result.xpathAnchorId, *anchorPage, result.pageNumber);
|
||||
result.pageNumber = *anchorPage;
|
||||
refined = true;
|
||||
} else {
|
||||
LOG_DBG("PM", "Anchor '%s' not found in section cache", result.xpathAnchorId);
|
||||
}
|
||||
}
|
||||
if (!refined && result.hasParagraphIndex) {
|
||||
const auto paragraphPage = tempSection.getPageForParagraphIndex(result.paragraphIndex);
|
||||
const auto nextParagraphPage = tempSection.getPageForParagraphIndex(result.paragraphIndex + 1);
|
||||
if (paragraphPage.has_value()) {
|
||||
int refinedPage = std::max(result.pageNumber, static_cast<int>(*paragraphPage));
|
||||
if (nextParagraphPage.has_value()) {
|
||||
const int lutSpan = static_cast<int>(*nextParagraphPage) - static_cast<int>(*paragraphPage);
|
||||
// Only cap when the LUT span is >1. A span of 1 means the LUT granularity is too
|
||||
// coarse to trust over the intra-spine position (e.g. a stale cache where the paragraph
|
||||
// occupies different pages than at build time).
|
||||
if (lutSpan > 1 && refinedPage >= static_cast<int>(*nextParagraphPage)) {
|
||||
refinedPage = static_cast<int>(*nextParagraphPage) - 1;
|
||||
}
|
||||
}
|
||||
char nextParaBuf[8];
|
||||
if (nextParagraphPage.has_value())
|
||||
snprintf(nextParaBuf, sizeof(nextParaBuf), "%d", *nextParagraphPage);
|
||||
else
|
||||
snprintf(nextParaBuf, sizeof(nextParaBuf), "none");
|
||||
LOG_DBG("PM", "Paragraph %u -> LUT page %d, nextPara page %s, intra page %d, using %d", result.paragraphIndex,
|
||||
*paragraphPage, nextParaBuf, result.pageNumber, refinedPage);
|
||||
result.pageNumber = refinedPage;
|
||||
} else {
|
||||
LOG_DBG("PM", "Paragraph %u not found in section LUT", result.paragraphIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -19,18 +20,18 @@ struct CrossPointPosition {
|
||||
};
|
||||
|
||||
/**
|
||||
* KOReader position representation.
|
||||
* Progress position representation.
|
||||
*/
|
||||
struct KOReaderPosition {
|
||||
struct SavedProgressPosition {
|
||||
std::string xpath; // XPath-like progress string
|
||||
float percentage; // Progress percentage (0.0 to 1.0)
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps between CrossPoint and KOReader position formats.
|
||||
* Maps between CrossPoint and SavedProgress position formats, such as those used by KOReader.
|
||||
*
|
||||
* CrossPoint tracks position as (spineIndex, pageNumber).
|
||||
* KOReader uses XPath-like strings + percentage.
|
||||
* SavedProgress uses XPath-like strings + percentage.
|
||||
*
|
||||
* Since CrossPoint discards HTML structure during parsing, we generate
|
||||
* synthetic XPath strings based on spine index, using percentage as the
|
||||
@@ -39,28 +40,30 @@ struct KOReaderPosition {
|
||||
class ProgressMapper {
|
||||
public:
|
||||
/**
|
||||
* Convert CrossPoint position to KOReader format.
|
||||
* Convert CrossPoint position to SavedProgress format.
|
||||
*
|
||||
* @param epub The EPUB book
|
||||
* @param pos CrossPoint position
|
||||
* @return KOReader position
|
||||
* @return SavedProgress position
|
||||
*/
|
||||
static KOReaderPosition toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos);
|
||||
static SavedProgressPosition toSavedProgress(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos);
|
||||
|
||||
/**
|
||||
* Convert KOReader position to CrossPoint format.
|
||||
* Convert SavedProgress position to CrossPoint format.
|
||||
*
|
||||
* Note: The returned pageNumber may be approximate since different
|
||||
* rendering settings produce different page counts.
|
||||
*
|
||||
* @param epub The EPUB book
|
||||
* @param koPos KOReader position
|
||||
* @param savedPos SavedProgress position
|
||||
* @param renderer GfxRenderer for page count estimation
|
||||
* @param currentSpineIndex Index of the currently open spine item (for density estimation)
|
||||
* @param totalPagesInCurrentSpine Total pages in the current spine item (for density estimation)
|
||||
* @return CrossPoint position
|
||||
*/
|
||||
static CrossPointPosition toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
|
||||
int currentSpineIndex = -1, int totalPagesInCurrentSpine = 0);
|
||||
static CrossPointPosition toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& savedPos,
|
||||
GfxRenderer& renderer, int currentSpineIndex = -1,
|
||||
int totalPagesInCurrentSpine = 0, int fallbackTotalPages = 0);
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user