mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
feat: optimize ThemeEngine render performance
- Add layout caching in UIElement to skip redundant layout passes - Use binary search for text wrapping in Label (O(log n) vs O(n)) - Cache rendered bitmaps using captureRegion/restoreRegion - Add RAM caching for BMP files to avoid SD card reads - Cache book metadata in HomeActivity.onEnter() instead of every render - Reuse recent books data to avoid duplicate ePub loads
This commit is contained in:
parent
93a94ea838
commit
7254d46401
@ -130,6 +130,7 @@ class ThemeManager {
|
||||
|
||||
// Asset caching
|
||||
const std::vector<uint8_t>* getCachedAsset(const std::string& path);
|
||||
void cacheAsset(const std::string& path, std::vector<uint8_t>&& data);
|
||||
const ProcessedAsset* getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
|
||||
int targetW = 0, int targetH = 0);
|
||||
void cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, int targetW = 0, int targetH = 0);
|
||||
|
||||
@ -29,6 +29,10 @@ class UIElement {
|
||||
// Recomputed every layout pass
|
||||
int absX = 0, absY = 0, absW = 0, absH = 0;
|
||||
|
||||
// Layout caching - track last params to skip redundant layout
|
||||
int lastParentX = -1, lastParentY = -1, lastParentW = -1, lastParentH = -1;
|
||||
bool layoutValid = false;
|
||||
|
||||
// Caching support
|
||||
bool cacheable = false; // Set true for expensive elements like bitmaps
|
||||
bool cacheValid = false; // Is the cached render still valid?
|
||||
@ -84,6 +88,7 @@ class UIElement {
|
||||
virtual void markDirty() {
|
||||
dirty = true;
|
||||
cacheValid = false;
|
||||
layoutValid = false;
|
||||
}
|
||||
|
||||
void markClean() { dirty = false; }
|
||||
@ -97,6 +102,18 @@ class UIElement {
|
||||
|
||||
// Calculate absolute position based on parent
|
||||
virtual void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) {
|
||||
// Skip layout if params unchanged and layout is still valid
|
||||
if (layoutValid && parentX == lastParentX && parentY == lastParentY && parentW == lastParentW &&
|
||||
parentH == lastParentH) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastParentX = parentX;
|
||||
lastParentY = parentY;
|
||||
lastParentW = parentW;
|
||||
lastParentH = parentH;
|
||||
layoutValid = true;
|
||||
|
||||
int newX = parentX + x.resolve(parentW);
|
||||
int newY = parentY + y.resolve(parentH);
|
||||
int newW = width.resolve(parentW);
|
||||
|
||||
@ -137,38 +137,18 @@ void Label::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Binary search for cut point
|
||||
// Binary search for maximum characters that fit (O(log n) instead of O(n))
|
||||
int len = remaining.length();
|
||||
int cut = len;
|
||||
|
||||
// Find split point
|
||||
// Optimistic start: approximate chars that fit
|
||||
int avgCharWidth = renderer.getTextWidth(fontId, "a");
|
||||
if (avgCharWidth < 1) avgCharWidth = 8;
|
||||
int approxChars = absW / avgCharWidth;
|
||||
if (approxChars < 1) approxChars = 1;
|
||||
if (approxChars >= len) approxChars = len - 1;
|
||||
|
||||
// Refine from approxChars
|
||||
int w = renderer.getTextWidth(fontId, remaining.substr(0, approxChars).c_str());
|
||||
if (w < absW) {
|
||||
// Grow
|
||||
for (int i = approxChars; i <= len; i++) {
|
||||
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) > absW) {
|
||||
cut = i - 1;
|
||||
break;
|
||||
}
|
||||
cut = i;
|
||||
}
|
||||
int lo = 1, hi = len;
|
||||
while (lo < hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (renderer.getTextWidth(fontId, remaining.substr(0, mid).c_str()) <= absW) {
|
||||
lo = mid;
|
||||
} else {
|
||||
// Shrink
|
||||
for (int i = approxChars; i > 0; i--) {
|
||||
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) <= absW) {
|
||||
cut = i;
|
||||
break;
|
||||
}
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
int cut = lo;
|
||||
|
||||
// Find last space before cut
|
||||
if (cut < (int)remaining.length()) {
|
||||
@ -249,96 +229,86 @@ void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& contex
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve simplified or relative paths
|
||||
if (path.find('/') == std::string::npos || (path.length() > 0 && path[0] != '/')) {
|
||||
path = ThemeManager::get().getAssetPath(path);
|
||||
}
|
||||
|
||||
// 1. Check if we have a cached 1-bit render
|
||||
// Fast path: use cached 1-bit render
|
||||
const ProcessedAsset* processed = ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH);
|
||||
if (processed && processed->w == absW && processed->h == absH) {
|
||||
const int rowBytes = (absW + 7) / 8;
|
||||
for (int y = 0; y < absH; y++) {
|
||||
const uint8_t* srcRow = processed->data.data() + y * rowBytes;
|
||||
for (int x = 0; x < absW; x++) {
|
||||
// Cached 1-bit data: 0=Black, 1=White
|
||||
bool isBlack = !(srcRow[x / 8] & (1 << (7 - (x % 8))));
|
||||
// Draw opaque (true=black, false=white)
|
||||
renderer.drawPixel(absX + x, absY + y, isBlack);
|
||||
}
|
||||
}
|
||||
renderer.restoreRegion(processed->data.data(), absX, absY, absW, absH);
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
bool drawSuccess = false;
|
||||
|
||||
// 2. Try Streaming (Absolute paths, large images)
|
||||
if (path.length() > 0 && path[0] == '/') {
|
||||
FsFile file;
|
||||
if (SdMan.openFileForRead("HOME", path, file)) {
|
||||
Bitmap bmp(file, true); // (file, dithering=true)
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
// Helper to draw bitmap with centering and optional rounded corners
|
||||
auto drawBmp = [&](Bitmap& bmp) {
|
||||
int drawX = absX;
|
||||
int drawY = absY;
|
||||
if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2;
|
||||
if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2;
|
||||
|
||||
if (borderRadius > 0) {
|
||||
renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius);
|
||||
} else {
|
||||
renderer.drawBitmap(bmp, drawX, drawY, absW, absH);
|
||||
}
|
||||
};
|
||||
|
||||
bool drawSuccess = false;
|
||||
|
||||
// Try RAM cache first
|
||||
const std::vector<uint8_t>* cachedData = ThemeManager::get().getCachedAsset(path);
|
||||
if (cachedData && !cachedData->empty()) {
|
||||
Bitmap bmp(cachedData->data(), cachedData->size());
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
drawBmp(bmp);
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: load from SD card
|
||||
if (!drawSuccess && path.length() > 0 && path[0] == '/') {
|
||||
FsFile file;
|
||||
if (SdMan.openFileForRead("HOME", path, file)) {
|
||||
size_t fileSize = file.size();
|
||||
if (fileSize > 0 && fileSize < 100000) {
|
||||
std::vector<uint8_t> fileData(fileSize);
|
||||
if (file.read(fileData.data(), fileSize) == fileSize) {
|
||||
ThemeManager::get().cacheAsset(path, std::move(fileData));
|
||||
const std::vector<uint8_t>* newCachedData = ThemeManager::get().getCachedAsset(path);
|
||||
if (newCachedData && !newCachedData->empty()) {
|
||||
Bitmap bmp(newCachedData->data(), newCachedData->size());
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
drawBmp(bmp);
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Bitmap bmp(file, true);
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
drawBmp(bmp);
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to RAM Cache (Standard method)
|
||||
if (!drawSuccess) {
|
||||
const std::vector<uint8_t>* data = ThemeManager::get().getCachedAsset(path);
|
||||
if (data && !data->empty()) {
|
||||
Bitmap bmp(data->data(), data->size());
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
int drawX = absX;
|
||||
int drawY = absY;
|
||||
if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2;
|
||||
if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2;
|
||||
|
||||
if (borderRadius > 0) {
|
||||
renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius);
|
||||
} else {
|
||||
renderer.drawBitmap(bmp, drawX, drawY, absW, absH);
|
||||
}
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Cache result if successful
|
||||
if (drawSuccess) {
|
||||
// Cache rendered result for fast subsequent draws using captureRegion
|
||||
if (drawSuccess && absW * absH <= 40000) {
|
||||
size_t capturedSize = 0;
|
||||
uint8_t* captured = renderer.captureRegion(absX, absY, absW, absH, &capturedSize);
|
||||
if (captured && capturedSize > 0) {
|
||||
ProcessedAsset asset;
|
||||
asset.w = absW;
|
||||
asset.h = absH;
|
||||
asset.orientation = renderer.getOrientation();
|
||||
|
||||
const int rowBytes = (absW + 7) / 8;
|
||||
asset.data.resize(rowBytes * absH, 0xFF); // Initialize to 0xFF (White)
|
||||
|
||||
for (int y = 0; y < absH; y++) {
|
||||
uint8_t* dstRow = asset.data.data() + y * rowBytes;
|
||||
for (int x = 0; x < absW; x++) {
|
||||
// Read precise pixel state from framebuffer
|
||||
bool isBlack = renderer.readPixel(absX + x, absY + y);
|
||||
if (isBlack) {
|
||||
// Clear bit for black (0)
|
||||
dstRow[x / 8] &= ~(1 << (7 - (x % 8)));
|
||||
}
|
||||
}
|
||||
}
|
||||
asset.data.assign(captured, captured + capturedSize);
|
||||
free(captured);
|
||||
ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH);
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
@ -421,12 +391,6 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
itemContext.setString("Item.Icon", context.getString(prefix + "Icon"));
|
||||
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
|
||||
itemContext.setString("Item.Progress", context.getString(prefix + "Progress"));
|
||||
itemContext.setInt("Item.Index", i);
|
||||
itemContext.setInt("Item.Count", count);
|
||||
// ValueIndex may not exist for all item types, so check first
|
||||
if (context.hasKey(prefix + "ValueIndex")) {
|
||||
itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex"));
|
||||
}
|
||||
|
||||
// Viewport check
|
||||
if (direction == Direction::Horizontal) {
|
||||
@ -449,6 +413,12 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
}
|
||||
if (currentY > absY + absH) break;
|
||||
}
|
||||
itemContext.setInt("Item.Index", i);
|
||||
itemContext.setInt("Item.Count", count);
|
||||
// ValueIndex may not exist for all item types, so check first
|
||||
if (context.hasKey(prefix + "ValueIndex")) {
|
||||
itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex"));
|
||||
}
|
||||
|
||||
// Layout and draw
|
||||
itemTemplate->layout(itemContext, currentX, currentY, itemW, itemH);
|
||||
|
||||
@ -396,6 +396,10 @@ const std::vector<uint8_t>* ThemeManager::getCachedAsset(const std::string& path
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ThemeManager::cacheAsset(const std::string& path, std::vector<uint8_t>&& data) {
|
||||
assetCache[path] = std::move(data);
|
||||
}
|
||||
|
||||
const ProcessedAsset* ThemeManager::getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
|
||||
int targetW, int targetH) {
|
||||
std::string cacheKey = path;
|
||||
@ -597,7 +601,23 @@ void ThemeManager::renderScreen(const std::string& screenName, const GfxRenderer
|
||||
|
||||
void ThemeManager::renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer,
|
||||
const ThemeContext& context, const ThemeContext* prevContext) {
|
||||
renderScreen(screenName, renderer, context);
|
||||
if (elements.count(screenName) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIElement* root = elements[screenName];
|
||||
|
||||
// Layout uses internal caching - will skip if params unchanged
|
||||
root->layout(context, 0, 0, renderer.getScreenWidth(), renderer.getScreenHeight());
|
||||
|
||||
// If no previous context provided, do full draw
|
||||
if (!prevContext) {
|
||||
root->draw(renderer, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw elements - dirty tracking is handled internally by each element
|
||||
root->draw(renderer, context);
|
||||
}
|
||||
|
||||
} // namespace ThemeEngine
|
||||
|
||||
@ -47,45 +47,86 @@ void HomeActivity::onEnter() {
|
||||
// Check if OPDS browser URL is configured
|
||||
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
||||
|
||||
// Load and cache recent books data FIRST (loads each book only once)
|
||||
loadRecentBooksData();
|
||||
|
||||
if (hasContinueReading) {
|
||||
// Extract filename from path for display
|
||||
// Initialize defaults
|
||||
cachedChapterTitle = "";
|
||||
cachedCurrentPage = "-";
|
||||
cachedTotalPages = "-";
|
||||
cachedProgressPercent = 0;
|
||||
|
||||
// Check if current book is in recent books - use cached data instead of reloading
|
||||
bool foundInRecent = false;
|
||||
for (const auto& book : cachedRecentBooks) {
|
||||
if (book.path == APP_STATE.openEpubPath) {
|
||||
lastBookTitle = book.title;
|
||||
coverBmpPath = book.coverPath;
|
||||
hasCoverImage = !book.coverPath.empty();
|
||||
cachedProgressPercent = book.progressPercent;
|
||||
foundInRecent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundInRecent) {
|
||||
// Book not in recent list, need to load it
|
||||
lastBookTitle = APP_STATE.openEpubPath;
|
||||
const size_t lastSlash = lastBookTitle.find_last_of('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
lastBookTitle = lastBookTitle.substr(lastSlash + 1);
|
||||
}
|
||||
|
||||
// If epub, try to load the metadata for title/author and cover
|
||||
if (StringUtils::checkFileExtension(lastBookTitle, ".epub")) {
|
||||
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
|
||||
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
epub.load(false);
|
||||
if (!epub.getTitle().empty()) {
|
||||
lastBookTitle = std::string(epub.getTitle());
|
||||
}
|
||||
if (!epub.getAuthor().empty()) {
|
||||
lastBookAuthor = std::string(epub.getAuthor());
|
||||
}
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (!epub.getTitle().empty()) lastBookTitle = epub.getTitle();
|
||||
if (!epub.getAuthor().empty()) lastBookAuthor = epub.getAuthor();
|
||||
if (epub.generateThumbBmp()) {
|
||||
coverBmpPath = epub.getThumbBmpPath();
|
||||
hasCoverImage = true;
|
||||
}
|
||||
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") ||
|
||||
StringUtils::checkFileExtension(lastBookTitle, ".xtc")) {
|
||||
// Handle XTC file
|
||||
// Get progress info from the same loaded epub
|
||||
FsFile f;
|
||||
if (SdMan.openFileForRead("HOME", epub.getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
int spineIndex = data[0] + (data[1] << 8);
|
||||
int spineCount = epub.getSpineItemsCount();
|
||||
cachedCurrentPage = std::to_string(spineIndex + 1);
|
||||
cachedTotalPages = std::to_string(spineCount);
|
||||
if (spineCount > 0) cachedProgressPercent = (spineIndex * 100) / spineCount;
|
||||
auto spineEntry = epub.getSpineItem(spineIndex);
|
||||
if (spineEntry.tocIndex != -1) {
|
||||
cachedChapterTitle = epub.getTocItem(spineEntry.tocIndex).title;
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
|
||||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
|
||||
Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
if (xtc.load()) {
|
||||
if (!xtc.getTitle().empty()) {
|
||||
lastBookTitle = std::string(xtc.getTitle());
|
||||
}
|
||||
if (!xtc.getAuthor().empty()) {
|
||||
lastBookAuthor = std::string(xtc.getAuthor());
|
||||
}
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (!xtc.getTitle().empty()) lastBookTitle = xtc.getTitle();
|
||||
if (xtc.generateThumbBmp()) {
|
||||
coverBmpPath = xtc.getThumbBmpPath();
|
||||
hasCoverImage = true;
|
||||
}
|
||||
// Get progress from same loaded xtc
|
||||
FsFile f;
|
||||
if (SdMan.openFileForRead("HOME", xtc.getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
uint32_t currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
||||
uint32_t totalPages = xtc.getPageCount();
|
||||
cachedCurrentPage = std::to_string(currentPage + 1);
|
||||
cachedTotalPages = std::to_string(totalPages);
|
||||
if (totalPages > 0) cachedProgressPercent = (currentPage * 100) / totalPages;
|
||||
cachedChapterTitle = "Page " + cachedCurrentPage;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
// Remove extension from title if we don't have metadata
|
||||
if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) {
|
||||
@ -95,15 +136,13 @@ void HomeActivity::onEnter() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectorIndex = 0;
|
||||
lastBatteryCheck = 0; // Force update on first render
|
||||
coverRendered = false;
|
||||
coverBufferStored = false;
|
||||
|
||||
// Load and cache recent books data (slow operation, do once)
|
||||
loadRecentBooksData();
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
@ -311,7 +350,7 @@ void HomeActivity::render() {
|
||||
lastBatteryCheck = now;
|
||||
}
|
||||
|
||||
// Always clear screen - ThemeEngine handles caching internally
|
||||
// Always clear screen - required because parent containers draw backgrounds
|
||||
renderer.clearScreen();
|
||||
|
||||
ThemeEngine::ThemeContext context;
|
||||
@ -352,73 +391,12 @@ void HomeActivity::render() {
|
||||
context.setBool("HasCover", hasContinueReading && hasCoverImage && !coverBmpPath.empty());
|
||||
context.setBool("ShowInfoBox", true);
|
||||
|
||||
// Default values
|
||||
std::string chapterTitle = "";
|
||||
std::string currentPageStr = "-";
|
||||
std::string totalPagesStr = "-";
|
||||
int progressPercent = 0;
|
||||
|
||||
if (hasContinueReading) {
|
||||
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
|
||||
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
epub.load(false);
|
||||
|
||||
// Read progress
|
||||
FsFile f;
|
||||
if (SdMan.openFileForRead("HOME", epub.getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
int spineIndex = data[0] + (data[1] << 8);
|
||||
int spineCount = epub.getSpineItemsCount();
|
||||
|
||||
currentPageStr = std::to_string(spineIndex + 1); // Display 1-based
|
||||
totalPagesStr = std::to_string(spineCount);
|
||||
|
||||
if (spineCount > 0) {
|
||||
progressPercent = (spineIndex * 100) / spineCount;
|
||||
}
|
||||
|
||||
// Resolve Chapter Title
|
||||
auto spineEntry = epub.getSpineItem(spineIndex);
|
||||
if (spineEntry.tocIndex != -1) {
|
||||
auto tocEntry = epub.getTocItem(spineEntry.tocIndex);
|
||||
chapterTitle = tocEntry.title;
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
|
||||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
|
||||
Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
if (xtc.load()) {
|
||||
// Read progress
|
||||
FsFile f;
|
||||
if (SdMan.openFileForRead("HOME", xtc.getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
uint32_t currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
||||
uint32_t totalPages = xtc.getPageCount();
|
||||
|
||||
currentPageStr = std::to_string(currentPage + 1); // 1-based
|
||||
totalPagesStr = std::to_string(totalPages);
|
||||
|
||||
if (totalPages > 0) {
|
||||
progressPercent = (currentPage * 100) / totalPages;
|
||||
}
|
||||
|
||||
chapterTitle = "Page " + currentPageStr;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.setString("BookChapter", chapterTitle);
|
||||
context.setString("BookCurrentPage", currentPageStr);
|
||||
context.setString("BookTotalPages", totalPagesStr);
|
||||
context.setInt("BookProgressPercent", progressPercent);
|
||||
context.setString("BookProgressPercentStr", std::to_string(progressPercent));
|
||||
// Use cached values (loaded in onEnter, NOT every render)
|
||||
context.setString("BookChapter", cachedChapterTitle);
|
||||
context.setString("BookCurrentPage", cachedCurrentPage);
|
||||
context.setString("BookTotalPages", cachedTotalPages);
|
||||
context.setInt("BookProgressPercent", cachedProgressPercent);
|
||||
context.setString("BookProgressPercentStr", std::to_string(cachedProgressPercent));
|
||||
|
||||
// --- Main Menu Data ---
|
||||
// Menu items start after the book slot
|
||||
|
||||
@ -28,12 +28,20 @@ class HomeActivity final : public Activity {
|
||||
bool coverRendered = false; // Track if cover has been rendered once
|
||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
||||
bool needsFullRender = true; // Force full render (first time or after layout changes)
|
||||
int lastSelectorIndex = -1; // Track selection for incremental updates
|
||||
std::string lastBookTitle;
|
||||
std::string lastBookAuthor;
|
||||
std::string coverBmpPath;
|
||||
uint8_t cachedBatteryLevel = 0;
|
||||
uint32_t lastBatteryCheck = 0;
|
||||
|
||||
// Cached "continue reading" info (loaded once in onEnter, NOT every render!)
|
||||
std::string cachedChapterTitle;
|
||||
std::string cachedCurrentPage;
|
||||
std::string cachedTotalPages;
|
||||
int cachedProgressPercent = 0;
|
||||
|
||||
// Cached recent books data (loaded once in onEnter)
|
||||
std::vector<CachedBookInfo> cachedRecentBooks;
|
||||
const std::function<void()> onContinueReading;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user