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

@@ -0,0 +1,50 @@
# Fresh Replay: Sync mod/master with upstream/master (continued)
## Task
Continue the "Fresh Replay" synchronization of mod/master with upstream/master (HEAD: 170cc25). This session picked up from Phase 2c (GfxRenderer/theme modifications) and completed through Phase 4 (verification).
## Changes Made
### Phase 2c-e: GfxRenderer, themes, SleepActivity, SettingsActivity, platformio
- Added `drawPixelGray` to GfxRenderer for letterbox fill rendering
- Added `PRERENDER_THUMB_HEIGHTS` to UITheme for placeholder cover generation
- Added `[env:mod]` build environment to platformio.ini
- Implemented sleep screen letterbox fill (solid/dithered) with edge caching in SleepActivity
- Added placeholder cover fallback (PlaceholderCoverGenerator) for XTC/TXT/EPUB sleep screens
- Added Clock settings category to SettingsActivity with timezone, NTP sync, set-time actions
- Replaced CalibreSettingsActivity with OpdsServerListActivity for OPDS server management
- Added DynamicEnum rendering support for settings
- Added long-press book management to RecentBooksActivity
### Phase 3: Re-port unmerged upstream PRs
- **#1055** (byte-level framebuffer writes): fillPhysicalHSpan*, optimized fillRect/drawLine/fillRectDither/fillPolygon
- **#1027** (word-width cache): 128-entry FNV-1a cache, hyphenation early exit (7-9% layout speedup)
- **#1068** (URL hyphenation): Already present in upstream
- **#1019** (file extensions in browser): Already present in upstream
- **#1090/#1185/#1217** (KOReader sync): Binary credential store, document hash caching, ChapterXPathIndexer
- **#1209** (OPDS multi-server): OpdsBookBrowserActivity accepts OpdsServer, directory picker, download-complete prompt
- **#857** (Dictionary): Activities already ported in Phase 1/2
- **#1003** (Placeholder covers): Already integrated in Phase 2
### Fixes
- Added `STR_OFF` i18n string for clock format setting
- Fixed include paths (ActivityResult.h from subdirectories)
- Replaced `Epub::isValidThumbnailBmp` with `Storage.exists()` (method doesn't exist in upstream)
- Replaced `StringUtils::checkFileExtension` with `FsHelpers` equivalents
### Image pipeline decision
- Kept upstream's JPEGDEC implementation — mod's picojpeg was a workaround for the older codebase
- No mod-specific image pipeline changes needed
## Branch Status
- **mod/master-resync**: 6 commits ahead of upstream/master (170cc25)
- **mod/backup-pre-sync-2026-03-07**: Safety snapshot of original mod/master
- 189 files changed, ~114,566 insertions, ~379 deletions vs upstream
## Follow-up Items
- Run full PlatformIO build on hardware to verify compilation
- Run clang-format on all modified files
- Test on device: clock display, sleep screen letterbox fill, dictionary, OPDS browsing
- KOReaderSyncActivity PUSH_ONLY mode (from PR #1090) not yet re-added to activity
- Consider adding `StringUtils.h` if other mod code needs `checkFileExtension`
- Update mod version string

View File

@@ -0,0 +1,42 @@
# KOReaderSyncActivity PUSH_ONLY Mode Re-addition
**Date**: 2026-03-07
**Branch**: `mod/master-resync`
## Task
Re-add the `PUSH_ONLY` sync mode to `KOReaderSyncActivity` that was lost during the upstream resync/ActivityManager migration (originally from PR #1090). This mode allows the reader to silently push local progress to the KOReader sync server and then enter deep sleep — no interactive UI.
## Changes
### `src/activities/reader/KOReaderSyncActivity.h`
- Added `enum class SyncMode { INTERACTIVE, PUSH_ONLY }` to the class
- Added `syncMode` constructor parameter (defaults to `INTERACTIVE`)
- Added `deferFinish(bool success)`, `pendingFinish`, `pendingFinishSuccess` for safe async finish from blocking calls
### `src/activities/reader/KOReaderSyncActivity.cpp`
- Implemented `deferFinish()` — sets a flag that `loop()` picks up to call `setResult()`/`finish()`
- `onEnter()`: In PUSH_ONLY mode, silently finish if no credentials (no error UI)
- `performSync()`: In PUSH_ONLY mode, skip remote fetch entirely and go straight to `performUpload()`
- `performUpload()`: In PUSH_ONLY mode, use `deferFinish()` instead of setting UI state on success/failure
- `loop()`: Check `pendingFinish` first and perform deferred finish if set
### `src/activities/ActivityManager.h`
- Added `requestSleep()` / `isSleepRequested()` — allows activities to signal that the device should enter deep sleep. Checked by the main loop.
### `src/main.cpp`
- Added `activityManager.isSleepRequested()` check in the main loop, before the auto-sleep timeout check
### `src/activities/reader/EpubReaderMenuActivity.h` / `.cpp`
- Added `PUSH_AND_SLEEP` to the `MenuAction` enum
- Added menu item `{PUSH_AND_SLEEP, STR_PUSH_AND_SLEEP}` between SYNC and CLOSE_BOOK
### `src/activities/reader/EpubReaderActivity.cpp`
- Added `#include "activities/ActivityManager.h"`
- Added `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`: launches `KOReaderSyncActivity` in `PUSH_ONLY` mode, then calls `activityManager.requestSleep()` on completion (regardless of success/failure)
### `lib/I18n/translations/english.yaml` / `lib/I18n/I18nKeys.h`
- Added `STR_PUSH_AND_SLEEP: "Push & Sleep"` and regenerated I18n keys
## Follow-up Items
- None

View File

@@ -0,0 +1,61 @@
# Missing Mod Features Audit — Implementation
**Date**: 2026-03-07
**Branch**: `mod/master-resync`
## Task
Comprehensive audit of `mod/master-resync` vs `mod/backup-pre-sync-2026-03-07` identified 4 mod features lost during the upstream resync. All 4 have been re-implemented.
## Changes
### 1. EndOfBookMenuActivity wired into EpubReaderActivity (HIGH)
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
- Added `pendingEndOfBookMenu` and `endOfBookMenuOpened` flags
- In `render()`: when reaching end-of-book, sets `pendingEndOfBookMenu = true` (deferred to avoid render-lock deadlock)
- In `loop()`: checks flag and launches `EndOfBookMenuActivity` via `startActivityForResult`
- Result handler covers all 6 actions: ARCHIVE (→ goHome), DELETE (→ goHome), TABLE_OF_CONTENTS (→ last chapter), BACK_TO_BEGINNING (→ spine 0), CLOSE_BOOK (→ goHome), CLOSE_MENU (→ stay at end)
- Added `#include "EndOfBookMenuActivity.h"` and `#include "util/BookManager.h"`
### 2. Book management from reader menu (MEDIUM)
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
- Added `ARCHIVE_BOOK`, `DELETE_BOOK`, `REINDEX_BOOK` to `MenuAction` enum
- Added corresponding menu items between CLOSE_BOOK and DELETE_CACHE
- Added handlers in `onReaderMenuConfirm`: each calls `BookManager::archiveBook/deleteBook/reindexBook` then `activityManager.goHome()`
### 3. Silent next-chapter pre-indexing (MEDIUM)
**Files**: `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
- Added `preIndexedNextSpine` field and `silentIndexNextChapterIfNeeded()` method
- Triggers when user is on second-to-last or last page of a chapter
- Creates section file for `currentSpineIndex + 1` in advance
- Called after every page turn in `loop()`
- ~35 lines of self-contained implementation
### 4. Letterbox fill toggle in reader menu (LOW)
**Files**: `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.cpp`
- Added `LETTERBOX_FILL` to `MenuAction` enum
- Added `bookCachePath` constructor parameter (with default `""` for backward compat)
- Added per-book `pendingLetterboxFill`, `letterboxFillLabels`, `letterboxFillToIndex()`, `indexToLetterboxFill()`, `saveLetterboxFill()`
- Cycles Default → Dithered → Solid → None → Default on Confirm
- Renders current value on right edge of menu item
- Loads/saves per-book setting via `BookSettings`
- Updated call site in `EpubReaderActivity` to pass `epub->getCachePath()`
## Audit False Positives (confirmed NOT gaps)
- GfxRenderer kerning/ligatures/wrappedText — present on resync
- HttpDownloader auth fallback — present with OPDS settings fallback
- Lyra3CoversTheme — exists on resync
- ActivityWithSubactivity → Activity migration — intentional upstream change
- EndOfBookMenuActivity callbacks → setResult/finish — correctly migrated
## Follow-up Items
- None

View File

@@ -300,6 +300,7 @@ STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_GO_TO_PERCENT: "Go to %" STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home" STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress" STR_SYNC_PROGRESS: "Sync Progress"
STR_PUSH_AND_SLEEP: "Push & Sleep"
STR_DELETE_CACHE: "Delete Book Cache" STR_DELETE_CACHE: "Delete Book Cache"
STR_DELETE: "Delete" STR_DELETE: "Delete"
STR_DISPLAY_QR: "Show page as QR" STR_DISPLAY_QR: "Show page as QR"

View File

@@ -63,6 +63,8 @@ class ActivityManager {
// This variable must only be set by the main loop, to avoid race conditions // This variable must only be set by the main loop, to avoid race conditions
bool requestedUpdate = false; bool requestedUpdate = false;
bool sleepRequested = false;
public: public:
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
: renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { : renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
@@ -101,6 +103,11 @@ class ActivityManager {
bool isReaderActivity() const; bool isReaderActivity() const;
bool skipLoopDelay() 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. // If immediate is true, the update will be triggered immediately.
// Otherwise, it will be deferred until the end of the current loop iteration. // Otherwise, it will be deferred until the end of the current loop iteration.
void requestUpdate(bool immediate = false); void requestUpdate(bool immediate = false);

View File

@@ -10,7 +10,9 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "activities/ActivityManager.h"
#include "DictionaryWordSelectActivity.h" #include "DictionaryWordSelectActivity.h"
#include "EndOfBookMenuActivity.h"
#include "EpubReaderBookmarkSelectionActivity.h" #include "EpubReaderBookmarkSelectionActivity.h"
#include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderChapterSelectionActivity.h"
#include "EpubReaderFootnotesActivity.h" #include "EpubReaderFootnotesActivity.h"
@@ -23,6 +25,7 @@
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/BookManager.h"
#include "util/BookmarkStore.h" #include "util/BookmarkStore.h"
#include "util/Dictionary.h" #include "util/Dictionary.h"
#include "util/ScreenshotUtil.h" #include "util/ScreenshotUtil.h"
@@ -133,6 +136,53 @@ void EpubReaderActivity::loop() {
return; 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 (automaticPageTurnActive) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) ||
mappedInput.wasReleased(MappedInputManager::Button::Back)) { mappedInput.wasReleased(MappedInputManager::Button::Back)) {
@@ -173,7 +223,8 @@ void EpubReaderActivity::loop() {
section ? BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage) : false; section ? BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage) : false;
startActivityForResult(std::make_unique<EpubReaderMenuActivity>( startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, 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) { [this](const ActivityResult& result) {
// Always apply orientation and font size even if the menu was cancelled // Always apply orientation and font size even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data); const auto& menu = std::get<MenuResult>(result.data);
@@ -254,6 +305,8 @@ void EpubReaderActivity::loop() {
} else { } else {
pageTurn(true); pageTurn(true);
} }
silentIndexNextChapterIfNeeded();
} }
// Translate an absolute percent into a spine index plus a normalized position // Translate an absolute percent into a spine index plus a normalized position
@@ -441,6 +494,19 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
} }
break; 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: case EpubReaderMenuActivity::MenuAction::CLOSE_BOOK:
onGoHome(); onGoHome();
return; return;
@@ -514,6 +580,23 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
requestUpdate(); requestUpdate();
break; 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(); 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 // TODO: Failure handling
void EpubReaderActivity::render(RenderLock&& lock) { void EpubReaderActivity::render(RenderLock&& lock) {
if (!epub) { if (!epub) {
@@ -633,12 +755,15 @@ void EpubReaderActivity::render(RenderLock&& lock) {
currentSpineIndex = epub->getSpineItemsCount(); 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()) { if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
automaticPageTurnActive = false; automaticPageTurnActive = false;
if (!endOfBookMenuOpened) {
pendingEndOfBookMenu = true;
}
return; return;
} }

View File

@@ -27,6 +27,13 @@ class EpubReaderActivity final : public Activity {
bool pendingScreenshot = false; bool pendingScreenshot = false;
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool automaticPageTurnActive = false; 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 // Footnote support
std::vector<FootnoteEntry> currentPageFootnotes; std::vector<FootnoteEntry> currentPageFootnotes;

View File

@@ -11,7 +11,8 @@
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& title, const int currentPage, const int totalPages, const std::string& title, const int currentPage, const int totalPages,
const int bookProgressPercent, const uint8_t currentOrientation, 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), : Activity("EpubReaderMenu", renderer, mappedInput),
menuItems(buildMenuItems(hasFootnotes, isBookmarked)), menuItems(buildMenuItems(hasFootnotes, isBookmarked)),
title(title), title(title),
@@ -19,12 +20,18 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
pendingFontSize(currentFontSize < CrossPointSettings::FONT_SIZE_COUNT ? currentFontSize : 0), pendingFontSize(currentFontSize < CrossPointSettings::FONT_SIZE_COUNT ? currentFontSize : 0),
currentPage(currentPage), currentPage(currentPage),
totalPages(totalPages), 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, std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
bool isBookmarked) { bool isBookmarked) {
std::vector<MenuItem> items; std::vector<MenuItem> items;
items.reserve(13); items.reserve(16);
// Mod menu order // Mod menu order
if (isBookmarked) { if (isBookmarked) {
items.push_back({MenuAction::REMOVE_BOOKMARK, StrId::STR_REMOVE_BOOKMARK}); 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::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::TOGGLE_ORIENTATION, StrId::STR_TOGGLE_ORIENTATION}); 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::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::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::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_CACHE, StrId::STR_DELETE_CACHE});
items.push_back({MenuAction::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE}); items.push_back({MenuAction::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE});
return items; return items;
@@ -81,6 +93,14 @@ void EpubReaderMenuActivity::loop() {
return; 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}); setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption, pendingFontSize});
finish(); finish();
return; return;
@@ -155,6 +175,12 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); 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 // Footer / Hints

View File

@@ -6,6 +6,7 @@
#include <vector> #include <vector>
#include "../Activity.h" #include "../Activity.h"
#include "util/BookSettings.h"
#include "util/ButtonNavigator.h" #include "util/ButtonNavigator.h"
class EpubReaderMenuActivity final : public Activity { class EpubReaderMenuActivity final : public Activity {
@@ -21,6 +22,7 @@ class EpubReaderMenuActivity final : public Activity {
DISPLAY_QR, DISPLAY_QR,
GO_HOME, GO_HOME,
SYNC, SYNC,
PUSH_AND_SLEEP,
DELETE_CACHE, DELETE_CACHE,
// Mod-specific actions // Mod-specific actions
ADD_BOOKMARK, ADD_BOOKMARK,
@@ -32,13 +34,18 @@ class EpubReaderMenuActivity final : public Activity {
TOGGLE_ORIENTATION, TOGGLE_ORIENTATION,
TOGGLE_FONT_SIZE, TOGGLE_FONT_SIZE,
CLOSE_BOOK, 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, explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
const int currentPage, const int totalPages, const int bookProgressPercent, const int currentPage, const int totalPages, const int bookProgressPercent,
const uint8_t currentOrientation, const bool hasFootnotes, 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 onEnter() override;
void onExit() override; void onExit() override;
@@ -70,4 +77,26 @@ class EpubReaderMenuActivity final : public Activity {
int currentPage = 0; int currentPage = 0;
int totalPages = 0; int totalPages = 0;
int bookProgressPercent = 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 } // namespace
void KOReaderSyncActivity::deferFinish(bool success) {
RenderLock lock(*this);
pendingFinishSuccess = success;
pendingFinish = true;
}
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
if (!success) { if (!success) {
LOG_DBG("KOSync", "WiFi connection failed, exiting"); LOG_DBG("KOSync", "WiFi connection failed, exiting");
@@ -89,6 +95,10 @@ void KOReaderSyncActivity::performSync() {
documentHash = KOReaderDocumentId::calculate(epubPath); documentHash = KOReaderDocumentId::calculate(epubPath);
} }
if (documentHash.empty()) { if (documentHash.empty()) {
if (syncMode == SyncMode::PUSH_ONLY) {
deferFinish(false);
return;
}
{ {
RenderLock lock(*this); RenderLock lock(*this);
state = SYNC_FAILED; state = SYNC_FAILED;
@@ -106,11 +116,16 @@ void KOReaderSyncActivity::performSync() {
} }
requestUpdateAndWait(); requestUpdateAndWait();
if (syncMode == SyncMode::PUSH_ONLY) {
// Skip fetching remote progress entirely -- just upload local position
performUpload();
return;
}
// Fetch remote progress // Fetch remote progress
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
if (result == KOReaderSyncClient::NOT_FOUND) { if (result == KOReaderSyncClient::NOT_FOUND) {
// No remote progress - offer to upload
{ {
RenderLock lock(*this); RenderLock lock(*this);
state = NO_REMOTE_PROGRESS; state = NO_REMOTE_PROGRESS;
@@ -174,6 +189,11 @@ void KOReaderSyncActivity::performUpload() {
if (result != KOReaderSyncClient::OK) { if (result != KOReaderSyncClient::OK) {
wifiOff(); wifiOff();
if (syncMode == SyncMode::PUSH_ONLY) {
LOG_DBG("KOSync", "PUSH_ONLY upload failed: %s", KOReaderSyncClient::errorString(result));
deferFinish(false);
return;
}
{ {
RenderLock lock(*this); RenderLock lock(*this);
state = SYNC_FAILED; state = SYNC_FAILED;
@@ -184,6 +204,11 @@ void KOReaderSyncActivity::performUpload() {
} }
wifiOff(); wifiOff();
if (syncMode == SyncMode::PUSH_ONLY) {
LOG_DBG("KOSync", "PUSH_ONLY upload succeeded");
deferFinish(true);
return;
}
{ {
RenderLock lock(*this); RenderLock lock(*this);
state = UPLOAD_COMPLETE; state = UPLOAD_COMPLETE;
@@ -196,6 +221,11 @@ void KOReaderSyncActivity::onEnter() {
// Check for credentials first // Check for credentials first
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
if (syncMode == SyncMode::PUSH_ONLY) {
LOG_DBG("KOSync", "PUSH_ONLY: no credentials, finishing silently");
deferFinish(false);
return;
}
state = NO_CREDENTIALS; state = NO_CREDENTIALS;
requestUpdate(); requestUpdate();
return; return;
@@ -335,6 +365,15 @@ void KOReaderSyncActivity::render(RenderLock&&) {
} }
void KOReaderSyncActivity::loop() { 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 (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result; ActivityResult result;

View File

@@ -11,18 +11,27 @@
/** /**
* Activity for syncing reading progress with KOReader sync server. * Activity for syncing reading progress with KOReader sync server.
* *
* Flow: * Flow (INTERACTIVE):
* 1. Connect to WiFi (if not connected) * 1. Connect to WiFi (if not connected)
* 2. Calculate document hash * 2. Calculate document hash
* 3. Fetch remote progress * 3. Fetch remote progress
* 4. Show comparison and options (Apply/Upload) * 4. Show comparison and options (Apply/Upload)
* 5. Apply or upload progress * 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 { class KOReaderSyncActivity final : public Activity {
public: public:
enum class SyncMode { INTERACTIVE, PUSH_ONLY };
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex, 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), : Activity("KOReaderSync", renderer, mappedInput),
epub(epub), epub(epub),
epubPath(epubPath), epubPath(epubPath),
@@ -31,7 +40,8 @@ class KOReaderSyncActivity final : public Activity {
totalPagesInSpine(totalPagesInSpine), totalPagesInSpine(totalPagesInSpine),
remoteProgress{}, remoteProgress{},
remotePosition{}, remotePosition{},
localProgress{} {} localProgress{},
syncMode(syncMode) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
@@ -73,6 +83,14 @@ class KOReaderSyncActivity final : public Activity {
// Selection in result screen (0=Apply, 1=Upload) // Selection in result screen (0=Apply, 1=Upload)
int selectedOption = 0; 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 onWifiSelectionComplete(bool success);
void performSync(); void performSync();
void performUpload(); void performUpload();

View File

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