Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 48 additions & 30 deletions src/activities/home/HomeActivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ void HomeActivity::onEnter() {
firstRenderDone = false;
recentsLoaded = false;
recentsLoading = false;
const auto snapshot = WeatherActivity::getSilentRefreshSnapshot();
weatherSyncSeenVersion = snapshot.completionVersion;
weatherRefreshing = snapshot.active;
loadRecentBooks(4);
requestUpdate();
}
Expand All @@ -135,6 +138,8 @@ void HomeActivity::onExit() {
// ── Input / Render dispatchers ────────────────────────────────────────────────

void HomeActivity::loop() {
pollWeatherSyncStatus();

if (SETTINGS.uiTheme <= CrossPointSettings::LYRA_3_COVERS)
loopOriginal();
else if (SETTINGS.uiTheme == CrossPointSettings::CROSSPET_CLASSIC)
Expand Down Expand Up @@ -251,40 +256,29 @@ void HomeActivity::renderHeaderClock() {

void HomeActivity::performSyncAfterWifi() {
static char syncBuf[24];
weatherRefreshing = true;
requestUpdateAndWait();

if (WiFi.status() != WL_CONNECTED) {
const auto& ssid = WIFI_STORE.getLastConnectedSsid();
const auto* cred = ssid.empty() ? nullptr : WIFI_STORE.findCredential(ssid);
if (cred) {
WiFi.mode(WIFI_STA);
WiFi.begin(cred->ssid.c_str(), cred->password.c_str());
const unsigned long connectStart = millis();
while (WiFi.status() != WL_CONNECTED && millis() - connectStart < 8000) {
delay(100);
}
if (WiFi.status() != WL_CONNECTED) {
WiFi.disconnect(false);
WiFi.mode(WIFI_OFF);
weatherRefreshing = false;
snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_WIFI_CONN_FAILED));
syncResultMsg = syncBuf;
syncResultExpiry = millis() + 3000;
requestUpdate();
return;
}
}

const auto startResult = WeatherActivity::startSilentRefreshAsync();
const auto snapshot = WeatherActivity::getSilentRefreshSnapshot();
weatherRefreshing = snapshot.active;

if (startResult == SilentRefreshStartResult::STARTED) {
syncResultMsg = nullptr;
requestUpdate();
return;
}

if (startResult == SilentRefreshStartResult::ALREADY_RUNNING) {
requestUpdate();
return;
}

int rc = WeatherActivity::silentRefresh();
weatherRefreshing = false;
if (rc == 0) snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_SYNC_OK));
else if (rc == 2) snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_WIFI_TIMEOUT));
else snprintf(syncBuf, sizeof(syncBuf), tr(STR_API_ERROR), rc);
if (startResult == SilentRefreshStartResult::BUSY_FOREGROUND) {
snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_FETCHING_WEATHER));
} else {
snprintf(syncBuf, sizeof(syncBuf), tr(STR_API_ERROR), 6);
}
syncResultMsg = syncBuf;
syncResultExpiry = millis() + 3000;
coverRendered = false;
requestUpdate();
}

Expand All @@ -300,6 +294,30 @@ void HomeActivity::doSync() {
performSyncAfterWifi();
}

void HomeActivity::pollWeatherSyncStatus() {
static char syncBuf[24];
const auto snapshot = WeatherActivity::getSilentRefreshSnapshot();
weatherRefreshing = snapshot.active;

if (snapshot.completionVersion != weatherSyncSeenVersion) {
weatherSyncSeenVersion = snapshot.completionVersion;
weatherRefreshing = false;
if (snapshot.result == 0) {
snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_SYNC_OK));
} else if (snapshot.result == 2) {
snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_WIFI_TIMEOUT));
} else if (snapshot.result == 1) {
snprintf(syncBuf, sizeof(syncBuf), "%s", tr(STR_WIFI_CONN_FAILED));
} else {
snprintf(syncBuf, sizeof(syncBuf), tr(STR_API_ERROR), snapshot.result);
}
syncResultMsg = syncBuf;
syncResultExpiry = millis() + 3000;
coverRendered = false;
requestUpdate();
}
}

// ── Actions ───────────────────────────────────────────────────────────────────

