From 9464df1727b390ddb83b71af3909a048d18eb22d Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 7 Mar 2026 16:53:17 -0500 Subject: [PATCH] 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 --- chat-summaries/2026-03-07_18-00-summary.md | 50 +++++++ chat-summaries/2026-03-07_19-00-summary.md | 42 ++++++ chat-summaries/2026-03-07_20-00-summary.md | 61 +++++++++ lib/I18n/translations/english.yaml | 1 + src/activities/ActivityManager.h | 7 + src/activities/reader/EpubReaderActivity.cpp | 129 +++++++++++++++++- src/activities/reader/EpubReaderActivity.h | 7 + .../reader/EpubReaderMenuActivity.cpp | 32 ++++- .../reader/EpubReaderMenuActivity.h | 33 ++++- .../reader/KOReaderSyncActivity.cpp | 41 +++++- src/activities/reader/KOReaderSyncActivity.h | 24 +++- src/main.cpp | 6 + 12 files changed, 422 insertions(+), 11 deletions(-) create mode 100644 chat-summaries/2026-03-07_18-00-summary.md create mode 100644 chat-summaries/2026-03-07_19-00-summary.md create mode 100644 chat-summaries/2026-03-07_20-00-summary.md diff --git a/chat-summaries/2026-03-07_18-00-summary.md b/chat-summaries/2026-03-07_18-00-summary.md new file mode 100644 index 00000000..5c8bca31 --- /dev/null +++ b/chat-summaries/2026-03-07_18-00-summary.md @@ -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 diff --git a/chat-summaries/2026-03-07_19-00-summary.md b/chat-summaries/2026-03-07_19-00-summary.md new file mode 100644 index 00000000..0c9afbb8 --- /dev/null +++ b/chat-summaries/2026-03-07_19-00-summary.md @@ -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 diff --git a/chat-summaries/2026-03-07_20-00-summary.md b/chat-summaries/2026-03-07_20-00-summary.md new file mode 100644 index 00000000..0b731931 --- /dev/null +++ b/chat-summaries/2026-03-07_20-00-summary.md @@ -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 diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index becc9a6f..f04ba181 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -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" diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 3d167e09..fc762c78 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -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); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 26a806d5..efeb991e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -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(renderer, mappedInput, epub->getPath()), + [this](const ActivityResult& result) { + if (result.isCancelled) { + return; + } + const auto action = static_cast(std::get(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( 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(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(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; } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 3bdd9372..dc8d0b4b 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -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 currentPageFootnotes; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 18c0e3fd..6cb44b93 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -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::buildMenuItems(bool hasFootnotes, bool isBookmarked) { std::vector 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::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(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 diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 34a14b17..467d8069 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -6,6 +6,7 @@ #include #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 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(index - 1); + } + + void saveLetterboxFill() const { + auto settings = BookSettings::load(bookCachePath); + settings.letterboxFillOverride = pendingLetterboxFill; + BookSettings::save(bookCachePath, settings); + } }; diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 4df71e93..8d092aa1 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -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; diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index 71bdbf2f..1d302434 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -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, 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(); diff --git a/src/main.cpp b/src/main.cpp index 5e51b1ae..e3fc5a2c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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);