feat: add EndOfBookMenuActivity replacing static end-of-book text

Interactive menu shown when reaching the end of a book with options:
Archive Book, Delete Book, Back to Beginning, Close Book, Close Menu.
Wired into EpubReaderActivity, XtcReaderActivity, and TxtReaderActivity
(TXT shows menu when user tries to advance past the last page).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-21 03:04:48 -05:00
parent f5b708424d
commit 98146f2545
8 changed files with 253 additions and 9 deletions

View File

@@ -0,0 +1,97 @@
#include "EndOfBookMenuActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
void EndOfBookMenuActivity::buildMenuItems() {
menuItems.clear();
menuItems.push_back({Action::ARCHIVE, StrId::STR_ARCHIVE_BOOK});
menuItems.push_back({Action::DELETE, StrId::STR_DELETE_BOOK});
menuItems.push_back({Action::BACK_TO_BEGINNING, StrId::STR_BACK_TO_BEGINNING});
menuItems.push_back({Action::CLOSE_BOOK, StrId::STR_CLOSE_BOOK});
menuItems.push_back({Action::CLOSE_MENU, StrId::STR_CLOSE_MENU});
}
void EndOfBookMenuActivity::onEnter() {
Activity::onEnter();
selectedIndex = 0;
requestUpdate();
}
void EndOfBookMenuActivity::onExit() { Activity::onExit(); }
void EndOfBookMenuActivity::loop() {
buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectedIndex < static_cast<int>(menuItems.size())) {
auto cb = onAction;
cb(menuItems[selectedIndex].action);
return;
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
auto cb = onAction;
cb(Action::CLOSE_MENU);
return;
}
}
void EndOfBookMenuActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
constexpr int popupMargin = 20;
constexpr int lineHeight = 30;
constexpr int titleHeight = 40;
const int optionCount = static_cast<int>(menuItems.size());
const int popupH = titleHeight + popupMargin + lineHeight * optionCount + popupMargin;
const int popupW = pageWidth - 60;
const int popupX = (pageWidth - popupW) / 2;
const int popupY = (pageHeight - popupH) / 2;
// Popup border and background
renderer.fillRect(popupX - 2, popupY - 2, popupW + 4, popupH + 4, true);
renderer.fillRect(popupX, popupY, popupW, popupH, false);
// Title
renderer.drawText(UI_12_FONT_ID, popupX + popupMargin, popupY + 8, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
// Divider line
const int dividerY = popupY + titleHeight;
renderer.fillRect(popupX + 4, dividerY, popupW - 8, 1, true);
// Menu items
const int startY = dividerY + popupMargin / 2;
for (int i = 0; i < optionCount; ++i) {
const int itemY = startY + i * lineHeight;
const bool isSelected = (i == selectedIndex);
if (isSelected) {
renderer.fillRect(popupX + 2, itemY, popupW - 4, lineHeight, true);
}
renderer.drawText(UI_10_FONT_ID, popupX + popupMargin, itemY, I18N.get(menuItems[i].labelId), !isSelected);
}
// Button hints
const auto labels = mappedInput.mapLabels(tr(STR_CLOSE_MENU), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <I18n.h>
#include <functional>
#include <string>
#include <vector>
#include "../Activity.h"
#include "util/ButtonNavigator.h"
class EndOfBookMenuActivity final : public Activity {
public:
enum class Action {
ARCHIVE,
DELETE,
BACK_TO_BEGINNING,
CLOSE_BOOK,
CLOSE_MENU,
};
explicit EndOfBookMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath,
const std::function<void(Action)>& onAction)
: Activity("EndOfBookMenu", renderer, mappedInput), bookPath(bookPath), onAction(onAction) {
buildMenuItems();
}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
struct MenuItem {
Action action;
StrId labelId;
};
std::string bookPath;
std::vector<MenuItem> menuItems;
int selectedIndex = 0;
ButtonNavigator buttonNavigator;
const std::function<void(Action)> onAction;
void buildMenuItems();
};

View File

@@ -13,6 +13,7 @@
#include "EpubReaderBookmarkSelectionActivity.h"
#include "EpubReaderChapterSelectionActivity.h"
#include "EpubReaderPercentSelectionActivity.h"
#include "EndOfBookMenuActivity.h"
#include "KOReaderCredentialStore.h"
#include "KOReaderSyncActivity.h"
#include "MappedInputManager.h"
@@ -300,10 +301,11 @@ void EpubReaderActivity::loop() {
return;
}
// any botton press when at end of the book goes back to the last page
// any button press when at end of the book goes back to the last page
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX;
endOfBookMenuOpened = false;
requestUpdate();
return;
}
@@ -840,9 +842,42 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
// Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
if (!endOfBookMenuOpened) {
endOfBookMenuOpened = true;
lock.unlock();
const std::string path = epub->getPath();
enterNewActivity(new EndOfBookMenuActivity(
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
exitActivity();
switch (action) {
case EndOfBookMenuActivity::Action::ARCHIVE:
if (epub) BookManager::archiveBook(epub->getPath());
pendingGoHome = true;
break;
case EndOfBookMenuActivity::Action::DELETE:
if (epub) BookManager::deleteBook(epub->getPath());
pendingGoHome = true;
break;
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING:
currentSpineIndex = 0;
nextPageNumber = 0;
section.reset();
endOfBookMenuOpened = false;
requestUpdate();
break;
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
pendingGoHome = true;
break;
case EndOfBookMenuActivity::Action::CLOSE_MENU:
currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX;
section.reset();
endOfBookMenuOpened = false;
requestUpdate();
break;
}
}));
}
return;
}