void HomeActivity::onSelectBook(const std::string& path) { freeCoverBuffer(); activityManager.goToReader(path); }
Expand Down
2 changes: 2 additions & 0 deletions src/activities/home/HomeActivity.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class HomeActivity final : public Activity {
const char* syncResultMsg = nullptr; // "OK" or "Failed" after sync
unsigned long syncResultExpiry = 0; // millis() when to clear message
bool syncTriggered = false; // Guard against re-triggering sync while held
uint32_t weatherSyncSeenVersion = 0;
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
Expand Down Expand Up @@ -65,6 +66,7 @@ class HomeActivity final : public Activity {
void renderHeaderClock();
void doSync();
void performSyncAfterWifi();
void pollWeatherSyncStatus();

// Theme-specific render/loop dispatchers
void renderCrossPet();
Expand Down
143 changes: 131 additions & 12 deletions src/activities/reader/EpubReaderActivity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <I18n.h>
#include <Logging.h>

#include <cstring>
#include <memory>

#include "CrossPointSettings.h"
Expand All @@ -34,6 +35,9 @@ namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
constexpr unsigned long skipChapterMs = 700;
constexpr unsigned long goHomeMs = 1000;
constexpr unsigned long bookmarkToastDurationMs = 900;
constexpr unsigned long progressSaveDebounceMs = 2000;
constexpr uint8_t maxPageLoadRetries = 3;
// Max pages per minute for auto-turn (setting range 1-20)
constexpr uint8_t AUTO_TURN_MAX_PPM = 20;

Expand Down Expand Up @@ -113,6 +117,10 @@ void EpubReaderActivity::onEnter() {
void EpubReaderActivity::onExit() {
Activity::onExit();

if (epub && section && section->pageCount > 0) {
flushProgressSave(currentSpineIndex, section->currentPage, section->pageCount);
}

// Reset text darkness to normal for UI screens
renderer.setTextDarkness(0);

Expand Down Expand Up @@ -189,6 +197,12 @@ void EpubReaderActivity::loop() {
requestUpdate();
}

// Dismiss bookmark toast after timeout (non-blocking)
if (showBookmarkToast && millis() - bookmarkToastTime > bookmarkToastDurationMs) {
showBookmarkToast = false;
requestUpdate();
}

if (automaticPageTurnActive) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) ||
mappedInput.wasReleased(MappedInputManager::Button::Back)) {
Expand Down Expand Up @@ -258,9 +272,7 @@ void EpubReaderActivity::loop() {
}
}
bookmarkStore.toggle(si, pg, snippet);
GUI.drawPopup(renderer, wasStarred ? tr(STR_PAGE_UNSTARRED) : tr(STR_PAGE_STARRED));
renderer.displayBuffer();
delay(600);
queueBookmarkToast(wasStarred ? tr(STR_PAGE_UNSTARRED) : tr(STR_PAGE_STARRED));
requestUpdate();
}
return;
Expand Down Expand Up @@ -356,9 +368,7 @@ void EpubReaderActivity::loop() {
}
}
bookmarkStore.toggle(si, pg, snippet);
GUI.drawPopup(renderer, wasStarred ? tr(STR_PAGE_UNSTARRED) : tr(STR_PAGE_STARRED));
renderer.displayBuffer();
delay(600);
queueBookmarkToast(wasStarred ? tr(STR_PAGE_UNSTARRED) : tr(STR_PAGE_STARRED));
requestUpdate();
}
return;
Expand Down Expand Up @@ -650,6 +660,9 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
// Update renderer orientation to match the new logical coordinate system.
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);

// Orientation changes invalidate glyph raster layout assumptions.
forceFontCacheClear = true;

// Reset section to force re-layout in the new orientation.
section.reset();
}
Expand Down Expand Up @@ -932,26 +945,75 @@ void EpubReaderActivity::render(RenderLock&& lock) {
}

{
const int attemptedPage = section->currentPage;
auto p = section->loadPageFromSectionFile();
if (!p) {
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
section->clearCache();
if (failedLoadSpineIndex == currentSpineIndex && failedLoadPage == attemptedPage) {
consecutiveLoadFailures++;
} else {
failedLoadSpineIndex = currentSpineIndex;
failedLoadPage = attemptedPage;
consecutiveLoadFailures = 1;
}

LOG_ERR("ERS", "Failed to load page from SD (spine=%d, page=%d, retry=%d)", currentSpineIndex,
attemptedPage, consecutiveLoadFailures);

const bool tooManyFailures = consecutiveLoadFailures >= maxPageLoadRetries;
if (!tooManyFailures) {
section->clearCache();
}
section.reset();
requestUpdate(); // Try again after clearing cache
// TODO: prevent infinite loop if the page keeps failing to load for some reason

if (tooManyFailures) {
automaticPageTurnActive = false;
consecutiveLoadFailures = 0;
failedLoadSpineIndex = -1;
failedLoadPage = -1;

if (currentSpineIndex < static_cast<int>(epub->getSpineItemsCount()) - 1) {
currentSpineIndex++;
nextPageNumber = 0;
queueBookmarkToast("Skipped bad chapter");
requestUpdate();
} else if (currentSpineIndex > 0) {
// Last chapter failed repeatedly: step back to previous chapter instead of looping forever.
currentSpineIndex--;
nextPageNumber = UINT16_MAX;
queueBookmarkToast("Skipped bad chapter");
requestUpdate();
} else {
// Single-chapter unrecoverable case: show once and stop auto-retrying this frame.
GUI.drawPopup(renderer, "Page load failed");
renderer.displayBuffer();
}
return;
}

requestUpdate();
automaticPageTurnActive = false;
return;
}

consecutiveLoadFailures = 0;
failedLoadSpineIndex = -1;
failedLoadPage = -1;

// Collect footnotes from the loaded page
currentPageFootnotes = std::move(p->footnotes);

const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
renderer.clearFontCache();
if (shouldClearFontCache()) {
renderer.clearFontCache();
forceFontCacheClear = false;
lastRenderedFontFamily = SETTINGS.fontFamily;
lastRenderedFontSize = SETTINGS.fontSize;
lastRenderedOrientation = SETTINGS.orientation;
}
}
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
maybeSaveProgress(currentSpineIndex, section->currentPage, section->pageCount);

