2025-12-17 23:32:18 +11:00
|
|
|
#include "EpubReaderActivity.h"
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
#include <Epub/Page.h>
|
2025-12-23 14:14:10 +11:00
|
|
|
#include <FsHelpers.h>
|
2025-12-08 22:06:09 +11:00
|
|
|
#include <GfxRenderer.h>
|
2026-02-08 21:29:14 +01:00
|
|
|
#include <HalStorage.h>
|
2026-02-13 12:16:39 +01:00
|
|
|
#include <Logging.h>
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-15 13:16:46 +01:00
|
|
|
#include "CrossPointSettings.h"
|
2025-12-21 18:41:52 +11:00
|
|
|
#include "CrossPointState.h"
|
2026-02-12 20:40:07 -05:00
|
|
|
#include "EpubReaderBookmarkSelectionActivity.h"
|
2025-12-17 23:32:18 +11:00
|
|
|
#include "EpubReaderChapterSelectionActivity.h"
|
2026-02-05 15:17:51 +03:00
|
|
|
#include "EpubReaderPercentSelectionActivity.h"
|
feat: Move Sync feature to menu (#680)
## Summary
* **What is the goal of this PR?**
Move the "Sync Progress" option from TOC (Chapter Selection) screen to
the Reader Menu, and fix use-after-free crashes related to callback
handling in activity lifecycle.
* **What changes are included?**
- Added "Sync Progress" as a menu item in `EpubReaderMenuActivity` (now
4 items: Go to Chapter, Sync Progress, Go Home, Delete Book Cache)
- Removed sync-related logic from `EpubReaderChapterSelectionActivity` -
TOC now only displays chapters
- Implemented `pendingGoHome` and `pendingSubactivityExit` flags in
`EpubReaderActivity` to safely handle activity destruction
- Fixed GO_HOME, DELETE_CACHE, and SYNC menu actions to use deferred
callbacks avoiding use-after-free
## Additional Context
* Root cause of crashes: callbacks like `onGoHome()` or `onCancel()`
invoked from activity handlers could destroy the current activity while
code was still executing, causing use-after-free and race conditions
with FreeRTOS display task.
* Solution: Deferred execution pattern - set flags and process them in
`loop()` after all nested activity loops have safely returned.
* Files changed: `EpubReaderMenuActivity.h`,
`EpubReaderActivity.h/.cpp`, `EpubReaderChapterSelectionActivity.h/.cpp`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Co-authored-by: danoooob <danoooob@example.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 22:04:38 +07:00
|
|
|
#include "KOReaderCredentialStore.h"
|
|
|
|
|
#include "KOReaderSyncActivity.h"
|
2025-12-30 15:09:30 +10:00
|
|
|
#include "MappedInputManager.h"
|
My Library: Tab bar w/ Recent Books + File Browser (#250)
# Summary
This PR introduces a reusable Tab Bar component and combines the Recent
Books and File Browser into a unified tabbed page called "My Library"
accessible from the Home screen.
## Features
### New Tab Bar Component
A flexible, reusable tab bar component added to `ScreenComponents` that
can be used throughout the application.
### New Scroll Indicator Component
A page position indicator for lists that span multiple pages.
**Features:**
- Up/down arrow indicators
- Current page fraction display (e.g., "1/3")
- Only renders when content spans multiple pages
### My Library Activity
A new unified view combining Recent Books and File Browser into a single
tabbed page.
**Tabs:**
- **Recent** - Shows recently opened books
- **Files** - Browse SD card directory structure
**Navigation:**
- Up/Down or Left/Right: Navigate through list items
- Left/Right (when first item selected): Switch between tabs
- Confirm: Open selected book or enter directory
- Back: Go up directory (Files tab) or return home
- Long press Back: Jump to root directory (Files tab)
**UI Elements:**
- Tab bar with selection indicator
- Scroll/page indicator on right side
- Side button hints (up/down arrows)
- Dynamic bottom button labels ("BACK" in subdirectories, "HOME" at
root)
## Tab Bar Usage
The tab bar component is designed to be reusable across different
activities. Here's how to use it:
### Basic Example
```cpp
#include "ScreenComponents.h"
void MyActivity::render() const {
renderer.clearScreen();
// Define tabs with labels and selection state
std::vector<TabInfo> tabs = {
{"Tab One", currentTab == 0}, // Selected when currentTab is 0
{"Tab Two", currentTab == 1}, // Selected when currentTab is 1
{"Tab Three", currentTab == 2} // Selected when currentTab is 2
};
// Draw tab bar at Y position 15, returns height of the tab bar
int tabBarHeight = ScreenComponents::drawTabBar(renderer, 15, tabs);
// Position your content below the tab bar
int contentStartY = 15 + tabBarHeight + 10; // Add some padding
// Draw content based on selected tab
if (currentTab == 0) {
renderTabOneContent(contentStartY);
} else if (currentTab == 1) {
renderTabTwoContent(contentStartY);
} else {
renderTabThreeContent(contentStartY);
}
renderer.displayBuffer();
}
```
Video Demo: https://share.cleanshot.com/P6NBncFS
<img width="250"
src="https://github.com/user-attachments/assets/07de4418-968e-4a88-9b42-ac5f53d8a832"
/>
<img width="250"
src="https://github.com/user-attachments/assets/e40201ed-dcc8-4568-b008-cd2bf13ebb2a"
/>
<img width="250"
src="https://github.com/user-attachments/assets/73db269f-e629-4696-b8ca-0b8443451a05"
/>
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-21 05:38:38 -06:00
|
|
|
#include "RecentBooksStore.h"
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
#include "components/UITheme.h"
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
#include "fontIds.h"
|
2026-02-12 20:40:07 -05:00
|
|
|
#include "util/BookmarkStore.h"
|
2026-02-12 19:36:14 -05:00
|
|
|
#include "util/Dictionary.h"
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-17 23:32:18 +11:00
|
|
|
namespace {
|
2026-01-03 08:33:42 +00:00
|
|
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
2025-12-17 23:32:18 +11:00
|
|
|
constexpr unsigned long skipChapterMs = 700;
|
2025-12-26 09:55:23 +09:00
|
|
|
constexpr unsigned long goHomeMs = 1000;
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
constexpr int statusBarMargin = 19;
|
2026-01-27 12:25:44 +00:00
|
|
|
constexpr int progressBarMarginTop = 1;
|
|
|
|
|
|
2026-02-05 15:17:51 +03:00
|
|
|
int clampPercent(int percent) {
|
|
|
|
|
if (percent < 0) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (percent > 100) {
|
|
|
|
|
return 100;
|
|
|
|
|
}
|
|
|
|
|
return percent;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:53:35 +03:00
|
|
|
// 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) {
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-05 14:53:35 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void EpubReaderActivity::taskTrampoline(void* param) {
|
|
|
|
|
auto* self = static_cast<EpubReaderActivity*>(param);
|
|
|
|
|
self->displayTaskLoop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EpubReaderActivity::onEnter() {
|
|
|
|
|
ActivityWithSubactivity::onEnter();
|
|
|
|
|
|
|
|
|
|
if (!epub) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configure screen orientation based on settings
|
|
|
|
|
// NOTE: This affects layout math and must be applied before any render calls.
|
|
|
|
|
applyReaderOrientation(renderer, SETTINGS.orientation);
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
|
2025-12-06 03:02:52 +11:00
|
|
|
renderingMutex = xSemaphoreCreateMutex();
|
2025-12-03 22:00:29 +11:00
|
|
|
|
|
|
|
|
epub->setupCacheDir();
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
FsFile f;
|
2026-02-08 21:29:14 +01:00
|
|
|
if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
2026-01-27 19:11:11 +08:00
|
|
|
uint8_t data[6];
|
|
|
|
|
int dataSize = f.read(data, 6);
|
|
|
|
|
if (dataSize == 4 || dataSize == 6) {
|
2025-12-19 13:27:08 +01:00
|
|
|
currentSpineIndex = data[0] + (data[1] << 8);
|
|
|
|
|
nextPageNumber = data[2] + (data[3] << 8);
|
2026-01-27 19:11:11 +08:00
|
|
|
cachedSpineIndex = currentSpineIndex;
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Loaded cache: %d, %d", currentSpineIndex, nextPageNumber);
|
2025-12-19 13:27:08 +01:00
|
|
|
}
|
2026-01-27 19:11:11 +08:00
|
|
|
if (dataSize == 6) {
|
|
|
|
|
cachedChapterTotalPageCount = data[4] + (data[5] << 8);
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
f.close();
|
|
|
|
|
}
|
2025-12-30 13:02:46 +01:00
|
|
|
// We may want a better condition to detect if we are opening for the first time.
|
|
|
|
|
// This will trigger if the book is re-opened at Chapter 0.
|
|
|
|
|
if (currentSpineIndex == 0) {
|
|
|
|
|
int textSpineIndex = epub->getSpineIndexForTextReference();
|
|
|
|
|
if (textSpineIndex != 0) {
|
|
|
|
|
currentSpineIndex = textSpineIndex;
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex);
|
2025-12-30 13:02:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2026-02-12 16:13:55 -05:00
|
|
|
// Prerender covers and thumbnails on first open so Home and Sleep screens are instant.
|
|
|
|
|
// Each generate* call is a no-op if the file already exists, so this only does work once.
|
|
|
|
|
{
|
|
|
|
|
int totalSteps = 0;
|
|
|
|
|
if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) totalSteps++;
|
|
|
|
|
if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) totalSteps++;
|
|
|
|
|
for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) {
|
|
|
|
|
if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) totalSteps++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (totalSteps > 0) {
|
|
|
|
|
Rect popupRect = GUI.drawPopup(renderer, "Preparing book...");
|
|
|
|
|
int completedSteps = 0;
|
|
|
|
|
|
|
|
|
|
auto updateProgress = [&]() {
|
|
|
|
|
completedSteps++;
|
|
|
|
|
GUI.fillPopupProgress(renderer, popupRect, completedSteps * 100 / totalSteps);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) {
|
|
|
|
|
epub->generateCoverBmp(false);
|
|
|
|
|
updateProgress();
|
|
|
|
|
}
|
|
|
|
|
if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) {
|
|
|
|
|
epub->generateCoverBmp(true);
|
|
|
|
|
updateProgress();
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) {
|
|
|
|
|
if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) {
|
|
|
|
|
epub->generateThumbBmp(PRERENDER_THUMB_HEIGHTS[i]);
|
|
|
|
|
updateProgress();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
My Library: Tab bar w/ Recent Books + File Browser (#250)
# Summary
This PR introduces a reusable Tab Bar component and combines the Recent
Books and File Browser into a unified tabbed page called "My Library"
accessible from the Home screen.
## Features
### New Tab Bar Component
A flexible, reusable tab bar component added to `ScreenComponents` that
can be used throughout the application.
### New Scroll Indicator Component
A page position indicator for lists that span multiple pages.
**Features:**
- Up/down arrow indicators
- Current page fraction display (e.g., "1/3")
- Only renders when content spans multiple pages
### My Library Activity
A new unified view combining Recent Books and File Browser into a single
tabbed page.
**Tabs:**
- **Recent** - Shows recently opened books
- **Files** - Browse SD card directory structure
**Navigation:**
- Up/Down or Left/Right: Navigate through list items
- Left/Right (when first item selected): Switch between tabs
- Confirm: Open selected book or enter directory
- Back: Go up directory (Files tab) or return home
- Long press Back: Jump to root directory (Files tab)
**UI Elements:**
- Tab bar with selection indicator
- Scroll/page indicator on right side
- Side button hints (up/down arrows)
- Dynamic bottom button labels ("BACK" in subdirectories, "HOME" at
root)
## Tab Bar Usage
The tab bar component is designed to be reusable across different
activities. Here's how to use it:
### Basic Example
```cpp
#include "ScreenComponents.h"
void MyActivity::render() const {
renderer.clearScreen();
// Define tabs with labels and selection state
std::vector<TabInfo> tabs = {
{"Tab One", currentTab == 0}, // Selected when currentTab is 0
{"Tab Two", currentTab == 1}, // Selected when currentTab is 1
{"Tab Three", currentTab == 2} // Selected when currentTab is 2
};
// Draw tab bar at Y position 15, returns height of the tab bar
int tabBarHeight = ScreenComponents::drawTabBar(renderer, 15, tabs);
// Position your content below the tab bar
int contentStartY = 15 + tabBarHeight + 10; // Add some padding
// Draw content based on selected tab
if (currentTab == 0) {
renderTabOneContent(contentStartY);
} else if (currentTab == 1) {
renderTabTwoContent(contentStartY);
} else {
renderTabThreeContent(contentStartY);
}
renderer.displayBuffer();
}
```
Video Demo: https://share.cleanshot.com/P6NBncFS
<img width="250"
src="https://github.com/user-attachments/assets/07de4418-968e-4a88-9b42-ac5f53d8a832"
/>
<img width="250"
src="https://github.com/user-attachments/assets/e40201ed-dcc8-4568-b008-cd2bf13ebb2a"
/>
<img width="250"
src="https://github.com/user-attachments/assets/73db269f-e629-4696-b8ca-0b8443451a05"
/>
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-21 05:38:38 -06:00
|
|
|
// Save current epub as last opened epub and add to recent books
|
2025-12-21 18:41:52 +11:00
|
|
|
APP_STATE.openEpubPath = epub->getPath();
|
|
|
|
|
APP_STATE.saveToFile();
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
|
2025-12-21 18:41:52 +11:00
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
// Trigger first update
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
|
2025-12-17 23:32:18 +11:00
|
|
|
xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
|
2025-12-03 22:00:29 +11:00
|
|
|
8192, // Stack size
|
|
|
|
|
this, // Parameters
|
|
|
|
|
1, // Priority
|
|
|
|
|
&displayTaskHandle // Task handle
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 23:32:18 +11:00
|
|
|
void EpubReaderActivity::onExit() {
|
2025-12-21 21:17:00 +11:00
|
|
|
ActivityWithSubactivity::onExit();
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
// Reset orientation back to portrait for the rest of the UI
|
|
|
|
|
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
|
|
|
|
|
2025-12-06 03:02:52 +11:00
|
|
|
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2025-12-06 02:49:10 +11:00
|
|
|
if (displayTaskHandle) {
|
|
|
|
|
vTaskDelete(displayTaskHandle);
|
|
|
|
|
displayTaskHandle = nullptr;
|
|
|
|
|
}
|
2025-12-06 03:02:52 +11:00
|
|
|
vSemaphoreDelete(renderingMutex);
|
|
|
|
|
renderingMutex = nullptr;
|
2026-02-05 19:45:09 +08:00
|
|
|
APP_STATE.readerActivityLoadCount = 0;
|
|
|
|
|
APP_STATE.saveToFile();
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
|
|
|
|
epub.reset();
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 23:32:18 +11:00
|
|
|
void EpubReaderActivity::loop() {
|
|
|
|
|
// Pass input responsibility to sub activity if exists
|
2025-12-21 21:17:00 +11:00
|
|
|
if (subActivity) {
|
|
|
|
|
subActivity->loop();
|
feat: Move Sync feature to menu (#680)
## Summary
* **What is the goal of this PR?**
Move the "Sync Progress" option from TOC (Chapter Selection) screen to
the Reader Menu, and fix use-after-free crashes related to callback
handling in activity lifecycle.
* **What changes are included?**
- Added "Sync Progress" as a menu item in `EpubReaderMenuActivity` (now
4 items: Go to Chapter, Sync Progress, Go Home, Delete Book Cache)
- Removed sync-related logic from `EpubReaderChapterSelectionActivity` -
TOC now only displays chapters
- Implemented `pendingGoHome` and `pendingSubactivityExit` flags in
`EpubReaderActivity` to safely handle activity destruction
- Fixed GO_HOME, DELETE_CACHE, and SYNC menu actions to use deferred
callbacks avoiding use-after-free
## Additional Context
* Root cause of crashes: callbacks like `onGoHome()` or `onCancel()`
invoked from activity handlers could destroy the current activity while
code was still executing, causing use-after-free and race conditions
with FreeRTOS display task.
* Solution: Deferred execution pattern - set flags and process them in
`loop()` after all nested activity loops have safely returned.
* Files changed: `EpubReaderMenuActivity.h`,
`EpubReaderActivity.h/.cpp`, `EpubReaderChapterSelectionActivity.h/.cpp`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Co-authored-by: danoooob <danoooob@example.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 22:04:38 +07:00
|
|
|
// Deferred exit: process after subActivity->loop() returns to avoid use-after-free
|
|
|
|
|
if (pendingSubactivityExit) {
|
|
|
|
|
pendingSubactivityExit = false;
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
skipNextButtonCheck = true; // Skip button processing to ignore stale events
|
|
|
|
|
}
|
|
|
|
|
// Deferred go home: process after subActivity->loop() returns to avoid race condition
|
|
|
|
|
if (pendingGoHome) {
|
|
|
|
|
pendingGoHome = false;
|
|
|
|
|
exitActivity();
|
|
|
|
|
if (onGoHome) {
|
|
|
|
|
onGoHome();
|
|
|
|
|
}
|
|
|
|
|
return; // Don't access 'this' after callback
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle pending go home when no subactivity (e.g., from long press back)
|
|
|
|
|
if (pendingGoHome) {
|
|
|
|
|
pendingGoHome = false;
|
|
|
|
|
if (onGoHome) {
|
|
|
|
|
onGoHome();
|
|
|
|
|
}
|
|
|
|
|
return; // Don't access 'this' after callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip button processing after returning from subactivity
|
|
|
|
|
// This prevents stale button release events from triggering actions
|
|
|
|
|
// We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared
|
|
|
|
|
if (skipNextButtonCheck) {
|
|
|
|
|
const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
|
|
|
|
!mappedInput.wasReleased(MappedInputManager::Button::Confirm);
|
|
|
|
|
const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) &&
|
|
|
|
|
!mappedInput.wasReleased(MappedInputManager::Button::Back);
|
|
|
|
|
if (confirmCleared && backCleared) {
|
|
|
|
|
skipNextButtonCheck = false;
|
|
|
|
|
}
|
2025-12-13 21:17:34 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 15:17:51 +03:00
|
|
|
// Enter reader menu activity.
|
2025-12-28 21:59:14 -06:00
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
2025-12-17 23:32:18 +11:00
|
|
|
// Don't start activity transition while rendering
|
2025-12-13 21:17:34 +11:00
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2026-02-05 15:17:51 +03:00
|
|
|
const int currentPage = section ? section->currentPage + 1 : 0;
|
|
|
|
|
const int totalPages = section ? section->pageCount : 0;
|
|
|
|
|
float bookProgress = 0.0f;
|
|
|
|
|
if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
|
|
|
|
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
|
|
|
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
|
|
|
|
}
|
|
|
|
|
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
2026-02-12 19:36:14 -05:00
|
|
|
const bool hasDictionary = Dictionary::exists();
|
2026-02-12 20:40:07 -05:00
|
|
|
const bool isBookmarked = BookmarkStore::hasBookmark(
|
|
|
|
|
epub->getCachePath(), currentSpineIndex, section ? section->currentPage : 0);
|
2025-12-21 21:17:00 +11:00
|
|
|
exitActivity();
|
2026-02-01 08:34:30 +01:00
|
|
|
enterNewActivity(new EpubReaderMenuActivity(
|
2026-02-05 15:17:51 +03:00
|
|
|
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
2026-02-13 16:07:38 -05:00
|
|
|
SETTINGS.orientation, hasDictionary, isBookmarked, epub->getCachePath(),
|
2026-02-12 20:40:07 -05:00
|
|
|
[this](const uint8_t orientation) { onReaderMenuBack(orientation); },
|
2026-02-01 08:34:30 +01:00
|
|
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
2025-12-13 21:17:34 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 18:17:00 +03:00
|
|
|
// Long press BACK (1s+) goes to file selection
|
2025-12-28 21:59:14 -06:00
|
|
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) {
|
2026-02-07 18:17:00 +03:00
|
|
|
onGoBack();
|
2025-12-26 09:55:23 +09:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 18:17:00 +03:00
|
|
|
// Short press BACK goes directly to home
|
2025-12-28 21:59:14 -06:00
|
|
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) {
|
2026-02-07 18:17:00 +03:00
|
|
|
onGoHome();
|
2025-12-06 12:35:41 +11:00
|
|
|
return;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2026-01-27 17:53:13 +05:00
|
|
|
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
|
|
|
|
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) {
|
2025-12-06 12:35:41 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 20:10:38 +11:00
|
|
|
// any botton press when at end of the book goes back to the last page
|
|
|
|
|
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
|
|
|
|
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
|
|
|
|
nextPageNumber = UINT16_MAX;
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 06:47:24 -05:00
|
|
|
const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs;
|
2025-12-06 12:35:41 +11:00
|
|
|
|
|
|
|
|
if (skipChapter) {
|
|
|
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
nextPageNumber = 0;
|
2026-01-27 17:53:13 +05:00
|
|
|
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-06 12:35:41 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No current section, attempt to rerender the book
|
|
|
|
|
if (!section) {
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 17:53:13 +05:00
|
|
|
if (prevTriggered) {
|
2025-12-06 12:35:41 +11:00
|
|
|
if (section->currentPage > 0) {
|
|
|
|
|
section->currentPage--;
|
|
|
|
|
} else {
|
|
|
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
nextPageNumber = UINT16_MAX;
|
2025-12-03 22:00:29 +11:00
|
|
|
currentSpineIndex--;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-06 12:35:41 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
} else {
|
|
|
|
|
if (section->currentPage < section->pageCount - 1) {
|
|
|
|
|
section->currentPage++;
|
|
|
|
|
} else {
|
|
|
|
|
// We don't want to delete the section mid-render, so grab the semaphore
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2025-12-03 22:00:29 +11:00
|
|
|
nextPageNumber = 0;
|
|
|
|
|
currentSpineIndex++;
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-06 12:35:41 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:53:35 +03:00
|
|
|
void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
|
2026-02-01 08:34:30 +01:00
|
|
|
exitActivity();
|
2026-02-05 14:53:35 +03:00
|
|
|
// Apply the user-selected orientation when the menu is dismissed.
|
|
|
|
|
// This ensures the menu can be navigated without immediately rotating the screen.
|
|
|
|
|
applyOrientation(orientation);
|
2026-02-12 20:40:07 -05:00
|
|
|
// Force a half refresh on the next render to clear menu/popup artifacts
|
|
|
|
|
pagesUntilFullRefresh = 1;
|
2026-02-01 08:34:30 +01:00
|
|
|
updateRequired = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 15:17:51 +03:00
|
|
|
// Translate an absolute percent into a spine index plus a normalized position
|
|
|
|
|
// within that spine so we can jump after the section is loaded.
|
|
|
|
|
void EpubReaderActivity::jumpToPercent(int percent) {
|
|
|
|
|
if (!epub) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t bookSize = epub->getBookSize();
|
|
|
|
|
if (bookSize == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize input to 0-100 to avoid invalid jumps.
|
|
|
|
|
percent = clampPercent(percent);
|
|
|
|
|
|
|
|
|
|
// Convert percent into a byte-like absolute position across the spine sizes.
|
|
|
|
|
// Use an overflow-safe computation: (bookSize / 100) * percent + (bookSize % 100) * percent / 100
|
|
|
|
|
size_t targetSize =
|
|
|
|
|
(bookSize / 100) * static_cast<size_t>(percent) + (bookSize % 100) * static_cast<size_t>(percent) / 100;
|
|
|
|
|
if (percent >= 100) {
|
|
|
|
|
// Ensure the final percent lands inside the last spine item.
|
|
|
|
|
targetSize = bookSize - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int spineCount = epub->getSpineItemsCount();
|
|
|
|
|
if (spineCount == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int targetSpineIndex = spineCount - 1;
|
|
|
|
|
size_t prevCumulative = 0;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < spineCount; i++) {
|
|
|
|
|
const size_t cumulative = epub->getCumulativeSpineItemSize(i);
|
|
|
|
|
if (targetSize <= cumulative) {
|
|
|
|
|
// Found the spine item containing the absolute position.
|
|
|
|
|
targetSpineIndex = i;
|
|
|
|
|
prevCumulative = (i > 0) ? epub->getCumulativeSpineItemSize(i - 1) : 0;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t cumulative = epub->getCumulativeSpineItemSize(targetSpineIndex);
|
|
|
|
|
const size_t spineSize = (cumulative > prevCumulative) ? (cumulative - prevCumulative) : 0;
|
|
|
|
|
// Store a normalized position within the spine so it can be applied once loaded.
|
|
|
|
|
pendingSpineProgress =
|
|
|
|
|
(spineSize == 0) ? 0.0f : static_cast<float>(targetSize - prevCumulative) / static_cast<float>(spineSize);
|
|
|
|
|
if (pendingSpineProgress < 0.0f) {
|
|
|
|
|
pendingSpineProgress = 0.0f;
|
|
|
|
|
} else if (pendingSpineProgress > 1.0f) {
|
|
|
|
|
pendingSpineProgress = 1.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset state so renderScreen() reloads and repositions on the target spine.
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
currentSpineIndex = targetSpineIndex;
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
pendingPercentJump = true;
|
|
|
|
|
section.reset();
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-01 08:34:30 +01:00
|
|
|
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
|
|
|
|
switch (action) {
|
2026-02-12 19:36:14 -05:00
|
|
|
case EpubReaderMenuActivity::MenuAction::ADD_BOOKMARK: {
|
2026-02-12 20:40:07 -05:00
|
|
|
const int page = section ? section->currentPage : 0;
|
|
|
|
|
|
|
|
|
|
// Extract first full sentence from the current page for the bookmark snippet.
|
|
|
|
|
// If the first word is lowercase, the page starts mid-sentence — skip to the
|
|
|
|
|
// next sentence boundary and start collecting from there.
|
|
|
|
|
std::string snippet;
|
|
|
|
|
if (section) {
|
|
|
|
|
auto p = section->loadPageFromSectionFile();
|
|
|
|
|
if (p) {
|
|
|
|
|
// Gather all words on the page into a flat list for easier traversal
|
|
|
|
|
std::vector<std::string> allWords;
|
|
|
|
|
for (const auto& element : p->elements) {
|
|
|
|
|
const auto* line = static_cast<const PageLine*>(element.get());
|
|
|
|
|
if (!line) continue;
|
|
|
|
|
const auto& block = line->getBlock();
|
|
|
|
|
if (!block) continue;
|
|
|
|
|
for (const auto& word : block->getWords()) {
|
|
|
|
|
allWords.push_back(word);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!allWords.empty()) {
|
|
|
|
|
size_t startIdx = 0;
|
|
|
|
|
|
|
|
|
|
// Check if the first word starts with a lowercase letter (mid-sentence)
|
|
|
|
|
const char firstChar = allWords[0].empty() ? '\0' : allWords[0][0];
|
|
|
|
|
if (firstChar >= 'a' && firstChar <= 'z') {
|
|
|
|
|
// Skip past the end of this partial sentence
|
|
|
|
|
for (size_t i = 0; i < allWords.size(); i++) {
|
|
|
|
|
if (!allWords[i].empty()) {
|
|
|
|
|
char last = allWords[i].back();
|
|
|
|
|
if (last == '.' || last == '!' || last == '?' || last == ':') {
|
|
|
|
|
startIdx = i + 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If no sentence boundary found, fall back to using everything from the start
|
|
|
|
|
if (startIdx >= allWords.size()) {
|
|
|
|
|
startIdx = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect words from startIdx until the next sentence boundary
|
|
|
|
|
for (size_t i = startIdx; i < allWords.size(); i++) {
|
|
|
|
|
if (!snippet.empty()) snippet += " ";
|
|
|
|
|
snippet += allWords[i];
|
|
|
|
|
if (!allWords[i].empty()) {
|
|
|
|
|
char last = allWords[i].back();
|
|
|
|
|
if (last == '.' || last == '!' || last == '?' || last == ':') {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BookmarkStore::addBookmark(epub->getCachePath(), currentSpineIndex, page, snippet);
|
2026-02-12 19:36:14 -05:00
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2026-02-12 20:40:07 -05:00
|
|
|
GUI.drawPopup(renderer, "Bookmark added");
|
2026-02-12 19:36:14 -05:00
|
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
2026-02-12 20:40:07 -05:00
|
|
|
vTaskDelay(750 / portTICK_PERIOD_MS);
|
|
|
|
|
// Exit the menu and return to reading — the bookmark indicator will show on re-render,
|
|
|
|
|
// and next menu open will reflect the updated state.
|
|
|
|
|
exitActivity();
|
|
|
|
|
pagesUntilFullRefresh = 1;
|
|
|
|
|
updateRequired = true;
|
2026-02-12 19:36:14 -05:00
|
|
|
break;
|
|
|
|
|
}
|
2026-02-12 20:40:07 -05:00
|
|
|
case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: {
|
|
|
|
|
const int page = section ? section->currentPage : 0;
|
|
|
|
|
BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, page);
|
2026-02-12 19:36:14 -05:00
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2026-02-12 20:40:07 -05:00
|
|
|
GUI.drawPopup(renderer, "Bookmark removed");
|
2026-02-12 19:36:14 -05:00
|
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
2026-02-12 20:40:07 -05:00
|
|
|
vTaskDelay(750 / portTICK_PERIOD_MS);
|
|
|
|
|
exitActivity();
|
|
|
|
|
pagesUntilFullRefresh = 1;
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::GO_TO_BOOKMARK: {
|
|
|
|
|
auto bookmarks = BookmarkStore::load(epub->getCachePath());
|
|
|
|
|
|
|
|
|
|
if (bookmarks.empty()) {
|
|
|
|
|
// No bookmarks: fall back to Table of Contents if available, otherwise go back
|
|
|
|
|
if (epub->getTocItemsCount() > 0) {
|
|
|
|
|
const int currentP = section ? section->currentPage : 0;
|
|
|
|
|
const int totalP = section ? section->pageCount : 0;
|
|
|
|
|
const int spineIdx = currentSpineIndex;
|
|
|
|
|
const std::string path = epub->getPath();
|
|
|
|
|
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
exitActivity();
|
|
|
|
|
enterNewActivity(new EpubReaderChapterSelectionActivity(
|
|
|
|
|
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
|
|
|
|
|
[this] {
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const int newSpineIndex) {
|
|
|
|
|
if (currentSpineIndex != newSpineIndex) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
|
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = newPage;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}));
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
// If no TOC either, just return to reader (menu already closed by callback)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
exitActivity();
|
|
|
|
|
enterNewActivity(new EpubReaderBookmarkSelectionActivity(
|
|
|
|
|
this->renderer, this->mappedInput, epub, std::move(bookmarks), epub->getCachePath(),
|
|
|
|
|
[this] {
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
|
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = newPage;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}));
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
2026-02-12 19:36:14 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE: {
|
|
|
|
|
if (Dictionary::cacheExists()) {
|
|
|
|
|
Dictionary::deleteCache();
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
GUI.drawPopup(renderer, "Dictionary cache deleted");
|
|
|
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
} else {
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
GUI.drawPopup(renderer, "No cache to delete");
|
|
|
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
vTaskDelay(1500 / portTICK_PERIOD_MS);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-01 08:34:30 +01:00
|
|
|
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
|
|
|
|
// Calculate values BEFORE we start destroying things
|
|
|
|
|
const int currentP = section ? section->currentPage : 0;
|
|
|
|
|
const int totalP = section ? section->pageCount : 0;
|
|
|
|
|
const int spineIdx = currentSpineIndex;
|
|
|
|
|
const std::string path = epub->getPath();
|
|
|
|
|
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
|
|
|
|
|
// 1. Close the menu
|
|
|
|
|
exitActivity();
|
|
|
|
|
|
|
|
|
|
// 2. Open the Chapter Selector
|
|
|
|
|
enterNewActivity(new EpubReaderChapterSelectionActivity(
|
|
|
|
|
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
|
|
|
|
|
[this] {
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const int newSpineIndex) {
|
|
|
|
|
if (currentSpineIndex != newSpineIndex) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = 0;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this](const int newSpineIndex, const int newPage) {
|
|
|
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = newPage;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-05 15:17:51 +03:00
|
|
|
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
|
|
|
|
|
// Launch the slider-based percent selector and return here on confirm/cancel.
|
|
|
|
|
float bookProgress = 0.0f;
|
|
|
|
|
if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
|
|
|
|
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
|
|
|
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
|
|
|
|
}
|
|
|
|
|
const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
exitActivity();
|
|
|
|
|
enterNewActivity(new EpubReaderPercentSelectionActivity(
|
|
|
|
|
renderer, mappedInput, initialPercent,
|
|
|
|
|
[this](const int percent) {
|
|
|
|
|
// Apply the new position and exit back to the reader.
|
|
|
|
|
jumpToPercent(percent);
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
},
|
|
|
|
|
[this]() {
|
|
|
|
|
// Cancel selection and return to the reader.
|
|
|
|
|
exitActivity();
|
|
|
|
|
updateRequired = true;
|
|
|
|
|
}));
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-12 19:36:14 -05:00
|
|
|
case EpubReaderMenuActivity::MenuAction::LOOKUP: {
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
|
|
|
|
|
// Compute margins (same logic as renderScreen)
|
|
|
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
|
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
|
|
|
&orientedMarginLeft);
|
|
|
|
|
orientedMarginTop += SETTINGS.screenMargin;
|
|
|
|
|
orientedMarginLeft += SETTINGS.screenMargin;
|
|
|
|
|
orientedMarginRight += SETTINGS.screenMargin;
|
|
|
|
|
orientedMarginBottom += SETTINGS.screenMargin;
|
|
|
|
|
|
|
|
|
|
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
|
|
|
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
|
|
|
const bool showProgressBar =
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
|
|
|
|
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
|
|
|
|
|
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load the current page
|
|
|
|
|
auto pageForLookup = section ? section->loadPageFromSectionFile() : nullptr;
|
|
|
|
|
const int readerFontId = SETTINGS.getReaderFontId();
|
|
|
|
|
const std::string bookCachePath = epub->getCachePath();
|
|
|
|
|
const uint8_t currentOrientation = SETTINGS.orientation;
|
|
|
|
|
|
feat: Integrate PR #857 dictionary intelligence and sub-activity refactor
Pull in the full feature update from PR #857 while preserving fork
advantages (HTML parsing, custom drawHints, PageForward/PageBack,
cache management, stardictCmp, /.dictionary/ paths).
- Add morphological stemming (getStemVariants), Levenshtein edit
distance, and fuzzy matching (findSimilar) to Dictionary
- Create DictionarySuggestionsActivity for "Did you mean?" flow
- Add onDone callback to DictionaryDefinitionActivity for direct
exit-to-reader via "Done" button
- Refactor DictionaryWordSelectActivity to ActivityWithSubactivity
with cascading lookup (exact → stems → suggestions → not found),
en-dash/em-dash splitting, and cross-page hyphenation
- Refactor LookedUpWordsActivity with reverse-chronological order,
inline cascading lookup, UITheme-aware rendering, and sub-activities
- Simplify EpubReaderActivity LOOKUP/LOOKED_UP_WORDS handlers
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 20:50:03 -05:00
|
|
|
// Get first word of next page for cross-page hyphenation
|
|
|
|
|
std::string nextPageFirstWord;
|
|
|
|
|
if (section && section->currentPage < section->pageCount - 1) {
|
|
|
|
|
int savedPage = section->currentPage;
|
|
|
|
|
section->currentPage = savedPage + 1;
|
|
|
|
|
auto nextPage = section->loadPageFromSectionFile();
|
|
|
|
|
section->currentPage = savedPage;
|
|
|
|
|
if (nextPage && !nextPage->elements.empty()) {
|
|
|
|
|
const auto* firstLine = static_cast<const PageLine*>(nextPage->elements[0].get());
|
|
|
|
|
if (firstLine->getBlock() && !firstLine->getBlock()->getWords().empty()) {
|
|
|
|
|
nextPageFirstWord = firstLine->getBlock()->getWords().front();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 19:36:14 -05:00
|
|
|
exitActivity();
|
|
|
|
|
|
|
|
|
|
if (pageForLookup) {
|
|
|
|
|
enterNewActivity(new DictionaryWordSelectActivity(
|
|
|
|
|
renderer, mappedInput, std::move(pageForLookup), readerFontId, orientedMarginLeft, orientedMarginTop,
|
feat: Integrate PR #857 dictionary intelligence and sub-activity refactor
Pull in the full feature update from PR #857 while preserving fork
advantages (HTML parsing, custom drawHints, PageForward/PageBack,
cache management, stardictCmp, /.dictionary/ paths).
- Add morphological stemming (getStemVariants), Levenshtein edit
distance, and fuzzy matching (findSimilar) to Dictionary
- Create DictionarySuggestionsActivity for "Did you mean?" flow
- Add onDone callback to DictionaryDefinitionActivity for direct
exit-to-reader via "Done" button
- Refactor DictionaryWordSelectActivity to ActivityWithSubactivity
with cascading lookup (exact → stems → suggestions → not found),
en-dash/em-dash splitting, and cross-page hyphenation
- Refactor LookedUpWordsActivity with reverse-chronological order,
inline cascading lookup, UITheme-aware rendering, and sub-activities
- Simplify EpubReaderActivity LOOKUP/LOOKED_UP_WORDS handlers
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 20:50:03 -05:00
|
|
|
bookCachePath, currentOrientation, [this]() { pendingSubactivityExit = true; }, nextPageFirstWord));
|
2026-02-12 19:36:14 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::LOOKED_UP_WORDS: {
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
|
|
|
|
|
exitActivity();
|
|
|
|
|
enterNewActivity(new LookedUpWordsActivity(
|
feat: Integrate PR #857 dictionary intelligence and sub-activity refactor
Pull in the full feature update from PR #857 while preserving fork
advantages (HTML parsing, custom drawHints, PageForward/PageBack,
cache management, stardictCmp, /.dictionary/ paths).
- Add morphological stemming (getStemVariants), Levenshtein edit
distance, and fuzzy matching (findSimilar) to Dictionary
- Create DictionarySuggestionsActivity for "Did you mean?" flow
- Add onDone callback to DictionaryDefinitionActivity for direct
exit-to-reader via "Done" button
- Refactor DictionaryWordSelectActivity to ActivityWithSubactivity
with cascading lookup (exact → stems → suggestions → not found),
en-dash/em-dash splitting, and cross-page hyphenation
- Refactor LookedUpWordsActivity with reverse-chronological order,
inline cascading lookup, UITheme-aware rendering, and sub-activities
- Simplify EpubReaderActivity LOOKUP/LOOKED_UP_WORDS handlers
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 20:50:03 -05:00
|
|
|
renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation,
|
|
|
|
|
[this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; }));
|
2026-02-12 19:36:14 -05:00
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-01 08:34:30 +01:00
|
|
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
feat: Move Sync feature to menu (#680)
## Summary
* **What is the goal of this PR?**
Move the "Sync Progress" option from TOC (Chapter Selection) screen to
the Reader Menu, and fix use-after-free crashes related to callback
handling in activity lifecycle.
* **What changes are included?**
- Added "Sync Progress" as a menu item in `EpubReaderMenuActivity` (now
4 items: Go to Chapter, Sync Progress, Go Home, Delete Book Cache)
- Removed sync-related logic from `EpubReaderChapterSelectionActivity` -
TOC now only displays chapters
- Implemented `pendingGoHome` and `pendingSubactivityExit` flags in
`EpubReaderActivity` to safely handle activity destruction
- Fixed GO_HOME, DELETE_CACHE, and SYNC menu actions to use deferred
callbacks avoiding use-after-free
## Additional Context
* Root cause of crashes: callbacks like `onGoHome()` or `onCancel()`
invoked from activity handlers could destroy the current activity while
code was still executing, causing use-after-free and race conditions
with FreeRTOS display task.
* Solution: Deferred execution pattern - set flags and process them in
`loop()` after all nested activity loops have safely returned.
* Files changed: `EpubReaderMenuActivity.h`,
`EpubReaderActivity.h/.cpp`, `EpubReaderChapterSelectionActivity.h/.cpp`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Co-authored-by: danoooob <danoooob@example.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 22:04:38 +07:00
|
|
|
// Defer go home to avoid race condition with display task
|
|
|
|
|
pendingGoHome = true;
|
2026-02-01 08:34:30 +01:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
if (epub) {
|
|
|
|
|
// 2. BACKUP: Read current progress
|
|
|
|
|
// We use the current variables that track our position
|
|
|
|
|
uint16_t backupSpine = currentSpineIndex;
|
|
|
|
|
uint16_t backupPage = section->currentPage;
|
|
|
|
|
uint16_t backupPageCount = section->pageCount;
|
|
|
|
|
|
|
|
|
|
section.reset();
|
|
|
|
|
// 3. WIPE: Clear the cache directory
|
|
|
|
|
epub->clearCache();
|
|
|
|
|
|
|
|
|
|
// 4. RESTORE: Re-setup the directory and rewrite the progress file
|
|
|
|
|
epub->setupCacheDir();
|
|
|
|
|
|
|
|
|
|
saveProgress(backupSpine, backupPage, backupPageCount);
|
|
|
|
|
}
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
feat: Move Sync feature to menu (#680)
## Summary
* **What is the goal of this PR?**
Move the "Sync Progress" option from TOC (Chapter Selection) screen to
the Reader Menu, and fix use-after-free crashes related to callback
handling in activity lifecycle.
* **What changes are included?**
- Added "Sync Progress" as a menu item in `EpubReaderMenuActivity` (now
4 items: Go to Chapter, Sync Progress, Go Home, Delete Book Cache)
- Removed sync-related logic from `EpubReaderChapterSelectionActivity` -
TOC now only displays chapters
- Implemented `pendingGoHome` and `pendingSubactivityExit` flags in
`EpubReaderActivity` to safely handle activity destruction
- Fixed GO_HOME, DELETE_CACHE, and SYNC menu actions to use deferred
callbacks avoiding use-after-free
## Additional Context
* Root cause of crashes: callbacks like `onGoHome()` or `onCancel()`
invoked from activity handlers could destroy the current activity while
code was still executing, causing use-after-free and race conditions
with FreeRTOS display task.
* Solution: Deferred execution pattern - set flags and process them in
`loop()` after all nested activity loops have safely returned.
* Files changed: `EpubReaderMenuActivity.h`,
`EpubReaderActivity.h/.cpp`, `EpubReaderChapterSelectionActivity.h/.cpp`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Co-authored-by: danoooob <danoooob@example.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 22:04:38 +07:00
|
|
|
// Defer go home to avoid race condition with display task
|
|
|
|
|
pendingGoHome = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
|
|
|
|
if (KOREADER_STORE.hasCredentials()) {
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
const int currentPage = section ? section->currentPage : 0;
|
|
|
|
|
const int totalPages = section ? section->pageCount : 0;
|
|
|
|
|
exitActivity();
|
|
|
|
|
enterNewActivity(new KOReaderSyncActivity(
|
|
|
|
|
renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages,
|
|
|
|
|
[this]() {
|
|
|
|
|
// On cancel - defer exit to avoid use-after-free
|
|
|
|
|
pendingSubactivityExit = true;
|
|
|
|
|
},
|
|
|
|
|
[this](int newSpineIndex, int newPage) {
|
|
|
|
|
// On sync complete - update position and defer exit
|
|
|
|
|
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
|
|
|
|
currentSpineIndex = newSpineIndex;
|
|
|
|
|
nextPageNumber = newPage;
|
|
|
|
|
section.reset();
|
|
|
|
|
}
|
|
|
|
|
pendingSubactivityExit = true;
|
|
|
|
|
}));
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
2026-02-01 08:34:30 +01:00
|
|
|
break;
|
|
|
|
|
}
|
2026-02-13 16:07:38 -05:00
|
|
|
// Handled locally in the menu activity (cycle on Confirm, never dispatched here)
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN:
|
|
|
|
|
case EpubReaderMenuActivity::MenuAction::LETTERBOX_FILL:
|
|
|
|
|
break;
|
2026-02-01 08:34:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:53:35 +03:00
|
|
|
void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
|
|
|
|
// No-op if the selected orientation matches current settings.
|
|
|
|
|
if (SETTINGS.orientation == orientation) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Preserve current reading position so we can restore after reflow.
|
|
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
|
|
|
if (section) {
|
|
|
|
|
cachedSpineIndex = currentSpineIndex;
|
|
|
|
|
cachedChapterTotalPageCount = section->pageCount;
|
|
|
|
|
nextPageNumber = section->currentPage;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Persist the selection so the reader keeps the new orientation on next launch.
|
|
|
|
|
SETTINGS.orientation = orientation;
|
|
|
|
|
SETTINGS.saveToFile();
|
|
|
|
|
|
|
|
|
|
// Update renderer orientation to match the new logical coordinate system.
|
|
|
|
|
applyReaderOrientation(renderer, SETTINGS.orientation);
|
|
|
|
|
|
|
|
|
|
// Reset section to force re-layout in the new orientation.
|
|
|
|
|
section.reset();
|
|
|
|
|
xSemaphoreGive(renderingMutex);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 23:32:18 +11:00
|
|
|
void EpubReaderActivity::displayTaskLoop() {
|
2025-12-03 22:00:29 +11:00
|
|
|
while (true) {
|
|
|
|
|
if (updateRequired) {
|
|
|
|
|
updateRequired = false;
|
2025-12-06 03:02:52 +11:00
|
|
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderScreen();
|
2025-12-06 03:02:52 +11:00
|
|
|
xSemaphoreGive(renderingMutex);
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
// TODO: Failure handling
|
2025-12-17 23:32:18 +11:00
|
|
|
void EpubReaderActivity::renderScreen() {
|
2025-12-03 22:00:29 +11:00
|
|
|
if (!epub) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 20:10:38 +11:00
|
|
|
// edge case handling for sub-zero spine index
|
|
|
|
|
if (currentSpineIndex < 0) {
|
2025-12-03 22:00:29 +11:00
|
|
|
currentSpineIndex = 0;
|
|
|
|
|
}
|
2025-12-13 20:10:38 +11:00
|
|
|
// based bounds of book, show end of book screen
|
|
|
|
|
if (currentSpineIndex > epub->getSpineItemsCount()) {
|
|
|
|
|
currentSpineIndex = epub->getSpineItemsCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show end of book screen
|
|
|
|
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
|
|
|
|
renderer.clearScreen();
|
2025-12-31 12:11:36 +10:00
|
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD);
|
2025-12-13 20:10:38 +11:00
|
|
|
renderer.displayBuffer();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
// Apply screen viewable areas and additional padding
|
|
|
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
|
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
|
|
|
&orientedMarginLeft);
|
2026-01-05 10:29:08 +01:00
|
|
|
orientedMarginTop += SETTINGS.screenMargin;
|
|
|
|
|
orientedMarginLeft += SETTINGS.screenMargin;
|
|
|
|
|
orientedMarginRight += SETTINGS.screenMargin;
|
2026-01-27 12:25:44 +00:00
|
|
|
orientedMarginBottom += SETTINGS.screenMargin;
|
|
|
|
|
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
|
|
|
|
2026-01-27 12:25:44 +00:00
|
|
|
// Add status bar margin
|
|
|
|
|
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
|
|
|
|
|
// Add additional margin for status bar if progress bar is shown
|
2026-02-05 10:49:38 -05:00
|
|
|
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
2026-01-27 12:25:44 +00:00
|
|
|
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
|
2026-01-27 12:25:44 +00:00
|
|
|
}
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
if (!section) {
|
2025-12-24 22:36:13 +11:00
|
|
|
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
|
2025-12-12 22:13:34 +11:00
|
|
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
|
2025-12-31 12:11:36 +10:00
|
|
|
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
|
|
|
|
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
|
2025-12-30 18:34:46 +10:00
|
|
|
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
2026-01-02 01:21:48 -06:00
|
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
2026-02-06 02:49:04 -05:00
|
|
|
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Cache not found, building...");
|
2025-12-05 21:12:15 +11:00
|
|
|
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); };
|
2025-12-28 13:59:44 +09:00
|
|
|
|
2025-12-30 18:34:46 +10:00
|
|
|
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
2026-01-02 01:21:48 -06:00
|
|
|
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
2026-02-06 02:49:04 -05:00
|
|
|
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("ERS", "Failed to persist page data to SD");
|
2025-12-12 22:13:34 +11:00
|
|
|
section.reset();
|
2025-12-03 22:00:29 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Cache found, skipping build...");
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextPageNumber == UINT16_MAX) {
|
|
|
|
|
section->currentPage = section->pageCount - 1;
|
|
|
|
|
} else {
|
|
|
|
|
section->currentPage = nextPageNumber;
|
|
|
|
|
}
|
2026-01-27 19:11:11 +08:00
|
|
|
|
|
|
|
|
// handles changes in reader settings and reset to approximate position based on cached progress
|
|
|
|
|
if (cachedChapterTotalPageCount > 0) {
|
|
|
|
|
// only goes to relative position if spine index matches cached value
|
|
|
|
|
if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) {
|
|
|
|
|
float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount);
|
|
|
|
|
int newPage = static_cast<int>(progress * section->pageCount);
|
|
|
|
|
section->currentPage = newPage;
|
|
|
|
|
}
|
|
|
|
|
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
|
|
|
|
|
}
|
2026-02-05 15:17:51 +03:00
|
|
|
|
|
|
|
|
if (pendingPercentJump && section->pageCount > 0) {
|
|
|
|
|
// Apply the pending percent jump now that we know the new section's page count.
|
|
|
|
|
int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount));
|
|
|
|
|
if (newPage >= section->pageCount) {
|
|
|
|
|
newPage = section->pageCount - 1;
|
|
|
|
|
}
|
|
|
|
|
section->currentPage = newPage;
|
|
|
|
|
pendingPercentJump = false;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:56:39 +11:00
|
|
|
renderer.clearScreen();
|
2025-12-08 19:48:49 +11:00
|
|
|
|
|
|
|
|
if (section->pageCount == 0) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "No pages to render");
|
2025-12-31 12:11:36 +10:00
|
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD);
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
return;
|
2025-12-05 22:19:44 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
|
2025-12-31 12:11:36 +10:00
|
|
|
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 22:13:34 +11:00
|
|
|
{
|
2025-12-29 12:19:54 +10:00
|
|
|
auto p = section->loadPageFromSectionFile();
|
2025-12-12 22:13:34 +11:00
|
|
|
if (!p) {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
|
2025-12-12 22:13:34 +11:00
|
|
|
section->clearCache();
|
|
|
|
|
section.reset();
|
|
|
|
|
return renderScreen();
|
|
|
|
|
}
|
|
|
|
|
const auto start = millis();
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
|
2025-12-12 22:13:34 +11:00
|
|
|
}
|
2026-02-01 08:34:30 +01:00
|
|
|
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
|
|
|
|
|
}
|
2025-12-08 19:48:49 +11:00
|
|
|
|
2026-02-01 08:34:30 +01:00
|
|
|
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
|
2025-12-30 15:09:30 +10:00
|
|
|
FsFile f;
|
2026-02-08 21:29:14 +01:00
|
|
|
if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
2026-01-27 19:11:11 +08:00
|
|
|
uint8_t data[6];
|
2025-12-23 14:14:10 +11:00
|
|
|
data[0] = currentSpineIndex & 0xFF;
|
|
|
|
|
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
2026-02-01 08:34:30 +01:00
|
|
|
data[2] = currentPage & 0xFF;
|
|
|
|
|
data[3] = (currentPage >> 8) & 0xFF;
|
|
|
|
|
data[4] = pageCount & 0xFF;
|
|
|
|
|
data[5] = (pageCount >> 8) & 0xFF;
|
2026-01-27 19:11:11 +08:00
|
|
|
f.write(data, 6);
|
2025-12-23 14:14:10 +11:00
|
|
|
f.close();
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
|
2026-02-01 08:34:30 +01:00
|
|
|
} else {
|
2026-02-13 12:16:39 +01:00
|
|
|
LOG_ERR("ERS", "Could not save progress!");
|
2025-12-23 14:14:10 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
|
|
|
|
const int orientedMarginRight, const int orientedMarginBottom,
|
|
|
|
|
const int orientedMarginLeft) {
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
2026-02-12 20:40:07 -05:00
|
|
|
|
|
|
|
|
// Draw bookmark ribbon indicator in top-right corner if current page is bookmarked
|
|
|
|
|
if (section && BookmarkStore::hasBookmark(epub->getCachePath(), currentSpineIndex, section->currentPage)) {
|
|
|
|
|
const int screenWidth = renderer.getScreenWidth();
|
|
|
|
|
const int bkWidth = 12;
|
|
|
|
|
const int bkHeight = 22;
|
|
|
|
|
const int bkX = screenWidth - orientedMarginRight - bkWidth + 2;
|
|
|
|
|
const int bkY = 0;
|
|
|
|
|
const int notchDepth = bkHeight / 3;
|
|
|
|
|
const int centerX = bkX + bkWidth / 2;
|
|
|
|
|
|
|
|
|
|
const int xPoints[5] = {bkX, bkX + bkWidth, bkX + bkWidth, centerX, bkX};
|
|
|
|
|
const int yPoints[5] = {bkY, bkY, bkY + bkHeight, bkY + bkHeight - notchDepth, bkY + bkHeight};
|
|
|
|
|
renderer.fillPolygon(xPoints, yPoints, 5, true);
|
|
|
|
|
}
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
2025-12-08 19:48:49 +11:00
|
|
|
if (pagesUntilFullRefresh <= 1) {
|
2026-01-27 18:50:15 +01:00
|
|
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
2026-01-03 08:33:42 +00:00
|
|
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
2025-12-08 19:48:49 +11:00
|
|
|
} else {
|
2025-12-08 22:06:09 +11:00
|
|
|
renderer.displayBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
pagesUntilFullRefresh--;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 00:17:49 +11:00
|
|
|
// Save bw buffer to reset buffer state after grayscale data sync
|
|
|
|
|
renderer.storeBwBuffer();
|
|
|
|
|
|
2025-12-08 19:48:49 +11:00
|
|
|
// grayscale rendering
|
2025-12-08 22:06:09 +11:00
|
|
|
// TODO: Only do this if font supports it
|
2026-01-07 10:14:35 +01:00
|
|
|
if (SETTINGS.textAntiAliasing) {
|
2025-12-08 19:48:49 +11:00
|
|
|
renderer.clearScreen(0x00);
|
2025-12-16 02:16:35 +11:00
|
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderer.copyGrayscaleLsbBuffers();
|
|
|
|
|
|
|
|
|
|
// Render and copy to MSB buffer
|
|
|
|
|
renderer.clearScreen(0x00);
|
2025-12-16 02:16:35 +11:00
|
|
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
2025-12-08 19:48:49 +11:00
|
|
|
renderer.copyGrayscaleMsbBuffers();
|
|
|
|
|
|
|
|
|
|
// display grayscale part
|
|
|
|
|
renderer.displayGrayBuffer();
|
2025-12-16 02:16:35 +11:00
|
|
|
renderer.setRenderMode(GfxRenderer::BW);
|
2025-12-08 19:48:49 +11:00
|
|
|
}
|
2025-12-17 00:17:49 +11:00
|
|
|
|
|
|
|
|
// restore the bw data
|
|
|
|
|
renderer.restoreBwBuffer();
|
2025-12-08 19:48:49 +11:00
|
|
|
}
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom,
|
|
|
|
|
const int orientedMarginLeft) const {
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
|
|
|
|
2025-12-27 17:48:27 -06:00
|
|
|
// determine visible status bar elements
|
2026-01-27 12:25:44 +00:00
|
|
|
const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
|
2026-02-05 10:49:38 -05:00
|
|
|
const bool showBookProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR;
|
|
|
|
|
const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
2026-01-27 12:25:44 +00:00
|
|
|
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
2026-02-05 10:49:38 -05:00
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR;
|
|
|
|
|
const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
2025-12-27 17:48:27 -06:00
|
|
|
const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
|
2026-01-27 12:25:44 +00:00
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
2026-02-05 10:49:38 -05:00
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
2025-12-27 17:48:27 -06:00
|
|
|
const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
|
2026-01-27 12:25:44 +00:00
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
|
2026-02-05 10:49:38 -05:00
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
|
|
|
|
|
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
|
2026-01-12 10:53:58 +01:00
|
|
|
const bool showBatteryPercentage =
|
|
|
|
|
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
|
2025-12-27 17:48:27 -06:00
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
// Position status bar near the bottom of the logical screen, regardless of orientation
|
|
|
|
|
const auto screenHeight = renderer.getScreenHeight();
|
2025-12-31 01:28:25 +10:00
|
|
|
const auto textY = screenHeight - orientedMarginBottom - 4;
|
2025-12-27 17:48:27 -06:00
|
|
|
int progressTextWidth = 0;
|
|
|
|
|
|
2026-01-27 12:25:44 +00:00
|
|
|
// Calculate progress in book
|
|
|
|
|
const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
|
|
|
|
|
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
|
2025-12-27 17:48:27 -06:00
|
|
|
|
2026-02-05 10:49:38 -05:00
|
|
|
if (showProgressText || showProgressPercentage || showBookPercentage) {
|
2025-12-27 17:48:27 -06:00
|
|
|
// Right aligned text for progress counter
|
2026-01-19 06:55:35 -05:00
|
|
|
char progressStr[32];
|
2026-01-27 12:25:44 +00:00
|
|
|
|
|
|
|
|
// Hide percentage when progress bar is shown to reduce clutter
|
|
|
|
|
if (showProgressPercentage) {
|
|
|
|
|
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount,
|
|
|
|
|
bookProgress);
|
2026-02-05 10:49:38 -05:00
|
|
|
} else if (showBookPercentage) {
|
|
|
|
|
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
|
2026-01-27 12:25:44 +00:00
|
|
|
} else {
|
|
|
|
|
snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY,
|
2026-01-27 12:25:44 +00:00
|
|
|
progressStr);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 10:49:38 -05:00
|
|
|
if (showBookProgressBar) {
|
2026-01-27 12:25:44 +00:00
|
|
|
// Draw progress bar at the very bottom of the screen, from edge to edge of viewable area
|
2026-02-05 10:49:38 -05:00
|
|
|
GUI.drawReadingProgressBar(renderer, static_cast<size_t>(bookProgress));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showChapterProgressBar) {
|
|
|
|
|
// Draw chapter progress bar at the very bottom of the screen, from edge to edge of viewable area
|
|
|
|
|
const float chapterProgress =
|
|
|
|
|
(section->pageCount > 0) ? (static_cast<float>(section->currentPage + 1) / section->pageCount) * 100 : 0;
|
|
|
|
|
GUI.drawReadingProgressBar(renderer, static_cast<size_t>(chapterProgress));
|
2025-12-27 17:48:27 -06:00
|
|
|
}
|
2025-12-17 13:05:24 +01:00
|
|
|
|
2025-12-27 17:48:27 -06:00
|
|
|
if (showBattery) {
|
feat: UI themes, Lyra (#528)
## Summary
### What is the goal of this PR?
- Visual UI overhaul
- UI theme selection
### What changes are included?
- Added a setting "UI Theme": Classic, Lyra
- The classic theme is the current Crosspoint theme
- The Lyra theme implements these mockups:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0
by Discord users yagofarias, ruby and gan_shu
- New functions in GFXRenderer to render rounded rectangles, greyscale
fills (using dithering) and thick lines
- Basic UI components are factored into BaseTheme methods which can be
overridden by each additional theme. Methods that are not overridden
will fallback to BaseTheme behavior. This means any new
features/components in CrossPoint only need to be developed for the
"Classic" BaseTheme.
- Additional themes can easily be developed by the community using this
foundation



## Additional Context
- Only the Home, Library and main Settings screens have been implemented
so far, this will be extended to the transfer screens and chapter
selection screen later on, but we need to get the ball rolling somehow
:)
- Loading extra covers on the home screen in the Lyra theme takes a
little more time (about 2 seconds), I added a loading bar popup (reusing
the Indexing progress bar from the reader view, factored into a neat UI
component) but the popup adds ~400ms to the loading time.
- ~~Home screen thumbnails will need to be generated separately for each
theme, because they are displayed in different sizes. Because we're
using dithering, displaying a thumb with the wrong size causes the
picture to look janky or dark as it does on the screenshots above. No
worries this will be fixed in a future PR.~~ Thumbs are now generated
with a size parameter
- UI Icons will need to be implemented in a future PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
This is not a vibe coded PR. Copilot was used for autocompletion to save
time but I reviewed, understood and edited all generated code.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
|
|
|
GUI.drawBattery(renderer, Rect{orientedMarginLeft + 1, textY, metrics.batteryWidth, metrics.batteryHeight},
|
|
|
|
|
showBatteryPercentage);
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-27 17:48:27 -06:00
|
|
|
|
|
|
|
|
if (showChapterTitle) {
|
|
|
|
|
// Centered chatper title text
|
|
|
|
|
// Page width minus existing content with 30px padding on each side
|
2026-01-21 13:09:48 +01:00
|
|
|
const int rendererableScreenWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
|
|
|
|
|
|
|
|
|
const int batterySize = showBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
|
|
|
|
const int titleMarginLeft = batterySize + 30;
|
|
|
|
|
const int titleMarginRight = progressTextWidth + 30;
|
|
|
|
|
|
|
|
|
|
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
|
|
|
|
// available space.
|
|
|
|
|
int titleMarginLeftAdjusted = std::max(titleMarginLeft, titleMarginRight);
|
|
|
|
|
int availableTitleSpace = rendererableScreenWidth - 2 * titleMarginLeftAdjusted;
|
2025-12-27 17:48:27 -06:00
|
|
|
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
|
|
|
|
|
|
|
|
|
std::string title;
|
|
|
|
|
int titleWidth;
|
|
|
|
|
if (tocIndex == -1) {
|
|
|
|
|
title = "Unnamed";
|
|
|
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed");
|
|
|
|
|
} else {
|
|
|
|
|
const auto tocItem = epub->getTocItem(tocIndex);
|
|
|
|
|
title = tocItem.title;
|
2025-12-13 21:17:22 +11:00
|
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
2026-01-21 13:09:48 +01:00
|
|
|
if (titleWidth > availableTitleSpace) {
|
|
|
|
|
// Not enough space to center on the screen, center it within the remaining space instead
|
|
|
|
|
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
|
|
|
|
titleMarginLeftAdjusted = titleMarginLeft;
|
|
|
|
|
}
|
2026-02-01 16:23:48 +05:00
|
|
|
if (titleWidth > availableTitleSpace) {
|
|
|
|
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
2025-12-27 17:48:27 -06:00
|
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
|
|
|
}
|
2025-12-13 21:17:22 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2026-01-21 13:09:48 +01:00
|
|
|
renderer.drawText(SMALL_FONT_ID,
|
|
|
|
|
titleMarginLeftAdjusted + orientedMarginLeft + (availableTitleSpace - titleWidth) / 2, textY,
|
|
|
|
|
title.c_str());
|
2025-12-27 17:48:27 -06:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|