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_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress"
STR_PUSH_AND_SLEEP: "Push & Sleep"
STR_DELETE_CACHE: "Delete Book Cache"
STR_DELETE: "Delete"
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
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);