refactor: use static design for reader TOC to avoid memory fragmentation

This commit is contained in:
Uri Tauber 2026-01-27 19:25:56 +02:00
parent 2df7a3124f
commit e1849983b8
8 changed files with 223 additions and 354 deletions

View File

@ -1,153 +0,0 @@
#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);
}

View File

@ -1,44 +0,0 @@
#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; }
};

View File

@ -5,7 +5,7 @@
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include "FootnotesTab.h" #include "FootnotesData.h"
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
class EpubReaderActivity final : public ActivityWithSubactivity { class EpubReaderActivity final : public ActivityWithSubactivity {

View File

@ -3,6 +3,7 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "KOReaderCredentialStore.h"
#include "KOReaderSyncActivity.h" #include "KOReaderSyncActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ScreenComponents.h" #include "ScreenComponents.h"
@ -11,6 +12,9 @@
namespace { namespace {
constexpr int TAB_BAR_Y = 15; constexpr int TAB_BAR_Y = 15;
constexpr int CONTENT_START_Y = 60; constexpr int CONTENT_START_Y = 60;
constexpr int CHAPTER_LINE_HEIGHT = 30;
constexpr int FOOTNOTE_LINE_HEIGHT = 40;
constexpr int SKIP_PAGE_MS = 700;
} // namespace } // namespace
void EpubReaderTocActivity::taskTrampoline(void* param) { void EpubReaderTocActivity::taskTrampoline(void* param) {
@ -22,8 +26,21 @@ void EpubReaderTocActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
chaptersTab->onEnter(); // Init chapters state
footnotesTab->onEnter(); buildFilteredChapterList();
chaptersSelectorIndex = 0;
for (size_t i = 0; i < filteredSpineIndices.size(); i++) {
if (filteredSpineIndices[i] == currentSpineIndex) {
chaptersSelectorIndex = i;
break;
}
}
if (hasSyncOption()) {
chaptersSelectorIndex += 1;
}
// Init footnotes state
footnotesSelectedIndex = 0;
updateRequired = true; updateRequired = true;
xTaskCreate(&EpubReaderTocActivity::taskTrampoline, "EpubReaderTocTask", 4096, this, 1, &displayTaskHandle); xTaskCreate(&EpubReaderTocActivity::taskTrampoline, "EpubReaderTocTask", 4096, this, 1, &displayTaskHandle);
@ -44,16 +61,14 @@ void EpubReaderTocActivity::launchSyncActivity() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new KOReaderSyncActivity( enterNewActivity(new KOReaderSyncActivity(
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine, renderer, mappedInput, this->epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
[this]() { [this]() {
// On cancel
exitActivity(); exitActivity();
updateRequired = true; this->updateRequired = true;
}, },
[this](int newSpineIndex, int newPage) { [this](int newSpineIndex, int newPage) {
// On sync complete
exitActivity(); exitActivity();
onSyncPosition(newSpineIndex, newPage); this->onSyncPosition(newSpineIndex, newPage);
})); }));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
@ -83,8 +98,65 @@ void EpubReaderTocActivity::loop() {
return; return;
} }
getCurrentTab()->loop(); if (currentTab == Tab::CHAPTERS) {
if (getCurrentTab()->isUpdateRequired()) { loopChapters();
} else {
loopFootnotes();
}
}
void EpubReaderTocActivity::loopChapters() {
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 = getChaptersTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (hasSyncOption() && (chaptersSelectorIndex == 0 || chaptersSelectorIndex == totalItems - 1)) {
launchSyncActivity();
return;
}
int filteredIndex = chaptersSelectorIndex;
if (hasSyncOption()) filteredIndex -= 1;
if (filteredIndex >= 0 && filteredIndex < static_cast<int>(filteredSpineIndices.size())) {
onSelectSpineIndex(filteredSpineIndices[filteredIndex]);
}
} else if (upReleased) {
if (totalItems > 0) {
chaptersSelectorIndex = (chaptersSelectorIndex + totalItems - 1) % totalItems;
updateRequired = true;
}
} else if (downReleased) {
if (totalItems > 0) {
chaptersSelectorIndex = (chaptersSelectorIndex + 1) % totalItems;
updateRequired = true;
}
}
}
void EpubReaderTocActivity::loopFootnotes() {
bool needsRedraw = false;
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
if (footnotesSelectedIndex > 0) {
footnotesSelectedIndex--;
needsRedraw = true;
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
if (footnotesSelectedIndex < footnotes.getCount() - 1) {
footnotesSelectedIndex++;
needsRedraw = true;
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const FootnoteEntry* entry = footnotes.getEntry(footnotesSelectedIndex);
if (entry) {
onSelectFootnote(entry->href);
}
}
if (needsRedraw) {
updateRequired = true; updateRequired = true;
} }
} }
@ -104,29 +176,128 @@ void EpubReaderTocActivity::displayTaskLoop() {
void EpubReaderTocActivity::renderScreen() { void EpubReaderTocActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
// Draw tab bar
std::vector<TabInfo> tabs = {{"Chapters", currentTab == Tab::CHAPTERS}, {"Footnotes", currentTab == Tab::FOOTNOTES}}; std::vector<TabInfo> tabs = {{"Chapters", currentTab == Tab::CHAPTERS}, {"Footnotes", currentTab == Tab::FOOTNOTES}};
ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs);
const int screenHeight = renderer.getScreenHeight(); const int screenHeight = renderer.getScreenHeight();
const int contentHeight = screenHeight - CONTENT_START_Y - 60; const int contentHeight = screenHeight - CONTENT_START_Y - 60;
getCurrentTab()->render(CONTENT_START_Y, contentHeight); if (currentTab == Tab::CHAPTERS) {
renderChapters(CONTENT_START_Y, contentHeight);
} else {
renderFootnotes(CONTENT_START_Y, contentHeight);
}
// Draw scroll indicator ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight);
ScreenComponents::drawScrollIndicator(renderer, getCurrentTab()->getCurrentPage(), getCurrentTab()->getTotalPages(),
CONTENT_START_Y, contentHeight);
// Draw button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "< Tab", "Tab >"); const auto labels = mappedInput.mapLabels("« Back", "Select", "< Tab", "Tab >");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<"); renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
renderer.displayBuffer(); renderer.displayBuffer();
} }
TocTab* EpubReaderTocActivity::getCurrentTab() const { void EpubReaderTocActivity::renderChapters(int contentTop, int contentHeight) {
return (currentTab == Tab::CHAPTERS) ? static_cast<TocTab*>(chaptersTab.get()) const auto pageWidth = renderer.getScreenWidth();
: static_cast<TocTab*>(footnotesTab.get()); const int pageItems = getChaptersPageItems(contentHeight);
const int totalItems = getChaptersTotalItems();
const auto pageStartIndex = chaptersSelectorIndex / pageItems * pageItems;
renderer.fillRect(0, contentTop + (chaptersSelectorIndex % pageItems) * CHAPTER_LINE_HEIGHT - 2, pageWidth - 1,
CHAPTER_LINE_HEIGHT);
for (int i = 0; i < pageItems; i++) {
int itemIndex = pageStartIndex + i;
if (itemIndex >= totalItems) break;
const int displayY = contentTop + i * CHAPTER_LINE_HEIGHT;
const bool isSelected = (itemIndex == chaptersSelectorIndex);
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 = this->epub->getTocIndexForSpineIndex(spineIndex);
if (tocIndex == -1) {
renderer.drawText(UI_10_FONT_ID, 20, displayY, "Unnamed", !isSelected);
} else {
auto item = this->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);
}
}
}
}
}
void EpubReaderTocActivity::renderFootnotes(int contentTop, int contentHeight) {
const int marginLeft = 20;
if (footnotes.getCount() == 0) {
renderer.drawText(SMALL_FONT_ID, marginLeft, contentTop + 20, "No footnotes on this page");
return;
}
for (int i = 0; i < footnotes.getCount(); i++) {
const FootnoteEntry* entry = footnotes.getEntry(i);
if (!entry) continue;
const int y = contentTop + i * FOOTNOTE_LINE_HEIGHT;
if (i == footnotesSelectedIndex) {
renderer.drawText(UI_12_FONT_ID, marginLeft - 10, y, ">", EpdFontFamily::BOLD);
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number, EpdFontFamily::BOLD);
} else {
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number);
}
}
}
void EpubReaderTocActivity::buildFilteredChapterList() {
filteredSpineIndices.clear();
for (int i = 0; i < this->epub->getSpineItemsCount(); i++) {
if (this->epub->shouldHideFromToc(i)) continue;
int tocIndex = this->epub->getTocIndexForSpineIndex(i);
if (tocIndex == -1) continue;
filteredSpineIndices.push_back(i);
}
}
bool EpubReaderTocActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
bool EpubReaderTocActivity::isSyncItem(int index) const {
if (!hasSyncOption()) return false;
return index == 0 || index == getChaptersTotalItems() - 1;
}
int EpubReaderTocActivity::getChaptersTotalItems() const {
const int syncCount = hasSyncOption() ? 2 : 0;
return filteredSpineIndices.size() + syncCount;
}
int EpubReaderTocActivity::getChaptersPageItems(int contentHeight) const {
int items = contentHeight / CHAPTER_LINE_HEIGHT;
return (items < 1) ? 1 : items;
}
int EpubReaderTocActivity::getCurrentPage() const {
if (currentTab == Tab::CHAPTERS) {
const int availableHeight = renderer.getScreenHeight() - 120;
const int itemsPerPage = availableHeight / CHAPTER_LINE_HEIGHT;
return chaptersSelectorIndex / (itemsPerPage > 0 ? itemsPerPage : 1) + 1;
}
return 1;
}
int EpubReaderTocActivity::getTotalPages() const {
if (currentTab == Tab::CHAPTERS) {
const int availableHeight = renderer.getScreenHeight() - 120;
const int itemsPerPage = availableHeight / CHAPTER_LINE_HEIGHT;
const int totalItems = getChaptersTotalItems();
if (totalItems == 0) return 1;
return (totalItems + itemsPerPage - 1) / (itemsPerPage > 0 ? itemsPerPage : 1);
}
return 1;
} }

