adds delete and archive abilities
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "BookManager.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "ScreenComponents.h"
|
||||
@@ -22,6 +23,7 @@ constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
|
||||
// Timing thresholds
|
||||
constexpr int SKIP_PAGE_MS = 700;
|
||||
constexpr unsigned long GO_HOME_MS = 1000;
|
||||
constexpr unsigned long ACTION_MENU_MS = 700; // Long press to open action menu
|
||||
|
||||
void sortFileList(std::vector<std::string>& strs) {
|
||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||
@@ -180,7 +182,122 @@ void MyLibraryActivity::onExit() {
|
||||
files.clear();
|
||||
}
|
||||
|
||||
bool MyLibraryActivity::isSelectedItemAFile() const {
|
||||
if (currentTab == Tab::Recent) {
|
||||
return !bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size());
|
||||
} else {
|
||||
// Files tab - check if it's a file (not a directory)
|
||||
if (files.empty() || selectorIndex >= static_cast<int>(files.size())) {
|
||||
return false;
|
||||
}
|
||||
return files[selectorIndex].back() != '/';
|
||||
}
|
||||
}
|
||||
|
||||
void MyLibraryActivity::openActionMenu() {
|
||||
if (!isSelectedItemAFile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTab == Tab::Recent) {
|
||||
actionTargetPath = bookPaths[selectorIndex];
|
||||
actionTargetName = bookTitles[selectorIndex];
|
||||
} else {
|
||||
if (basepath.back() != '/') {
|
||||
actionTargetPath = basepath + "/" + files[selectorIndex];
|
||||
} else {
|
||||
actionTargetPath = basepath + files[selectorIndex];
|
||||
}
|
||||
actionTargetName = files[selectorIndex];
|
||||
}
|
||||
|
||||
uiState = UIState::ActionMenu;
|
||||
menuSelection = 0; // Default to Archive
|
||||
ignoreNextConfirmRelease = true; // Ignore the release from the long-press that opened this menu
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void MyLibraryActivity::executeAction() {
|
||||
bool success = false;
|
||||
|
||||
if (selectedAction == ActionType::Archive) {
|
||||
success = BookManager::archiveBook(actionTargetPath);
|
||||
} else {
|
||||
success = BookManager::deleteBook(actionTargetPath);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Reload data
|
||||
loadRecentBooks();
|
||||
loadFiles();
|
||||
|
||||
// Adjust selector if needed
|
||||
const int itemCount = getCurrentItemCount();
|
||||
if (selectorIndex >= itemCount && itemCount > 0) {
|
||||
selectorIndex = itemCount - 1;
|
||||
} else if (itemCount == 0) {
|
||||
selectorIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
uiState = UIState::Normal;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void MyLibraryActivity::loop() {
|
||||
// Handle action menu state
|
||||
if (uiState == UIState::ActionMenu) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
uiState = UIState::Normal;
|
||||
ignoreNextConfirmRelease = false;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||
menuSelection = 0; // Archive
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||
menuSelection = 1; // Delete
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
// Ignore the release from the long-press that opened this menu
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
selectedAction = (menuSelection == 0) ? ActionType::Archive : ActionType::Delete;
|
||||
uiState = UIState::Confirming;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle confirmation state
|
||||
if (uiState == UIState::Confirming) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
uiState = UIState::ActionMenu;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
executeAction();
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal state handling
|
||||
const int itemCount = getCurrentItemCount();
|
||||
const int pageItems = getPageItems();
|
||||
|
||||
@@ -196,6 +313,13 @@ void MyLibraryActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Long press Confirm to open action menu (only for files, not directories)
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
||||
mappedInput.getHeldTime() >= ACTION_MENU_MS && isSelectedItemAFile()) {
|
||||
openActionMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||
@@ -203,8 +327,13 @@ void MyLibraryActivity::loop() {
|
||||
|
||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||
|
||||
// Confirm button - open selected item
|
||||
// Confirm button - open selected item (short press)
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
// Ignore if it was a long press that triggered the action menu
|
||||
if (mappedInput.getHeldTime() >= ACTION_MENU_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTab == Tab::Recent) {
|
||||
if (!bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size())) {
|
||||
onSelectBook(bookPaths[selectorIndex], currentTab);
|
||||
@@ -302,6 +431,20 @@ void MyLibraryActivity::displayTaskLoop() {
|
||||
void MyLibraryActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
// Handle different UI states
|
||||
if (uiState == UIState::ActionMenu) {
|
||||
renderActionMenu();
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiState == UIState::Confirming) {
|
||||
renderConfirmation();
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal state - draw library view
|
||||
// Draw tab bar
|
||||
std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}};
|
||||
ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs);
|
||||
@@ -376,3 +519,69 @@ void MyLibraryActivity::renderFilesTab() const {
|
||||
i != selectorIndex);
|
||||
}
|
||||
}
|
||||
|
||||
void MyLibraryActivity::renderActionMenu() const {
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 20, "Book Actions", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Show filename
|
||||
const int filenameY = 70;
|
||||
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
||||
|
||||
// Menu options
|
||||
const int menuStartY = pageHeight / 2 - 30;
|
||||
constexpr int menuLineHeight = 40;
|
||||
constexpr int menuItemWidth = 120;
|
||||
const int menuX = (pageWidth - menuItemWidth) / 2;
|
||||
|
||||
// Archive option
|
||||
if (menuSelection == 0) {
|
||||
renderer.fillRect(menuX - 10, menuStartY - 5, menuItemWidth + 20, menuLineHeight);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY, "Archive", menuSelection != 0);
|
||||
|
||||
// Delete option
|
||||
if (menuSelection == 1) {
|
||||
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight - 5, menuItemWidth + 20, menuLineHeight);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight, "Delete", menuSelection != 1);
|
||||
|
||||
// Draw side button hints (up/down navigation)
|
||||
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
|
||||
|
||||
// Draw bottom button hints
|
||||
const auto labels = mappedInput.mapLabels("« Cancel", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
void MyLibraryActivity::renderConfirmation() const {
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Title based on action
|
||||
const char* actionTitle = (selectedAction == ActionType::Archive) ? "Archive Book?" : "Delete Book?";
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 20, actionTitle, true, EpdFontFamily::BOLD);
|
||||
|
||||
// Show filename
|
||||
const int filenameY = pageHeight / 2 - 40;
|
||||
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
||||
|
||||
// Warning text
|
||||
const int warningY = pageHeight / 2;
|
||||
if (selectedAction == ActionType::Archive) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY, "Book will be moved to archive.");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 25, "Reading progress will be saved.");
|
||||
} else {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY, "Book will be permanently deleted!", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 25, "This cannot be undone.");
|
||||
}
|
||||
|
||||
// Draw bottom button hints
|
||||
const auto labels = mappedInput.mapLabels("« Cancel", "Confirm", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
class MyLibraryActivity final : public Activity {
|
||||
public:
|
||||
enum class Tab { Recent, Files };
|
||||
enum class UIState { Normal, ActionMenu, Confirming };
|
||||
enum class ActionType { Archive, Delete };
|
||||
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
@@ -21,6 +23,14 @@ class MyLibraryActivity final : public Activity {
|
||||
int selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
|
||||
// Action menu state
|
||||
UIState uiState = UIState::Normal;
|
||||
ActionType selectedAction = ActionType::Archive;
|
||||
std::string actionTargetPath;
|
||||
std::string actionTargetName;
|
||||
int menuSelection = 0; // 0 = Archive, 1 = Delete
|
||||
bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu
|
||||
|
||||
// Recent tab state
|
||||
std::vector<std::string> bookTitles; // Display titles for each book
|
||||
std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing)
|
||||
@@ -50,6 +60,13 @@ class MyLibraryActivity final : public Activity {
|
||||
void render() const;
|
||||
void renderRecentTab() const;
|
||||
void renderFilesTab() const;
|
||||
void renderActionMenu() const;
|
||||
void renderConfirmation() const;
|
||||
|
||||
// Action handling
|
||||
void openActionMenu();
|
||||
void executeAction();
|
||||
bool isSelectedItemAFile() const;
|
||||
|
||||
public:
|
||||
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include "BookManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
@@ -119,6 +120,33 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle end-of-book prompt
|
||||
if (showingEndOfBookPrompt) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||
endOfBookSelection = (endOfBookSelection + 2) % 3;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||
endOfBookSelection = (endOfBookSelection + 1) % 3;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
handleEndOfBookAction();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
// Go back to last page instead
|
||||
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||
nextPageNumber = UINT16_MAX;
|
||||
showingEndOfBookPrompt = false;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter chapter selection activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
// Don't start activity transition while rendering
|
||||
@@ -260,11 +288,9 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// any botton 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;
|
||||
updateRequired = true;
|
||||
// any button press when at end of the book - this is now handled by the prompt
|
||||
// Just ensure we don't go past the end
|
||||
if (currentSpineIndex >= epub->getSpineItemsCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,13 +367,13 @@ void EpubReaderActivity::renderScreen() {
|
||||
currentSpineIndex = epub->getSpineItemsCount();
|
||||
}
|
||||
|
||||
// Show end of book screen
|
||||
// Show end of book prompt
|
||||
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD);
|
||||
renderer.displayBuffer();
|
||||
showingEndOfBookPrompt = true;
|
||||
renderEndOfBookPrompt();
|
||||
return;
|
||||
}
|
||||
showingEndOfBookPrompt = false;
|
||||
|
||||
// Apply screen viewable areas and additional padding
|
||||
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
||||
@@ -586,3 +612,60 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
||||
title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::renderEndOfBookPrompt() {
|
||||
const int pageWidth = renderer.getScreenWidth();
|
||||
const int pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 80, "Finished!", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Book title (truncated if needed)
|
||||
std::string bookTitle = epub->getTitle();
|
||||
if (bookTitle.length() > 30) {
|
||||
bookTitle = bookTitle.substr(0, 27) + "...";
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, bookTitle.c_str());
|
||||
|
||||
// Menu options
|
||||
const int menuStartY = pageHeight / 2 - 30;
|
||||
constexpr int menuLineHeight = 45;
|
||||
constexpr int menuItemWidth = 140;
|
||||
const int menuX = (pageWidth - menuItemWidth) / 2;
|
||||
|
||||
const char* options[] = {"Archive", "Delete", "Keep"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const int optionY = menuStartY + i * menuLineHeight;
|
||||
if (endOfBookSelection == i) {
|
||||
renderer.fillRect(menuX - 10, optionY - 5, menuItemWidth + 20, menuLineHeight - 5);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, optionY, options[i], endOfBookSelection != i);
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void EpubReaderActivity::handleEndOfBookAction() {
|
||||
const std::string bookPath = epub->getPath();
|
||||
|
||||
switch (endOfBookSelection) {
|
||||
case 0: // Archive
|
||||
BookManager::archiveBook(bookPath);
|
||||
onGoHome();
|
||||
break;
|
||||
case 1: // Delete
|
||||
BookManager::deleteBook(bookPath);
|
||||
onGoHome();
|
||||
break;
|
||||
case 2: // Keep
|
||||
default:
|
||||
onGoHome();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,18 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// End-of-book prompt state
|
||||
bool showingEndOfBookPrompt = false;
|
||||
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option)
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void renderScreen();
|
||||
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
|
||||
int orientedMarginBottom, int orientedMarginLeft);
|
||||
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
|
||||
void renderEndOfBookPrompt();
|
||||
void handleEndOfBookAction();
|
||||
|
||||
public:
|
||||
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <Serialization.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include "BookManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
@@ -95,6 +96,30 @@ void TxtReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle end-of-book prompt
|
||||
if (showingEndOfBookPrompt) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||
endOfBookSelection = (endOfBookSelection + 2) % 3;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||
endOfBookSelection = (endOfBookSelection + 1) % 3;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
handleEndOfBookAction();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
showingEndOfBookPrompt = false;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes directly to home
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
||||
onGoHome();
|
||||
@@ -121,9 +146,15 @@ void TxtReaderActivity::loop() {
|
||||
if (prevReleased && currentPage > 0) {
|
||||
currentPage--;
|
||||
updateRequired = true;
|
||||
} else if (nextReleased && currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
updateRequired = true;
|
||||
} else if (nextReleased) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
updateRequired = true;
|
||||
} else {
|
||||
// At last page, show end-of-book prompt
|
||||
showingEndOfBookPrompt = true;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +412,12 @@ void TxtReaderActivity::renderScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show end-of-book prompt if active
|
||||
if (showingEndOfBookPrompt) {
|
||||
renderEndOfBookPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize reader if not done
|
||||
if (!initialized) {
|
||||
renderer.clearScreen();
|
||||
@@ -698,3 +735,64 @@ void TxtReaderActivity::savePageIndexCache() const {
|
||||
f.close();
|
||||
Serial.printf("[%lu] [TRS] Saved page index cache: %d pages\n", millis(), totalPages);
|
||||
}
|
||||
|
||||
void TxtReaderActivity::renderEndOfBookPrompt() {
|
||||
const int pageWidth = renderer.getScreenWidth();
|
||||
const int pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 80, "Finished!", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Filename (truncated if needed)
|
||||
std::string filename = txt->getPath();
|
||||
const size_t lastSlash = filename.find_last_of('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
filename = filename.substr(lastSlash + 1);
|
||||
}
|
||||
if (filename.length() > 30) {
|
||||
filename = filename.substr(0, 27) + "...";
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, filename.c_str());
|
||||
|
||||
// Menu options
|
||||
const int menuStartY = pageHeight / 2 - 30;
|
||||
constexpr int menuLineHeight = 45;
|
||||
constexpr int menuItemWidth = 140;
|
||||
const int menuX = (pageWidth - menuItemWidth) / 2;
|
||||
|
||||
const char* options[] = {"Archive", "Delete", "Keep"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const int optionY = menuStartY + i * menuLineHeight;
|
||||
if (endOfBookSelection == i) {
|
||||
renderer.fillRect(menuX - 10, optionY - 5, menuItemWidth + 20, menuLineHeight - 5);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, optionY, options[i], endOfBookSelection != i);
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void TxtReaderActivity::handleEndOfBookAction() {
|
||||
const std::string bookPath = txt->getPath();
|
||||
|
||||
switch (endOfBookSelection) {
|
||||
case 0: // Archive
|
||||
BookManager::archiveBook(bookPath);
|
||||
onGoHome();
|
||||
break;
|
||||
case 1: // Delete
|
||||
BookManager::deleteBook(bookPath);
|
||||
onGoHome();
|
||||
break;
|
||||
case 2: // Keep
|
||||
default:
|
||||
onGoHome();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// End-of-book prompt state
|
||||
bool showingEndOfBookPrompt = false;
|
||||
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep
|
||||
|
||||
// Streaming text reader - stores file offsets for each page
|
||||
std::vector<size_t> pageOffsets; // File offset for start of each page
|
||||
std::vector<std::string> currentPageLines;
|
||||
@@ -38,6 +42,8 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
||||
void renderScreen();
|
||||
void renderPage();
|
||||
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
|
||||
void renderEndOfBookPrompt();
|
||||
void handleEndOfBookAction();
|
||||
|
||||
void initializeReader();
|
||||
bool loadPageAtOffset(size_t offset, std::vector<std::string>& outLines, size_t& nextOffset);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include "BookManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
@@ -79,6 +80,32 @@ void XtcReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle end-of-book prompt
|
||||
if (showingEndOfBookPrompt) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||
endOfBookSelection = (endOfBookSelection + 2) % 3;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||
endOfBookSelection = (endOfBookSelection + 1) % 3;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
handleEndOfBookAction();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
// Go back to last page
|
||||
currentPage = xtc->getPageCount() - 1;
|
||||
showingEndOfBookPrompt = false;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter chapter selection activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||
@@ -122,10 +149,8 @@ void XtcReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle end of book
|
||||
// If at end of book prompt position, handle differently
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
currentPage = xtc->getPageCount() - 1;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +167,7 @@ void XtcReaderActivity::loop() {
|
||||
} else if (nextReleased) {
|
||||
currentPage += skipAmount;
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
currentPage = xtc->getPageCount(); // Allow showing "End of book"
|
||||
currentPage = xtc->getPageCount(); // Will trigger end-of-book prompt
|
||||
}
|
||||
updateRequired = true;
|
||||
}
|
||||
@@ -165,14 +190,13 @@ void XtcReaderActivity::renderScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
// Bounds check - show end-of-book prompt
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
// Show end of book screen
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD);
|
||||
renderer.displayBuffer();
|
||||
showingEndOfBookPrompt = true;
|
||||
renderEndOfBookPrompt();
|
||||
return;
|
||||
}
|
||||
showingEndOfBookPrompt = false;
|
||||
|
||||
renderPage();
|
||||
saveProgress();
|
||||
@@ -389,3 +413,64 @@ void XtcReaderActivity::loadProgress() {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
void XtcReaderActivity::renderEndOfBookPrompt() {
|
||||
const int pageWidth = renderer.getScreenWidth();
|
||||
const int pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 80, "Finished!", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Filename (truncated if needed)
|
||||
std::string filename = xtc->getPath();
|
||||
const size_t lastSlash = filename.find_last_of('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
filename = filename.substr(lastSlash + 1);
|
||||
}
|
||||
if (filename.length() > 30) {
|
||||
filename = filename.substr(0, 27) + "...";
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, filename.c_str());
|
||||
|
||||
// Menu options
|
||||
const int menuStartY = pageHeight / 2 - 30;
|
||||
constexpr int menuLineHeight = 45;
|
||||
constexpr int menuItemWidth = 140;
|
||||
const int menuX = (pageWidth - menuItemWidth) / 2;
|
||||
|
||||
const char* options[] = {"Archive", "Delete", "Keep"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const int optionY = menuStartY + i * menuLineHeight;
|
||||
if (endOfBookSelection == i) {
|
||||
renderer.fillRect(menuX - 10, optionY - 5, menuItemWidth + 20, menuLineHeight - 5);
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, optionY, options[i], endOfBookSelection != i);
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void XtcReaderActivity::handleEndOfBookAction() {
|
||||
const std::string bookPath = xtc->getPath();
|
||||
|
||||
switch (endOfBookSelection) {
|
||||
case 0: // Archive
|
||||
BookManager::archiveBook(bookPath);
|
||||
onGoHome();
|
||||
break;
|
||||
case 1: // Delete
|
||||
BookManager::deleteBook(bookPath);
|
||||
onGoHome();
|
||||
break;
|
||||
case 2: // Keep
|
||||
default:
|
||||
onGoHome();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,18 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// End-of-book prompt state
|
||||
bool showingEndOfBookPrompt = false;
|
||||
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void renderScreen();
|
||||
void renderPage();
|
||||
void saveProgress() const;
|
||||
void loadProgress();
|
||||
void renderEndOfBookPrompt();
|
||||
void handleEndOfBookAction();
|
||||
|
||||
public:
|
||||
explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Xtc> xtc,
|
||||
|
||||
Reference in New Issue
Block a user