Compare commits

...

13 Commits

Author SHA1 Message Date
Maeve Andrews
28c4da546c
Merge ba700eef84 into e5c0ddc9fa 2026-02-01 18:35:09 +00:00
Maeve Andrews
ba700eef84 feat: Apply orientation to UI too, move setting to Display 2026-02-01 12:29:32 -06:00
Uri Tauber
e5c0ddc9fa
feat: Debugging monitor script (#555)
Some checks are pending
CI / build (push) Waiting to run
## Summary

* **What is the goal of this PR?**
Add a debugging script to help developers monitor the ESP32 serial port
directly from a PC.

* **What changes are included?**
Added a new script: scripts/debugging_monitor.py

## Additional Context

While working on a new Crosspoint-Reader feature, it quickly became
clear that watching the ESP32 serial output without any visual cues was
inconvenient and easy to mess up.

This script improves the debugging experience by reading data from the
serial port and providing:

1. A timestamp prefix for every log line (instead of milliseconds since
power-up)
2. Color-coded output for different message types
3. A secondary window displaying a live graph of RAM usage, which is
especially useful for tracking the memory impact of new features

<img width="1916" height="1049" alt="Screenshot_20260126_183811"
src="https://github.com/user-attachments/assets/6291887f-ac17-43ac-9e43-f5dec8a7097e"
/>

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY >**_
I wrote the initial version of the script. Gemini was used to help add
the Matplotlib-based graphing and threading logic.
2026-02-01 22:53:20 +11:00
Arthur Tazhitdinov
b1dcb7733b
fix: truncating chapter titles using UTF-8 safe function (#599)
## Summary

* Truncating chapter titles using utf8 safe functions (Cyrillic titles
were split mid codepoint)
* refactoring of lib/Utf8

---

### 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 >**_
2026-02-01 22:23:48 +11:00
Arthur Tazhitdinov
0d82b03981
fix: don't wake up after USB connect (#644)
## Summary

* fixes problem that if short power button press is enabled, connecting
device to usb leads to waking up
2026-02-01 22:19:33 +11:00
Dave Allie
5a97334ace
Revert "fix: don't wake up after USB connect" (#643)
Reverts crosspoint-reader/crosspoint-reader#576

Causing a boot loop on master
2026-02-01 21:35:25 +11:00
Gaspar Fabrega Ragni
4dd73a211a
fix: custom sleep not showing image at index 0 (#639)
Some checks are pending
CI / build (push) Waiting to run
## Summary

* Fixing custom sleep behaviour where the first image in the /sleep
directory is not shown
* image at index 0 is not being rendered when more than 1 image is
stored in /sleep directory, because `APP_STATE.lastSleepImage` is always
0.

## Additional Context

* `APP_STATE.lastSleepImage` is reset to 0 when a epub is open, this
value is only used to compare it to the randomly selected one in
`renderCustomSleepScreen()` that should always be a valid index, since
the list of valid bmp images is colected from scratch. -> no need to
reset it and block image @ index 0 from being rendered

---

### 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? _**NO**_

Co-authored-by: Oyster <Oyster@home>
2026-02-01 19:22:52 +11:00
Thomas Foskolos
634f6279cb
docs: Update USER_GUIDE.md (#625)
Add Greek as not supported language atm on the use guide
2026-02-01 19:21:36 +11:00
nscheung
11b2a59233
fix: Hide button hints in landscape CW mode (#637)
## Summary

* This change hides the button hints from overlapping chapter titles
when in landscape CW mode.

Before

![Before](https://github.com/user-attachments/assets/3015a9b3-3fa5-443b-a641-3e65841a6fbc)
After

![After](https://github.com/user-attachments/assets/011de15d-5ae6-429c-8f91-d8f37abe52d9)

## Additional Context

* I initially considered implementing an offset fix, but with potential
UI changes on the horizon, hiding the button hints appears to be the
simplest solution for now.

---

### 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 >
2026-02-01 19:21:28 +11:00
Luke Stein
12c20bb09e
fix: WiFi error screen text clarifications (#612)
## Summary

* Clarify strings on Wifi connection error screens
* I have confirmed on device that these are short enough not to overflow
screen margins

## Additional Context

* Several screens give duplicative text (e.g., header "Connection
Failed" with contents text "Connection failed") or slightly confusing
text (header "Forget Network?" with text "Remove saved password?")

---

### AI Usage

Did you use AI tools to help write this code? **No**
2026-02-01 19:19:23 +11:00
Arthur Tazhitdinov
6b7065b986
fix: don't wake up after USB connect (#576)
## Summary

* fixes problem that if short power button press is enabled, connecting
device to usb leads to waking up
2026-02-01 18:51:31 +11:00
Arthur Tazhitdinov
f4df513bf3
feat(ui): change popup logic (#442)
## Summary

* refactors Indexing popups into ScreenComponents (they had different
implementations in different files)
* removes Indexing popup for small chapters
* only show Indexing popup (without progress bar) for large chapters
(using same minimum file size condition as for progress bar before)

## Additional Context

* Having to show even single popup message and redraw the screen slows
down the flow significantly
* Testing results:
    * Opening large chapter with progress bar - 11 seconds
* Same chapter without progress bar, only single Indexing popup - 5
seconds

---

### AI Usage

Did you use AI tools to help write this code? _**< PARTIALLY>**_
2026-02-01 18:41:24 +11:00
Jonas Diemer
f935b59a41
feat: Add reading menu and delete cache function (#433)
## Summary

* Adds a menu in the Epub reader
* The Chapter selection is moved there to pos 1 (so it can be reached by
double tapping the confirm button)
* A Go Home is there, too
* Most significantly, a function "Delete Book Cache" is added. This
returns to main (to avoid directly rebuilding cached items, eg. if this
is used to debug/develop other areas - and it's also easier ;))

Probably, the Sync function could now be moved from the Chapter
selection to this menu, too.

---

### 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**_
2026-02-01 18:34:30 +11:00
48 changed files with 920 additions and 485 deletions

View File

@ -95,6 +95,20 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh ```sh
pio run --target upload pio run --target upload
``` ```
### Debugging
After flashing the new features, its recommended to capture detailed logs from the serial port.
First, make sure all required Python packages are installed:
```python
python3 -m pip install serial colorama matplotlib
```
after that run the script:
```sh
python3 scripts/debugging_monitor.py
```
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
## Internals ## Internals

View File

@ -201,7 +201,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others. * **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others. * **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi. What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi.
--- ---

View File

@ -123,9 +123,7 @@ bool Section::clearCache() const {
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void()>& progressSetupFn, const std::function<void()>& popupFn) {
const std::function<void(int)>& progressFn) {
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
const auto localPath = epub->getSpineItem(spineIndex).href; const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
@ -171,11 +169,6 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize); Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize);
// Only show progress bar for larger chapters where rendering overhead is worth it
if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) {
progressSetupFn();
}
if (!SdMan.openFileForWrite("SCT", filePath, file)) { if (!SdMan.openFileForWrite("SCT", filePath, file)) {
return false; return false;
} }
@ -186,8 +179,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
ChapterHtmlSlimParser visitor( ChapterHtmlSlimParser visitor(
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, viewportHeight, hyphenationEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, [this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn);
progressFn);
Hyphenator::setPreferredLanguage(epub->getLanguage()); Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages(); success = visitor.parseAndBuildPages();

View File

@ -33,7 +33,6 @@ class Section {
bool clearCache() const; bool clearCache() const;
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
const std::function<void()>& progressSetupFn = nullptr, const std::function<void()>& popupFn = nullptr);
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile(); std::unique_ptr<Page> loadPageFromSectionFile();
}; };

View File

@ -10,8 +10,8 @@
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
@ -289,10 +289,10 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return false; return false;
} }
// Get file size for progress calculation // Get file size to decide whether to show indexing popup.
const size_t totalSize = file.size(); if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
size_t bytesRead = 0; popupFn();
int lastProgress = -1; }
XML_SetUserData(parser, this); XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement); XML_SetElementHandler(parser, startElement, endElement);
@ -322,17 +322,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return false; return false;
} }
// Update progress (call every 10% change to avoid too frequent updates)
// Only show progress for larger chapters where rendering overhead is worth it
bytesRead += len;
if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) {
const int progress = static_cast<int>((bytesRead * 100) / totalSize);
if (lastProgress / 10 != progress / 10) {
lastProgress = progress;
progressFn(progress);
}
}
done = file.available() == 0; done = file.available() == 0;
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) { if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {

View File

@ -18,7 +18,7 @@ class ChapterHtmlSlimParser {
const std::string& filepath; const std::string& filepath;
GfxRenderer& renderer; GfxRenderer& renderer;
std::function<void(std::unique_ptr<Page>)> completePageFn; std::function<void(std::unique_ptr<Page>)> completePageFn;
std::function<void(int)> progressFn; // Progress callback (0-100) std::function<void()> popupFn; // Popup callback
int depth = 0; int depth = 0;
int skipUntilDepth = INT_MAX; int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX;
@ -52,7 +52,7 @@ class ChapterHtmlSlimParser {
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint8_t paragraphAlignment, const uint16_t viewportWidth,
const uint16_t viewportHeight, const bool hyphenationEnabled, const uint16_t viewportHeight, const bool hyphenationEnabled,
const std::function<void(std::unique_ptr<Page>)>& completePageFn, const std::function<void(std::unique_ptr<Page>)>& completePageFn,
const std::function<void(int)>& progressFn = nullptr) const std::function<void()>& popupFn = nullptr)
: filepath(filepath), : filepath(filepath),
renderer(renderer), renderer(renderer),
fontId(fontId), fontId(fontId),
@ -63,7 +63,7 @@ class ChapterHtmlSlimParser {
viewportHeight(viewportHeight), viewportHeight(viewportHeight),
hyphenationEnabled(hyphenationEnabled), hyphenationEnabled(hyphenationEnabled),
completePageFn(completePageFn), completePageFn(completePageFn),
progressFn(progressFn) {} popupFn(popupFn) {}
~ChapterHtmlSlimParser() = default; ~ChapterHtmlSlimParser() = default;
bool parseAndBuildPages(); bool parseAndBuildPages();
void addLineToPage(std::shared_ptr<TextBlock> line); void addLineToPage(std::shared_ptr<TextBlock> line);

View File

@ -415,13 +415,21 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
if (!text || maxWidth <= 0) return "";
std::string item = text; std::string item = text;
int itemWidth = getTextWidth(fontId, item.c_str(), style); const char* ellipsis = "...";
while (itemWidth > maxWidth && item.length() > 8) { int textWidth = getTextWidth(fontId, item.c_str(), style);
item.replace(item.length() - 5, 5, "..."); if (textWidth <= maxWidth) {
itemWidth = getTextWidth(fontId, item.c_str(), style); // Text fits, return as is
return item;
} }
return item;
while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) {
utf8RemoveLastChar(item);
}
return item.empty() ? ellipsis : item + ellipsis;
} }
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
@ -486,11 +494,11 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
setOrientation(Orientation::Portrait); setOrientation(Orientation::Portrait);
const int pageHeight = getScreenHeight(); const int pageHeight = getScreenHeight();
constexpr int buttonWidth = 106; constexpr int buttonWidth = 94;
constexpr int buttonHeight = 40; constexpr int buttonHeight = 40;
constexpr int buttonY = 40; // Distance from bottom constexpr int buttonY = 40; // Distance from bottom
constexpr int textYOffset = 7; // Distance from top of button to text baseline constexpr int textYOffset = 7; // Distance from top of button to text baseline
constexpr int buttonPositions[] = {25, 130, 245, 350}; constexpr int buttonPositions[] = {50, 143, 247, 340};
const char* labels[] = {btn1, btn2, btn3, btn4}; const char* labels[] = {btn1, btn2, btn3, btn4};
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
@ -508,7 +516,10 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
setOrientation(orig_orientation); setOrientation(orig_orientation);
} }
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const { void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) {
const Orientation orig_orientation = getOrientation();
setOrientation(Orientation::Portrait);
const int screenWidth = getScreenWidth(); const int screenWidth = getScreenWidth();
constexpr int buttonWidth = 40; // Width on screen (height when rotated) constexpr int buttonWidth = 40; // Width on screen (height when rotated)
constexpr int buttonHeight = 80; // Height on screen (width when rotated) constexpr int buttonHeight = 80; // Height on screen (width when rotated)
@ -557,6 +568,8 @@ void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, cons
drawTextRotated90CW(fontId, textX, textY, labels[i]); drawTextRotated90CW(fontId, textX, textY, labels[i]);
} }
} }
setOrientation(orig_orientation);
} }
int GfxRenderer::getTextHeight(const int fontId) const { int GfxRenderer::getTextHeight(const int fontId) const {

View File

@ -56,7 +56,7 @@ class GfxRenderer {
int getScreenHeight() const; int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region // EXPERIMENTAL: Windowed update - display only a rectangular region
void displayWindow(int x, int y, int width, int height) const; // void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;
void clearScreen(uint8_t color = 0xFF) const; void clearScreen(uint8_t color = 0xFF) const;
@ -85,7 +85,7 @@ class GfxRenderer {
// UI Components // UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4); void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const; void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn);
private: private:
// Helper for drawing rotated text (90 degrees clockwise, for side buttons) // Helper for drawing rotated text (90 degrees clockwise, for side buttons)

View File

@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
return cp; return cp;
} }
size_t utf8RemoveLastChar(std::string& str) {
if (str.empty()) return 0;
size_t pos = str.size() - 1;
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
--pos;
}
str.resize(pos);
return pos;
}
// Truncate string by removing N UTF-8 characters from the end
void utf8TruncateChars(std::string& str, const size_t numChars) {
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
utf8RemoveLastChar(str);
}
}

View File

@ -1,7 +1,11 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <string>
#define REPLACEMENT_GLYPH 0xFFFD #define REPLACEMENT_GLYPH 0xFFFD
uint32_t utf8NextCodepoint(const unsigned char** string); uint32_t utf8NextCodepoint(const unsigned char** string);
// Remove the last UTF-8 codepoint from a std::string and return the new size.
size_t utf8RemoveLastChar(std::string& str);
// Truncate string by removing N UTF-8 codepoints from the end.
void utf8TruncateChars(std::string& str, size_t numChars);

View File

@ -24,12 +24,13 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
void HalGPIO::startDeepSleep() { void HalGPIO::startDeepSleep() {
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it // Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (inputMgr.isPressed(BTN_POWER)) { while (inputMgr.isPressed(BTN_POWER)) {
delay(50); delay(50);
inputMgr.update(); inputMgr.update();
} }
// Arm the wakeup trigger *after* the button is released
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep // Enter Deep Sleep
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
@ -44,12 +45,20 @@ bool HalGPIO::isUsbConnected() const {
return digitalRead(UART0_RXD) == HIGH; return digitalRead(UART0_RXD) == HIGH;
} }
bool HalGPIO::isWakeupByPowerButton() const { HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
const bool usbConnected = isUsbConnected();
const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto wakeupCause = esp_sleep_get_wakeup_cause();
const auto resetReason = esp_reset_reason(); const auto resetReason = esp_reset_reason();
if (isUsbConnected()) {
return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) ||
} else { (wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) {
return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); return WakeupReason::PowerButton;
} }
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) {
return WakeupReason::AfterFlash;
}
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && usbConnected) {
return WakeupReason::AfterUSBPower;
}
return WakeupReason::Other;
} }

View File

@ -47,8 +47,9 @@ class HalGPIO {
// Check if USB is connected // Check if USB is connected
bool isUsbConnected() const; bool isUsbConnected() const;
// Check if wakeup was caused by power button press enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other };
bool isWakeupByPowerButton() const;
WakeupReason getWakeupReason() const;
// Button indices // Button indices
static constexpr uint8_t BTN_BACK = 0; static constexpr uint8_t BTN_BACK = 0;

214
scripts/debugging_monitor.py Executable file
View File

@ -0,0 +1,214 @@
import sys
import argparse
import re
import threading
from datetime import datetime
from collections import deque
import time
# Try to import potentially missing packages
try:
import serial
from colorama import init, Fore, Style
import matplotlib.pyplot as plt
import matplotlib.animation as animation
except ImportError as e:
missing_package = e.name
print("\n" + "!" * 50)
print(f" Error: The required package '{missing_package}' is not installed.")
print("!" * 50)
print(f"\nTo fix this, please run the following command in your terminal:\n")
install_cmd = "pip install "
packages = []
if 'serial' in str(e): packages.append("pyserial")
if 'colorama' in str(e): packages.append("colorama")
if 'matplotlib' in str(e): packages.append("matplotlib")
print(f" {install_cmd}{' '.join(packages)}")
print("\nExiting...")
sys.exit(1)
# --- Global Variables for Data Sharing ---
# Store last 50 data points
MAX_POINTS = 50
time_data = deque(maxlen=MAX_POINTS)
free_mem_data = deque(maxlen=MAX_POINTS)
total_mem_data = deque(maxlen=MAX_POINTS)
data_lock = threading.Lock() # Prevent reading while writing
# Initialize colors
init(autoreset=True)
def get_color_for_line(line):
"""
Classify log lines by type and assign appropriate colors.
"""
line_upper = line.upper()
if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]):
return Fore.RED
if "[MEM]" in line_upper or "FREE:" in line_upper:
return Fore.CYAN
if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]):
return Fore.MAGENTA
if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]):
return Fore.GREEN
if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper:
return Fore.YELLOW
if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]):
return Fore.BLUE
if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]):
return Fore.LIGHTYELLOW_EX
if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]):
return Fore.LIGHTBLACK_EX
if "[RBS]" in line_upper:
return Fore.LIGHTCYAN_EX
if "[KRS]" in line_upper:
return Fore.LIGHTMAGENTA_EX
if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]):
return Fore.LIGHTMAGENTA_EX
if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]):
return Fore.LIGHTGREEN_EX
if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]):
return Fore.LIGHTYELLOW_EX
return Fore.WHITE
def parse_memory_line(line):
"""
Extracts Free and Total bytes from the specific log line.
Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes
"""
# Regex to find 'Free: <digits>' and 'Total: <digits>'
match = re.search(r"Free:\s*(\d+).*Total:\s*(\d+)", line)
if match:
try:
free_bytes = int(match.group(1))
total_bytes = int(match.group(2))
return free_bytes, total_bytes
except ValueError:
return None, None
return None, None
def serial_worker(port, baud):
"""
Runs in a background thread. Handles reading serial, printing to console,
and updating the data lists.
"""
print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}")
try:
ser = serial.Serial(port, baud, timeout=0.1)
ser.dtr = False
ser.rts = False
except serial.SerialException as e:
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
return
try:
while True:
try:
raw_data = ser.readline().decode('utf-8', errors='replace')
if not raw_data:
continue
clean_line = raw_data.strip()
if not clean_line:
continue
# Add PC timestamp
pc_time = datetime.now().strftime("%H:%M:%S")
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
# Check for Memory Line
if "[MEM]" in formatted_line:
free_val, total_val = parse_memory_line(formatted_line)
if free_val is not None:
with data_lock:
time_data.append(pc_time)
free_mem_data.append(free_val / 1024) # Convert to KB
total_mem_data.append(total_val / 1024) # Convert to KB
# Print to console
line_color = get_color_for_line(formatted_line)
print(f"{line_color}{formatted_line}")
except OSError:
print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}")
break
except Exception as e:
# If thread is killed violently (e.g. main exit), silence errors
pass
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
def update_graph(frame):
"""
Called by Matplotlib animation to redraw the chart.
"""
with data_lock:
if not time_data:
return
# Convert deques to lists for plotting
x = list(time_data)
y_free = list(free_mem_data)
y_total = list(total_mem_data)
plt.cla() # Clear axis
# Plot Total RAM
plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--')
# Plot Free RAM
plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o')
# Fill area under Free RAM
plt.fill_between(x, y_free, color='green', alpha=0.1)
plt.title("ESP32 Memory Monitor")
plt.ylabel("Memory (KB)")
plt.xlabel("Time")
plt.legend(loc='upper left')
plt.grid(True, linestyle=':', alpha=0.6)
# Rotate date labels
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
def main():
parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph")
parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port")
parser.add_argument("--baud", type=int, default=115200, help="Baud rate")
args = parser.parse_args()
# 1. Start the Serial Reader in a separate thread
# Daemon=True means this thread dies when the main program closes
t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True)
t.start()
# 2. Set up the Graph (Main Thread)
try:
plt.style.use('light_background')
except:
pass
fig = plt.figure(figsize=(10, 6))
# Update graph every 1000ms
ani = animation.FuncAnimation(fig, update_graph, interval=1000)
try:
print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}")
plt.show()
except KeyboardInterrupt:
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
plt.close('all') # Force close any lingering plot windows
if __name__ == "__main__":
main()

View File

@ -42,6 +42,38 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left,
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
} }
ScreenComponents::PopupLayout ScreenComponents::drawPopup(const GfxRenderer& renderer, const char* message) {
constexpr int margin = 15;
constexpr int y = 60;
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD);
const int textHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int w = textWidth + margin * 2;
const int h = textHeight + margin * 2;
const int x = (renderer.getScreenWidth() - w) / 2;
renderer.fillRect(x - 2, y - 2, w + 4, h + 4, true); // frame thickness 2
renderer.fillRect(x, y, w, h, false);
const int textX = x + (w - textWidth) / 2;
const int textY = y + margin - 2;
renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return {x, y, w, h};
}
void ScreenComponents::fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, const int progress) {
constexpr int barHeight = 4;
const int barWidth = layout.width - 30; // twice the margin in drawPopup to match text width
const int barX = layout.x + (layout.width - barWidth) / 2;
const int barY = layout.y + layout.height - 10;
int fillWidth = barWidth * progress / 100;
renderer.fillRect(barX, barY, fillWidth, barHeight, true);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) { void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) {
int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft;
renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom,

View File

@ -15,9 +15,20 @@ class ScreenComponents {
public: public:
static const int BOOK_PROGRESS_BAR_HEIGHT = 4; static const int BOOK_PROGRESS_BAR_HEIGHT = 4;
struct PopupLayout {
int x;
int y;
int width;
int height;
};
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress);
static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message);
static void fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, int progress);
// Draw a horizontal tab bar with underline indicator for selected tab // Draw a horizontal tab bar with underline indicator for selected tab
// Returns the height of the tab bar (for positioning content below) // Returns the height of the tab bar (for positioning content below)
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs); static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs);

View File

@ -0,0 +1,78 @@
#include "Activity.h"
#include <GfxRenderer.h>
#include "CrossPointSettings.h"
void Activity::onEnter() {
Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str());
updateMargins();
}
void Activity::onExit() { Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str()); }
void Activity::updateMargins() {
switch (SETTINGS.orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
marginTop = 15;
marginBottom = 50;
marginLeft = marginRight = 20;
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
marginTop = 15;
marginLeft = 50;
marginRight = marginBottom = 20;
break;
case CrossPointSettings::ORIENTATION::INVERTED:
marginTop = 50;
marginBottom = 15;
marginLeft = marginRight = 20;
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
marginTop = 15;
marginRight = 50;
marginLeft = marginBottom = 20;
break;
default:
break;
}
contentStartY = marginTop + 45;
}
void Activity::updateRendererOrientation() {
switch (SETTINGS.orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
break;
case CrossPointSettings::ORIENTATION::INVERTED:
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
break;
}
updateMargins();
}
// Number of items that fit on a page, derived from logical screen height.
// This adapts automatically when switching between portrait and landscape.
int Activity::getPageItems() const {
// Layout constants used in renderScreen
const int startY = contentStartY;
constexpr int lineHeight = LINE_HEIGHT;
const int screenHeight = renderer.getScreenHeight();
const int endY = screenHeight - lineHeight;
const int availableHeight = endY - startY;
int items = availableHeight / lineHeight;
// Ensure we always have at least one item per page to avoid division by zero
if (items < 1) {
items = 1;
}
return items;
}

View File

@ -8,19 +8,31 @@
class MappedInputManager; class MappedInputManager;
class GfxRenderer; class GfxRenderer;
constexpr int LINE_HEIGHT = 30;
class Activity { class Activity {
protected: protected:
std::string name; std::string name;
GfxRenderer& renderer; GfxRenderer& renderer;
MappedInputManager& mappedInput; MappedInputManager& mappedInput;
int marginTop = 15;
int marginBottom = 50;
int marginLeft = 20;
int marginRight = 20;
int contentStartY = 60;
public: public:
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} : name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
virtual ~Activity() = default; virtual ~Activity() = default;
virtual void onEnter() { Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str()); } virtual void onEnter();
virtual void onExit() { Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str()); } virtual void onExit();
virtual void loop() {} virtual void loop() {}
virtual bool skipLoopDelay() { return false; } virtual bool skipLoopDelay() { return false; }
virtual bool preventAutoSleep() { return false; } virtual bool preventAutoSleep() { return false; }
void updateMargins();
void updateRendererOrientation();
int getPageItems() const;
}; };

View File

@ -7,6 +7,7 @@
void BootActivity::onEnter() { void BootActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
updateRendererOrientation();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();

View File

@ -8,13 +8,15 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "ScreenComponents.h"
#include "fontIds.h" #include "fontIds.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
void SleepActivity::onEnter() { void SleepActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderPopup("Entering Sleep...");
ScreenComponents::drawPopup(renderer, "Entering Sleep...");
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) { if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) {
return renderBlankSleepScreen(); return renderBlankSleepScreen();
@ -31,20 +33,6 @@ void SleepActivity::onEnter() {
renderDefaultSleepScreen(); renderDefaultSleepScreen();
} }
void SleepActivity::renderPopup(const char* message) const {
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD);
constexpr int margin = 20;
const int x = (renderer.getScreenWidth() - textWidth - margin * 2) / 2;
constexpr int y = 117;
const int w = textWidth + margin * 2;
const int h = renderer.getLineHeight(UI_12_FONT_ID) + margin * 2;
// renderer.clearScreen();
renderer.fillRect(x - 5, y - 5, w + 10, h + 10, true);
renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false);
renderer.drawText(UI_12_FONT_ID, x + margin, y + margin, message, true, EpdFontFamily::BOLD);
renderer.displayBuffer();
}
void SleepActivity::renderCustomSleepScreen() const { void SleepActivity::renderCustomSleepScreen() const {
// Check if we have a /sleep directory // Check if we have a /sleep directory
auto dir = SdMan.open("/sleep"); auto dir = SdMan.open("/sleep");
@ -138,6 +126,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
int x, y; int x, y;
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
float cropX = 0, cropY = 0; float cropX = 0, cropY = 0;

View File

@ -10,7 +10,6 @@ class SleepActivity final : public Activity {
void onEnter() override; void onEnter() override;
private: private:
void renderPopup(const char* message) const;
void renderDefaultSleepScreen() const; void renderDefaultSleepScreen() const;
void renderCustomSleepScreen() const; void renderCustomSleepScreen() const;
void renderCoverSleepScreen() const; void renderCoverSleepScreen() const;

View File

@ -228,7 +228,7 @@ void OpdsBookBrowserActivity::render() const {
} }
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + (selectorIndex % PAGE_ITEMS) * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) { for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
const auto& entry = entries[i]; const auto& entry = entries[i];
@ -246,7 +246,7 @@ void OpdsBookBrowserActivity::render() const {
} }
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40); auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40);
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), renderer.drawText(UI_10_FONT_ID, 20, contentStartY + (i % PAGE_ITEMS) * LINE_HEIGHT, item.c_str(),
i != static_cast<size_t>(selectorIndex)); i != static_cast<size_t>(selectorIndex));
} }

View File

@ -4,6 +4,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <Utf8.h>
#include <Xtc.h> #include <Xtc.h>
#include <cstring> #include <cstring>
@ -219,14 +220,29 @@ void HomeActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
constexpr int margin = 20; // --- Bottom menu tiles ---
constexpr int bottomMargin = 60; // Build menu items dynamically
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"};
if (hasOpdsUrl) {
// Insert OPDS Browser after My Library
menuItems.insert(menuItems.begin() + 1, "OPDS Browser");
}
const int menuTileWidth = pageWidth - marginLeft - marginRight;
constexpr int menuTileHeight = 45;
constexpr int menuSpacing = 8;
const int totalMenuHeight =
static_cast<int>(menuItems.size()) * menuTileHeight + (static_cast<int>(menuItems.size()) - 1) * menuSpacing;
// --- Top "book" card for the current title (selectorIndex == 0) --- // --- Top "book" card for the current title (selectorIndex == 0) ---
// if we're in inverted portrait, battery and hints can overlap
GfxRenderer::Orientation orientation = renderer.getOrientation();
const int batteryHeight = (orientation == GfxRenderer::Orientation::PortraitInverted ? 0 : 20);
const int bookWidth = pageWidth / 2; const int bookWidth = pageWidth / 2;
const int bookHeight = pageHeight / 2; const int maxBookHeight = pageHeight - marginTop - marginBottom - totalMenuHeight - batteryHeight;
const int bookX = (pageWidth - bookWidth) / 2; const int bookHeight = std::min(pageHeight / 2, maxBookHeight);
constexpr int bookY = 30; const int bookX = marginLeft + (pageWidth - marginLeft - marginRight - bookWidth) / 2;
const int bookY = marginTop + batteryHeight;
const bool bookSelected = hasContinueReading && selectorIndex == 0; const bool bookSelected = hasContinueReading && selectorIndex == 0;
// Bookmark dimensions (used in multiple places) // Bookmark dimensions (used in multiple places)
@ -366,7 +382,7 @@ void HomeActivity::render() {
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
// Remove "..." first, then remove one UTF-8 char, then add "..." back // Remove "..." first, then remove one UTF-8 char, then add "..." back
lines.back().resize(lines.back().size() - 3); // Remove "..." lines.back().resize(lines.back().size() - 3); // Remove "..."
StringUtils::utf8RemoveLastChar(lines.back()); utf8RemoveLastChar(lines.back());
lines.back().append("..."); lines.back().append("...");
} }
break; break;
@ -375,7 +391,7 @@ void HomeActivity::render() {
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
while (wordWidth > maxLineWidth && !i.empty()) { while (wordWidth > maxLineWidth && !i.empty()) {
// Word itself is too long, trim it (UTF-8 safe) // Word itself is too long, trim it (UTF-8 safe)
StringUtils::utf8RemoveLastChar(i); utf8RemoveLastChar(i);
// Check if we have room for ellipsis // Check if we have room for ellipsis
std::string withEllipsis = i + "..."; std::string withEllipsis = i + "...";
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str()); wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
@ -428,7 +444,7 @@ void HomeActivity::render() {
if (!lastBookAuthor.empty()) { if (!lastBookAuthor.empty()) {
std::string trimmedAuthor = lastBookAuthor; std::string trimmedAuthor = lastBookAuthor;
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor); utf8RemoveLastChar(trimmedAuthor);
} }
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) < if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) { renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
@ -442,7 +458,7 @@ void HomeActivity::render() {
const int boxWidth = maxTextWidth + boxPadding * 2; const int boxWidth = maxTextWidth + boxPadding * 2;
const int boxHeight = totalTextHeight + boxPadding * 2; const int boxHeight = totalTextHeight + boxPadding * 2;
const int boxX = (pageWidth - boxWidth) / 2; const int boxX = bookX + (bookWidth - boxWidth) / 2;
const int boxY = titleYStart - boxPadding; const int boxY = titleYStart - boxPadding;
// Draw box (inverted when selected: black box instead of white) // Draw box (inverted when selected: black box instead of white)
@ -452,7 +468,8 @@ void HomeActivity::render() {
} }
for (const auto& line : lines) { for (const auto& line : lines) {
renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); renderer.drawText(UI_12_FONT_ID, bookX + (bookWidth - renderer.getTextWidth(UI_12_FONT_ID, line.c_str())) / 2,
titleYStart, line.c_str(), !bookSelected);
titleYStart += renderer.getLineHeight(UI_12_FONT_ID); titleYStart += renderer.getLineHeight(UI_12_FONT_ID);
} }
@ -462,37 +479,38 @@ void HomeActivity::render() {
// Trim author if too long (UTF-8 safe) // Trim author if too long (UTF-8 safe)
bool wasTrimmed = false; bool wasTrimmed = false;
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor); utf8RemoveLastChar(trimmedAuthor);
wasTrimmed = true; wasTrimmed = true;
} }
if (wasTrimmed && !trimmedAuthor.empty()) { if (wasTrimmed && !trimmedAuthor.empty()) {
// Make room for ellipsis // Make room for ellipsis
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth && while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
!trimmedAuthor.empty()) { !trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor); utf8RemoveLastChar(trimmedAuthor);
} }
trimmedAuthor.append("..."); trimmedAuthor.append("...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected); renderer.drawText(UI_10_FONT_ID,
bookX + (bookWidth - renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str())) / 2,
titleYStart, trimmedAuthor.c_str(), !bookSelected);
} }
// "Continue Reading" label at the bottom // "Continue Reading" label at the bottom
const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
const char* continueText = "Continue Reading";
const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText);
if (coverRendered) { if (coverRendered) {
// Draw box behind "Continue Reading" text (inverted when selected: black box instead of white) // Draw box behind "Continue Reading" text (inverted when selected: black box instead of white)
const char* continueText = "Continue Reading";
const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText);
constexpr int continuePadding = 6; constexpr int continuePadding = 6;
const int continueBoxWidth = continueTextWidth + continuePadding * 2; const int continueBoxWidth = continueTextWidth + continuePadding * 2;
const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding; const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding;
const int continueBoxX = (pageWidth - continueBoxWidth) / 2; const int continueBoxX = bookX + (bookWidth - continueBoxWidth) / 2;
const int continueBoxY = continueY - continuePadding / 2; const int continueBoxY = continueY - continuePadding / 2;
renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected); renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected);
renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected); renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected);
renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected);
} else {
renderer.drawCenteredText(UI_10_FONT_ID, continueY, "Continue Reading", !bookSelected);
} }
renderer.drawText(UI_10_FONT_ID, bookX + (bookWidth - continueTextWidth) / 2, continueY, continueText,
!bookSelected);
} else { } else {
// No book to continue reading // No book to continue reading
const int y = const int y =
@ -501,30 +519,11 @@ void HomeActivity::render() {
renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below");
} }
// --- Bottom menu tiles --- int menuStartY = bookY + bookHeight + menuSpacing;
// Build menu items dynamically
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"};
if (hasOpdsUrl) {
// Insert OPDS Browser after My Library
menuItems.insert(menuItems.begin() + 1, "OPDS Browser");
}
const int menuTileWidth = pageWidth - 2 * margin;
constexpr int menuTileHeight = 45;
constexpr int menuSpacing = 8;
const int totalMenuHeight =
static_cast<int>(menuItems.size()) * menuTileHeight + (static_cast<int>(menuItems.size()) - 1) * menuSpacing;
int menuStartY = bookY + bookHeight + 15;
// Ensure we don't collide with the bottom button legend
const int maxMenuStartY = pageHeight - bottomMargin - totalMenuHeight - margin;
if (menuStartY > maxMenuStartY) {
menuStartY = maxMenuStartY;
}
for (size_t i = 0; i < menuItems.size(); ++i) { for (size_t i = 0; i < menuItems.size(); ++i) {
const int overallIndex = static_cast<int>(i) + (hasContinueReading ? 1 : 0); const int overallIndex = static_cast<int>(i) + (hasContinueReading ? 1 : 0);
constexpr int tileX = margin; const int tileX = marginLeft;
const int tileY = menuStartY + static_cast<int>(i) * (menuTileHeight + menuSpacing); const int tileY = menuStartY + static_cast<int>(i) * (menuTileHeight + menuSpacing);
const bool selected = selectorIndex == overallIndex; const bool selected = selectorIndex == overallIndex;
@ -552,7 +551,7 @@ void HomeActivity::render() {
// get percentage so we can align text properly // get percentage so we can align text properly
const uint16_t percentage = battery.readPercentage(); const uint16_t percentage = battery.readPercentage();
const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : ""; const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : "";
const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); const auto batteryX = pageWidth - 30 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
ScreenComponents::drawBattery(renderer, batteryX, 10, showBatteryPercentage); ScreenComponents::drawBattery(renderer, batteryX, 10, showBatteryPercentage);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -13,12 +13,8 @@
namespace { namespace {
// Layout constants // Layout constants
constexpr int TAB_BAR_Y = 15;
constexpr int CONTENT_START_Y = 60;
constexpr int LINE_HEIGHT = 30;
constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items
constexpr int LEFT_MARGIN = 20; constexpr int scrollIndicatorWidth = 20;
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
// Timing thresholds // Timing thresholds
constexpr int SKIP_PAGE_MS = 700; constexpr int SKIP_PAGE_MS = 700;
@ -35,17 +31,6 @@ void sortFileList(std::vector<std::string>& strs) {
} }
} // namespace } // namespace
int MyLibraryActivity::getPageItems() const {
const int screenHeight = renderer.getScreenHeight();
const int bottomBarHeight = 60; // Space for button hints
const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight;
int items = availableHeight / LINE_HEIGHT;
if (items < 1) {
items = 1;
}
return items;
}
int MyLibraryActivity::getCurrentItemCount() const { int MyLibraryActivity::getCurrentItemCount() const {
if (currentTab == Tab::Recent) { if (currentTab == Tab::Recent) {
return static_cast<int>(recentBooks.size()); return static_cast<int>(recentBooks.size());
@ -286,7 +271,7 @@ void MyLibraryActivity::render() const {
// Draw tab bar // Draw tab bar
std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}};
ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); ScreenComponents::drawTabBar(renderer, marginTop, tabs);
// Draw content based on current tab // Draw content based on current tab
if (currentTab == Tab::Recent) { if (currentTab == Tab::Recent) {
@ -297,8 +282,8 @@ void MyLibraryActivity::render() const {
// Draw scroll indicator // Draw scroll indicator
const int screenHeight = renderer.getScreenHeight(); const int screenHeight = renderer.getScreenHeight();
const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar const int contentHeight = screenHeight - contentStartY - marginBottom;
ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight); ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), contentStartY, contentHeight);
// Draw side button hints (up/down navigation on right side) // Draw side button hints (up/down navigation on right side)
// Note: text is rotated 90° CW, so ">" appears as "^" and "<" appears as "v" // Note: text is rotated 90° CW, so ">" appears as "^" and "<" appears as "v"
@ -317,20 +302,20 @@ void MyLibraryActivity::renderRecentTab() const {
const int bookCount = static_cast<int>(recentBooks.size()); const int bookCount = static_cast<int>(recentBooks.size());
if (bookCount == 0) { if (bookCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); renderer.drawText(UI_10_FONT_ID, marginLeft, contentStartY, "No recent books");
return; return;
} }
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2, renderer.fillRect(0, contentStartY + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN, RECENTS_LINE_HEIGHT); pageWidth - marginRight - scrollIndicatorWidth, RECENTS_LINE_HEIGHT);
// Draw items // Draw items
for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) {
const auto& book = recentBooks[i]; const auto& book = recentBooks[i];
const int y = CONTENT_START_Y + (i % pageItems) * RECENTS_LINE_HEIGHT; const int y = contentStartY + (i % pageItems) * RECENTS_LINE_HEIGHT;
// Line 1: Title // Line 1: Title
std::string title = book.title; std::string title = book.title;
@ -346,14 +331,15 @@ void MyLibraryActivity::renderRecentTab() const {
title.resize(dot); title.resize(dot);
} }
} }
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(),
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedTitle.c_str(), i != selectorIndex); pageWidth - marginLeft - marginRight - scrollIndicatorWidth);
renderer.drawText(UI_12_FONT_ID, marginLeft, y + 2, truncatedTitle.c_str(), i != selectorIndex);
// Line 2: Author // Line 2: Author
if (!book.author.empty()) { if (!book.author.empty()) {
auto truncatedAuthor = auto truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(),
renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); pageWidth - marginLeft - marginRight - scrollIndicatorWidth);
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), i != selectorIndex); renderer.drawText(UI_10_FONT_ID, marginLeft, y + 32, truncatedAuthor.c_str(), i != selectorIndex);
} }
} }
} }
@ -364,20 +350,21 @@ void MyLibraryActivity::renderFilesTab() const {
const int fileCount = static_cast<int>(files.size()); const int fileCount = static_cast<int>(files.size());
if (fileCount == 0) { if (fileCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); renderer.drawText(UI_10_FONT_ID, marginLeft, contentStartY, "No books found");
return; return;
} }
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, renderer.fillRect(0, contentStartY + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
LINE_HEIGHT); pageWidth - marginRight - scrollIndicatorWidth, LINE_HEIGHT);
// Draw items // Draw items
for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) { for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) {
auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(),
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), pageWidth - marginLeft - marginRight - scrollIndicatorWidth);
renderer.drawText(UI_10_FONT_ID, marginLeft, contentStartY + (i % pageItems) * LINE_HEIGHT, item.c_str(),
i != selectorIndex); i != selectorIndex);
} }
} }

View File

@ -34,7 +34,6 @@ class MyLibraryActivity final : public Activity {
const std::function<void(const std::string& path, Tab fromTab)> onSelectBook; const std::function<void(const std::string& path, Tab fromTab)> onSelectBook;
// Number of items that fit on a page // Number of items that fit on a page
int getPageItems() const;
int getCurrentItemCount() const; int getCurrentItemCount() const;
int getTotalPages() const; int getTotalPages() const;
int getCurrentPage() const; int getCurrentPage() const;

View File

@ -417,12 +417,13 @@ void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std
void CrossPointWebServerActivity::renderServerRunning() const { void CrossPointWebServerActivity::renderServerRunning() const {
// Use consistent line spacing // Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines constexpr int LINE_SPACING = 28; // Space between lines
const int screenWidth = renderer.getScreenWidth();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "File Transfer", true, EpdFontFamily::BOLD);
if (isApMode) { if (isApMode) {
// AP mode display - center the content block // AP mode display - center the content block
int startY = 55; int startY = marginTop + 35;
renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD);
@ -435,7 +436,7 @@ void CrossPointWebServerActivity::renderServerRunning() const {
"or scan QR code with your phone to connect to Wifi."); "or scan QR code with your phone to connect to Wifi.");
// Show QR code for URL // Show QR code for URL
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); drawQRCode(renderer, (screenWidth - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
startY += 6 * 29 + 3 * LINE_SPACING; startY += 6 * 29 + 3 * LINE_SPACING;
// Show primary URL (hostname) // Show primary URL (hostname)
@ -449,10 +450,10 @@ void CrossPointWebServerActivity::renderServerRunning() const {
// Show QR code for URL // Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:");
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); drawQRCode(renderer, (screenWidth - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl);
} else { } else {
// STA mode display (original behavior) // STA mode display (original behavior)
const int startY = 65; const int startY = marginTop + 50;
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
@ -474,7 +475,7 @@ void CrossPointWebServerActivity::renderServerRunning() const {
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser");
// Show QR code for URL // Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo); drawQRCode(renderer, (screenWidth - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:");
} }

View File

@ -105,10 +105,10 @@ void NetworkModeSelectionActivity::render() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "File Transfer", true, EpdFontFamily::BOLD);
// Draw subtitle // Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 35, "How would you like to connect?");
// Draw menu items centered on screen // Draw menu items centered on screen
constexpr int itemHeight = 50; // Height for each menu item (including description) constexpr int itemHeight = 50; // Height for each menu item (including description)
@ -120,13 +120,13 @@ void NetworkModeSelectionActivity::render() const {
// Draw selection highlight (black fill) for selected item // Draw selection highlight (black fill) for selected item
if (isSelected) { if (isSelected) {
renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6); renderer.fillRect(marginLeft, itemY - 2, pageWidth - marginLeft - marginRight, itemHeight - 6);
} }
// Draw text: black=false (white text) when selected (on black background) // Draw text: black=false (white text) when selected (on black background)
// black=true (black text) when not selected (on white background) // black=true (black text) when not selected (on white background)
renderer.drawText(UI_10_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected); renderer.drawText(UI_10_FONT_ID, marginLeft + 10, itemY, MENU_ITEMS[i], /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected); renderer.drawText(SMALL_FONT_ID, marginLeft + 10, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected);
} }
// Draw help text at bottom // Draw help text at bottom

View File

@ -266,9 +266,9 @@ void WifiSelectionActivity::checkConnectionStatus() {
} }
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) {
connectionError = "Connection failed"; connectionError = "Error: General failure";
if (status == WL_NO_SSID_AVAIL) { if (status == WL_NO_SSID_AVAIL) {
connectionError = "Network not found"; connectionError = "Error: Network not found";
} }
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; updateRequired = true;
@ -278,7 +278,7 @@ void WifiSelectionActivity::checkConnectionStatus() {
// Check for timeout // Check for timeout
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
WiFi.disconnect(); WiFi.disconnect();
connectionError = "Connection timeout"; connectionError = "Error: Connection timeout";
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; updateRequired = true;
return; return;
@ -513,7 +513,7 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "WiFi Networks", true, EpdFontFamily::BOLD);
if (networks.empty()) { if (networks.empty()) {
// No networks found or scan failed // No networks found or scan failed
@ -523,8 +523,8 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again"); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
} else { } else {
// Calculate how many networks we can display // Calculate how many networks we can display
constexpr int startY = 60; const int startY = contentStartY;
constexpr int lineHeight = 25; constexpr int lineHeight = 25; // tighter spacing than normal menus
const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight; const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight;
// Calculate scroll offset to keep selected item visible // Calculate scroll offset to keep selected item visible
@ -541,7 +541,7 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw selection indicator // Draw selection indicator
if (static_cast<int>(i) == selectedNetworkIndex) { if (static_cast<int>(i) == selectedNetworkIndex) {
renderer.drawText(UI_10_FONT_ID, 5, networkY, ">"); renderer.drawText(UI_10_FONT_ID, marginLeft - 15, networkY, ">");
} }
// Draw network name (truncate if too long) // Draw network name (truncate if too long)
@ -549,42 +549,47 @@ void WifiSelectionActivity::renderNetworkList() const {
if (displayName.length() > 16) { if (displayName.length() > 16) {
displayName.replace(13, displayName.length() - 13, "..."); displayName.replace(13, displayName.length() - 13, "...");
} }
renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str()); renderer.drawText(UI_10_FONT_ID, marginLeft, networkY, displayName.c_str());
// Draw signal strength indicator // Draw signal strength indicator
std::string signalStr = getSignalStrengthIndicator(network.rssi); std::string signalStr = getSignalStrengthIndicator(network.rssi);
renderer.drawText(UI_10_FONT_ID, pageWidth - 90, networkY, signalStr.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - marginRight - 90, networkY, signalStr.c_str());
// Draw saved indicator (checkmark) for networks with saved passwords // Draw saved indicator (checkmark) for networks with saved passwords
if (network.hasSavedPassword) { if (network.hasSavedPassword) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 50, networkY, "+"); renderer.drawText(UI_10_FONT_ID, pageWidth - marginRight - 50, networkY, "+");
} }
// Draw lock icon for encrypted networks // Draw lock icon for encrypted networks
if (network.isEncrypted) { if (network.isEncrypted) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 30, networkY, "*"); renderer.drawText(UI_10_FONT_ID, pageWidth - marginRight - 30, networkY, "*");
} }
} }
// Draw scroll indicators if needed // Draw scroll indicators if needed
if (scrollOffset > 0) { if (scrollOffset > 0) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^"); renderer.drawText(SMALL_FONT_ID, pageWidth - marginRight - 15, startY - 10, "^");
} }
if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) { if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v"); renderer.drawText(SMALL_FONT_ID, pageWidth - marginRight - 15, startY + maxVisibleNetworks * lineHeight, "v");
} }
// Show network count // Show network count
char countStr[32]; char countStr[32];
snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size()); snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size());
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); renderer.drawText(SMALL_FONT_ID, pageWidth - marginRight - renderer.getTextWidth(SMALL_FONT_ID, countStr) - 95,
pageHeight - 90, countStr);
} }
// Show MAC address above the network count and legend // Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str()); renderer.drawText(SMALL_FONT_ID,
pageWidth - marginRight - renderer.getTextWidth(SMALL_FONT_ID, cachedMacAddress.c_str()) - 95,
pageHeight - 105, cachedMacAddress.c_str());
// Draw help text // Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); const char helpLabel[] = "* = Encrypted | + = Saved";
renderer.drawText(SMALL_FONT_ID, pageWidth - marginRight - renderer.getTextWidth(SMALL_FONT_ID, helpLabel) - 95,
pageHeight - 75, helpLabel);
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@ -689,7 +694,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network?", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = "Network: " + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
@ -697,7 +702,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
} }
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Remove saved password?"); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Forget network and remove saved password?");
// Draw Cancel/Forget network buttons // Draw Cancel/Forget network buttons
const int buttonY = top + 80; const int buttonY = top + 80;

View File

@ -34,24 +34,6 @@ void EpubReaderActivity::onEnter() {
return; return;
} }
// Configure screen orientation based on settings
switch (SETTINGS.orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
break;
case CrossPointSettings::ORIENTATION::INVERTED:
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
break;
default:
break;
}
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
epub->setupCacheDir(); epub->setupCacheDir();
@ -101,9 +83,6 @@ void EpubReaderActivity::onEnter() {
void EpubReaderActivity::onExit() { void EpubReaderActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
// Reset orientation back to portrait for the rest of the UI
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -130,31 +109,9 @@ void EpubReaderActivity::loop() {
const int currentPage = section ? section->currentPage : 0; const int currentPage = section ? section->currentPage : 0;
const int totalPages = section ? section->pageCount : 0; const int totalPages = section ? section->pageCount : 0;
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderChapterSelectionActivity( enterNewActivity(new EpubReaderMenuActivity(
this->renderer, this->mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, this->renderer, this->mappedInput, epub->getTitle(), [this]() { onReaderMenuBack(); },
[this] { [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
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) {
// Handle sync position
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
currentSpineIndex = newSpineIndex;
nextPageNumber = newPage;
section.reset();
}
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
@ -242,6 +199,89 @@ void EpubReaderActivity::loop() {
} }
} }
void EpubReaderActivity::onReaderMenuBack() {
exitActivity();
updateRequired = true;
}
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
switch (action) {
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;
}
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
// 2. Trigger the reader's "Go Home" callback
if (onGoHome) {
onGoHome();
}
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);
}
exitActivity();
updateRequired = true;
xSemaphoreGive(renderingMutex);
if (onGoHome) onGoHome();
break;
}
}
}
void EpubReaderActivity::displayTaskLoop() { void EpubReaderActivity::displayTaskLoop() {
while (true) { while (true) {
if (updateRequired) { if (updateRequired) {
@ -308,49 +348,11 @@ void EpubReaderActivity::renderScreen() {
viewportHeight, SETTINGS.hyphenationEnabled)) { viewportHeight, SETTINGS.hyphenationEnabled)) {
Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis());
// Progress bar dimensions const auto popupFn = [this]() { ScreenComponents::drawPopup(renderer, "Indexing..."); };
constexpr int barWidth = 200;
constexpr int barHeight = 10;
constexpr int boxMargin = 20;
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing...");
const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2;
const int boxWidthNoBar = textWidth + boxMargin * 2;
const int boxHeightWithBar = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3;
const int boxHeightNoBar = renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2;
const int boxXWithBar = (renderer.getScreenWidth() - boxWidthWithBar) / 2;
const int boxXNoBar = (renderer.getScreenWidth() - boxWidthNoBar) / 2;
constexpr int boxY = 50;
const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2;
const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2;
// Always show "Indexing..." text first
{
renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false);
renderer.drawText(UI_12_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10);
renderer.displayBuffer();
pagesUntilFullRefresh = 0;
}
// Setup callback - only called for chapters >= 50KB, redraws with progress bar
auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] {
renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false);
renderer.drawText(UI_12_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10);
renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer();
};
// Progress callback to update progress bar
auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) {
const int fillWidth = (barWidth - 2) * progress / 100;
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
};
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) { viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
section.reset(); section.reset();
return; return;
@ -407,21 +409,26 @@ void EpubReaderActivity::renderScreen() {
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
} }
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
}
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
FsFile f; FsFile f;
if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
uint8_t data[6]; uint8_t data[6];
data[0] = currentSpineIndex & 0xFF; data[0] = currentSpineIndex & 0xFF;
data[1] = (currentSpineIndex >> 8) & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF;
data[2] = section->currentPage & 0xFF; data[2] = currentPage & 0xFF;
data[3] = (section->currentPage >> 8) & 0xFF; data[3] = (currentPage >> 8) & 0xFF;
data[4] = section->pageCount & 0xFF; data[4] = pageCount & 0xFF;
data[5] = (section->pageCount >> 8) & 0xFF; data[5] = (pageCount >> 8) & 0xFF;
f.write(data, 6); f.write(data, 6);
f.close(); f.close();
Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage);
} else {
Serial.printf("[ERS] Could not save progress!\n");
} }
} }
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop, void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) { const int orientedMarginLeft) {
@ -542,8 +549,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight; availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
titleMarginLeftAdjusted = titleMarginLeft; titleMarginLeftAdjusted = titleMarginLeft;
} }
while (titleWidth > availableTitleSpace && title.length() > 11) { if (titleWidth > availableTitleSpace) {
title.replace(title.length() - 8, 8, "..."); title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} }
} }

View File

@ -5,6 +5,7 @@
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include "EpubReaderMenuActivity.h"
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
class EpubReaderActivity final : public ActivityWithSubactivity { class EpubReaderActivity final : public ActivityWithSubactivity {
@ -27,6 +28,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight, void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft); int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
void saveProgress(int spineIndex, int currentPage, int pageCount);
void onReaderMenuBack();
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
public: public:
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub, explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,

View File

@ -32,24 +32,6 @@ int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) con
return itemIndex - offset; return itemIndex - offset;
} }
int EpubReaderChapterSelectionActivity::getPageItems() const {
// Layout constants used in renderScreen
constexpr int startY = 60;
constexpr int lineHeight = 30;
const int screenHeight = renderer.getScreenHeight();
const int endY = screenHeight - lineHeight;
const int availableHeight = endY - startY;
int items = availableHeight / lineHeight;
// Ensure we always have at least one item per page to avoid division by zero
if (items < 1) {
items = 1;
}
return items;
}
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param); auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
@ -181,26 +163,24 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
const int totalItems = getTotalItems(); const int totalItems = getTotalItems();
const std::string title = renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Chapter", true, EpdFontFamily::BOLD);
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
for (int i = 0; i < pageItems; i++) { for (int i = 0; i < pageItems; i++) {
int itemIndex = pageStartIndex + i; int itemIndex = pageStartIndex + i;
if (itemIndex >= totalItems) break; if (itemIndex >= totalItems) break;
const int displayY = 60 + i * 30; const int displayY = contentStartY + (itemIndex % pageItems) * LINE_HEIGHT;
const bool isSelected = (itemIndex == selectorIndex); const bool isSelected = (itemIndex == selectorIndex);
if (isSyncItem(itemIndex)) { if (isSyncItem(itemIndex)) {
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected); renderer.drawText(UI_10_FONT_ID, marginLeft, displayY, ">> Sync Progress", !isSelected);
} else { } else {
const int tocIndex = tocIndexFromItemIndex(itemIndex); const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex); auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + (item.level - 1) * 15; const int indentSize = marginLeft + (item.level - 1) * 15;
const std::string chapterName = const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize); renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);

View File

@ -22,10 +22,6 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
const std::function<void(int newSpineIndex)> onSelectSpineIndex; const std::function<void(int newSpineIndex)> onSelectSpineIndex;
const std::function<void(int newSpineIndex, int newPage)> onSyncPosition; const std::function<void(int newSpineIndex, int newPage)> onSyncPosition;
// Number of items that fit on a page, derived from logical screen height.
// This adapts automatically when switching between portrait and landscape.
int getPageItems() const;
// Total items including sync options (top and bottom) // Total items including sync options (top and bottom)
int getTotalItems() const; int getTotalItems() const;

View File

@ -0,0 +1,103 @@
#include "EpubReaderMenuActivity.h"
#include <GfxRenderer.h>
#include "fontIds.h"
void EpubReaderMenuActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
updateRequired = true;
xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubMenuTask", 4096, this, 1, &displayTaskHandle);
}
void EpubReaderMenuActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderMenuActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderMenuActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderMenuActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderMenuActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
// Use local variables for items we need to check after potential deletion
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
mappedInput.wasReleased(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size();
updateRequired = true;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Down) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % menuItems.size();
updateRequired = true;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// 1. Capture the callback and action locally
auto actionCallback = onAction;
auto selectedAction = menuItems[selectedIndex].action;
// 2. Execute the callback
actionCallback(selectedAction);
// 3. CRITICAL: Return immediately. 'this' is likely deleted now.
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onBack();
return; // Also return here just in case
}
}
void EpubReaderMenuActivity::renderScreen() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
// Title
const std::string truncTitle =
renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - 40, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15, truncTitle.c_str(), true, EpdFontFamily::BOLD);
// Menu Items
constexpr int startY = 60;
constexpr int lineHeight = 30;
for (size_t i = 0; i < menuItems.size(); ++i) {
const int displayY = startY + (i * lineHeight);
const bool isSelected = (static_cast<int>(i) == selectedIndex);
if (isSelected) {
renderer.fillRect(0, displayY, pageWidth - 1, lineHeight, true);
}
renderer.drawText(UI_10_FONT_ID, 20, displayY, menuItems[i].label.c_str(), !isSelected);
}
// Footer / Hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@ -0,0 +1,51 @@
#pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#include "../ActivityWithSubactivity.h"
#include "MappedInputManager.h"
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
public:
enum class MenuAction { SELECT_CHAPTER, GO_HOME, DELETE_CACHE };
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
const std::function<void()>& onBack, const std::function<void(MenuAction)>& onAction)
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
title(title),
onBack(onBack),
onAction(onAction) {}
void onEnter() override;
void onExit() override;
void loop() override;
private:
struct MenuItem {
MenuAction action;
std::string label;
};
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"},
{MenuAction::GO_HOME, "Go Home"},
{MenuAction::DELETE_CACHE, "Delete Book Cache"}};
int selectedIndex = 0;
bool updateRequired = false;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::string title = "Reader Menu";
const std::function<void()> onBack;
const std::function<void(MenuAction)> onAction;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
};

View File

@ -259,11 +259,11 @@ void KOReaderSyncActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "KOReader Sync", true, EpdFontFamily::BOLD);
if (state == NO_CREDENTIALS) { if (state == NO_CREDENTIALS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No credentials configured", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "No credentials configured", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Set up KOReader account in Settings"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, "Set up KOReader account in Settings");
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels("Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -272,14 +272,14 @@ void KOReaderSyncActivity::render() {
} }
if (state == SYNCING || state == UPLOADING) { if (state == SYNCING || state == UPLOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, statusMessage.c_str(), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == SHOWING_RESULT) { if (state == SHOWING_RESULT) {
// Show comparison // Show comparison
renderer.drawCenteredText(UI_10_FONT_ID, 120, "Progress found!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 105, "Progress found!", true, EpdFontFamily::BOLD);
// Get chapter names from TOC // Get chapter names from TOC
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex); const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
@ -291,52 +291,52 @@ void KOReaderSyncActivity::render() {
: ("Section " + std::to_string(currentSpineIndex + 1)); : ("Section " + std::to_string(currentSpineIndex + 1));
// Remote progress - chapter and page // Remote progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 160, "Remote:", true); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 145, "Remote:", true);
char remoteChapterStr[128]; char remoteChapterStr[128];
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str()); snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 170, remoteChapterStr);
char remotePageStr[64]; char remotePageStr[64];
snprintf(remotePageStr, sizeof(remotePageStr), " Page %d, %.2f%% overall", remotePosition.pageNumber + 1, snprintf(remotePageStr, sizeof(remotePageStr), " Page %d, %.2f%% overall", remotePosition.pageNumber + 1,
remoteProgress.percentage * 100); remoteProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 195, remotePageStr);
if (!remoteProgress.device.empty()) { if (!remoteProgress.device.empty()) {
char deviceStr[64]; char deviceStr[64];
snprintf(deviceStr, sizeof(deviceStr), " From: %s", remoteProgress.device.c_str()); snprintf(deviceStr, sizeof(deviceStr), " From: %s", remoteProgress.device.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 220, deviceStr);
} }
// Local progress - chapter and page // Local progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 270, "Local:", true); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 255, "Local:", true);
char localChapterStr[128]; char localChapterStr[128];
snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str()); snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 280, localChapterStr);
char localPageStr[64]; char localPageStr[64];
snprintf(localPageStr, sizeof(localPageStr), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine, snprintf(localPageStr, sizeof(localPageStr), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine,
localProgress.percentage * 100); localProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 305, localPageStr);
// Options // Options
const int optionY = 350; const int optionY = marginTop + 335;
const int optionHeight = 30; const int optionHeight = 30;
// Apply option // Apply option
if (selectedOption == 0) { if (selectedOption == 0) {
renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight); renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight);
} }
renderer.drawText(UI_10_FONT_ID, 20, optionY, "Apply remote progress", selectedOption != 0); renderer.drawText(UI_10_FONT_ID, marginLeft, optionY, "Apply remote progress", selectedOption != 0);
// Upload option // Upload option
if (selectedOption == 1) { if (selectedOption == 1) {
renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight); renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight);
} }
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, "Upload local progress", selectedOption != 1); renderer.drawText(UI_10_FONT_ID, marginLeft, optionY + optionHeight, "Upload local progress", selectedOption != 1);
// Cancel option // Cancel option
if (selectedOption == 2) { if (selectedOption == 2) {
renderer.fillRect(0, optionY + optionHeight * 2 - 2, pageWidth - 1, optionHeight); renderer.fillRect(0, optionY + optionHeight * 2 - 2, pageWidth - 1, optionHeight);
} }
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight * 2, "Cancel", selectedOption != 2); renderer.drawText(UI_10_FONT_ID, marginLeft, optionY + optionHeight * 2, "Cancel", selectedOption != 2);
const auto labels = mappedInput.mapLabels("", "Select", "", ""); const auto labels = mappedInput.mapLabels("", "Select", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -345,8 +345,8 @@ void KOReaderSyncActivity::render() {
} }
if (state == NO_REMOTE_PROGRESS) { if (state == NO_REMOTE_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "No remote progress found", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, "Upload current position?");
const auto labels = mappedInput.mapLabels("Cancel", "Upload", "", ""); const auto labels = mappedInput.mapLabels("Cancel", "Upload", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -355,7 +355,7 @@ void KOReaderSyncActivity::render() {
} }
if (state == UPLOAD_COMPLETE) { if (state == UPLOAD_COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Progress uploaded!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, "Progress uploaded!", true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels("Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -364,8 +364,8 @@ void KOReaderSyncActivity::render() {
} }
if (state == SYNC_FAILED) { if (state == SYNC_FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Sync failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "Sync failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels("Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -35,24 +35,6 @@ void TxtReaderActivity::onEnter() {
return; return;
} }
// Configure screen orientation based on settings
switch (SETTINGS.orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CW:
renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise);
break;
case CrossPointSettings::ORIENTATION::INVERTED:
renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted);
break;
case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW:
renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise);
break;
default:
break;
}
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
txt->setupCacheDir(); txt->setupCacheDir();
@ -76,9 +58,6 @@ void TxtReaderActivity::onEnter() {
void TxtReaderActivity::onExit() { void TxtReaderActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
// Reset orientation back to portrait for the rest of the UI
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
// Wait until not rendering to delete task // Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -207,28 +186,10 @@ void TxtReaderActivity::buildPageIndex() {
size_t offset = 0; size_t offset = 0;
const size_t fileSize = txt->getFileSize(); const size_t fileSize = txt->getFileSize();
int lastProgressPercent = -1;
Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize); Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize);
// Progress bar dimensions (matching EpubReaderActivity style) ScreenComponents::drawPopup(renderer, "Indexing...");
constexpr int barWidth = 200;
constexpr int barHeight = 10;
constexpr int boxMargin = 20;
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing...");
const int boxWidth = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2;
const int boxHeight = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3;
const int boxX = (renderer.getScreenWidth() - boxWidth) / 2;
constexpr int boxY = 50;
const int barX = boxX + (boxWidth - barWidth) / 2;
const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2;
// Draw initial progress box
renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false);
renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Indexing...");
renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10);
renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer();
while (offset < fileSize) { while (offset < fileSize) {
std::vector<std::string> tempLines; std::vector<std::string> tempLines;
@ -248,17 +209,6 @@ void TxtReaderActivity::buildPageIndex() {
pageOffsets.push_back(offset); pageOffsets.push_back(offset);
} }
// Update progress bar every 10% (matching EpubReaderActivity logic)
int progressPercent = (offset * 100) / fileSize;
if (lastProgressPercent / 10 != progressPercent / 10) {
lastProgressPercent = progressPercent;
// Fill progress bar
const int fillWidth = (barWidth - 2) * progressPercent / 100;
renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
// Yield to other tasks periodically // Yield to other tasks periodically
if (pageOffsets.size() % 20 == 0) { if (pageOffsets.size() % 20 == 0) {
vTaskDelay(1); vTaskDelay(1);
@ -402,9 +352,6 @@ void TxtReaderActivity::renderScreen() {
// Initialize reader if not done // Initialize reader if not done
if (!initialized) { if (!initialized) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Indexing...", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
initializeReader(); initializeReader();
} }
@ -565,8 +512,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
std::string title = txt->getTitle(); std::string title = txt->getTitle();
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth && title.length() > 11) { if (titleWidth > availableTextWidth) {
title.replace(title.length() - 8, 8, "..."); title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} }

View File

@ -30,6 +30,7 @@ void XtcReaderActivity::taskTrampoline(void* param) {
void XtcReaderActivity::onEnter() { void XtcReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
if (!xtc) { if (!xtc) {
return; return;
@ -60,6 +61,7 @@ void XtcReaderActivity::onEnter() {
void XtcReaderActivity::onExit() { void XtcReaderActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
updateRendererOrientation();
// Wait until not rendering to delete task // Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -84,6 +86,7 @@ void XtcReaderActivity::loop() {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
updateRendererOrientation();
enterNewActivity(new XtcReaderChapterSelectionActivity( enterNewActivity(new XtcReaderChapterSelectionActivity(
this->renderer, this->mappedInput, xtc, currentPage, this->renderer, this->mappedInput, xtc, currentPage,
[this] { [this] {
@ -93,6 +96,7 @@ void XtcReaderActivity::loop() {
[this](const uint32_t newPage) { [this](const uint32_t newPage) {
currentPage = newPage; currentPage = newPage;
exitActivity(); exitActivity();
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);

View File

@ -9,21 +9,6 @@ namespace {
constexpr int SKIP_PAGE_MS = 700; constexpr int SKIP_PAGE_MS = 700;
} // namespace } // namespace
int XtcReaderChapterSelectionActivity::getPageItems() const {
constexpr int startY = 60;
constexpr int lineHeight = 30;
const int screenHeight = renderer.getScreenHeight();
const int endY = screenHeight - lineHeight;
const int availableHeight = endY - startY;
int items = availableHeight / lineHeight;
if (items < 1) {
items = 1;
}
return items;
}
int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) const { int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) const {
if (!xtc) { if (!xtc) {
return 0; return 0;
@ -132,21 +117,22 @@ void XtcReaderChapterSelectionActivity::renderScreen() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const int pageItems = getPageItems(); const int pageItems = getPageItems();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Select Chapter", true, EpdFontFamily::BOLD);
const auto& chapters = xtc->getChapters(); const auto& chapters = xtc->getChapters();
if (chapters.empty()) { if (chapters.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No chapters"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 105, "No chapters");
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) { for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
const auto& chapter = chapters[i]; const auto& chapter = chapters[i];
const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str(); const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str();
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex); renderer.drawText(UI_10_FONT_ID, marginLeft, contentStartY + (i % pageItems) * LINE_HEIGHT, title,
i != selectorIndex);
} }
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");

View File

@ -18,7 +18,6 @@ class XtcReaderChapterSelectionActivity final : public Activity {
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(uint32_t newPage)> onSelectPage; const std::function<void(uint32_t newPage)> onSelectPage;
int getPageItems() const;
int findChapterIndexForPage(uint32_t page) const; int findChapterIndexForPage(uint32_t page) const;
static void taskTrampoline(void* param); static void taskTrampoline(void* param);

View File

@ -80,7 +80,7 @@ void CalibreSettingsActivity::handleSelection() {
// OPDS Server URL // OPDS Server URL
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "OPDS Server URL", SETTINGS.opdsServerUrl, 10, renderer, mappedInput, "OPDS Server URL", SETTINGS.opdsServerUrl, 50,
127, // maxLength 127, // maxLength
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@ -98,7 +98,7 @@ void CalibreSettingsActivity::handleSelection() {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Username", SETTINGS.opdsUsername, 10, renderer, mappedInput, "Username", SETTINGS.opdsUsername, 50,
63, // maxLength 63, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
@ -116,7 +116,7 @@ void CalibreSettingsActivity::handleSelection() {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Password", SETTINGS.opdsPassword, 10, renderer, mappedInput, "Password", SETTINGS.opdsPassword, 50,
63, // maxLength 63, // maxLength
false, // not password mode false, // not password mode
[this](const std::string& password) { [this](const std::string& password) {
@ -153,20 +153,20 @@ void CalibreSettingsActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "OPDS Browser", true, EpdFontFamily::BOLD);
// Draw info text about Calibre // Draw info text about Calibre
renderer.drawCenteredText(UI_10_FONT_ID, 40, "For Calibre, add /opds to your URL"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 25, "For Calibre, add /opds to your URL");
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + 10 + selectedIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
// Draw menu items // Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) { for (int i = 0; i < MENU_ITEMS; i++) {
const int settingY = 70 + i * 30; const int settingY = contentStartY + 10 + i * LINE_HEIGHT;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); renderer.drawText(UI_10_FONT_ID, marginLeft, settingY, menuNames[i], !isSelected);
// Draw status for each setting // Draw status for each setting
const char* status = "[Not Set]"; const char* status = "[Not Set]";
@ -178,7 +178,7 @@ void CalibreSettingsActivity::render() {
status = (strlen(SETTINGS.opdsPassword) > 0) ? "[Set]" : "[Not Set]"; status = (strlen(SETTINGS.opdsPassword) > 0) ? "[Set]" : "[Not Set]";
} }
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - marginRight - width, settingY, status, !isSelected);
} }
// Draw button hints // Draw button hints

View File

@ -87,6 +87,9 @@ void CategorySettingsActivity::toggleCurrentSetting() {
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr); const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size()); SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
if (strcmp(setting.name, "Orientation") == 0) {
updateRendererOrientation();
}
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
const int8_t currentValue = SETTINGS.*(setting.valuePtr); const int8_t currentValue = SETTINGS.*(setting.valuePtr);
if (currentValue + setting.valueRange.step > setting.valueRange.max) { if (currentValue + setting.valueRange.step > setting.valueRange.max) {
@ -151,20 +154,22 @@ void CategorySettingsActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const int pageItems = getPageItems();
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, categoryName, true, EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + (selectedSettingIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - 1,
LINE_HEIGHT);
// Draw all settings // Draw all settings
for (int i = 0; i < settingsCount; i++) { const auto pageStartIndex = selectedSettingIndex / pageItems * pageItems;
const int settingY = 60 + i * 30; // 30 pixels between settings for (int i = pageStartIndex; i < settingsCount && i < pageStartIndex + pageItems; i++) {
const int settingY = contentStartY + (i % pageItems) * LINE_HEIGHT;
const bool isSelected = (i == selectedSettingIndex); const bool isSelected = (i == selectedSettingIndex);
// Draw setting name // Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); renderer.drawText(UI_10_FONT_ID, marginLeft, settingY, settingsList[i].name, !isSelected);
// Draw value based on setting type // Draw value based on setting type
std::string valueText; std::string valueText;
@ -179,12 +184,12 @@ void CategorySettingsActivity::render() const {
} }
if (!valueText.empty()) { if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - marginRight - width, settingY, valueText.c_str(), !isSelected);
} }
} }
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), renderer.drawText(SMALL_FONT_ID, pageWidth - marginRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION); marginTop + 5, CROSSPOINT_VERSION);
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -56,7 +56,7 @@ void ClearCacheActivity::render() {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Clear Cache", true, EpdFontFamily::BOLD);
if (state == WARNING) { if (state == WARNING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true);

View File

@ -123,17 +123,17 @@ void KOReaderAuthActivity::render() {
} }
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "KOReader Auth", true, EpdFontFamily::BOLD);
if (state == AUTHENTICATING) { if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, statusMessage.c_str(), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == SUCCESS) { if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "Success!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, "KOReader sync is ready to use");
const auto labels = mappedInput.mapLabels("Done", "", "", ""); const auto labels = mappedInput.mapLabels("Done", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -142,8 +142,8 @@ void KOReaderAuthActivity::render() {
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 265, "Authentication Failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 305, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels("Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -81,7 +81,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "KOReader Username", KOREADER_STORE.getUsername(), 10, renderer, mappedInput, "KOReader Username", KOREADER_STORE.getUsername(), 50,
64, // maxLength 64, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
@ -98,7 +98,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "KOReader Password", KOREADER_STORE.getPassword(), 10, renderer, mappedInput, "KOReader Password", KOREADER_STORE.getPassword(), 50,
64, // maxLength 64, // maxLength
false, // show characters false, // show characters
[this](const std::string& password) { [this](const std::string& password) {
@ -117,7 +117,7 @@ void KOReaderSettingsActivity::handleSelection() {
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Sync Server URL", prefillUrl, 10, renderer, mappedInput, "Sync Server URL", prefillUrl, 50,
128, // maxLength - URLs can be long 128, // maxLength - URLs can be long
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@ -175,17 +175,17 @@ void KOReaderSettingsActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "KOReader Sync", true, EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + selectedIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
// Draw menu items // Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) { for (int i = 0; i < MENU_ITEMS; i++) {
const int settingY = 60 + i * 30; const int settingY = contentStartY + i * LINE_HEIGHT;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); renderer.drawText(UI_10_FONT_ID, marginLeft, settingY, menuNames[i], !isSelected);
// Draw status for each item // Draw status for each item
const char* status = ""; const char* status = "";
@ -202,7 +202,7 @@ void KOReaderSettingsActivity::render() {
} }
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - marginRight - width, settingY, status, !isSelected);
} }
// Draw button hints // Draw button hints

View File

@ -128,18 +128,19 @@ void OtaUpdateActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Update", true, EpdFontFamily::BOLD);
if (state == CHECKING_FOR_UPDATE) { if (state == CHECKING_FOR_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, "Checking for update...", true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == WAITING_CONFIRMATION) { if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 185, "New update available!", true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 235, "Current Version: " CROSSPOINT_VERSION);
renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str()); renderer.drawText(UI_10_FONT_ID, marginLeft, marginTop + 255,
("New Version: " + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", ""); const auto labels = mappedInput.mapLabels("Cancel", "Update", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -148,33 +149,34 @@ void OtaUpdateActivity::render() {
} }
if (state == UPDATE_IN_PROGRESS) { if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 295, "Updating...", true, EpdFontFamily::BOLD);
renderer.drawRect(20, 350, pageWidth - 40, 50); renderer.drawRect(marginLeft, marginTop + 335, pageWidth - 40, 50);
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42); renderer.fillRect(marginLeft + 4, marginTop + 339,
renderer.drawCenteredText(UI_10_FONT_ID, 420, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 405,
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str()); (std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
renderer.drawCenteredText( renderer.drawCenteredText(
UI_10_FONT_ID, 440, UI_10_FONT_ID, marginTop + 425,
(std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str());
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == NO_UPDATE) { if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, "No update available", true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, "Update failed", true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FINISHED) { if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 285, "Update complete", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on"); renderer.drawCenteredText(UI_10_FONT_ID, marginTop + 335, "Press and hold power button to turn back on");
renderer.displayBuffer(); renderer.displayBuffer();
state = SHUTTING_DOWN; state = SHUTTING_DOWN;
return; return;

View File

@ -11,9 +11,11 @@
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace { namespace {
constexpr int displaySettingsCount = 6; constexpr int displaySettingsCount = 7;
const SettingInfo displaySettings[displaySettingsCount] = { const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
@ -24,7 +26,7 @@ const SettingInfo displaySettings[displaySettingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
constexpr int readerSettingsCount = 9; constexpr int readerSettingsCount = 8;
const SettingInfo readerSettings[readerSettingsCount] = { const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
@ -33,8 +35,6 @@ const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
{"Justify", "Left", "Center", "Right"}), {"Justify", "Left", "Center", "Right"}),
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
@ -157,6 +157,7 @@ void SettingsActivity::enterCategory(int categoryIndex) {
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList, enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
settingsCount, [this] { settingsCount, [this] {
exitActivity(); exitActivity();
updateMargins(); // we may have changed orientation
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
@ -178,25 +179,24 @@ void SettingsActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Settings", true, EpdFontFamily::BOLD);
// Draw selection // Draw selection
renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, contentStartY + selectedCategoryIndex * LINE_HEIGHT - 2, pageWidth - 1, LINE_HEIGHT);
// Draw all categories // Draw all categories
for (int i = 0; i < categoryCount; i++) { for (int i = 0; i < categoryCount; i++) {
const int categoryY = 60 + i * 30; // 30 pixels between categories const int categoryY = contentStartY + i * LINE_HEIGHT;
// Draw category name // Draw category name
renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], i != selectedCategoryIndex); renderer.drawText(UI_10_FONT_ID, marginLeft, categoryY, categoryNames[i], i != selectedCategoryIndex);
} }
// Draw version text above button hints // Draw version text in corner of header
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), renderer.drawText(SMALL_FONT_ID, pageWidth - marginRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION); marginTop + 5, CROSSPOINT_VERSION);
// Draw help text // Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");

View File

@ -294,10 +294,22 @@ void setup() {
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
KOREADER_STORE.loadFromFile(); KOREADER_STORE.loadFromFile();
if (gpio.isWakeupByPowerButton()) { switch (gpio.getWakeupReason()) {
// For normal wakeups, verify power button press duration case HalGPIO::WakeupReason::PowerButton:
Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); // For normal wakeups, verify power button press duration
verifyPowerButtonDuration(); Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis());
verifyPowerButtonDuration();
break;
case HalGPIO::WakeupReason::AfterUSBPower:
// If USB power caused a cold boot, go back to sleep
Serial.printf("[%lu] [ ] Wakeup reason: After USB Power\n", millis());
gpio.startDeepSleep();
break;
case HalGPIO::WakeupReason::AfterFlash:
// After flashing, just proceed to boot
case HalGPIO::WakeupReason::Other:
default:
break;
} }
// First serial output only here to avoid timing inconsistencies for power button press duration verification // First serial output only here to avoid timing inconsistencies for power button press duration verification
@ -317,7 +329,6 @@ void setup() {
// Clear app state to avoid getting into a boot loop if the epub doesn't load // Clear app state to avoid getting into a boot loop if the epub doesn't load
const auto path = APP_STATE.openEpubPath; const auto path = APP_STATE.openEpubPath;
APP_STATE.openEpubPath = ""; APP_STATE.openEpubPath = "";
APP_STATE.lastSleepImage = 0;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
onGoToReader(path, MyLibraryActivity::Tab::Recent); onGoToReader(path, MyLibraryActivity::Tab::Recent);
} }

View File

@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) {
return localFile.endsWith(localExtension); return localFile.endsWith(localExtension);
} }
size_t utf8RemoveLastChar(std::string& str) {
if (str.empty()) return 0;
size_t pos = str.size() - 1;
// Walk back to find the start of the last UTF-8 character
// UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF)
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
--pos;
}
str.resize(pos);
return pos;
}
// Truncate string by removing N UTF-8 characters from the end
void utf8TruncateChars(std::string& str, const size_t numChars) {
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
utf8RemoveLastChar(str);
}
}
} // namespace StringUtils } // namespace StringUtils

View File

@ -19,10 +19,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
bool checkFileExtension(const std::string& fileName, const char* extension); bool checkFileExtension(const std::string& fileName, const char* extension);
bool checkFileExtension(const String& fileName, const char* extension); bool checkFileExtension(const String& fileName, const char* extension);
// UTF-8 safe string truncation - removes one character from the end
// Returns the new size after removing one UTF-8 character
size_t utf8RemoveLastChar(std::string& str);
// Truncate string by removing N UTF-8 characters from the end
void utf8TruncateChars(std::string& str, size_t numChars);
} // namespace StringUtils } // namespace StringUtils