mod: restore missing mod features from resync audit

Re-add KOReaderSyncActivity PUSH_ONLY mode (PR #1090):
- SyncMode enum with INTERACTIVE/PUSH_ONLY, deferFinish pattern
- Push & Sleep menu action in EpubReaderMenuActivity
- ActivityManager::requestSleep() for activity-initiated sleep
- main.cpp checks isSleepRequested() each loop iteration

Wire EndOfBookMenuActivity into EpubReaderActivity:
- pendingEndOfBookMenu deferred flag avoids render-lock deadlock
- Handles all 6 actions: ARCHIVE, DELETE, TABLE_OF_CONTENTS,
  BACK_TO_BEGINNING, CLOSE_BOOK, CLOSE_MENU

Add book management to reader menu:
- ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK actions with handlers

Port silent next-chapter pre-indexing:
- silentIndexNextChapterIfNeeded() proactively indexes next chapter
  when user is near end of current one, eliminating load screens

Add per-book letterbox fill toggle in reader menu:
- LETTERBOX_FILL cycles Default/Dithered/Solid/None
- Loads/saves per-book override via BookSettings
- bookCachePath constructor param added to EpubReaderMenuActivity

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-07 16:53:17 -05:00
parent 60a3e21c0e
commit 9464df1727
12 changed files with 422 additions and 11 deletions

View File

@@ -63,6 +63,8 @@ class ActivityManager {
// This variable must only be set by the main loop, to avoid race conditions
bool requestedUpdate = false;
bool sleepRequested = false;
public:
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
: renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
@@ -101,6 +103,11 @@ class ActivityManager {
bool isReaderActivity() const;
bool skipLoopDelay() const;
// Activities can request sleep (e.g. PUSH_AND_SLEEP). The main loop checks
// this flag after each loop() call and triggers enterDeepSleep() if set.
void requestSleep() { sleepRequested = true; }
bool isSleepRequested() const { return sleepRequested; }
// If immediate is true, the update will be triggered immediately.
// Otherwise, it will be deferred until the end of the current loop iteration.
void requestUpdate(bool immediate = false);

View File

@@ -10,7 +10,9 @@
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "activities/ActivityManager.h"
#include "DictionaryWordSelectActivity.h"
#include "EndOfBookMenuActivity.h"
#include "EpubReaderBookmarkSelectionActivity.h"
#include "EpubReaderChapterSelectionActivity.h"
#include "EpubReaderFootnotesActivity.h"
@@ -23,6 +25,7 @@
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/BookManager.h"
#include "util/BookmarkStore.h"
#include "util/Dictionary.h"
#include "util/ScreenshotUtil.h"
@@ -133,6 +136,53 @@ void EpubReaderActivity::loop() {
return;
}
if (pendingEndOfBookMenu) {
pendingEndOfBookMenu = false;
endOfBookMenuOpened = true;
startActivityForResult(
std::make_unique<EndOfBookMenuActivity>(renderer, mappedInput, epub->getPath()),
[this](const ActivityResult& result) {
if (result.isCancelled) {
return;
}
const auto action = static_cast<EndOfBookMenuActivity::Action>(std::get<MenuResult>(result.data).action);
switch (action) {
case EndOfBookMenuActivity::Action::ARCHIVE:
BookManager::archiveBook(epub->getPath());
activityManager.goHome();
return;
case EndOfBookMenuActivity::Action::DELETE:
BookManager::deleteBook(epub->getPath());
activityManager.goHome();
return;
case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS: {
endOfBookMenuOpened = false;
RenderLock lock(*this);
currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX;
section.reset();
break;
}
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: {
endOfBookMenuOpened = false;
RenderLock lock(*this);
currentSpineIndex = 0;
nextPageNumber = 0;
section.reset();
break;
}
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
activityManager.goHome();
return;
case EndOfBookMenuActivity::Action::CLOSE_MENU:
endOfBookMenuOpened = false;
break;
}
requestUpdate();
});
return;
}
if (automaticPageTurnActive) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) ||
mappedInput.wasReleased(MappedInputManager::Button::Back)) {
@@ -173,7 +223,8 @@ void EpubReaderActivity::loop() {
section ? BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage) : false;
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), isBookmarked, SETTINGS.fontSize),
SETTINGS.orientation, !currentPageFootnotes.empty(), isBookmarked, SETTINGS.fontSize,
epub->getCachePath()),
[this](const ActivityResult& result) {
// Always apply orientation and font size even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
@@ -254,6 +305,8 @@ void EpubReaderActivity::loop() {
} else {
pageTurn(true);
}
silentIndexNextChapterIfNeeded();
}
// Translate an absolute percent into a spine index plus a normalized position
@@ -441,6 +494,19 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
}
break;
}
case EpubReaderMenuActivity::MenuAction::PUSH_AND_SLEEP: {
const int cp = section ? section->currentPage : 0;
const int tp = section ? section->pageCount : 0;
if (KOREADER_STORE.hasCredentials()) {
startActivityForResult(
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, cp,
tp, KOReaderSyncActivity::SyncMode::PUSH_ONLY),
[](const ActivityResult&) { activityManager.requestSleep(); });
} else {
activityManager.requestSleep();
}
break;
}
case EpubReaderMenuActivity::MenuAction::CLOSE_BOOK:
onGoHome();
return;
@@ -514,6 +580,23 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
requestUpdate();
break;
}
case EpubReaderMenuActivity::MenuAction::ARCHIVE_BOOK: {
if (epub) BookManager::archiveBook(epub->getPath());
activityManager.goHome();
return;
}
case EpubReaderMenuActivity::MenuAction::DELETE_BOOK: {
if (epub) BookManager::deleteBook(epub->getPath());
activityManager.goHome();
return;
}
case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK: {
if (epub) BookManager::reindexBook(epub->getPath(), false);
activityManager.goHome();
return;
}
case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL:
break;
}
}
@@ -618,6 +701,45 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn) {
requestUpdate();
}
bool EpubReaderActivity::silentIndexNextChapterIfNeeded() {
if (!epub || !section) return false;
if (preIndexedNextSpine == currentSpineIndex + 1) return false;
const bool nearEnd = (section->pageCount == 1 && section->currentPage == 0) ||
(section->pageCount >= 2 && section->currentPage == section->pageCount - 2);
if (!nearEnd) return false;
const int nextSpine = currentSpineIndex + 1;
if (nextSpine >= epub->getSpineItemsCount()) return false;
int marginTop, marginRight, marginBottom, marginLeft;
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
marginTop += SETTINGS.screenMargin;
marginLeft += SETTINGS.screenMargin;
marginRight += SETTINGS.screenMargin;
marginBottom += std::max(SETTINGS.screenMargin, UITheme::getInstance().getStatusBarHeight());
const uint16_t vpWidth = renderer.getScreenWidth() - marginLeft - marginRight;
const uint16_t vpHeight = renderer.getScreenHeight() - marginTop - marginBottom;
Section nextSection(epub, nextSpine, renderer);
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, vpWidth, vpHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
preIndexedNextSpine = nextSpine;
return false;
}
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpine);
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, vpWidth, vpHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpine);
return false;
}
preIndexedNextSpine = nextSpine;
return true;
}
// TODO: Failure handling
void EpubReaderActivity::render(RenderLock&& lock) {
if (!epub) {
@@ -633,12 +755,15 @@ void EpubReaderActivity::render(RenderLock&& lock) {
currentSpineIndex = epub->getSpineItemsCount();
}
// Show end of book screen
// Show end of book screen (defer menu launch to loop() to avoid deadlock)
if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
automaticPageTurnActive = false;
if (!endOfBookMenuOpened) {
pendingEndOfBookMenu = true;
}
return;
}

View File

@@ -27,6 +27,13 @@ class EpubReaderActivity final : public Activity {
bool pendingScreenshot = false;
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool automaticPageTurnActive = false;
bool pendingEndOfBookMenu = false;
bool endOfBookMenuOpened = false;
// Silent pre-indexing: proactively creates section files for the next chapter
// when the user is near the end of the current one, eliminating load times.
int preIndexedNextSpine = -1;
bool silentIndexNextChapterIfNeeded();
// Footnote support
std::vector<FootnoteEntry> currentPageFootnotes;

View File

@@ -11,7 +11,8 @@
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& title, const int currentPage, const int totalPages,
const int bookProgressPercent, const uint8_t currentOrientation,
const bool hasFootnotes, bool isBookmarked, uint8_t currentFontSize)
const bool hasFootnotes, bool isBookmarked, uint8_t currentFontSize,
const std::string& bookCachePath)
: Activity("EpubReaderMenu", renderer, mappedInput),
menuItems(buildMenuItems(hasFootnotes, isBookmarked)),
title(title),
@@ -19,12 +20,18 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
pendingFontSize(currentFontSize < CrossPointSettings::FONT_SIZE_COUNT ? currentFontSize : 0),
currentPage(currentPage),
totalPages(totalPages),
bookProgressPercent(bookProgressPercent) {}
bookProgressPercent(bookProgressPercent),
bookCachePath(bookCachePath) {
if (!bookCachePath.empty()) {
auto bookSettings = BookSettings::load(bookCachePath);
pendingLetterboxFill = bookSettings.letterboxFillOverride;
}
}
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
bool isBookmarked) {
std::vector<MenuItem> items;
items.reserve(13);
items.reserve(16);
// Mod menu order
if (isBookmarked) {
items.push_back({MenuAction::REMOVE_BOOKMARK, StrId::STR_REMOVE_BOOKMARK});
@@ -38,8 +45,13 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::TOGGLE_ORIENTATION, StrId::STR_TOGGLE_ORIENTATION});
items.push_back({MenuAction::TOGGLE_FONT_SIZE, StrId::STR_TOGGLE_FONT_SIZE});
items.push_back({MenuAction::LETTERBOX_FILL, StrId::STR_OVERRIDE_LETTERBOX_FILL});
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
items.push_back({MenuAction::PUSH_AND_SLEEP, StrId::STR_PUSH_AND_SLEEP});
items.push_back({MenuAction::CLOSE_BOOK, StrId::STR_CLOSE_BOOK});
items.push_back({MenuAction::ARCHIVE_BOOK, StrId::STR_ARCHIVE_BOOK});
items.push_back({MenuAction::DELETE_BOOK, StrId::STR_DELETE_BOOK});
items.push_back({MenuAction::REINDEX_BOOK, StrId::STR_REINDEX_BOOK});
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
items.push_back({MenuAction::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE});
return items;
@@ -81,6 +93,14 @@ void EpubReaderMenuActivity::loop() {
return;
}
if (selectedAction == MenuAction::LETTERBOX_FILL) {
int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT;
pendingLetterboxFill = indexToLetterboxFill(idx);
if (!bookCachePath.empty()) saveLetterboxFill();
requestUpdate();
return;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption, pendingFontSize});
finish();
return;
@@ -155,6 +175,12 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
}
if (menuItems[i].action == MenuAction::LETTERBOX_FILL) {
const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
}
}
// Footer / Hints

