diff --git a/src/activities/reader/EndOfBookMenuActivity.cpp b/src/activities/reader/EndOfBookMenuActivity.cpp new file mode 100644 index 00000000..f50af8eb --- /dev/null +++ b/src/activities/reader/EndOfBookMenuActivity.cpp @@ -0,0 +1,97 @@ +#include "EndOfBookMenuActivity.h" + +#include +#include + +#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(menuItems.size())); + requestUpdate(); + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast(menuItems.size())); + requestUpdate(); + }); + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (selectedIndex < static_cast(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(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(); +} diff --git a/src/activities/reader/EndOfBookMenuActivity.h b/src/activities/reader/EndOfBookMenuActivity.h new file mode 100644 index 00000000..dc0af064 --- /dev/null +++ b/src/activities/reader/EndOfBookMenuActivity.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include + +#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& 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 menuItems; + int selectedIndex = 0; + ButtonNavigator buttonNavigator; + const std::function onAction; + + void buildMenuItems(); +}; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 9dfaaebe..68902dcd 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -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; } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index bec036db..804e6fe1 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -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 onGoBack; const std::function onGoHome; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 9af68d00..974ec6ec 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -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; + } + })); } } diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index ca061ce2..291f673e 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -14,6 +14,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity { int totalPages = 1; int pagesUntilFullRefresh = 0; + bool endOfBookMenuOpened = false; const std::function onGoBack; const std::function onGoHome; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 1181ce3e..eeab0afa 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -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; } diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index c9e8997c..73d8cd33 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -17,6 +17,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity { uint32_t currentPage = 0; int pagesUntilFullRefresh = 0; + bool endOfBookMenuOpened = false; const std::function onGoBack; const std::function onGoHome;