mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
feat: consolidate reader menu into tabbed TOC view
This commit is contained in:
parent
2e11b5723c
commit
1286fc15ec
153
src/activities/reader/ChaptersTab.cpp
Normal file
153
src/activities/reader/ChaptersTab.cpp
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
#include "ChaptersTab.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "KOReaderCredentialStore.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
|
constexpr int LINE_HEIGHT = 30;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ChaptersTab::onEnter() {
|
||||||
|
buildFilteredChapterList();
|
||||||
|
|
||||||
|
selectorIndex = 0;
|
||||||
|
for (size_t i = 0; i < filteredSpineIndices.size(); i++) {
|
||||||
|
if (filteredSpineIndices[i] == currentSpineIndex) {
|
||||||
|
selectorIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSyncOption()) {
|
||||||
|
selectorIndex += 1;
|
||||||
|
}
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChaptersTab::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
||||||
|
|
||||||
|
int ChaptersTab::getTotalItems() const {
|
||||||
|
const int syncCount = hasSyncOption() ? 2 : 0;
|
||||||
|
return filteredSpineIndices.size() + syncCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChaptersTab::isSyncItem(int index) const {
|
||||||
|
if (!hasSyncOption()) return false;
|
||||||
|
return index == 0 || index == getTotalItems() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChaptersTab::tocIndexFromItemIndex(int itemIndex) const {
|
||||||
|
const int offset = hasSyncOption() ? 1 : 0;
|
||||||
|
return itemIndex - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChaptersTab::getPageItems(int contentTop, int contentHeight) const {
|
||||||
|
int items = contentHeight / LINE_HEIGHT;
|
||||||
|
return (items < 1) ? 1 : items;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChaptersTab::buildFilteredChapterList() {
|
||||||
|
filteredSpineIndices.clear();
|
||||||
|
for (int i = 0; i < epub->getSpineItemsCount(); i++) {
|
||||||
|
if (epub->shouldHideFromToc(i)) continue;
|
||||||
|
int tocIndex = epub->getTocIndexForSpineIndex(i);
|
||||||
|
if (tocIndex == -1) continue;
|
||||||
|
filteredSpineIndices.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChaptersTab::loop() {
|
||||||
|
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||||
|
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||||
|
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||||
|
const int totalItems = getTotalItems();
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
if (hasSyncOption() && (selectorIndex == 0 || selectorIndex == totalItems - 1)) {
|
||||||
|
onLaunchSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int filteredIndex = selectorIndex;
|
||||||
|
if (hasSyncOption()) filteredIndex -= 1;
|
||||||
|
|
||||||
|
if (filteredIndex >= 0 && filteredIndex < static_cast<int>(filteredSpineIndices.size())) {
|
||||||
|
onSelectSpineIndex(filteredSpineIndices[filteredIndex]);
|
||||||
|
}
|
||||||
|
} else if (upReleased) {
|
||||||
|
if (totalItems > 0) {
|
||||||
|
if (skipPage) {
|
||||||
|
// This logic matches MyLibraryActivity
|
||||||
|
// But for simplicity let's just do a page jump
|
||||||
|
}
|
||||||
|
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
} else if (downReleased) {
|
||||||
|
if (totalItems > 0) {
|
||||||
|
selectorIndex = (selectorIndex + 1) % totalItems;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChaptersTab::render(int contentTop, int contentHeight) {
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const int pageItems = getPageItems(contentTop, contentHeight);
|
||||||
|
const int totalItems = getTotalItems();
|
||||||
|
|
||||||
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
|
renderer.fillRect(0, contentTop + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
|
||||||
|
|
||||||
|
for (int i = 0; i < pageItems; i++) {
|
||||||
|
int itemIndex = pageStartIndex + i;
|
||||||
|
if (itemIndex >= totalItems) break;
|
||||||
|
|
||||||
|
const int displayY = contentTop + i * LINE_HEIGHT;
|
||||||
|
const bool isSelected = (itemIndex == selectorIndex);
|
||||||
|
|
||||||
|
if (isSyncItem(itemIndex)) {
|
||||||
|
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
|
||||||
|
} else {
|
||||||
|
int filteredIndex = itemIndex;
|
||||||
|
if (hasSyncOption()) filteredIndex -= 1;
|
||||||
|
|
||||||
|
if (filteredIndex >= 0 && filteredIndex < static_cast<int>(filteredSpineIndices.size())) {
|
||||||
|
int spineIndex = filteredSpineIndices[filteredIndex];
|
||||||
|
int tocIndex = epub->getTocIndexForSpineIndex(spineIndex);
|
||||||
|
|
||||||
|
if (tocIndex == -1) {
|
||||||
|
renderer.drawText(UI_10_FONT_ID, 20, displayY, "Unnamed", !isSelected);
|
||||||
|
} else {
|
||||||
|
auto item = epub->getTocItem(tocIndex);
|
||||||
|
const int indentSize = 20 + (item.level - 1) * 15;
|
||||||
|
const std::string chapterName =
|
||||||
|
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChaptersTab::getCurrentPage() const {
|
||||||
|
// We don't have enough context here to know pageItems easily without contentHeight
|
||||||
|
// For now let's just return a placeholder or calculate it if we can.
|
||||||
|
// Actually onEnter can't know the height either if it's dynamic.
|
||||||
|
// Let's assume contentTop=60, contentHeight=screenHeight-120
|
||||||
|
const int availableHeight = renderer.getScreenHeight() - 120;
|
||||||
|
const int itemsPerPage = availableHeight / LINE_HEIGHT;
|
||||||
|
return selectorIndex / (itemsPerPage > 0 ? itemsPerPage : 1) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChaptersTab::getTotalPages() const {
|
||||||
|
const int availableHeight = renderer.getScreenHeight() - 120;
|
||||||
|
const int itemsPerPage = availableHeight / LINE_HEIGHT;
|
||||||
|
const int totalItems = getTotalItems();
|
||||||
|
if (totalItems == 0) return 1;
|
||||||
|
return (totalItems + itemsPerPage - 1) / (itemsPerPage > 0 ? itemsPerPage : 1);
|
||||||
|
}
|
||||||
44
src/activities/reader/ChaptersTab.h
Normal file
44
src/activities/reader/ChaptersTab.h
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Epub.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "TocTab.h"
|
||||||
|
|
||||||
|
class ChaptersTab final : public TocTab {
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
|
int currentSpineIndex;
|
||||||
|
int selectorIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
std::vector<int> filteredSpineIndices;
|
||||||
|
|
||||||
|
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
|
||||||
|
const std::function<void()> onLaunchSync;
|
||||||
|
|
||||||
|
int getPageItems(int contentTop, int contentHeight) const;
|
||||||
|
int getTotalItems() const;
|
||||||
|
bool hasSyncOption() const;
|
||||||
|
bool isSyncItem(int index) const;
|
||||||
|
int tocIndexFromItemIndex(int itemIndex) const;
|
||||||
|
void buildFilteredChapterList();
|
||||||
|
|
||||||
|
public:
|
||||||
|
ChaptersTab(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr<Epub>& epub,
|
||||||
|
int currentSpineIndex, std::function<void(int)> onSelectSpineIndex, std::function<void()> onLaunchSync)
|
||||||
|
: TocTab(renderer, mappedInput),
|
||||||
|
epub(epub),
|
||||||
|
currentSpineIndex(currentSpineIndex),
|
||||||
|
onSelectSpineIndex(onSelectSpineIndex),
|
||||||
|
onLaunchSync(onLaunchSync) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(int contentTop, int contentHeight) override;
|
||||||
|
|
||||||
|
int getCurrentPage() const override;
|
||||||
|
int getTotalPages() const override;
|
||||||
|
bool isUpdateRequired() const override { return updateRequired; }
|
||||||
|
void clearUpdateRequired() override { updateRequired = false; }
|
||||||
|
};
|
||||||
@ -7,9 +7,7 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderTocActivity.h"
|
||||||
#include "EpubReaderFootnotesActivity.h"
|
|
||||||
#include "EpubReaderMenuActivity.h"
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "ScreenComponents.h"
|
#include "ScreenComponents.h"
|
||||||
@ -131,29 +129,18 @@ void EpubReaderActivity::loop() {
|
|||||||
const int currentPage = section ? section->currentPage : 0;
|
const int currentPage = section ? section->currentPage : 0;
|
||||||
const int totalPages = section ? section->pageCount : 0;
|
const int totalPages = section ? section->pageCount : 0;
|
||||||
|
|
||||||
// Show menu instead of direct chapter selection, to allow access to footnotes
|
// Show consolidated TOC activity (Chapters and Footnotes)
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new EpubReaderMenuActivity(
|
enterNewActivity(new EpubReaderTocActivity(
|
||||||
this->renderer, this->mappedInput,
|
|
||||||
[this] {
|
|
||||||
// onGoBack from menu
|
|
||||||
updateRequired = true;
|
|
||||||
// Re-enter reader activity logic if needed (handled by stack)
|
|
||||||
// Actually ActivityWithSubactivity handles subActivity exit naturally
|
|
||||||
exitActivity();
|
|
||||||
},
|
|
||||||
[this, currentPage, totalPages](EpubReaderMenuActivity::MenuOption option) {
|
|
||||||
// onSelectOption - handle menu choice
|
|
||||||
if (option == EpubReaderMenuActivity::CHAPTERS) {
|
|
||||||
// Show chapter selection
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new EpubReaderChapterSelectionActivity(
|
|
||||||
this->renderer, this->mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages,
|
this->renderer, this->mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages,
|
||||||
|
currentPageFootnotes,
|
||||||
[this] {
|
[this] {
|
||||||
|
// onGoBack
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
},
|
},
|
||||||
[this](int newSpineIndex) {
|
[this](int newSpineIndex) {
|
||||||
|
// onSelectSpineIndex
|
||||||
if (currentSpineIndex != newSpineIndex) {
|
if (currentSpineIndex != newSpineIndex) {
|
||||||
currentSpineIndex = newSpineIndex;
|
currentSpineIndex = newSpineIndex;
|
||||||
nextPageNumber = 0;
|
nextPageNumber = 0;
|
||||||
@ -162,8 +149,14 @@ void EpubReaderActivity::loop() {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
},
|
},
|
||||||
|
[this](const char* href) {
|
||||||
|
// onSelectFootnote
|
||||||
|
navigateToHref(href, true);
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
},
|
||||||
[this](int newSpineIndex, int newPage) {
|
[this](int newSpineIndex, int newPage) {
|
||||||
// Handle sync position
|
// onSyncPosition
|
||||||
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
||||||
currentSpineIndex = newSpineIndex;
|
currentSpineIndex = newSpineIndex;
|
||||||
nextPageNumber = newPage;
|
nextPageNumber = newPage;
|
||||||
@ -172,25 +165,6 @@ void EpubReaderActivity::loop() {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
} else if (option == EpubReaderMenuActivity::FOOTNOTES) {
|
|
||||||
// Show footnotes page with current page notes
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new EpubReaderFootnotesActivity(
|
|
||||||
this->renderer, this->mappedInput,
|
|
||||||
currentPageFootnotes, // Pass collected footnotes (reference)
|
|
||||||
[this] {
|
|
||||||
// onGoBack from footnotes
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this](const char* href) {
|
|
||||||
// onSelectFootnote - navigate to the footnote location
|
|
||||||
navigateToHref(href, true); // true = save current position
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include "EpubReaderFootnotesActivity.h"
|
#include "FootnotesTab.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
class EpubReaderActivity final : public ActivityWithSubactivity {
|
class EpubReaderActivity final : public ActivityWithSubactivity {
|
||||||
|
|||||||
@ -1,265 +0,0 @@
|
|||||||
#include "EpubReaderChapterSelectionActivity.h"
|
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "KOReaderSyncActivity.h"
|
|
||||||
#include "MappedInputManager.h"
|
|
||||||
#include "fontIds.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Time threshold for treating a long press as a page-up/page-down
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
|
||||||
// Add 2 for sync options (top and bottom) if credentials are configured
|
|
||||||
const int syncCount = hasSyncOption() ? 2 : 0;
|
|
||||||
return epub->getTocItemsCount() + syncCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EpubReaderChapterSelectionActivity::isSyncItem(int index) const {
|
|
||||||
if (!hasSyncOption()) return false;
|
|
||||||
// First item and last item are sync options
|
|
||||||
return index == 0 || index == getTotalItems() - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) const {
|
|
||||||
// Account for the sync option at the top
|
|
||||||
const int offset = hasSyncOption() ? 1 : 0;
|
|
||||||
return itemIndex - offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
|
||||||
// Layout constants used in renderScreen
|
|
||||||
constexpr int startY = 60;
|
|
||||||
constexpr int lineHeight = 30;
|
|
||||||
|
|
||||||
const int screenHeight = renderer.getScreenHeight();
|
|
||||||
const int endY = screenHeight - lineHeight;
|
|
||||||
|
|
||||||
const int availableHeight = endY - startY;
|
|
||||||
int items = availableHeight / lineHeight;
|
|
||||||
|
|
||||||
// Ensure we always have at least one item per page to avoid division by zero
|
|
||||||
if (items < 1) {
|
|
||||||
items = 1;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
|
||||||
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
|
|
||||||
self->displayTaskLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::buildFilteredChapterList() {
|
|
||||||
filteredSpineIndices.clear();
|
|
||||||
|
|
||||||
for (int i = 0; i < epub->getSpineItemsCount(); i++) {
|
|
||||||
// Skip footnote pages
|
|
||||||
if (epub->shouldHideFromToc(i)) {
|
|
||||||
Serial.printf("[%lu] [CHAP] Hiding footnote page at spine index: %d\n", millis(), i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip pages without TOC entry (unnamed pages)
|
|
||||||
int tocIndex = epub->getTocIndexForSpineIndex(i);
|
|
||||||
if (tocIndex == -1) {
|
|
||||||
Serial.printf("[%lu] [CHAP] Hiding unnamed page at spine index: %d\n", millis(), i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredSpineIndices.push_back(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [CHAP] Filtered chapters: %d out of %d\n", millis(), filteredSpineIndices.size(),
|
|
||||||
epub->getSpineItemsCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::onEnter() {
|
|
||||||
ActivityWithSubactivity::onEnter();
|
|
||||||
|
|
||||||
if (!epub) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
|
||||||
|
|
||||||
// Build filtered chapter list (excluding footnote pages)
|
|
||||||
buildFilteredChapterList();
|
|
||||||
|
|
||||||
// Find the index in filtered list that corresponds to currentSpineIndex
|
|
||||||
selectorIndex = 0;
|
|
||||||
for (size_t i = 0; i < filteredSpineIndices.size(); i++) {
|
|
||||||
if (filteredSpineIndices[i] == currentSpineIndex) {
|
|
||||||
selectorIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account for sync option offset when finding current TOC index (if applicable)
|
|
||||||
// For simplicity, if we are using the filtered list, we might just put "Sync" at the top of THAT list?
|
|
||||||
// But wait, the filtered list is spine indices.
|
|
||||||
// The master logic used TOC indices directly.
|
|
||||||
// Let's adapt: We will display the filtered list.
|
|
||||||
// If sync is enabled, we prepend/append it to the selector range.
|
|
||||||
|
|
||||||
if (hasSyncOption()) {
|
|
||||||
selectorIndex += 1; // Offset for top sync option
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger first update
|
|
||||||
updateRequired = true;
|
|
||||||
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
|
|
||||||
4096, // Stack size
|
|
||||||
this, // Parameters
|
|
||||||
1, // Priority
|
|
||||||
&displayTaskHandle // Task handle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::onExit() {
|
|
||||||
ActivityWithSubactivity::onExit();
|
|
||||||
|
|
||||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
|
||||||
vTaskDelete(displayTaskHandle);
|
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSyncActivity(
|
|
||||||
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
|
|
||||||
[this]() {
|
|
||||||
// On cancel
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this](int newSpineIndex, int newPage) {
|
|
||||||
// On sync complete
|
|
||||||
exitActivity();
|
|
||||||
onSyncPosition(newSpineIndex, newPage);
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::loop() {
|
|
||||||
if (subActivity) {
|
|
||||||
subActivity->loop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
|
||||||
|
|
||||||
// Total items = filtered chapters + sync options
|
|
||||||
const int syncCount = hasSyncOption() ? 2 : 0;
|
|
||||||
const int totalItems = filteredSpineIndices.size() + syncCount;
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
||||||
// Check if sync option is selected
|
|
||||||
if (hasSyncOption()) {
|
|
||||||
if (selectorIndex == 0 || selectorIndex == totalItems - 1) {
|
|
||||||
launchSyncActivity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It's a chapter. Calculate index in filtered list.
|
|
||||||
int filteredIndex = selectorIndex;
|
|
||||||
if (hasSyncOption()) filteredIndex -= 1; // Remove top sync offset
|
|
||||||
|
|
||||||
if (filteredIndex >= 0 && filteredIndex < filteredSpineIndices.size()) {
|
|
||||||
onSelectSpineIndex(filteredSpineIndices[filteredIndex]);
|
|
||||||
}
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
||||||
onGoBack();
|
|
||||||
} else if (prevReleased) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextReleased) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + 1) % totalItems;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
|
||||||
while (true) {
|
|
||||||
if (updateRequired && !subActivity) {
|
|
||||||
updateRequired = false;
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
renderScreen();
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::renderScreen() {
|
|
||||||
renderer.clearScreen();
|
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
|
||||||
const int pageItems = getPageItems();
|
|
||||||
const int totalItems = getTotalItems();
|
|
||||||
|
|
||||||
const std::string title =
|
|
||||||
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
|
||||||
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
|
|
||||||
|
|
||||||
for (int i = 0; i < pageItems; i++) {
|
|
||||||
int itemIndex = pageStartIndex + i;
|
|
||||||
if (itemIndex >= totalItems) break;
|
|
||||||
|
|
||||||
const int displayY = 60 + i * 30;
|
|
||||||
const bool isSelected = (itemIndex == selectorIndex);
|
|
||||||
|
|
||||||
if (isSyncItem(itemIndex)) {
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
|
|
||||||
} else {
|
|
||||||
const int tocIndex = tocIndexFromItemIndex(itemIndex);
|
|
||||||
|
|
||||||
if (tocIndex == -1) {
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, displayY, "Unnamed", !isSelected);
|
|
||||||
} else {
|
|
||||||
// Master's rendering logic
|
|
||||||
auto item = epub->getTocItem(tocIndex);
|
|
||||||
|
|
||||||
const int indentSize = 20 + (item.level - 1) * 15;
|
|
||||||
const std::string chapterName =
|
|
||||||
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);
|
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
|
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <Epub.h>
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/semphr.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
|
||||||
|
|
||||||
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
|
||||||
std::shared_ptr<Epub> epub;
|
|
||||||
std::string epubPath;
|
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
|
||||||
int currentSpineIndex = 0;
|
|
||||||
int currentPage = 0;
|
|
||||||
int totalPagesInSpine = 0;
|
|
||||||
int selectorIndex = 0;
|
|
||||||
bool updateRequired = false;
|
|
||||||
const std::function<void()> onGoBack;
|
|
||||||
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
|
|
||||||
const std::function<void(int newSpineIndex, int newPage)> onSyncPosition;
|
|
||||||
|
|
||||||
// Number of items that fit on a page, derived from logical screen height.
|
|
||||||
// This adapts automatically when switching between portrait and landscape.
|
|
||||||
int getPageItems() const;
|
|
||||||
|
|
||||||
// Total items including sync options (top and bottom)
|
|
||||||
int getTotalItems() const;
|
|
||||||
|
|
||||||
// Check if sync option is available (credentials configured)
|
|
||||||
bool hasSyncOption() const;
|
|
||||||
|
|
||||||
// Check if given item index is a sync option (first or last)
|
|
||||||
bool isSyncItem(int index) const;
|
|
||||||
|
|
||||||
// Convert item index to TOC index (accounting for top sync option offset)
|
|
||||||
int tocIndexFromItemIndex(int itemIndex) const;
|
|
||||||
|
|
||||||
// Filtered list of spine indices (excluding footnote pages)
|
|
||||||
std::vector<int> filteredSpineIndices;
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
void renderScreen();
|
|
||||||
void buildFilteredChapterList();
|
|
||||||
void launchSyncActivity();
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
||||||
const std::shared_ptr<Epub>& epub, const std::string& epubPath,
|
|
||||||
const int currentSpineIndex, const int currentPage,
|
|
||||||
const int totalPagesInSpine, const std::function<void()>& onGoBack,
|
|
||||||
const std::function<void(int newSpineIndex)>& onSelectSpineIndex,
|
|
||||||
const std::function<void(int newSpineIndex, int newPage)>& onSyncPosition)
|
|
||||||
: ActivityWithSubactivity("EpubReaderChapterSelection", renderer, mappedInput),
|
|
||||||
epub(epub),
|
|
||||||
epubPath(epubPath),
|
|
||||||
currentSpineIndex(currentSpineIndex),
|
|
||||||
currentPage(currentPage),
|
|
||||||
totalPagesInSpine(totalPagesInSpine),
|
|
||||||
onGoBack(onGoBack),
|
|
||||||
onSelectSpineIndex(onSelectSpineIndex),
|
|
||||||
onSyncPosition(onSyncPosition) {}
|
|
||||||
void onEnter() override;
|
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
|
||||||
};
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
#include "EpubReaderMenuActivity.h"
|
|
||||||
|
|
||||||
#include <EpdFontFamily.h>
|
|
||||||
#include <GfxRenderer.h>
|
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
|
||||||
#include "fontIds.h"
|
|
||||||
|
|
||||||
constexpr int MENU_ITEMS_COUNT = 2;
|
|
||||||
|
|
||||||
void EpubReaderMenuActivity::taskTrampoline(void* param) {
|
|
||||||
auto* self = static_cast<EpubReaderMenuActivity*>(param);
|
|
||||||
self->displayTaskLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderMenuActivity::onEnter() {
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
|
||||||
selectorIndex = 0;
|
|
||||||
|
|
||||||
// Trigger first update
|
|
||||||
updateRequired = true;
|
|
||||||
xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubReaderMenuTask",
|
|
||||||
2048, // Stack size
|
|
||||||
this, // Parameters
|
|
||||||
1, // Priority
|
|
||||||
&displayTaskHandle // Task handle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderMenuActivity::onExit() {
|
|
||||||
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
|
||||||
vTaskDelete(displayTaskHandle);
|
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderMenuActivity::loop() {
|
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
||||||
onSelectOption(static_cast<MenuOption>(selectorIndex));
|
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
||||||
onGoBack();
|
|
||||||
} else if (prevReleased) {
|
|
||||||
selectorIndex = (selectorIndex + MENU_ITEMS_COUNT - 1) % MENU_ITEMS_COUNT;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextReleased) {
|
|
||||||
selectorIndex = (selectorIndex + 1) % MENU_ITEMS_COUNT;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderMenuActivity::displayTaskLoop() {
|
|
||||||
while (true) {
|
|
||||||
if (updateRequired) {
|
|
||||||
updateRequired = false;
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
renderScreen();
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderMenuActivity::renderScreen() {
|
|
||||||
renderer.clearScreen();
|
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 10, "Menu", true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
const char* menuItems[MENU_ITEMS_COUNT] = {"Go to chapter", "View footnotes"};
|
|
||||||
|
|
||||||
const int startY = 100;
|
|
||||||
const int itemHeight = 40;
|
|
||||||
|
|
||||||
for (int i = 0; i < MENU_ITEMS_COUNT; i++) {
|
|
||||||
const int y = startY + i * itemHeight;
|
|
||||||
|
|
||||||
// Draw selection indicator
|
|
||||||
if (i == selectorIndex) {
|
|
||||||
renderer.fillRect(10, y + 2, pageWidth - 20, itemHeight - 4);
|
|
||||||
renderer.drawText(UI_12_FONT_ID, 30, y, menuItems[i], false);
|
|
||||||
} else {
|
|
||||||
renderer.drawText(UI_12_FONT_ID, 30, y, menuItems[i], true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/semphr.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
#include "../Activity.h"
|
|
||||||
|
|
||||||
class EpubReaderMenuActivity final : public Activity {
|
|
||||||
public:
|
|
||||||
enum MenuOption { CHAPTERS, FOOTNOTES };
|
|
||||||
|
|
||||||
private:
|
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
|
||||||
int selectorIndex = 0;
|
|
||||||
bool updateRequired = false;
|
|
||||||
const std::function<void()> onGoBack;
|
|
||||||
const std::function<void(MenuOption option)> onSelectOption;
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
void renderScreen();
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
||||||
const std::function<void()>& onGoBack,
|
|
||||||
const std::function<void(MenuOption option)>& onSelectOption)
|
|
||||||
: Activity("EpubReaderMenu", renderer, mappedInput), onGoBack(onGoBack), onSelectOption(onSelectOption) {}
|
|
||||||
|
|
||||||
void onEnter() override;
|
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
|
||||||
};
|
|
||||||
132
src/activities/reader/EpubReaderTocActivity.cpp
Normal file
132
src/activities/reader/EpubReaderTocActivity.cpp
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
#include "EpubReaderTocActivity.h"
|
||||||
|
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "KOReaderSyncActivity.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "ScreenComponents.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int TAB_BAR_Y = 15;
|
||||||
|
constexpr int CONTENT_START_Y = 60;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<EpubReaderTocActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::onEnter() {
|
||||||
|
ActivityWithSubactivity::onEnter();
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
chaptersTab->onEnter();
|
||||||
|
footnotesTab->onEnter();
|
||||||
|
|
||||||
|
updateRequired = true;
|
||||||
|
xTaskCreate(&EpubReaderTocActivity::taskTrampoline, "EpubReaderTocTask", 4096, this, 1, &displayTaskHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::onExit() {
|
||||||
|
ActivityWithSubactivity::onExit();
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::launchSyncActivity() {
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new KOReaderSyncActivity(
|
||||||
|
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
|
||||||
|
[this]() {
|
||||||
|
// On cancel
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
},
|
||||||
|
[this](int newSpineIndex, int newPage) {
|
||||||
|
// On sync complete
|
||||||
|
exitActivity();
|
||||||
|
onSyncPosition(newSpineIndex, newPage);
|
||||||
|
}));
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
onGoBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||||
|
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
|
||||||
|
|
||||||
|
if (leftReleased && currentTab == Tab::FOOTNOTES) {
|
||||||
|
currentTab = Tab::CHAPTERS;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rightReleased && currentTab == Tab::CHAPTERS) {
|
||||||
|
currentTab = Tab::FOOTNOTES;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentTab()->loop();
|
||||||
|
if (getCurrentTab()->isUpdateRequired()) {
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired && !subActivity) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
renderScreen();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderTocActivity::renderScreen() {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
// Draw tab bar
|
||||||
|
std::vector<TabInfo> tabs = {{"Chapters", currentTab == Tab::CHAPTERS}, {"Footnotes", currentTab == Tab::FOOTNOTES}};
|
||||||
|
ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs);
|
||||||
|
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
const int contentHeight = screenHeight - CONTENT_START_Y - 60;
|
||||||
|
|
||||||
|
getCurrentTab()->render(CONTENT_START_Y, contentHeight);
|
||||||
|
|
||||||
|
// Draw scroll indicator
|
||||||
|
ScreenComponents::drawScrollIndicator(renderer, getCurrentTab()->getCurrentPage(), getCurrentTab()->getTotalPages(),
|
||||||
|
CONTENT_START_Y, contentHeight);
|
||||||
|
|
||||||
|
// Draw button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("« Back", "Select", "< Tab", "Tab >");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
TocTab* EpubReaderTocActivity::getCurrentTab() const {
|
||||||
|
return (currentTab == Tab::CHAPTERS) ? static_cast<TocTab*>(chaptersTab.get())
|
||||||
|
: static_cast<TocTab*>(footnotesTab.get());
|
||||||
|
}
|
||||||
75
src/activities/reader/EpubReaderTocActivity.h
Normal file
75
src/activities/reader/EpubReaderTocActivity.h
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "ChaptersTab.h"
|
||||||
|
#include "FootnotesTab.h"
|
||||||
|
|
||||||
|
class EpubReaderTocActivity final : public ActivityWithSubactivity {
|
||||||
|
public:
|
||||||
|
enum class Tab { CHAPTERS, FOOTNOTES };
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
|
std::string epubPath;
|
||||||
|
const FootnotesData& footnotes;
|
||||||
|
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
|
||||||
|
int currentSpineIndex = 0;
|
||||||
|
int currentPage = 0;
|
||||||
|
int totalPagesInSpine = 0;
|
||||||
|
|
||||||
|
Tab currentTab = Tab::CHAPTERS;
|
||||||
|
std::unique_ptr<ChaptersTab> chaptersTab;
|
||||||
|
std::unique_ptr<FootnotesTab> footnotesTab;
|
||||||
|
|
||||||
|
bool updateRequired = false;
|
||||||
|
|
||||||
|
const std::function<void()> onGoBack;
|
||||||
|
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
|
||||||
|
const std::function<void(const char* href)> onSelectFootnote;
|
||||||
|
const std::function<void(int newSpineIndex, int newPage)> onSyncPosition;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void renderScreen();
|
||||||
|
TocTab* getCurrentTab() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
EpubReaderTocActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr<Epub>& epub,
|
||||||
|
const std::string& epubPath, int currentSpineIndex, int currentPage, int totalPagesInSpine,
|
||||||
|
const FootnotesData& footnotes, std::function<void()> onGoBack,
|
||||||
|
std::function<void(int)> onSelectSpineIndex, std::function<void(const char*)> onSelectFootnote,
|
||||||
|
std::function<void(int, int)> onSyncPosition)
|
||||||
|
: ActivityWithSubactivity("EpubReaderToc", renderer, mappedInput),
|
||||||
|
epub(epub),
|
||||||
|
epubPath(epubPath),
|
||||||
|
currentSpineIndex(currentSpineIndex),
|
||||||
|
currentPage(currentPage),
|
||||||
|
totalPagesInSpine(totalPagesInSpine),
|
||||||
|
footnotes(footnotes),
|
||||||
|
onGoBack(onGoBack),
|
||||||
|
onSelectSpineIndex(onSelectSpineIndex),
|
||||||
|
onSelectFootnote(onSelectFootnote),
|
||||||
|
onSyncPosition(onSyncPosition) {
|
||||||
|
chaptersTab = std::unique_ptr<ChaptersTab>(new ChaptersTab(
|
||||||
|
renderer, mappedInput, epub, currentSpineIndex,
|
||||||
|
[this](int spineIndex) { this->onSelectSpineIndex(spineIndex); },
|
||||||
|
[this]() { this->launchSyncActivity(); }));
|
||||||
|
footnotesTab = std::unique_ptr<FootnotesTab>(new FootnotesTab(
|
||||||
|
renderer, mappedInput, footnotes, [this](const char* href) { this->onSelectFootnote(href); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
void launchSyncActivity();
|
||||||
|
};
|
||||||
@ -1,4 +1,4 @@
|
|||||||
#include "EpubReaderFootnotesActivity.h"
|
#include "FootnotesTab.h"
|
||||||
|
|
||||||
#include <EpdFontFamily.h>
|
#include <EpdFontFamily.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
@ -6,30 +6,16 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
void EpubReaderFootnotesActivity::onEnter() {
|
namespace {
|
||||||
|
constexpr int LINE_HEIGHT = 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FootnotesTab::onEnter() {
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
render();
|
updateRequired = true;
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderFootnotesActivity::onExit() {
|
|
||||||
// Nothing to clean up
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderFootnotesActivity::loop() {
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
||||||
onGoBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
||||||
const FootnoteEntry* entry = footnotes.getEntry(selectedIndex);
|
|
||||||
if (entry) {
|
|
||||||
Serial.printf("[%lu] [FNS] Selected footnote: %s -> %s\n", millis(), entry->number, entry->href);
|
|
||||||
onSelectFootnote(entry->href);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FootnotesTab::loop() {
|
||||||
bool needsRedraw = false;
|
bool needsRedraw = false;
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
||||||
@ -46,35 +32,32 @@ void EpubReaderFootnotesActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
const FootnoteEntry* entry = footnotes.getEntry(selectedIndex);
|
||||||
|
if (entry) {
|
||||||
|
onSelectFootnote(entry->href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (needsRedraw) {
|
if (needsRedraw) {
|
||||||
render();
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderFootnotesActivity::render() {
|
void FootnotesTab::render(int contentTop, int contentHeight) {
|
||||||
renderer.clearScreen();
|
const int marginLeft = 20;
|
||||||
|
|
||||||
constexpr int startY = 50;
|
|
||||||
constexpr int lineHeight = 40;
|
|
||||||
constexpr int marginLeft = 20;
|
|
||||||
|
|
||||||
// Title
|
|
||||||
renderer.drawText(UI_12_FONT_ID, marginLeft, 20, "Footnotes", EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
if (footnotes.getCount() == 0) {
|
if (footnotes.getCount() == 0) {
|
||||||
renderer.drawText(SMALL_FONT_ID, marginLeft, startY + 20, "No footnotes on this page");
|
renderer.drawText(SMALL_FONT_ID, marginLeft, contentTop + 20, "No footnotes on this page");
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display footnotes
|
|
||||||
for (int i = 0; i < footnotes.getCount(); i++) {
|
for (int i = 0; i < footnotes.getCount(); i++) {
|
||||||
const FootnoteEntry* entry = footnotes.getEntry(i);
|
const FootnoteEntry* entry = footnotes.getEntry(i);
|
||||||
if (!entry) continue;
|
if (!entry) continue;
|
||||||
|
|
||||||
const int y = startY + i * lineHeight;
|
const int y = contentTop + i * LINE_HEIGHT;
|
||||||
|
|
||||||
// Draw selection indicator (arrow)
|
|
||||||
if (i == selectedIndex) {
|
if (i == selectedIndex) {
|
||||||
renderer.drawText(UI_12_FONT_ID, marginLeft - 10, y, ">", EpdFontFamily::BOLD);
|
renderer.drawText(UI_12_FONT_ID, marginLeft - 10, y, ">", EpdFontFamily::BOLD);
|
||||||
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number, EpdFontFamily::BOLD);
|
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number, EpdFontFamily::BOLD);
|
||||||
@ -82,10 +65,7 @@ void EpubReaderFootnotesActivity::render() {
|
|||||||
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number);
|
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instructions at bottom
|
|
||||||
renderer.drawText(SMALL_FONT_ID, marginLeft, renderer.getScreenHeight() - 40,
|
|
||||||
"UP/DOWN: Select CONFIRM: Go to footnote BACK: Return");
|
|
||||||
|
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int FootnotesTab::getCurrentPage() const { return 1; }
|
||||||
|
int FootnotesTab::getTotalPages() const { return 1; }
|
||||||
@ -1,10 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "../../lib/Epub/Epub/FootnoteEntry.h"
|
#include "../../lib/Epub/Epub/FootnoteEntry.h"
|
||||||
#include "../Activity.h"
|
#include "TocTab.h"
|
||||||
|
|
||||||
class FootnotesData {
|
class FootnotesData {
|
||||||
private:
|
private:
|
||||||
@ -47,26 +46,24 @@ class FootnotesData {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class EpubReaderFootnotesActivity final : public Activity {
|
class FootnotesTab final : public TocTab {
|
||||||
const FootnotesData& footnotes;
|
const FootnotesData& footnotes;
|
||||||
const std::function<void()> onGoBack;
|
int selectedIndex = 0;
|
||||||
const std::function<void(const char*)> onSelectFootnote;
|
bool updateRequired = false;
|
||||||
int selectedIndex;
|
|
||||||
|
const std::function<void(const char* href)> onSelectFootnote;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EpubReaderFootnotesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const FootnotesData& footnotes,
|
FootnotesTab(GfxRenderer& renderer, MappedInputManager& mappedInput, const FootnotesData& footnotes,
|
||||||
const std::function<void()>& onGoBack,
|
std::function<void(const char*)> onSelectFootnote)
|
||||||
const std::function<void(const char*)>& onSelectFootnote)
|
: TocTab(renderer, mappedInput), footnotes(footnotes), onSelectFootnote(onSelectFootnote) {}
|
||||||
: Activity("EpubReaderFootnotes", renderer, mappedInput),
|
|
||||||
footnotes(footnotes),
|
|
||||||
onGoBack(onGoBack),
|
|
||||||
onSelectFootnote(onSelectFootnote),
|
|
||||||
selectedIndex(0) {}
|
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
void render(int contentTop, int contentHeight) override;
|
||||||
|
|
||||||
private:
|
int getCurrentPage() const override;
|
||||||
void render();
|
int getTotalPages() const override;
|
||||||
|
bool isUpdateRequired() const override { return updateRequired; }
|
||||||
|
void clearUpdateRequired() override { updateRequired = false; }
|
||||||
};
|
};
|
||||||
24
src/activities/reader/TocTab.h
Normal file
24
src/activities/reader/TocTab.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
class MappedInputManager;
|
||||||
|
|
||||||
|
class TocTab {
|
||||||
|
protected:
|
||||||
|
GfxRenderer& renderer;
|
||||||
|
MappedInputManager& mappedInput;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TocTab(GfxRenderer& renderer, MappedInputManager& mappedInput) : renderer(renderer), mappedInput(mappedInput) {}
|
||||||
|
virtual ~TocTab() = default;
|
||||||
|
|
||||||
|
virtual void onEnter() = 0;
|
||||||
|
virtual void onExit() {}
|
||||||
|
virtual void loop() = 0;
|
||||||
|
virtual void render(int contentTop, int contentHeight) = 0;
|
||||||
|
|
||||||
|
virtual int getCurrentPage() const = 0;
|
||||||
|
virtual int getTotalPages() const = 0;
|
||||||
|
virtual bool isUpdateRequired() const = 0;
|
||||||
|
virtual void clearUpdateRequired() = 0;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user