View File

@@ -6,6 +6,7 @@
#include <vector>
#include "../Activity.h"
#include "util/BookSettings.h"
#include "util/ButtonNavigator.h"
class EpubReaderMenuActivity final : public Activity {
@@ -21,6 +22,7 @@ class EpubReaderMenuActivity final : public Activity {
DISPLAY_QR,
GO_HOME,
SYNC,
PUSH_AND_SLEEP,
DELETE_CACHE,
// Mod-specific actions
ADD_BOOKMARK,
@@ -32,13 +34,18 @@ class EpubReaderMenuActivity final : public Activity {
TOGGLE_ORIENTATION,
TOGGLE_FONT_SIZE,
CLOSE_BOOK,
DELETE_DICT_CACHE
DELETE_DICT_CACHE,
ARCHIVE_BOOK,
DELETE_BOOK,
REINDEX_BOOK,
LETTERBOX_FILL
};
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
const int currentPage, const int totalPages, const int bookProgressPercent,
const uint8_t currentOrientation, const bool hasFootnotes,
bool isBookmarked = false, uint8_t currentFontSize = 0);
bool isBookmarked = false, uint8_t currentFontSize = 0,
const std::string& bookCachePath = "");
void onEnter() override;
void onExit() override;
@@ -70,4 +77,26 @@ class EpubReaderMenuActivity final : public Activity {
int currentPage = 0;
int totalPages = 0;
int bookProgressPercent = 0;
std::string bookCachePath;
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4;
const std::vector<StrId> letterboxFillLabels = {StrId::STR_DEFAULT_OPTION, StrId::STR_DITHERED, StrId::STR_SOLID,
StrId::STR_NONE_OPT};
int letterboxFillToIndex() const {
if (pendingLetterboxFill == BookSettings::USE_GLOBAL) return 0;
return pendingLetterboxFill + 1;
}
static uint8_t indexToLetterboxFill(int index) {
if (index == 0) return BookSettings::USE_GLOBAL;
return static_cast<uint8_t>(index - 1);
}
void saveLetterboxFill() const {
auto settings = BookSettings::load(bookCachePath);
settings.letterboxFillOverride = pendingLetterboxFill;
BookSettings::save(bookCachePath, settings);
}
};