if (pendingScreenshot) {
pendingScreenshot = false;
Expand Down Expand Up @@ -979,6 +1041,10 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
data[5] = (pageCount >> 8) & 0xFF;
f.write(data, 6);
f.close();
lastSavedSpineIndex = spineIndex;
lastSavedPage = currentPage;
lastSavedPageCount = pageCount;
lastProgressSaveMs = millis();
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
} else {
LOG_ERR("ERS", "Could not save progress!");
Expand All @@ -990,6 +1056,48 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
const uint8_t percent = static_cast<uint8_t>(clampPercent(static_cast<int>(bookProg + 0.5f)));
RECENT_BOOKS.updateBookProgress(epub->getPath(), percent);
}

void EpubReaderActivity::queueBookmarkToast(const char* text) {
if (!text) {
return;
}
std::strncpy(bookmarkToastText, text, sizeof(bookmarkToastText) - 1);
bookmarkToastText[sizeof(bookmarkToastText) - 1] = '\0';
showBookmarkToast = true;
bookmarkToastTime = millis();
}

void EpubReaderActivity::maybeSaveProgress(int spineIndex, int currentPage, int pageCount) {
const bool changed = spineIndex != lastSavedSpineIndex ||
currentPage != lastSavedPage ||
pageCount != lastSavedPageCount;
if (!changed) {
return;
}

if (millis() - lastProgressSaveMs < progressSaveDebounceMs) {
return;
}

saveProgress(spineIndex, currentPage, pageCount);
}

void EpubReaderActivity::flushProgressSave(int spineIndex, int currentPage, int pageCount) {
const bool changed = spineIndex != lastSavedSpineIndex ||
currentPage != lastSavedPage ||
pageCount != lastSavedPageCount;
if (changed) {
saveProgress(spineIndex, currentPage, pageCount);
}
}

bool EpubReaderActivity::shouldClearFontCache() const {
return forceFontCacheClear ||
lastRenderedFontFamily != SETTINGS.fontFamily ||
lastRenderedFontSize != SETTINGS.fontSize ||
lastRenderedOrientation != SETTINGS.orientation;
}

void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) {
Expand All @@ -1011,6 +1119,17 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
renderer.drawText(SMALL_FONT_ID, tx + 8, ty + 4, milestoneText, true, EpdFontFamily::BOLD);
}

if (showBookmarkToast && bookmarkToastText[0]) {
const int screenW = renderer.getScreenWidth();
const int toastW = renderer.getTextWidth(SMALL_FONT_ID, bookmarkToastText) + 16;
const int toastH = renderer.getLineHeight(SMALL_FONT_ID) + 8;
const int tx = (screenW - toastW) / 2;
const int ty = renderer.getScreenHeight() - orientedMarginBottom - toastH - 4;
renderer.fillRect(tx, ty, toastW, toastH, false);
renderer.drawRect(tx, ty, toastW, toastH);
renderer.drawText(SMALL_FONT_ID, tx + 8, ty + 4, bookmarkToastText, true, EpdFontFamily::BOLD);
}

if (imagePageWithAA) {
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
Expand Down
26 changes: 26 additions & 0 deletions src/activities/reader/EpubReaderActivity.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ class EpubReaderActivity final : public Activity {
unsigned long milestoneToastTime = 0;
char milestoneText[48] = {};

// Bookmark toast (non-blocking)
bool showBookmarkToast = false;
unsigned long bookmarkToastTime = 0;
char bookmarkToastText[32] = {};

// Progress write debounce state
int lastSavedSpineIndex = -1;
int lastSavedPage = -1;
int lastSavedPageCount = -1;
unsigned long lastProgressSaveMs = 0;

// Font cache invalidation state
bool forceFontCacheClear = true;
uint8_t lastRenderedFontFamily = 0xFF;
uint8_t lastRenderedFontSize = 0xFF;
uint8_t lastRenderedOrientation = 0xFF;

// Page load failure guard to avoid infinite retry loops
int failedLoadSpineIndex = -1;
int failedLoadPage = -1;
uint8_t consecutiveLoadFailures = 0;

// Bookmarks (starred pages)
BookmarkStore bookmarkStore;

Expand All @@ -69,6 +91,10 @@ class EpubReaderActivity final : public Activity {
void renderStatusBar() const;
int getEstimatedMinutesLeft() const;
void saveProgress(int spineIndex, int currentPage, int pageCount);
void queueBookmarkToast(const char* text);
void maybeSaveProgress(int spineIndex, int currentPage, int pageCount);
void flushProgressSave(int spineIndex, int currentPage, int pageCount);
bool shouldClearFontCache() const;
// Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent);
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
Expand Down
Loading
Loading