Merge branch 'feat-page-overlay' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -277,6 +277,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
||||
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||
STR_OPDS_BROWSER: "OPDS Browser"
|
||||
STR_COVER_CUSTOM: "Cover + Custom"
|
||||
STR_PAGE_OVERLAY: "Page overlay"
|
||||
STR_RECENTS: "Recents"
|
||||
STR_MENU_RECENT_BOOKS: "Recent Books"
|
||||
STR_NO_RECENT_BOOKS: "No recent books"
|
||||
|
||||
@@ -24,6 +24,7 @@ class CrossPointSettings {
|
||||
COVER = 3,
|
||||
BLANK = 4,
|
||||
COVER_CUSTOM = 5,
|
||||
OVERLAY = 6,
|
||||
SLEEP_SCREEN_MODE_COUNT
|
||||
};
|
||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
||||
// --- Display ---
|
||||
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
|
||||
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT,
|
||||
StrId::STR_COVER_CUSTOM},
|
||||
StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY},
|
||||
"sleepScreen", StrId::STR_CAT_DISPLAY),
|
||||
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
|
||||
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
|
||||
|
||||
@@ -5,18 +5,166 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
#include <PNGdec.h>
|
||||
#include <Txt.h>
|
||||
#include <Xtc.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#include "../reader/EpubReaderActivity.h"
|
||||
#include "../reader/TxtReaderActivity.h"
|
||||
#include "../reader/XtcReaderActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "images/Logo120.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Context passed through PNGdec's decode() user-pointer to the per-scanline draw callback.
|
||||
struct PngOverlayCtx {
|
||||
const GfxRenderer* renderer;
|
||||
int screenW;
|
||||
int screenH;
|
||||
int srcWidth;
|
||||
int dstWidth;
|
||||
int dstX;
|
||||
int dstY;
|
||||
float yScale;
|
||||
int lastDstY;
|
||||
// Color-key transparency (tRNS chunk) for TRUECOLOR and GRAYSCALE images.
|
||||
// Initialized lazily on the first draw callback because tRNS is processed during decode(),
|
||||
// not during open() — so hasAlpha()/getTransparentColor() are only valid once decode() starts.
|
||||
// -2 = not yet read; -1 = no color key; >=0 = 0x00RRGGBB (TRUECOLOR) or low-byte gray.
|
||||
int32_t transparentColor;
|
||||
PNG* pngObj; // for lazy-init of transparentColor on first callback
|
||||
};
|
||||
|
||||
// PNGdec file I/O callbacks — mirror the pattern in PngToFramebufferConverter.cpp.
|
||||
void* pngSleepOpen(const char* filename, int32_t* size) {
|
||||
FsFile* f = new FsFile();
|
||||
if (!Storage.openFileForRead("SLP", std::string(filename), *f)) {
|
||||
delete f;
|
||||
return nullptr;
|
||||
}
|
||||
*size = f->size();
|
||||
return f;
|
||||
}
|
||||
void pngSleepClose(void* handle) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(handle);
|
||||
if (f) {
|
||||
f->close();
|
||||
delete f;
|
||||
}
|
||||
}
|
||||
int32_t pngSleepRead(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||
return f ? f->read(pBuf, len) : 0;
|
||||
}
|
||||
int32_t pngSleepSeek(PNGFILE* pFile, int32_t pos) {
|
||||
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||
if (!f) return -1;
|
||||
return f->seek(pos);
|
||||
}
|
||||
|
||||
// Per-scanline draw callback for PNG overlay compositing.
|
||||
// Transparent pixels (alpha < 128) are skipped so the reader page shows through.
|
||||
// Opaque pixels are drawn in their grayscale brightness (dark → black, light → white).
|
||||
int pngOverlayDraw(PNGDRAW* pDraw) {
|
||||
PngOverlayCtx* ctx = reinterpret_cast<PngOverlayCtx*>(pDraw->pUser);
|
||||
|
||||
// Lazy-init: tRNS chunk is processed during decode() before any IDAT data, so by the time
|
||||
// the first draw callback fires, hasAlpha() / getTransparentColor() are already valid.
|
||||
if (ctx->transparentColor == -2) {
|
||||
const int pt = pDraw->iPixelType;
|
||||
ctx->transparentColor = (pDraw->iHasAlpha && (pt == PNG_PIXEL_TRUECOLOR || pt == PNG_PIXEL_GRAYSCALE))
|
||||
? (int32_t)ctx->pngObj->getTransparentColor()
|
||||
: -1;
|
||||
}
|
||||
|
||||
const int destY = ctx->dstY + (int)(pDraw->y * ctx->yScale);
|
||||
if (destY == ctx->lastDstY) return 1; // skip duplicate rows from Y scaling
|
||||
ctx->lastDstY = destY;
|
||||
if (destY < 0 || destY >= ctx->screenH) return 1;
|
||||
|
||||
const int srcWidth = ctx->srcWidth;
|
||||
const int dstWidth = ctx->dstWidth;
|
||||
const uint8_t* pixels = pDraw->pPixels;
|
||||
const int pixelType = pDraw->iPixelType;
|
||||
const int hasAlpha = pDraw->iHasAlpha;
|
||||
|
||||
int srcX = 0, error = 0;
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
const int outX = ctx->dstX + dstX;
|
||||
if (outX >= 0 && outX < ctx->screenW) {
|
||||
uint8_t alpha = 255, gray = 0;
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA: {
|
||||
const uint8_t* p = &pixels[srcX * 4];
|
||||
alpha = p[3];
|
||||
gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
break;
|
||||
}
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
gray = pixels[srcX * 2];
|
||||
alpha = pixels[srcX * 2 + 1];
|
||||
break;
|
||||
case PNG_PIXEL_TRUECOLOR: {
|
||||
const uint8_t* p = &pixels[srcX * 3];
|
||||
gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
// tRNS color-key: if pixel matches the designated transparent color, skip it
|
||||
if (ctx->transparentColor >= 0 && p[0] == (uint8_t)((ctx->transparentColor >> 16) & 0xFF) &&
|
||||
p[1] == (uint8_t)((ctx->transparentColor >> 8) & 0xFF) &&
|
||||
p[2] == (uint8_t)(ctx->transparentColor & 0xFF)) {
|
||||
alpha = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
gray = pixels[srcX];
|
||||
// tRNS color-key: transparent gray value stored in low byte
|
||||
if (ctx->transparentColor >= 0 && gray == (uint8_t)(ctx->transparentColor & 0xFF)) {
|
||||
alpha = 0;
|
||||
}
|
||||
break;
|
||||
case PNG_PIXEL_INDEXED:
|
||||
if (pDraw->pPalette) {
|
||||
const uint8_t idx = pixels[srcX];
|
||||
const uint8_t* p = &pDraw->pPalette[idx * 3];
|
||||
gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
if (hasAlpha) alpha = pDraw->pPalette[768 + idx];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
gray = pixels[srcX];
|
||||
break;
|
||||
}
|
||||
|
||||
if (alpha >= 128) {
|
||||
ctx->renderer->drawPixel(outX, destY, gray < 128); // true = black, false = white
|
||||
}
|
||||
// alpha < 128: transparent — leave the reader page pixel intact
|
||||
}
|
||||
|
||||
// Bresenham-style X stepping (handles downscaling; 1:1 when srcWidth == dstWidth)
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SleepActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
|
||||
// For OVERLAY mode the popup is suppressed so the frame buffer (reader page) stays intact
|
||||
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY) {
|
||||
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
|
||||
}
|
||||
|
||||
switch (SETTINGS.sleepScreen) {
|
||||
case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK):
|
||||
@@ -26,6 +174,8 @@ void SleepActivity::onEnter() {
|
||||
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER):
|
||||
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM):
|
||||
return renderCoverSleepScreen();
|
||||
case (CrossPointSettings::SLEEP_SCREEN_MODE::OVERLAY):
|
||||
return renderOverlaySleepScreen();
|
||||
default:
|
||||
return renderDefaultSleepScreen();
|
||||
}
|
||||
@@ -294,3 +444,175 @@ void SleepActivity::renderBlankSleepScreen() const {
|
||||
renderer.clearScreen();
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
}
|
||||
|
||||
void SleepActivity::renderOverlaySleepScreen() const {
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Step 1: Ensure the frame buffer contains the reader page.
|
||||
// When coming from a reader activity the frame buffer already holds the page.
|
||||
// When coming from a non-reader activity we re-render it from the saved progress.
|
||||
if (!APP_STATE.lastSleepFromReader && !APP_STATE.openEpubPath.empty()) {
|
||||
const auto& path = APP_STATE.openEpubPath;
|
||||
bool rendered = false;
|
||||
|
||||
if (FsHelpers::checkFileExtension(path, ".xtc") || FsHelpers::checkFileExtension(path, ".xtch")) {
|
||||
rendered = XtcReaderActivity::drawCurrentPageToBuffer(path, renderer);
|
||||
} else if (FsHelpers::checkFileExtension(path, ".txt")) {
|
||||
rendered = TxtReaderActivity::drawCurrentPageToBuffer(path, renderer);
|
||||
} else if (FsHelpers::checkFileExtension(path, ".epub")) {
|
||||
rendered = EpubReaderActivity::drawCurrentPageToBuffer(path, renderer);
|
||||
}
|
||||
|
||||
if (!rendered) {
|
||||
LOG_DBG("SLP", "Page re-render failed, using white background");
|
||||
renderer.clearScreen();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Load the overlay image using the same selection logic as renderCustomSleepScreen.
|
||||
// BMP: white pixels are skipped (transparent via drawBitmap), black pixels composited on top.
|
||||
// PNG: pixels with alpha < 128 are skipped; opaque pixels are drawn with their grayscale value.
|
||||
auto tryDrawOverlay = [&](const std::string& filename) -> bool {
|
||||
FsFile file;
|
||||
if (!Storage.openFileForRead("SLP", filename, file)) return false;
|
||||
Bitmap bitmap(file, true);
|
||||
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int x, y;
|
||||
float cropX = 0, cropY = 0;
|
||||
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
||||
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
||||
if (ratio > screenRatio) {
|
||||
x = 0;
|
||||
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
||||
} else {
|
||||
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
|
||||
y = 0;
|
||||
}
|
||||
} else {
|
||||
x = (pageWidth - bitmap.getWidth()) / 2;
|
||||
y = (pageHeight - bitmap.getHeight()) / 2;
|
||||
}
|
||||
|
||||
// Draw without clearScreen so the reader page remains in the frame buffer beneath
|
||||
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
|
||||
file.close();
|
||||
return true;
|
||||
};
|
||||
|
||||
auto tryDrawPngOverlay = [&](const std::string& filename) -> bool {
|
||||
constexpr size_t MIN_FREE_HEAP = 60 * 1024; // PNG decoder ~42 KB + overhead
|
||||
if (ESP.getFreeHeap() < MIN_FREE_HEAP) {
|
||||
LOG_ERR("SLP", "Not enough heap for PNG overlay decoder");
|
||||
return false;
|
||||
}
|
||||
PNG* png = new (std::nothrow) PNG();
|
||||
if (!png) return false;
|
||||
|
||||
int rc = png->open(filename.c_str(), pngSleepOpen, pngSleepClose, pngSleepRead, pngSleepSeek, pngOverlayDraw);
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_DBG("SLP", "PNG open failed: %s (%d)", filename.c_str(), rc);
|
||||
delete png;
|
||||
return false;
|
||||
}
|
||||
|
||||
const int srcW = png->getWidth(), srcH = png->getHeight();
|
||||
float yScale = 1.0f;
|
||||
int dstW = srcW, dstH = srcH;
|
||||
if (srcW > pageWidth || srcH > pageHeight) {
|
||||
const float scaleX = (float)pageWidth / srcW, scaleY = (float)pageHeight / srcH;
|
||||
const float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
dstW = (int)(srcW * scale);
|
||||
dstH = (int)(srcH * scale);
|
||||
yScale = (float)dstH / srcH;
|
||||
}
|
||||
|
||||
PngOverlayCtx ctx;
|
||||
ctx.renderer = &renderer;
|
||||
ctx.screenW = pageWidth;
|
||||
ctx.screenH = pageHeight;
|
||||
ctx.srcWidth = srcW;
|
||||
ctx.dstWidth = dstW;
|
||||
ctx.dstX = (pageWidth - dstW) / 2;
|
||||
ctx.dstY = (pageHeight - dstH) / 2;
|
||||
ctx.yScale = yScale;
|
||||
ctx.lastDstY = -1;
|
||||
ctx.transparentColor = -2; // will be resolved on first draw callback (after tRNS is parsed)
|
||||
ctx.pngObj = png;
|
||||
|
||||
rc = png->decode(&ctx, 0);
|
||||
png->close();
|
||||
delete png;
|
||||
return rc == PNG_SUCCESS;
|
||||
};
|
||||
|
||||
// Try /sleep/ directory first (random selection, same as renderCustomSleepScreen).
|
||||
// Accepts both .bmp and .png files; .bmp headers are validated during the scan.
|
||||
bool overlayDrawn = false;
|
||||
auto dir = Storage.open("/sleep");
|
||||
if (dir && dir.isDirectory()) {
|
||||
std::vector<std::string> files;
|
||||
char name[500];
|
||||
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
|
||||
if (file.isDirectory()) {
|
||||
file.close();
|
||||
continue;
|
||||
}
|
||||
file.getName(name, sizeof(name));
|
||||
auto filename = std::string(name);
|
||||
if (filename[0] == '.') {
|
||||
file.close();
|
||||
continue;
|
||||
}
|
||||
const bool isBmp = FsHelpers::checkFileExtension(filename, ".bmp");
|
||||
const bool isPng = FsHelpers::checkFileExtension(filename, ".png");
|
||||
if (!isBmp && !isPng) {
|
||||
file.close();
|
||||
continue;
|
||||
}
|
||||
if (isBmp) {
|
||||
Bitmap bmp(file);
|
||||
if (bmp.parseHeaders() != BmpReaderError::Ok) {
|
||||
file.close();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
files.emplace_back(filename);
|
||||
file.close();
|
||||
}
|
||||
const auto numFiles = files.size();
|
||||
if (numFiles > 0) {
|
||||
auto randomFileIndex = random(numFiles);
|
||||
while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) {
|
||||
randomFileIndex = random(numFiles);
|
||||
}
|
||||
APP_STATE.lastSleepImage = randomFileIndex;
|
||||
APP_STATE.saveToFile();
|
||||
const std::string selected = "/sleep/" + files[randomFileIndex];
|
||||
if (FsHelpers::checkFileExtension(selected, ".png")) {
|
||||
overlayDrawn = tryDrawPngOverlay(selected);
|
||||
} else {
|
||||
overlayDrawn = tryDrawOverlay(selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dir) dir.close();
|
||||
|
||||
if (!overlayDrawn) {
|
||||
overlayDrawn = tryDrawOverlay("/sleep.bmp");
|
||||
}
|
||||
if (!overlayDrawn) {
|
||||
overlayDrawn = tryDrawPngOverlay("/sleep.png");
|
||||
}
|
||||
|
||||
if (!overlayDrawn) {
|
||||
LOG_DBG("SLP", "No overlay image found, displaying page without overlay");
|
||||
}
|
||||
|
||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||
}
|
||||
|
||||
@@ -15,4 +15,5 @@ class SleepActivity final : public Activity {
|
||||
void renderCoverSleepScreen() const;
|
||||
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
|
||||
void renderBlankSleepScreen() const;
|
||||
void renderOverlaySleepScreen() const;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
@@ -850,3 +852,73 @@ void EpubReaderActivity::restoreSavedPosition() {
|
||||
}
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
bool EpubReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) {
|
||||
auto epub = std::make_shared<Epub>(filePath, "/.crosspoint");
|
||||
// Load CSS when embeddedStyle is enabled, as createSectionFile may need it to rebuild the cache.
|
||||
if (!epub->load(true, SETTINGS.embeddedStyle == 0)) {
|
||||
LOG_DBG("SLP", "EPUB: failed to load %s", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
epub->setupCacheDir();
|
||||
|
||||
// Load saved spine index and page number
|
||||
int spineIndex = 0, pageNumber = 0;
|
||||
FsFile f;
|
||||
if (Storage.openFileForRead("SLP", epub->getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[6];
|
||||
if (f.read(data, 6) == 6) {
|
||||
spineIndex = (int)((uint32_t)data[0] | ((uint32_t)data[1] << 8));
|
||||
pageNumber = (int)((uint32_t)data[2] | ((uint32_t)data[3] << 8));
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
if (spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) spineIndex = 0;
|
||||
|
||||
// Apply the reader orientation so margins match what the reader would produce
|
||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
||||
|
||||
// Compute margins exactly as render() does
|
||||
int marginTop, marginRight, marginBottom, marginLeft;
|
||||
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
|
||||
marginTop += SETTINGS.screenMargin;
|
||||
marginLeft += SETTINGS.screenMargin;
|
||||
marginRight += SETTINGS.screenMargin;
|
||||
const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight();
|
||||
marginBottom += std::max(SETTINGS.screenMargin, statusBarHeight);
|
||||
|
||||
const uint16_t viewportWidth = renderer.getScreenWidth() - marginLeft - marginRight;
|
||||
const uint16_t viewportHeight = renderer.getScreenHeight() - marginTop - marginBottom;
|
||||
|
||||
// Load or rebuild the section cache. Rebuilding is needed when the cache is missing or stale
|
||||
// (e.g. after a firmware update). A no-op popup callback avoids any UI during sleep preparation.
|
||||
auto section = std::make_unique<Section>(epub, spineIndex, renderer);
|
||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
LOG_DBG("SLP", "EPUB: section cache not found for spine %d, rebuilding", spineIndex);
|
||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, []() {})) {
|
||||
LOG_ERR("SLP", "EPUB: failed to rebuild section cache for spine %d", spineIndex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (pageNumber < 0 || pageNumber >= section->pageCount) pageNumber = 0;
|
||||
section->currentPage = pageNumber;
|
||||
|
||||
auto page = section->loadPageFromSectionFile();
|
||||
if (!page) {
|
||||
LOG_DBG("SLP", "EPUB: failed to load page %d", pageNumber);
|
||||
return false;
|
||||
}
|
||||
|
||||
renderer.clearScreen();
|
||||
page->render(renderer, SETTINGS.getReaderFontId(), marginLeft, marginTop);
|
||||
// No displayBuffer call — caller (SleepActivity) handles that after compositing the overlay
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,4 +61,9 @@ class EpubReaderActivity final : public Activity {
|
||||
void loop() override;
|
||||
void render(RenderLock&& lock) override;
|
||||
bool isReaderActivity() const override { return true; }
|
||||
|
||||
// Renders the last saved page to the frame buffer without flushing to display.
|
||||
// Used by SleepActivity to prepare the background for the overlay sleep mode.
|
||||
// Returns false if the page cannot be loaded (missing cache / file error).
|
||||
static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,65 @@ constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
||||
// Cache file magic and version
|
||||
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
||||
constexpr uint32_t MAX_CACHE_PAGES = 65535; // Sanity cap to prevent unbounded reserve()
|
||||
|
||||
// Parses and word-wraps lines from a file chunk into outLines.
|
||||
// Returns the number of bytes consumed from the start of buffer.
|
||||
size_t parseAndWrapLines(const uint8_t* buffer, size_t chunkSize, size_t fileOffset, size_t fileSize, int linesPerPage,
|
||||
GfxRenderer& renderer, int fontId, int vw, std::vector<std::string>& outLines) {
|
||||
size_t pos = 0;
|
||||
while (pos < chunkSize && static_cast<int>(outLines.size()) < linesPerPage) {
|
||||
size_t lineEnd = pos;
|
||||
while (lineEnd < chunkSize && buffer[lineEnd] != '\n') lineEnd++;
|
||||
bool lineComplete = (lineEnd < chunkSize) || (fileOffset + lineEnd >= fileSize);
|
||||
if (!lineComplete && !outLines.empty()) break;
|
||||
|
||||
size_t lineContentLen = lineEnd - pos;
|
||||
bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r');
|
||||
size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen;
|
||||
std::string line(reinterpret_cast<const char*>(buffer + pos), displayLen);
|
||||
size_t lineBytePos = 0;
|
||||
|
||||
while (!line.empty() && static_cast<int>(outLines.size()) < linesPerPage) {
|
||||
if (renderer.getTextWidth(fontId, line.c_str()) <= vw) {
|
||||
outLines.push_back(line);
|
||||
lineBytePos = displayLen;
|
||||
line.clear();
|
||||
break;
|
||||
}
|
||||
size_t breakPos = line.length();
|
||||
while (breakPos > 0 && renderer.getTextWidth(fontId, line.substr(0, breakPos).c_str()) > vw) {
|
||||
size_t spacePos = line.rfind(' ', breakPos - 1);
|
||||
if (spacePos != std::string::npos && spacePos > 0) {
|
||||
breakPos = spacePos;
|
||||
} else {
|
||||
breakPos--;
|
||||
while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) breakPos--;
|
||||
}
|
||||
}
|
||||
if (breakPos == 0) {
|
||||
breakPos = 1;
|
||||
while (breakPos < line.length() && (line[breakPos] & 0xC0) == 0x80) breakPos++;
|
||||
}
|
||||
outLines.push_back(line.substr(0, breakPos));
|
||||
size_t skipChars = breakPos;
|
||||
if (breakPos < line.length() && line[breakPos] == ' ') skipChars++;
|
||||
lineBytePos += skipChars;
|
||||
line = line.substr(skipChars);
|
||||
}
|
||||
|
||||
if (line.empty()) {
|
||||
pos = lineEnd + 1;
|
||||
} else {
|
||||
pos = pos + lineBytePos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pos == 0 && !outLines.empty()) {
|
||||
pos = 1;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void TxtReaderActivity::onEnter() {
|
||||
@@ -216,101 +275,9 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
|
||||
}
|
||||
buffer[chunkSize] = '\0';
|
||||
|
||||
// Parse lines from buffer
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < chunkSize && static_cast<int>(outLines.size()) < linesPerPage) {
|
||||
// Find end of line
|
||||
size_t lineEnd = pos;
|
||||
while (lineEnd < chunkSize && buffer[lineEnd] != '\n') {
|
||||
lineEnd++;
|
||||
}
|
||||
|
||||
// Check if we have a complete line
|
||||
bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize);
|
||||
|
||||
if (!lineComplete && static_cast<int>(outLines.size()) > 0) {
|
||||
// Incomplete line and we already have some lines, stop here
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate the actual length of line content in the buffer (excluding newline)
|
||||
size_t lineContentLen = lineEnd - pos;
|
||||
|
||||
// Check for carriage return
|
||||
bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r');
|
||||
size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen;
|
||||
|
||||
// Extract line content for display (without CR/LF)
|
||||
std::string line(reinterpret_cast<char*>(buffer + pos), displayLen);
|
||||
|
||||
// Track position within this source line (in bytes from pos)
|
||||
size_t lineBytePos = 0;
|
||||
|
||||
// Word wrap if needed
|
||||
while (!line.empty() && static_cast<int>(outLines.size()) < linesPerPage) {
|
||||
int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str());
|
||||
|
||||
if (lineWidth <= viewportWidth) {
|
||||
outLines.push_back(line);
|
||||
lineBytePos = displayLen; // Consumed entire display content
|
||||
line.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
// Find break point
|
||||
size_t breakPos = line.length();
|
||||
while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) {
|
||||
// Try to break at space
|
||||
size_t spacePos = line.rfind(' ', breakPos - 1);
|
||||
if (spacePos != std::string::npos && spacePos > 0) {
|
||||
breakPos = spacePos;
|
||||
} else {
|
||||
// Break at character boundary for UTF-8
|
||||
breakPos--;
|
||||
// Make sure we don't break in the middle of a UTF-8 sequence
|
||||
while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) {
|
||||
breakPos--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (breakPos == 0) {
|
||||
breakPos = 1;
|
||||
}
|
||||
|
||||
outLines.push_back(line.substr(0, breakPos));
|
||||
|
||||
// Skip space at break point
|
||||
size_t skipChars = breakPos;
|
||||
if (breakPos < line.length() && line[breakPos] == ' ') {
|
||||
skipChars++;
|
||||
}
|
||||
lineBytePos += skipChars;
|
||||
line = line.substr(skipChars);
|
||||
}
|
||||
|
||||
// Determine how much of the source buffer we consumed
|
||||
if (line.empty()) {
|
||||
// Fully consumed this source line, move past the newline
|
||||
pos = lineEnd + 1;
|
||||
} else {
|
||||
// Partially consumed - page is full mid-line
|
||||
// Move pos to where we stopped in the line (NOT past the line)
|
||||
pos = pos + lineBytePos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we make progress even if calculations go wrong
|
||||
if (pos == 0 && !outLines.empty()) {
|
||||
// Fallback: at minimum, consume something to avoid infinite loop
|
||||
pos = 1;
|
||||
}
|
||||
|
||||
size_t pos = parseAndWrapLines(buffer, chunkSize, offset, fileSize, linesPerPage, renderer, cachedFontId,
|
||||
viewportWidth, outLines);
|
||||
nextOffset = offset + pos;
|
||||
|
||||
// Make sure we don't go past the file
|
||||
if (nextOffset > fileSize) {
|
||||
nextOffset = fileSize;
|
||||
}
|
||||
@@ -441,12 +408,17 @@ void TxtReaderActivity::renderStatusBar() const {
|
||||
void TxtReaderActivity::saveProgress() const {
|
||||
FsFile f;
|
||||
if (Storage.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
// 6-byte format: page(2 bytes LE) + file offset(4 bytes LE)
|
||||
// The offset lets drawCurrentPageToBuffer render without requiring index.bin.
|
||||
const size_t offset = (currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
|
||||
uint8_t data[6];
|
||||
data[0] = currentPage & 0xFF;
|
||||
data[1] = (currentPage >> 8) & 0xFF;
|
||||
data[2] = 0;
|
||||
data[3] = 0;
|
||||
f.write(data, 4);
|
||||
data[2] = offset & 0xFF;
|
||||
data[3] = (offset >> 8) & 0xFF;
|
||||
data[4] = (offset >> 16) & 0xFF;
|
||||
data[5] = (offset >> 24) & 0xFF;
|
||||
f.write(data, 6);
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
@@ -556,6 +528,11 @@ bool TxtReaderActivity::loadPageIndexCache() {
|
||||
|
||||
uint32_t numPages;
|
||||
serialization::readPod(f, numPages);
|
||||
if (numPages > MAX_CACHE_PAGES) {
|
||||
LOG_ERR("TRS", "Cache numPages %u exceeds cap %u, cache invalid", numPages, MAX_CACHE_PAGES);
|
||||
f.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read page offsets
|
||||
pageOffsets.clear();
|
||||
@@ -600,3 +577,170 @@ void TxtReaderActivity::savePageIndexCache() const {
|
||||
f.close();
|
||||
LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages);
|
||||
}
|
||||
|
||||
bool TxtReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) {
|
||||
Txt txt(filePath, "/.crosspoint");
|
||||
if (!txt.load()) {
|
||||
LOG_DBG("SLP", "TXT: failed to load %s", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply the reader orientation so margins match what the reader would produce
|
||||
switch (SETTINGS.orientation) {
|
||||
case CrossPointSettings::ORIENTATION::PORTRAIT:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::INVERTED:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
|
||||
break;
|
||||
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
|
||||
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Compute layout values that match what initializeReader() produces
|
||||
const int fontId = SETTINGS.getReaderFontId();
|
||||
const uint8_t screenMargin = SETTINGS.screenMargin;
|
||||
const uint8_t paragraphAlignment = SETTINGS.paragraphAlignment;
|
||||
|
||||
int marginTop, marginRight, marginBottom, marginLeft;
|
||||
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
|
||||
marginTop += screenMargin;
|
||||
marginLeft += screenMargin;
|
||||
marginRight += screenMargin;
|
||||
marginBottom += std::max(screenMargin, static_cast<uint8_t>(UITheme::getInstance().getStatusBarHeight()));
|
||||
|
||||
const int vw = renderer.getScreenWidth() - marginLeft - marginRight;
|
||||
const int vh = renderer.getScreenHeight() - marginTop - marginBottom;
|
||||
const int lineHeight = renderer.getLineHeight(fontId);
|
||||
const int linesPerPage = std::max(1, vh / lineHeight);
|
||||
|
||||
// Step 1: Try to read the saved page and its file offset from progress.bin.
|
||||
// The 6-byte format (written by saveProgress) stores: page(2) + offset(4).
|
||||
// This lets us skip index.bin entirely, so the overlay works even when the
|
||||
// page index cache is missing or stale (e.g. after a firmware update).
|
||||
int savedPage = 0;
|
||||
size_t savedOffset = 0;
|
||||
bool offsetKnown = false;
|
||||
{
|
||||
FsFile progFile;
|
||||
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", progFile)) {
|
||||
uint8_t data[6] = {0};
|
||||
const int n = progFile.read(data, 6);
|
||||
progFile.close();
|
||||
if (n >= 2) {
|
||||
savedPage = (int)((uint32_t)data[0] | ((uint32_t)data[1] << 8));
|
||||
}
|
||||
if (n >= 6) {
|
||||
const uint32_t off =
|
||||
(uint32_t)data[2] | ((uint32_t)data[3] << 8) | ((uint32_t)data[4] << 16) | ((uint32_t)data[5] << 24);
|
||||
if (off < txt.getFileSize()) {
|
||||
savedOffset = off;
|
||||
offsetKnown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: If progress.bin didn't provide the offset, fall back to index.bin.
|
||||
if (!offsetKnown) {
|
||||
std::string cachePath = txt.getCachePath() + "/index.bin";
|
||||
FsFile cacheFile;
|
||||
if (Storage.openFileForRead("SLP", cachePath, cacheFile)) {
|
||||
uint32_t magic;
|
||||
serialization::readPod(cacheFile, magic);
|
||||
uint8_t version;
|
||||
serialization::readPod(cacheFile, version);
|
||||
uint32_t cachedFileSize;
|
||||
serialization::readPod(cacheFile, cachedFileSize);
|
||||
int32_t cachedVw, cachedLpp, cachedFontId, cachedMargin;
|
||||
serialization::readPod(cacheFile, cachedVw);
|
||||
serialization::readPod(cacheFile, cachedLpp);
|
||||
serialization::readPod(cacheFile, cachedFontId);
|
||||
serialization::readPod(cacheFile, cachedMargin);
|
||||
uint8_t cachedAlignment;
|
||||
serialization::readPod(cacheFile, cachedAlignment);
|
||||
uint32_t numPages;
|
||||
serialization::readPod(cacheFile, numPages);
|
||||
|
||||
if (magic == CACHE_MAGIC && version == CACHE_VERSION && cachedFileSize == txt.getFileSize() && cachedVw == vw &&
|
||||
cachedLpp == linesPerPage && cachedFontId == fontId && cachedMargin == screenMargin &&
|
||||
cachedAlignment == paragraphAlignment && numPages > 0 && numPages <= MAX_CACHE_PAGES) {
|
||||
if (savedPage < 0 || savedPage >= static_cast<int>(numPages)) savedPage = 0;
|
||||
for (uint32_t i = 0; i < numPages; i++) {
|
||||
uint32_t off;
|
||||
serialization::readPod(cacheFile, off);
|
||||
if (static_cast<int>(i) == savedPage) {
|
||||
if (off < txt.getFileSize()) {
|
||||
savedOffset = off;
|
||||
offsetKnown = true;
|
||||
} else {
|
||||
LOG_DBG("SLP", "TXT: index.bin offset %u out of range (fileSize=%u), ignoring", off, txt.getFileSize());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_DBG("SLP", "TXT: index cache invalid or stale");
|
||||
}
|
||||
cacheFile.close();
|
||||
}
|
||||
|
||||
// Step 3: No valid cache at all — render from the start of the file as a last resort.
|
||||
// This shows page 1 rather than a blank screen, which is always preferable.
|
||||
if (!offsetKnown) {
|
||||
LOG_DBG("SLP", "TXT: no valid cache, falling back to start of file");
|
||||
savedOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the page lines from file
|
||||
std::vector<std::string> pageLines;
|
||||
const size_t fileSize = txt.getFileSize();
|
||||
size_t offset = savedOffset;
|
||||
if (offset >= fileSize) {
|
||||
LOG_DBG("SLP", "TXT: page offset out of bounds");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset);
|
||||
auto* buffer = static_cast<uint8_t*>(malloc(chunkSize + 1));
|
||||
if (!buffer) return false;
|
||||
|
||||
if (!txt.readContent(buffer, offset, chunkSize)) {
|
||||
free(buffer);
|
||||
return false;
|
||||
}
|
||||
buffer[chunkSize] = '\0';
|
||||
|
||||
parseAndWrapLines(buffer, chunkSize, offset, fileSize, linesPerPage, renderer, fontId, vw, pageLines);
|
||||
free(buffer);
|
||||
|
||||
if (pageLines.empty()) return false;
|
||||
|
||||
// Render lines to frame buffer (no displayBuffer call)
|
||||
renderer.clearScreen();
|
||||
int y = marginTop;
|
||||
for (const auto& line : pageLines) {
|
||||
if (!line.empty()) {
|
||||
int x = marginLeft;
|
||||
switch (paragraphAlignment) {
|
||||
case CrossPointSettings::CENTER_ALIGN:
|
||||
x = marginLeft + (vw - renderer.getTextWidth(fontId, line.c_str())) / 2;
|
||||
break;
|
||||
case CrossPointSettings::RIGHT_ALIGN:
|
||||
x = marginLeft + vw - renderer.getTextWidth(fontId, line.c_str());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
renderer.drawText(fontId, x, y, line.c_str());
|
||||
}
|
||||
y += lineHeight;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -49,4 +49,9 @@ class TxtReaderActivity final : public Activity {
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
bool isReaderActivity() const override { return true; }
|
||||
|
||||
// Renders the last saved page to the frame buffer without flushing to display.
|
||||
// Used by SleepActivity to prepare the background for the overlay sleep mode.
|
||||
// Returns false if the page cannot be loaded (missing cache / file error).
|
||||
static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer);
|
||||
};
|
||||
|
||||
@@ -351,3 +351,78 @@ void XtcReaderActivity::loadProgress() {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
bool XtcReaderActivity::drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer) {
|
||||
Xtc xtc(filePath, "/.crosspoint");
|
||||
if (!xtc.load()) {
|
||||
LOG_DBG("SLP", "XTC: failed to load %s", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load saved page number
|
||||
uint32_t savedPage = 0;
|
||||
FsFile f;
|
||||
if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) {
|
||||
uint8_t data[4];
|
||||
if (f.read(data, 4) == 4) {
|
||||
savedPage = (uint32_t)data[0] | ((uint32_t)data[1] << 8) | ((uint32_t)data[2] << 16) | ((uint32_t)data[3] << 24);
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
if (savedPage >= xtc.getPageCount()) savedPage = 0;
|
||||
|
||||
const uint16_t pageWidth = xtc.getPageWidth();
|
||||
const uint16_t pageHeight = xtc.getPageHeight();
|
||||
const uint8_t bitDepth = xtc.getBitDepth();
|
||||
|
||||
// Only use the 1-bit BW path; grayscale is not needed as a background under the overlay
|
||||
const size_t pageBufferSize = (bitDepth == 2) ? ((static_cast<size_t>(pageWidth) * pageHeight + 7) / 8) * 2
|
||||
: ((pageWidth + 7) / 8) * pageHeight;
|
||||
|
||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(pageBufferSize));
|
||||
if (!pageBuffer) {
|
||||
LOG_ERR("SLP", "XTC: failed to allocate page buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (xtc.loadPage(savedPage, pageBuffer, pageBufferSize) == 0) {
|
||||
LOG_ERR("SLP", "XTC: failed to load page %lu", savedPage);
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
if (bitDepth == 2) {
|
||||
// 2-bit XTH: draw all non-white pixels as black (BW pass only)
|
||||
const size_t planeSize = (static_cast<size_t>(pageWidth) * pageHeight + 7) / 8;
|
||||
const uint8_t* plane1 = pageBuffer;
|
||||
const uint8_t* plane2 = pageBuffer + planeSize;
|
||||
const size_t colBytes = (pageHeight + 7) / 8;
|
||||
for (uint16_t y = 0; y < pageHeight; y++) {
|
||||
for (uint16_t x = 0; x < pageWidth; x++) {
|
||||
const size_t colIndex = pageWidth - 1 - x;
|
||||
const size_t byteInCol = y / 8;
|
||||
const size_t bitInByte = 7 - (y % 8);
|
||||
const size_t byteOffset = colIndex * colBytes + byteInCol;
|
||||
const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1;
|
||||
const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1;
|
||||
if ((bit1 << 1) | bit2) {
|
||||
renderer.drawPixel(x, y, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 1-bit XTG: draw black pixels
|
||||
const size_t srcRowBytes = (pageWidth + 7) / 8;
|
||||
for (uint16_t srcY = 0; srcY < pageHeight; srcY++) {
|
||||
for (uint16_t srcX = 0; srcX < pageWidth; srcX++) {
|
||||
const bool isBlack = !((pageBuffer[srcY * srcRowBytes + srcX / 8] >> (7 - srcX % 8)) & 1);
|
||||
if (isBlack) renderer.drawPixel(srcX, srcY, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(pageBuffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -29,4 +29,9 @@ class XtcReaderActivity final : public Activity {
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
bool isReaderActivity() const override { return true; }
|
||||
|
||||
// Renders the last saved page to the frame buffer without flushing to display.
|
||||
// Used by SleepActivity to prepare the background for the overlay sleep mode.
|
||||
// Returns false if the page cannot be loaded (missing cache / file error).
|
||||
static bool drawCurrentPageToBuffer(const std::string& filePath, GfxRenderer& renderer);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user