View File

@@ -50,6 +50,12 @@ void wifiOff() {
}
} // namespace
void KOReaderSyncActivity::deferFinish(bool success) {
RenderLock lock(*this);
pendingFinishSuccess = success;
pendingFinish = true;
}
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
if (!success) {
LOG_DBG("KOSync", "WiFi connection failed, exiting");
@@ -89,6 +95,10 @@ void KOReaderSyncActivity::performSync() {
documentHash = KOReaderDocumentId::calculate(epubPath);
}
if (documentHash.empty()) {
if (syncMode == SyncMode::PUSH_ONLY) {
deferFinish(false);
return;
}
{
RenderLock lock(*this);
state = SYNC_FAILED;
@@ -106,11 +116,16 @@ void KOReaderSyncActivity::performSync() {
}
requestUpdateAndWait();
if (syncMode == SyncMode::PUSH_ONLY) {
// Skip fetching remote progress entirely -- just upload local position
performUpload();
return;
}
// Fetch remote progress
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
if (result == KOReaderSyncClient::NOT_FOUND) {
// No remote progress - offer to upload
{
RenderLock lock(*this);
state = NO_REMOTE_PROGRESS;
@@ -174,6 +189,11 @@ void KOReaderSyncActivity::performUpload() {
if (result != KOReaderSyncClient::OK) {
wifiOff();
if (syncMode == SyncMode::PUSH_ONLY) {
LOG_DBG("KOSync", "PUSH_ONLY upload failed: %s", KOReaderSyncClient::errorString(result));
deferFinish(false);
return;
}
{
RenderLock lock(*this);
state = SYNC_FAILED;
@@ -184,6 +204,11 @@ void KOReaderSyncActivity::performUpload() {
}
wifiOff();
if (syncMode == SyncMode::PUSH_ONLY) {
LOG_DBG("KOSync", "PUSH_ONLY upload succeeded");
deferFinish(true);
return;
}
{
RenderLock lock(*this);
state = UPLOAD_COMPLETE;
@@ -196,6 +221,11 @@ void KOReaderSyncActivity::onEnter() {
// Check for credentials first
if (!KOREADER_STORE.hasCredentials()) {
if (syncMode == SyncMode::PUSH_ONLY) {
LOG_DBG("KOSync", "PUSH_ONLY: no credentials, finishing silently");
deferFinish(false);
return;
}
state = NO_CREDENTIALS;
requestUpdate();
return;
@@ -335,6 +365,15 @@ void KOReaderSyncActivity::render(RenderLock&&) {
}
void KOReaderSyncActivity::loop() {
if (pendingFinish) {
pendingFinish = false;
ActivityResult result;
result.isCancelled = !pendingFinishSuccess;
setResult(std::move(result));
finish();
return;
}
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result;

View File

@@ -11,18 +11,27 @@
/**
* Activity for syncing reading progress with KOReader sync server.
*
* Flow:
* Flow (INTERACTIVE):
* 1. Connect to WiFi (if not connected)
* 2. Calculate document hash
* 3. Fetch remote progress
* 4. Show comparison and options (Apply/Upload)
* 5. Apply or upload progress
*
* Flow (PUSH_ONLY):
* 1. Connect to WiFi (if not connected)
* 2. Calculate document hash
* 3. Upload local progress (no UI interaction)
* 4. Finish silently
*/
class KOReaderSyncActivity final : public Activity {
public:
enum class SyncMode { INTERACTIVE, PUSH_ONLY };
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
int currentPage, int totalPagesInSpine)
int currentPage, int totalPagesInSpine,
SyncMode syncMode = SyncMode::INTERACTIVE)
: Activity("KOReaderSync", renderer, mappedInput),
epub(epub),
epubPath(epubPath),
@@ -31,7 +40,8 @@ class KOReaderSyncActivity final : public Activity {
totalPagesInSpine(totalPagesInSpine),
remoteProgress{},
remotePosition{},
localProgress{} {}
localProgress{},
syncMode(syncMode) {}
void onEnter() override;
void onExit() override;
@@ -73,6 +83,14 @@ class KOReaderSyncActivity final : public Activity {
// Selection in result screen (0=Apply, 1=Upload)
int selectedOption = 0;
SyncMode syncMode;
// Deferred finish for PUSH_ONLY (async upload completes on a blocking call,
// so the actual finish() must happen in loop() where it's safe).
bool pendingFinish = false;
bool pendingFinishSuccess = false;
void deferFinish(bool success);
void onWifiSelectionComplete(bool success);
void performSync();
void performUpload();

View File

@@ -381,6 +381,12 @@ void loop() {
screenshotButtonsReleased = true;
}
if (activityManager.isSleepRequested()) {
LOG_DBG("SLP", "Activity requested sleep (push-and-sleep)");
enterDeepSleep();
return;
}
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
if (millis() - lastActivityTime >= sleepTimeoutMs) {
LOG_DBG("SLP", "Auto-sleep triggered after %lu ms of inactivity", sleepTimeoutMs);