mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +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
|
// Asset caching
|
||||||
const std::vector<uint8_t>* getCachedAsset(const std::string& path);
|
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,
|
const ProcessedAsset* getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
|
||||||
int targetW = 0, int targetH = 0);
|
int targetW = 0, int targetH = 0);
|
||||||
void cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, 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
|
// Recomputed every layout pass
|
||||||
int absX = 0, absY = 0, absW = 0, absH = 0;
|
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
|
// Caching support
|
||||||
bool cacheable = false; // Set true for expensive elements like bitmaps
|
bool cacheable = false; // Set true for expensive elements like bitmaps
|
||||||
bool cacheValid = false; // Is the cached render still valid?
|
bool cacheValid = false; // Is the cached render still valid?
|
||||||
@ -84,6 +88,7 @@ class UIElement {
|
|||||||
virtual void markDirty() {
|
virtual void markDirty() {
|
||||||
dirty = true;
|
dirty = true;
|
||||||
cacheValid = false;
|
cacheValid = false;
|
||||||
|
layoutValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void markClean() { dirty = false; }
|
void markClean() { dirty = false; }
|
||||||
@ -97,6 +102,18 @@ class UIElement {
|
|||||||
|
|
||||||
// Calculate absolute position based on parent
|
// Calculate absolute position based on parent
|
||||||
virtual void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) {
|
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 newX = parentX + x.resolve(parentW);
|
||||||
int newY = parentY + y.resolve(parentH);
|
int newY = parentY + y.resolve(parentH);
|
||||||
int newW = width.resolve(parentW);
|
int newW = width.resolve(parentW);
|
||||||
|
|||||||
@ -137,38 +137,18 @@ void Label::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
|||||||
break;
|
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 len = remaining.length();
|
||||||
int cut = len;
|
int lo = 1, hi = len;
|
||||||
|
while (lo < hi) {
|
||||||
// Find split point
|
int mid = (lo + hi + 1) / 2;
|
||||||
// Optimistic start: approximate chars that fit
|
if (renderer.getTextWidth(fontId, remaining.substr(0, mid).c_str()) <= absW) {
|
||||||
int avgCharWidth = renderer.getTextWidth(fontId, "a");
|
lo = mid;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Shrink
|
hi = mid - 1;
|
||||||
for (int i = approxChars; i > 0; i--) {
|
|
||||||
if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) <= absW) {
|
|
||||||
cut = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
int cut = lo;
|
||||||
|
|
||||||
// Find last space before cut
|
// Find last space before cut
|
||||||
if (cut < (int)remaining.length()) {
|
if (cut < (int)remaining.length()) {
|
||||||
@ -249,96 +229,86 @@ void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& contex
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve simplified or relative paths
|
|
||||||
if (path.find('/') == std::string::npos || (path.length() > 0 && path[0] != '/')) {
|
if (path.find('/') == std::string::npos || (path.length() > 0 && path[0] != '/')) {
|
||||||
path = ThemeManager::get().getAssetPath(path);
|
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);
|
const ProcessedAsset* processed = ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH);
|
||||||
if (processed && processed->w == absW && processed->h == absH) {
|
if (processed && processed->w == absW && processed->h == absH) {
|
||||||
const int rowBytes = (absW + 7) / 8;
|
renderer.restoreRegion(processed->data.data(), absX, absY, absW, absH);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markClean();
|
markClean();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool drawSuccess = false;
|
// Helper to draw bitmap with centering and optional rounded corners
|
||||||
|
auto drawBmp = [&](Bitmap& bmp) {
|
||||||
// 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) {
|
|
||||||
int drawX = absX;
|
int drawX = absX;
|
||||||
int drawY = absY;
|
int drawY = absY;
|
||||||
if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2;
|
if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2;
|
||||||
if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2;
|
if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2;
|
||||||
|
|
||||||
if (borderRadius > 0) {
|
if (borderRadius > 0) {
|
||||||
renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius);
|
renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius);
|
||||||
} else {
|
} else {
|
||||||
renderer.drawBitmap(bmp, drawX, drawY, absW, absH);
|
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;
|
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();
|
file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback to RAM Cache (Standard method)
|
// Cache rendered result for fast subsequent draws using captureRegion
|
||||||
if (!drawSuccess) {
|
if (drawSuccess && absW * absH <= 40000) {
|
||||||
const std::vector<uint8_t>* data = ThemeManager::get().getCachedAsset(path);
|
size_t capturedSize = 0;
|
||||||
if (data && !data->empty()) {
|
uint8_t* captured = renderer.captureRegion(absX, absY, absW, absH, &capturedSize);
|
||||||
Bitmap bmp(data->data(), data->size());
|
if (captured && capturedSize > 0) {
|
||||||
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) {
|
|
||||||
ProcessedAsset asset;
|
ProcessedAsset asset;
|
||||||
asset.w = absW;
|
asset.w = absW;
|
||||||
asset.h = absH;
|
asset.h = absH;
|
||||||
asset.orientation = renderer.getOrientation();
|
asset.orientation = renderer.getOrientation();
|
||||||
|
asset.data.assign(captured, captured + capturedSize);
|
||||||
const int rowBytes = (absW + 7) / 8;
|
free(captured);
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH);
|
ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
markClean();
|
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.Icon", context.getString(prefix + "Icon"));
|
||||||
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
|
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
|
||||||
itemContext.setString("Item.Progress", context.getString(prefix + "Progress"));
|
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
|
// Viewport check
|
||||||
if (direction == Direction::Horizontal) {
|
if (direction == Direction::Horizontal) {
|
||||||
@ -449,6 +413,12 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
|||||||
}
|
}
|
||||||
if (currentY > absY + absH) break;
|
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
|
// Layout and draw
|
||||||
itemTemplate->layout(itemContext, currentX, currentY, itemW, itemH);
|
itemTemplate->layout(itemContext, currentX, currentY, itemW, itemH);
|
||||||
|
|||||||
@ -396,6 +396,10 @@ const std::vector<uint8_t>* ThemeManager::getCachedAsset(const std::string& path
|
|||||||
return nullptr;
|
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,
|
const ProcessedAsset* ThemeManager::getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
|
||||||
int targetW, int targetH) {
|
int targetW, int targetH) {
|
||||||
std::string cacheKey = path;
|
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,
|
void ThemeManager::renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer,
|
||||||
const ThemeContext& context, const ThemeContext* prevContext) {
|
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
|
} // namespace ThemeEngine
|
||||||
|
|||||||
@ -47,45 +47,86 @@ void HomeActivity::onEnter() {
|
|||||||
// Check if OPDS browser URL is configured
|
// Check if OPDS browser URL is configured
|
||||||
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
||||||
|
|
||||||
|
// Load and cache recent books data FIRST (loads each book only once)
|
||||||
|
loadRecentBooksData();
|
||||||
|
|
||||||
if (hasContinueReading) {
|
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;
|
lastBookTitle = APP_STATE.openEpubPath;
|
||||||
const size_t lastSlash = lastBookTitle.find_last_of('/');
|
const size_t lastSlash = lastBookTitle.find_last_of('/');
|
||||||
if (lastSlash != std::string::npos) {
|
if (lastSlash != std::string::npos) {
|
||||||
lastBookTitle = lastBookTitle.substr(lastSlash + 1);
|
lastBookTitle = lastBookTitle.substr(lastSlash + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If epub, try to load the metadata for title/author and cover
|
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
|
||||||
if (StringUtils::checkFileExtension(lastBookTitle, ".epub")) {
|
|
||||||
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
|
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
epub.load(false);
|
epub.load(false);
|
||||||
if (!epub.getTitle().empty()) {
|
if (!epub.getTitle().empty()) lastBookTitle = epub.getTitle();
|
||||||
lastBookTitle = std::string(epub.getTitle());
|
if (!epub.getAuthor().empty()) lastBookAuthor = epub.getAuthor();
|
||||||
}
|
|
||||||
if (!epub.getAuthor().empty()) {
|
|
||||||
lastBookAuthor = std::string(epub.getAuthor());
|
|
||||||
}
|
|
||||||
// Try to generate thumbnail image for Continue Reading card
|
|
||||||
if (epub.generateThumbBmp()) {
|
if (epub.generateThumbBmp()) {
|
||||||
coverBmpPath = epub.getThumbBmpPath();
|
coverBmpPath = epub.getThumbBmpPath();
|
||||||
hasCoverImage = true;
|
hasCoverImage = true;
|
||||||
}
|
}
|
||||||
} else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") ||
|
// Get progress info from the same loaded epub
|
||||||
StringUtils::checkFileExtension(lastBookTitle, ".xtc")) {
|
FsFile f;
|
||||||
// Handle XTC file
|
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");
|
Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
if (xtc.load()) {
|
if (xtc.load()) {
|
||||||
if (!xtc.getTitle().empty()) {
|
if (!xtc.getTitle().empty()) lastBookTitle = xtc.getTitle();
|
||||||
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.generateThumbBmp()) {
|
if (xtc.generateThumbBmp()) {
|
||||||
coverBmpPath = xtc.getThumbBmpPath();
|
coverBmpPath = xtc.getThumbBmpPath();
|
||||||
hasCoverImage = true;
|
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
|
// Remove extension from title if we don't have metadata
|
||||||
if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) {
|
if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) {
|
||||||
@ -95,15 +136,13 @@ void HomeActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
lastBatteryCheck = 0; // Force update on first render
|
lastBatteryCheck = 0; // Force update on first render
|
||||||
coverRendered = false;
|
coverRendered = false;
|
||||||
coverBufferStored = false;
|
coverBufferStored = false;
|
||||||
|
|
||||||
// Load and cache recent books data (slow operation, do once)
|
|
||||||
loadRecentBooksData();
|
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
@ -311,7 +350,7 @@ void HomeActivity::render() {
|
|||||||
lastBatteryCheck = now;
|
lastBatteryCheck = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always clear screen - ThemeEngine handles caching internally
|
// Always clear screen - required because parent containers draw backgrounds
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
ThemeEngine::ThemeContext context;
|
ThemeEngine::ThemeContext context;
|
||||||
@ -352,73 +391,12 @@ void HomeActivity::render() {
|
|||||||
context.setBool("HasCover", hasContinueReading && hasCoverImage && !coverBmpPath.empty());
|
context.setBool("HasCover", hasContinueReading && hasCoverImage && !coverBmpPath.empty());
|
||||||
context.setBool("ShowInfoBox", true);
|
context.setBool("ShowInfoBox", true);
|
||||||
|
|
||||||
// Default values
|
// Use cached values (loaded in onEnter, NOT every render)
|
||||||
std::string chapterTitle = "";
|
context.setString("BookChapter", cachedChapterTitle);
|
||||||
std::string currentPageStr = "-";
|
context.setString("BookCurrentPage", cachedCurrentPage);
|
||||||
std::string totalPagesStr = "-";
|
context.setString("BookTotalPages", cachedTotalPages);
|
||||||
int progressPercent = 0;
|
context.setInt("BookProgressPercent", cachedProgressPercent);
|
||||||
|
context.setString("BookProgressPercentStr", std::to_string(cachedProgressPercent));
|
||||||
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));
|
|
||||||
|
|
||||||
// --- Main Menu Data ---
|
// --- Main Menu Data ---
|
||||||
// Menu items start after the book slot
|
// 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 coverRendered = false; // Track if cover has been rendered once
|
||||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
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 lastBookTitle;
|
||||||
std::string lastBookAuthor;
|
std::string lastBookAuthor;
|
||||||
std::string coverBmpPath;
|
std::string coverBmpPath;
|
||||||
uint8_t cachedBatteryLevel = 0;
|
uint8_t cachedBatteryLevel = 0;
|
||||||
uint32_t lastBatteryCheck = 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)
|
// Cached recent books data (loaded once in onEnter)
|
||||||
std::vector<CachedBookInfo> cachedRecentBooks;
|
std::vector<CachedBookInfo> cachedRecentBooks;
|
||||||
const std::function<void()> onContinueReading;
|
const std::function<void()> onContinueReading;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user