View File

@@ -27,6 +27,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)
bool silentIndexingActive = false; // True while silently pre-indexing the next chapter
int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop)
bool endOfBookMenuOpened = false; // Guard to prevent repeated opening of EndOfBookMenuActivity
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;

View File

@@ -9,10 +9,12 @@
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "EndOfBookMenuActivity.h"
#include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/BookManager.h"
namespace {
constexpr unsigned long goHomeMs = 1000;
@@ -153,10 +155,41 @@ void TxtReaderActivity::loop() {
if (prevTriggered && currentPage > 0) {
currentPage--;
endOfBookMenuOpened = false;
requestUpdate();
} else if (nextTriggered && currentPage < totalPages - 1) {
currentPage++;
requestUpdate();
} else if (nextTriggered && currentPage == totalPages - 1 && !endOfBookMenuOpened) {
// At last page and trying to advance → show end of book menu
endOfBookMenuOpened = true;
const std::string path = txt->getPath();
enterNewActivity(new EndOfBookMenuActivity(
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
exitActivity();
switch (action) {
case EndOfBookMenuActivity::Action::ARCHIVE:
if (txt) BookManager::archiveBook(txt->getPath());
if (onGoHome) onGoHome();
return;
case EndOfBookMenuActivity::Action::DELETE:
if (txt) BookManager::deleteBook(txt->getPath());
if (onGoHome) onGoHome();
return;
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING:
currentPage = 0;
endOfBookMenuOpened = false;
requestUpdate();
break;
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
if (onGoHome) onGoHome();
return;
case EndOfBookMenuActivity::Action::CLOSE_MENU:
endOfBookMenuOpened = false;
requestUpdate();
break;
}
}));
}
}

View File

@@ -14,6 +14,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
int totalPages = 1;
int pagesUntilFullRefresh = 0;
bool endOfBookMenuOpened = false;
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;

View File

@@ -15,11 +15,13 @@
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "EndOfBookMenuActivity.h"
#include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "XtcReaderChapterSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/BookManager.h"
namespace {
constexpr unsigned long skipPageMs = 700;
@@ -155,6 +157,7 @@ void XtcReaderActivity::loop() {
// Handle end of book
if (currentPage >= xtc->getPageCount()) {
currentPage = xtc->getPageCount() - 1;
endOfBookMenuOpened = false;
requestUpdate();
return;
}
@@ -183,12 +186,39 @@ void XtcReaderActivity::render(Activity::RenderLock&&) {
return;
}
// Bounds check
// Bounds check - end of book
if (currentPage >= xtc->getPageCount()) {
// Show end of book screen
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
if (!endOfBookMenuOpened) {
endOfBookMenuOpened = true;
const std::string path = xtc->getPath();
enterNewActivity(new EndOfBookMenuActivity(
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
exitActivity();
switch (action) {
case EndOfBookMenuActivity::Action::ARCHIVE:
if (xtc) BookManager::archiveBook(xtc->getPath());
if (onGoHome) onGoHome();
break;
case EndOfBookMenuActivity::Action::DELETE:
if (xtc) BookManager::deleteBook(xtc->getPath());
if (onGoHome) onGoHome();
break;
case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING:
currentPage = 0;
endOfBookMenuOpened = false;
requestUpdate();
break;
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
if (onGoHome) onGoHome();
break;
case EndOfBookMenuActivity::Action::CLOSE_MENU:
currentPage = xtc->getPageCount() - 1;
endOfBookMenuOpened = false;
requestUpdate();
break;
}
}));
}
return;
}

View File

@@ -17,6 +17,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
uint32_t currentPage = 0;
int pagesUntilFullRefresh = 0;
bool endOfBookMenuOpened = false;
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;