refactor: reader utils (#1329)
## Summary Extract shared reader utilities (`ReaderUtils.h`) to reduce duplication across `EpubReaderActivity`, `TxtReaderActivity`, and (upcoming) `MarkdownReaderActivity`. Utilities extracted: - `applyOrientation()` — orientation switch logic - `detectPageTurn()` — page navigation input detection - `renderAntiAliased()` — grayscale anti-aliasing pass - `displayWithRefreshCycle()` — refresh mode cadence - `GO_HOME_MS` — back button timing constant ## Impact Flash: 32 bytes saved (6006441 → 6006409 bytes). Minimal immediate gain, but meaningful once markdown reader and future reader types share these functions. Code quality: Eliminates ~100 lines of duplicated logic spread across multiple files. All readers now follow the same patterns for orientation, input handling, and rendering. ## Rationale This refactor is preparation for markdown support, which requires identical input and rendering logic. Instead of copy-pasting these patterns a third time, all readers now share a single, tested implementation. Future reader types can reuse `ReaderUtils` without duplication. --- ## AI Usage Did you use AI tools to help write this code? YES Claude extracted the code, under my guidance. Tested on my device and seems to work fine.
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
#include "KOReaderSyncActivity.h"
|
#include "KOReaderSyncActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "QrDisplayActivity.h"
|
#include "QrDisplayActivity.h"
|
||||||
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@@ -25,7 +26,6 @@
|
|||||||
namespace {
|
namespace {
|
||||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||||
constexpr unsigned long skipChapterMs = 700;
|
constexpr unsigned long skipChapterMs = 700;
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
|
||||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||||
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
||||||
|
|
||||||
@@ -39,27 +39,6 @@ int clampPercent(int percent) {
|
|||||||
return percent;
|
return percent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the logical reader orientation to the renderer.
|
|
||||||
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
|
|
||||||
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
|
||||||
switch (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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void EpubReaderActivity::onEnter() {
|
void EpubReaderActivity::onEnter() {
|
||||||
@@ -71,7 +50,7 @@ void EpubReaderActivity::onEnter() {
|
|||||||
|
|
||||||
// Configure screen orientation based on settings
|
// Configure screen orientation based on settings
|
||||||
// NOTE: This affects layout math and must be applied before any render calls.
|
// NOTE: This affects layout math and must be applied before any render calls.
|
||||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||||
|
|
||||||
epub->setupCacheDir();
|
epub->setupCacheDir();
|
||||||
|
|
||||||
@@ -179,13 +158,14 @@ void EpubReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes to file selection
|
// Long press BACK (1s+) goes to file selection
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||||
activityManager.goToFileBrowser(epub ? epub->getPath() : "");
|
activityManager.goToFileBrowser(epub ? epub->getPath() : "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes directly to home (or restores position if viewing footnote)
|
// Short press BACK goes directly to home (or restores position if viewing footnote)
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||||
|
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||||
if (footnoteDepth > 0) {
|
if (footnoteDepth > 0) {
|
||||||
restoreSavedPosition();
|
restoreSavedPosition();
|
||||||
return;
|
return;
|
||||||
@@ -194,20 +174,7 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||||
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
|
||||||
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
|
||||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
|
||||||
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
|
||||||
const bool nextTriggered = usePressForPageTurn
|
|
||||||
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
|
||||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
|
||||||
|
|
||||||
if (!prevTriggered && !nextTriggered) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -455,7 +422,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
|||||||
SETTINGS.saveToFile();
|
SETTINGS.saveToFile();
|
||||||
|
|
||||||
// Update renderer orientation to match the new logical coordinate system.
|
// Update renderer orientation to match the new logical coordinate system.
|
||||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||||
|
|
||||||
// Reset section to force re-layout in the new orientation.
|
// Reset section to force re-layout in the new orientation.
|
||||||
section.reset();
|
section.reset();
|
||||||
@@ -719,12 +686,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
|
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
|
||||||
} else if (pagesUntilFullRefresh <= 1) {
|
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||||
pagesUntilFullRefresh--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save bw buffer to reset buffer state after grayscale data sync
|
// Save bw buffer to reset buffer state after grayscale data sync
|
||||||
|
|||||||
89
src/activities/reader/ReaderUtils.h
Normal file
89
src/activities/reader/ReaderUtils.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <CrossPointSettings.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
|
||||||
|
namespace ReaderUtils {
|
||||||
|
|
||||||
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
|
|
||||||
|
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||||
|
switch (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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PageTurnResult {
|
||||||
|
bool prev;
|
||||||
|
bool next;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
||||||
|
const bool usePress = !SETTINGS.longPressChapterSkip;
|
||||||
|
const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) ||
|
||||||
|
input.wasPressed(MappedInputManager::Button::Left))
|
||||||
|
: (input.wasReleased(MappedInputManager::Button::PageBack) ||
|
||||||
|
input.wasReleased(MappedInputManager::Button::Left));
|
||||||
|
const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
||||||
|
input.wasReleased(MappedInputManager::Button::Power);
|
||||||
|
const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn ||
|
||||||
|
input.wasPressed(MappedInputManager::Button::Right))
|
||||||
|
: (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn ||
|
||||||
|
input.wasReleased(MappedInputManager::Button::Right));
|
||||||
|
return {prev, next};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
|
||||||
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
pagesUntilFullRefresh--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grayscale anti-aliasing pass. Renders content twice (LSB + MSB) to build
|
||||||
|
// the grayscale buffer. Only the content callback is re-rendered — status bars
|
||||||
|
// and other overlays should be drawn before calling this.
|
||||||
|
// Kept as a template to avoid std::function overhead; instantiated once per reader type.
|
||||||
|
template <typename RenderFn>
|
||||||
|
void renderAntiAliased(GfxRenderer& renderer, RenderFn&& renderFn) {
|
||||||
|
if (!renderer.storeBwBuffer()) {
|
||||||
|
LOG_ERR("READER", "Failed to store BW buffer for anti-aliasing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.clearScreen(0x00);
|
||||||
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
|
renderFn();
|
||||||
|
renderer.copyGrayscaleLsbBuffers();
|
||||||
|
|
||||||
|
renderer.clearScreen(0x00);
|
||||||
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||||
|
renderFn();
|
||||||
|
renderer.copyGrayscaleMsbBuffers();
|
||||||
|
|
||||||
|
renderer.displayGrayBuffer();
|
||||||
|
renderer.setRenderMode(GfxRenderer::BW);
|
||||||
|
|
||||||
|
renderer.restoreBwBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ReaderUtils
|
||||||
@@ -9,14 +9,13 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
|
||||||
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
||||||
|
|
||||||
// Cache file magic and version
|
// Cache file magic and version
|
||||||
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||||
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
||||||
@@ -29,23 +28,7 @@ void TxtReaderActivity::onEnter() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure screen orientation based on settings
|
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
txt->setupCacheDir();
|
txt->setupCacheDir();
|
||||||
|
|
||||||
@@ -75,31 +58,19 @@ void TxtReaderActivity::onExit() {
|
|||||||
|
|
||||||
void TxtReaderActivity::loop() {
|
void TxtReaderActivity::loop() {
|
||||||
// Long press BACK (1s+) goes to file selection
|
// Long press BACK (1s+) goes to file selection
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) {
|
||||||
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes directly to home
|
// Short press BACK goes directly to home
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
|
||||||
|
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||||
const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip;
|
|
||||||
const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left))
|
|
||||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left));
|
|
||||||
const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN &&
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Power);
|
|
||||||
const bool nextTriggered = usePressForPageTurn
|
|
||||||
? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right))
|
|
||||||
: (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right));
|
|
||||||
|
|
||||||
if (!prevTriggered && !nextTriggered) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -398,34 +369,10 @@ void TxtReaderActivity::renderPage() {
|
|||||||
renderLines();
|
renderLines();
|
||||||
renderStatusBar();
|
renderStatusBar();
|
||||||
|
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
|
||||||
} else {
|
|
||||||
renderer.displayBuffer();
|
|
||||||
pagesUntilFullRefresh--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grayscale rendering pass (for anti-aliased fonts)
|
|
||||||
if (SETTINGS.textAntiAliasing) {
|
if (SETTINGS.textAntiAliasing) {
|
||||||
// Save BW buffer for restoration after grayscale pass
|
ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); });
|
||||||
renderer.storeBwBuffer();
|
|
||||||
|
|
||||||
renderer.clearScreen(0x00);
|
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
|
||||||
renderLines();
|
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
|
||||||
|
|
||||||
renderer.clearScreen(0x00);
|
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
|
||||||
renderLines();
|
|
||||||
renderer.copyGrayscaleMsbBuffers();
|
|
||||||
|
|
||||||
renderer.displayGrayBuffer();
|
|
||||||
renderer.setRenderMode(GfxRenderer::BW);
|
|
||||||
|
|
||||||
// Restore BW buffer
|
|
||||||
renderer.restoreBwBuffer();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user