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:
97
src/activities/reader/EndOfBookMenuActivity.cpp
Normal file
97
src/activities/reader/EndOfBookMenuActivity.cpp
Normal 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();
|
||||||
|
}
|
||||||
46
src/activities/reader/EndOfBookMenuActivity.h
Normal file
46
src/activities/reader/EndOfBookMenuActivity.h
Normal 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();
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "EpubReaderBookmarkSelectionActivity.h"
|
#include "EpubReaderBookmarkSelectionActivity.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
#include "EpubReaderPercentSelectionActivity.h"
|
#include "EpubReaderPercentSelectionActivity.h"
|
||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
#include "KOReaderSyncActivity.h"
|
#include "KOReaderSyncActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
@@ -300,10 +301,11 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
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()) {
|
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
||||||
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||||
nextPageNumber = UINT16_MAX;
|
nextPageNumber = UINT16_MAX;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -840,9 +842,42 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) {
|
|||||||
|
|
||||||
// Show end of book screen
|
// Show end of book screen
|
||||||
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
||||||
renderer.clearScreen();
|
if (!endOfBookMenuOpened) {
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
endOfBookMenuOpened = true;
|
||||||
renderer.displayBuffer();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)
|
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
|
bool silentIndexingActive = false; // True while silently pre-indexing the next chapter
|
||||||
int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop)
|
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()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
constexpr unsigned long goHomeMs = 1000;
|
||||||
@@ -153,10 +155,41 @@ void TxtReaderActivity::loop() {
|
|||||||
|
|
||||||
if (prevTriggered && currentPage > 0) {
|
if (prevTriggered && currentPage > 0) {
|
||||||
currentPage--;
|
currentPage--;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else if (nextTriggered && currentPage < totalPages - 1) {
|
} else if (nextTriggered && currentPage < totalPages - 1) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
requestUpdate();
|
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;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
|||||||
int totalPages = 1;
|
int totalPages = 1;
|
||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
|
|
||||||
|
bool endOfBookMenuOpened = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
|
#include "EndOfBookMenuActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "XtcReaderChapterSelectionActivity.h"
|
#include "XtcReaderChapterSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/BookManager.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long skipPageMs = 700;
|
constexpr unsigned long skipPageMs = 700;
|
||||||
@@ -155,6 +157,7 @@ void XtcReaderActivity::loop() {
|
|||||||
// Handle end of book
|
// Handle end of book
|
||||||
if (currentPage >= xtc->getPageCount()) {
|
if (currentPage >= xtc->getPageCount()) {
|
||||||
currentPage = xtc->getPageCount() - 1;
|
currentPage = xtc->getPageCount() - 1;
|
||||||
|
endOfBookMenuOpened = false;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,12 +186,39 @@ void XtcReaderActivity::render(Activity::RenderLock&&) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounds check
|
// Bounds check - end of book
|
||||||
if (currentPage >= xtc->getPageCount()) {
|
if (currentPage >= xtc->getPageCount()) {
|
||||||
// Show end of book screen
|
if (!endOfBookMenuOpened) {
|
||||||
renderer.clearScreen();
|
endOfBookMenuOpened = true;
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
const std::string path = xtc->getPath();
|
||||||
renderer.displayBuffer();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
|||||||
uint32_t currentPage = 0;
|
uint32_t currentPage = 0;
|
||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
|
|
||||||
|
bool endOfBookMenuOpened = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user