Temporarily release framebuffer memory during section pagination to reduce heap pressure. The framebuffer can be released before memory-intensive operations and restored afterward, allowing BLE stack startup threshold to be lowered from 80KB to 70KB. Implements RAII-style FrameBufferBuildLoan class to ensure proper cleanup and automatic restart on restoration failure.
180 lines
7.6 KiB
C++
180 lines
7.6 KiB
C++
#include "EpubReaderMenuActivity.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <I18n.h>
|
|
#include <Logging.h>
|
|
|
|
#include "BleInput.h"
|
|
#include "CrossPointSettings.h"
|
|
#include "MappedInputManager.h"
|
|
#include "SilentRestart.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
|
|
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
const std::string& title, const int currentPage, const int totalPages,
|
|
const int bookProgressPercent, const uint8_t currentOrientation,
|
|
const bool hasFootnotes, const bool hasBookmarks)
|
|
: Activity("EpubReaderMenu", renderer, mappedInput),
|
|
menuItems(buildMenuItems(hasFootnotes, hasBookmarks)),
|
|
title(title),
|
|
pendingOrientation(currentOrientation),
|
|
currentPage(currentPage),
|
|
totalPages(totalPages),
|
|
bookProgressPercent(bookProgressPercent) {}
|
|
|
|
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
|
|
bool hasBookmarks) {
|
|
std::vector<MenuItem> items;
|
|
items.reserve(13);
|
|
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
|
if (hasFootnotes) {
|
|
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
|
}
|
|
if (hasBookmarks) {
|
|
items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS});
|
|
}
|
|
items.push_back({MenuAction::TOGGLE_BOOKMARK, StrId::STR_TOGGLE_BOOKMARK});
|
|
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
|
|
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
|
|
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
|
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
|
|
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
|
|
items.push_back({MenuAction::TOGGLE_BLUETOOTH, StrId::STR_TOGGLE_BLUETOOTH});
|
|
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
|
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
|
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
|
return items;
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onEnter() {
|
|
Activity::onEnter();
|
|
requestUpdate();
|
|
}
|
|
|
|
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
|
|
|
|
void EpubReaderMenuActivity::loop() {
|
|
if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return;
|
|
|
|
// Handle navigation
|
|
buttonNavigator.onNext([this] {
|
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator.onPrevious([this] {
|
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
|
requestUpdate();
|
|
});
|
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
const auto selectedAction = menuItems[selectedIndex].action;
|
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
|
optionPopup.show(StrId::STR_ORIENTATION, orientationLabels.data(), static_cast<int>(orientationLabels.size()),
|
|
pendingOrientation, [this](int idx) {
|
|
pendingOrientation = idx;
|
|
requestUpdate();
|
|
});
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
if (selectedAction == MenuAction::AUTO_PAGE_TURN) {
|
|
optionPopup.show(I18N.get(StrId::STR_AUTO_TURN_PAGES_PER_MIN), pageTurnLabels.data(),
|
|
static_cast<int>(pageTurnLabels.size()), selectedPageTurnOption, [this](int idx) {
|
|
selectedPageTurnOption = idx;
|
|
requestUpdate();
|
|
});
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
if (selectedAction == MenuAction::TOGGLE_BLUETOOTH) {
|
|
// Just flip the preference and stay in the menu. The main-loop lifecycle check
|
|
// brings the BLE stack up/down to match, so start/stop has a single owner.
|
|
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
|
|
SETTINGS.saveToFile();
|
|
// Turning BT on below the lifecycle's heap floor would otherwise wait
|
|
// until the heap happens to recover -- which a long session's fragmentation never
|
|
// gives back. The user asked for BT *now*: silent-restart into this book to
|
|
// defrag (fresh boot is ~118 KB free, comfortably above the floor), and BT
|
|
// auto-starts on the way back in.
|
|
if (SETTINGS.bluetoothEnabled && !BleHid.isRunning() && ESP.getFreeHeap() < bleinput::kStartMinFreeHeap) {
|
|
LOG_INF("ERM", "BT enabled below heap floor (%u); silent restart to defrag", ESP.getFreeHeap());
|
|
silentRestartToReader();
|
|
}
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
|
|
finish();
|
|
return;
|
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
ActivityResult result;
|
|
result.isCancelled = true;
|
|
result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption};
|
|
setResult(std::move(result));
|
|
finish();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void EpubReaderMenuActivity::render(RenderLock&&) {
|
|
if (optionPopup.processRender(renderer, mappedInput)) return;
|
|
|
|
renderer.clearScreen();
|
|
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false);
|
|
|
|
GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight},
|
|
title.c_str());
|
|
|
|
// Progress summary
|
|
std::string progressLine;
|
|
if (totalPages > 0) {
|
|
progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
|
|
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
|
|
}
|
|
progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
|
|
GUI.drawSubHeader(
|
|
renderer,
|
|
Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight},
|
|
progressLine.c_str());
|
|
|
|
const int contentTop =
|
|
screen.y + metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
|
|
const int contentHeight = screen.height - contentTop - metrics.verticalSpacing;
|
|
|
|
GUI.drawList(
|
|
renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, menuItems.size(), selectedIndex,
|
|
[this](int index) { return I18N.get(menuItems[index].labelId); }, nullptr, nullptr,
|
|
[this](int index) {
|
|
const auto value = menuItems[index].action;
|
|
if (value == MenuAction::ROTATE_SCREEN) {
|
|
// Render current orientation value on the right edge of the content area.
|
|
return I18N.get(orientationLabels[pendingOrientation]);
|
|
} else if (value == MenuAction::AUTO_PAGE_TURN) {
|
|
// Render current page turn value on the right edge of the content area.
|
|
return pageTurnLabels[selectedPageTurnOption];
|
|
} else if (value == MenuAction::TOGGLE_BLUETOOTH) {
|
|
if (SETTINGS.bluetoothEnabled) {
|
|
if (!BleHid.isRunning()) return tr(STR_CONNECTING);
|
|
return BleHid.isConnected() ? tr(STR_STATE_ON) : tr(STR_CONNECTING);
|
|
}
|
|
return tr(STR_STATE_OFF);
|
|
} else {
|
|
return "";
|
|
}
|
|
},
|
|
true);
|
|
|
|
// Footer / Hints
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
|
|
renderer.displayBuffer();
|
|
}
|