View File

@ -1,4 +1,5 @@
#pragma once #pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
@ -7,8 +8,7 @@
#include <vector> #include <vector>
#include "../ActivityWithSubactivity.h" #include "../ActivityWithSubactivity.h"
#include "ChaptersTab.h" #include "FootnotesData.h"
#include "FootnotesTab.h"
class EpubReaderTocActivity final : public ActivityWithSubactivity { class EpubReaderTocActivity final : public ActivityWithSubactivity {
public: public:
@ -27,11 +27,16 @@ class EpubReaderTocActivity final : public ActivityWithSubactivity {
int totalPagesInSpine = 0; int totalPagesInSpine = 0;
Tab currentTab = Tab::CHAPTERS; Tab currentTab = Tab::CHAPTERS;
std::unique_ptr<ChaptersTab> chaptersTab;
std::unique_ptr<FootnotesTab> footnotesTab;
bool updateRequired = false; bool updateRequired = false;
// Chapters tab state
int chaptersSelectorIndex = 0;
std::vector<int> filteredSpineIndices;
// Footnotes tab state
int footnotesSelectedIndex = 0;
// Callbacks
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(int newSpineIndex)> onSelectSpineIndex; const std::function<void(int newSpineIndex)> onSelectSpineIndex;
const std::function<void(const char* href)> onSelectFootnote; const std::function<void(const char* href)> onSelectFootnote;
@ -40,16 +45,33 @@ class EpubReaderTocActivity final : public ActivityWithSubactivity {
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void renderScreen(); void renderScreen();
TocTab* getCurrentTab() const;
// Tab-specific methods
void loopChapters();
void loopFootnotes();
void renderChapters(int contentTop, int contentHeight);
void renderFootnotes(int contentTop, int contentHeight);
// Chapters helpers
void buildFilteredChapterList();
bool hasSyncOption() const;
bool isSyncItem(int index) const;
int getChaptersTotalItems() const;
int getChaptersPageItems(int contentHeight) const;
int tocIndexFromItemIndex(int itemIndex) const;
// Indicator helpers
int getCurrentPage() const;
int getTotalPages() const;
public: public:
EpubReaderTocActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr<Epub>& epub, EpubReaderTocActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr<Epub>& epub_ptr,
const std::string& epubPath, int currentSpineIndex, int currentPage, int totalPagesInSpine, const std::string& epubPath, int currentSpineIndex, int currentPage, int totalPagesInSpine,
const FootnotesData& footnotes, std::function<void()> onGoBack, const FootnotesData& footnotes, std::function<void()> onGoBack,
std::function<void(int)> onSelectSpineIndex, std::function<void(const char*)> onSelectFootnote, std::function<void(int)> onSelectSpineIndex, std::function<void(const char*)> onSelectFootnote,
std::function<void(int, int)> onSyncPosition) std::function<void(int, int)> onSyncPosition)
: ActivityWithSubactivity("EpubReaderToc", renderer, mappedInput), : ActivityWithSubactivity("EpubReaderToc", renderer, mappedInput),
epub(epub), epub(epub_ptr),
epubPath(epubPath), epubPath(epubPath),
currentSpineIndex(currentSpineIndex), currentSpineIndex(currentSpineIndex),
currentPage(currentPage), currentPage(currentPage),
@ -58,14 +80,7 @@ class EpubReaderTocActivity final : public ActivityWithSubactivity {
onGoBack(onGoBack), onGoBack(onGoBack),
onSelectSpineIndex(onSelectSpineIndex), onSelectSpineIndex(onSelectSpineIndex),
onSelectFootnote(onSelectFootnote), onSelectFootnote(onSelectFootnote),
onSyncPosition(onSyncPosition) { 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 onEnter() override;
void onExit() override; void onExit() override;

View File

@ -1,9 +1,6 @@
#pragma once #pragma once
#include <cstring> #include <cstring>
#include <functional> #include <Epub/FootnoteEntry.h>
#include "../../lib/Epub/Epub/FootnoteEntry.h"
#include "TocTab.h"
class FootnotesData { class FootnotesData {
private: private:
@ -45,25 +42,3 @@ class FootnotesData {
return nullptr; return nullptr;
} }
}; };
class FootnotesTab final : public TocTab {
const FootnotesData& footnotes;
int selectedIndex = 0;
bool updateRequired = false;
const std::function<void(const char* href)> onSelectFootnote;
public:
FootnotesTab(GfxRenderer& renderer, MappedInputManager& mappedInput, const FootnotesData& footnotes,
std::function<void(const char*)> onSelectFootnote)
: TocTab(renderer, mappedInput), footnotes(footnotes), onSelectFootnote(onSelectFootnote) {}
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; }
};

View File

@ -1,71 +0,0 @@
#include "FootnotesTab.h"
#include <EpdFontFamily.h>
#include <GfxRenderer.h>
#include "MappedInputManager.h"
#include "fontIds.h"
namespace {
constexpr int LINE_HEIGHT = 40;
}
void FootnotesTab::onEnter() {
selectedIndex = 0;
updateRequired = true;
}
void FootnotesTab::loop() {
bool needsRedraw = false;
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
if (selectedIndex > 0) {
selectedIndex--;
needsRedraw = true;
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
if (selectedIndex < footnotes.getCount() - 1) {
selectedIndex++;
needsRedraw = true;
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const FootnoteEntry* entry = footnotes.getEntry(selectedIndex);
if (entry) {
onSelectFootnote(entry->href);
}
}
if (needsRedraw) {
updateRequired = true;
}
}
void FootnotesTab::render(int contentTop, int contentHeight) {
const int marginLeft = 20;
if (footnotes.getCount() == 0) {
renderer.drawText(SMALL_FONT_ID, marginLeft, contentTop + 20, "No footnotes on this page");
return;
}
for (int i = 0; i < footnotes.getCount(); i++) {
const FootnoteEntry* entry = footnotes.getEntry(i);
if (!entry) continue;
const int y = contentTop + i * LINE_HEIGHT;
if (i == selectedIndex) {
renderer.drawText(UI_12_FONT_ID, marginLeft - 10, y, ">", EpdFontFamily::BOLD);
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number, EpdFontFamily::BOLD);
} else {
renderer.drawText(UI_12_FONT_ID, marginLeft + 10, y, entry->number);
}
}
}
int FootnotesTab::getCurrentPage() const { return 1; }
int FootnotesTab::getTotalPages() const { return 1; }

View File

@ -1,24 +0,0 @@
#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;
};