Merge remote-tracking branch 'origin/master' into feat-autosync

This commit is contained in:
jpirnay
2026-04-27 20:42:15 +02:00
23 changed files with 750 additions and 604 deletions
+109 -145
View File
@@ -155,6 +155,15 @@ void EpubReaderActivity::onEnter() {
}
// We may want a better condition to detect if we are opening for the first time.
// This will trigger if the book is re-opened at Chapter 0.
if (currentSpineIndex < 0 || currentSpineIndex >= epub->getSpineItemsCount()) {
LOG_ERR("ERS", "Invalid saved spine index %d (valid 0..%d), resetting to start", currentSpineIndex,
epub->getSpineItemsCount() > 0 ? epub->getSpineItemsCount() - 1 : 0);
currentSpineIndex = 0;
nextPageNumber = 0;
cachedSpineIndex = 0;
cachedChapterTotalPageCount = 0;
}
if (currentSpineIndex == 0) {
int textSpineIndex = epub->getSpineIndexForTextReference();
if (textSpineIndex != 0) {
@@ -219,12 +228,14 @@ void EpubReaderActivity::loop() {
}
if (inputDrainGuard.shouldDrain(mappedInput)) {
buttonEvents.drain();
return;
}
if (automaticPageTurnActive) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) ||
mappedInput.wasReleased(MappedInputManager::Button::Back)) {
buttonEvents.drain();
automaticPageTurnActive = false;
// updates chapter title space to indicate page turn disabled
requestUpdate();
@@ -248,71 +259,68 @@ void EpubReaderActivity::loop() {
}
}
// Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured.
// We intentionally keep long-press on the richer compare flow so advanced
// conflict-resolution behavior stays available even after simplifying menu UX.
// Without credentials, fall through to the regular menu on release.
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) {
launchKOReaderSync(SyncLaunchMode::COMPARE);
return;
}
bool delayedPrevTurn = false;
bool delayedNextTurn = false;
using BA = CrossPointSettings::BUTTON_ACTION;
// Short press CONFIRM enters reader menu activity.
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
ButtonEventManager::ButtonEvent ev;
while (buttonEvents.consumeEvent(ev)) {
if (ev.button == MappedInputManager::Button::Confirm) {
if (ev.type == ButtonEventManager::PressType::Long && KOREADER_STORE.hasCredentials()) {
launchKOReaderSync(SyncLaunchMode::COMPARE);
return;
}
if (ev.type == ButtonEventManager::PressType::Short) {
openReaderMenu();
return;
}
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride,
SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
// Always apply orientation/darkness change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride,
menu.fontFamilyOverride, menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
}
// Long press BACK (1s+) goes to home screen
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) return;
onGoHome();
return;
}
if (ev.button == MappedInputManager::Button::Back) {
if (ev.type == ButtonEventManager::PressType::Long) {
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) return;
onGoHome();
return;
}
if (ev.type == ButtonEventManager::PressType::Short) {
if (footnoteDepth > 0) {
restoreSavedPosition();
return;
}
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) return;
finish();
return;
}
}
// Short press BACK returns to the calling activity (or restores position if viewing footnote)
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
if (footnoteDepth > 0) {
restoreSavedPosition();
return;
if (ev.type == ButtonEventManager::PressType::Short) {
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
delayedPrevTurn = true;
continue;
}
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
delayedNextTurn = true;
continue;
}
}
ReaderUtils::enforceExitFullRefresh(renderer);
if (tryAutoPushOnClose()) return;
finish();
return;
}
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
if (!prevTriggered && !nextTriggered) {
if (!delayedPrevTurn && !delayedNextTurn) {
return;
}
prevTriggered = delayedPrevTurn;
nextTriggered = delayedNextTurn;
}
if (!prevTriggered && !nextTriggered) {
return;
}
@@ -330,65 +338,6 @@ void EpubReaderActivity::loop() {
return;
}
const bool skipChapter = mappedInput.getHeldTime() > skipChapterMs;
// Chapter skip navigates by TOC entries, not spine boundaries.
// Spine items without their own TOC entry inherit the previous spine's tocIndex
// (see BookMetadataCache), so they're treated as continuations of the last chapter.
// At the boundaries: skipping forward past the last TOC entry jumps to end-of-book
// (clamped in render()); skipping backward before the first TOC entry jumps to the
// spine before the current chapter's first spine (clamped to 0 in render()).
if (skipChapter) {
lastPageTurnTime = millis();
{
RenderLock lock(*this);
if (section && section->pageCount > 0) {
const int curTocIndex = section->getTocIndexForPage(section->currentPage);
const int nextTocIndex = nextTriggered ? curTocIndex + 1 : curTocIndex - 1;
if (curTocIndex < 0) {
// No TOC entry for this spine, fall back to spine-level skip
nextPageNumber = 0;
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
section.reset();
} else if (nextTocIndex >= 0 && nextTocIndex < epub->getTocItemsCount()) {
const int newSpineIndex = epub->getSpineIndexForTocIndex(nextTocIndex);
if (newSpineIndex == currentSpineIndex) {
if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) {
section->currentPage = *resolvedPage;
} else {
LOG_DBG("ERS", "No page boundary for TOC %d in spine %d, staying on current page", nextTocIndex,
currentSpineIndex);
}
} else {
pendingTocIndex = nextTocIndex;
nextPageNumber = 0;
currentSpineIndex = newSpineIndex;
section.reset();
}
} else if (nextTriggered) {
// Beyond last TOC entry, go to end of book
nextPageNumber = 0;
currentSpineIndex = epub->getSpineItemsCount();
section.reset();
} else {
// Before first TOC entry, skip to spine before the current chapter
nextPageNumber = 0;
currentSpineIndex = epub->getTocItem(curTocIndex).spineIndex - 1;
section.reset();
}
} else {
nextPageNumber = 0;
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
section.reset();
}
}
requestUpdate();
return;
}
// No current section, attempt to rerender the book
if (!section) {
requestUpdate();
@@ -946,6 +895,14 @@ void EpubReaderActivity::applyPendingSyncSession() {
pendingParagraphLookup = sync.hasParagraphIndex;
pendingParagraphIndex = sync.paragraphIndex;
if (restoreSpineIndex < 0 || restoreSpineIndex >= epub->getSpineItemsCount()) {
LOG_ERR("ERS", "Invalid sync restore spine index %d, resetting to 0", restoreSpineIndex);
restoreSpineIndex = 0;
restorePage = 0;
pendingParagraphLookup = false;
pendingParagraphIndex = 0;
}
if (sync.outcome == KOReaderSyncOutcomeState::APPLIED_REMOTE) {
restoreSpineIndex = sync.resultSpineIndex;
restorePage = sync.resultPage;
@@ -991,6 +948,11 @@ void EpubReaderActivity::applyPendingBookmarkJump() {
return;
}
LOG_DBG("ERS", "Applying pending bookmark jump: spine=%u page=%u", jump.spineIndex, jump.pageNumber);
if (jump.spineIndex >= static_cast<uint16_t>(epub->getSpineItemsCount())) {
LOG_ERR("ERS", "Invalid bookmark jump spine index %u, resetting to 0", jump.spineIndex);
jump.spineIndex = 0;
jump.pageNumber = 0;
}
// Transient write before initializeReader; saveProgress() overwrites with the real percent.
if (writeReaderProgressCache(epub->getCachePath(), jump.spineIndex, jump.pageNumber, 0, 0)) {
cachedSpineIndex = jump.spineIndex;
@@ -1827,6 +1789,36 @@ bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, Gf
return true;
}
void EpubReaderActivity::openReaderMenu() {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation,
!currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride,
bookFontSizeOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
}
void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION action) {
using BA = CrossPointSettings::BUTTON_ACTION;
switch (action) {
@@ -1946,35 +1938,7 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
break;
case BA::BTN_READER_MENU:
if (epub) {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress =
static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
const bool isCurrentPageStarred = section && bookmarkStore.has(static_cast<uint16_t>(currentSpineIndex),
static_cast<uint16_t>(section->currentPage));
ReaderUtils::enforceExitFullRefresh(renderer);
startActivityForResult(
std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride,
bookImageRenderingOverride, bookFontFamilyOverride, bookFontSizeOverride, SETTINGS.textDarkness,
!bookmarkStore.isEmpty(), isCurrentPageStarred),
[this](const ActivityResult& result) {
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
applyTextDarkness(menu.textDarkness);
toggleAutoPageTurn(menu.pageTurnOption);
applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride,
menu.fontSizeOverride);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
openReaderMenu();
}
break;
case BA::BTN_KOREADER_SYNC: