Compare commits

...

23 Commits

Author SHA1 Message Date
Luke Stein
52cc90eec0
Merge 4436e44666 into e5c0ddc9fa 2026-02-01 08:32:50 -05:00
Uri Tauber
4436e44666 feat: Debugging monitor script (#555)
## 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 08:32:47 -05:00
Arthur Tazhitdinov
4bb8a869e7 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 08:32:47 -05:00
Arthur Tazhitdinov
cc2f125809 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 08:32:47 -05:00
Dave Allie
6228a9f14c 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 08:32:47 -05:00
Gaspar Fabrega Ragni
81f4770618 fix: custom sleep not showing image at index 0 (#639)
## 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 08:32:47 -05:00
Thomas Foskolos
c79fcac3ad docs: Update USER_GUIDE.md (#625)
Add Greek as not supported language atm on the use guide
2026-02-01 08:32:47 -05:00
nscheung
1b11e91932 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 08:32:47 -05:00
Luke Stein
2add1e63c0 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 08:32:47 -05:00
Arthur Tazhitdinov
9870c662c7 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 08:32:47 -05:00
Arthur Tazhitdinov
28fdbe0938 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 08:32:47 -05:00
Jonas Diemer
1eeea5459d 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 08:32:47 -05: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
Luke Stein
0b9a2c77d7 Fix progress bar vertical alignment 2026-01-31 22:58:56 -05:00
copilot-swe-agent[bot]
547d1e4963 Fix text overlap with chapter progress bar by adjusting textY position
Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com>
2026-01-31 21:36:08 +00:00
copilot-swe-agent[bot]
7667935aed Remove accidentally added llvm.sh script
Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com>
2026-01-31 21:01:00 +00:00
copilot-swe-agent[bot]
8658786db5 Run clang-format on modified files
Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com>
2026-01-31 21:00:49 +00:00
Luke Stein
533a8f2eb4 Rename progress bar height variable and make 6 pixels 2026-01-31 15:52:07 -05:00
copilot-swe-agent[bot]
4d437032fb Add Chapter Progress Bar status bar option
Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com>
2026-01-31 20:39:38 +00:00
copilot-swe-agent[bot]
b000b42963 Initial plan 2026-01-31 20:34:52 +00:00
14 changed files with 343 additions and 58 deletions

View File

@ -95,6 +95,20 @@ Connect your Xteink X4 to your computer via USB-C and run the following command.
```sh
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

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,
const EpdFontFamily::Style style) const {
if (!text || maxWidth <= 0) return "";
std::string item = text;
int itemWidth = getTextWidth(fontId, item.c_str(), style);
while (itemWidth > maxWidth && item.length() > 8) {
item.replace(item.length() - 5, 5, "...");
itemWidth = getTextWidth(fontId, item.c_str(), style);
const char* ellipsis = "...";
int textWidth = getTextWidth(fontId, item.c_str(), style);
if (textWidth <= maxWidth) {
// 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

View File

@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
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
#include <cstdint>
#include <string>
#define REPLACEMENT_GLYPH 0xFFFD
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);

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

@ -31,6 +31,7 @@ class CrossPointSettings {
FULL = 2,
FULL_WITH_PROGRESS_BAR = 3,
ONLY_PROGRESS_BAR = 4,
CHAPTER_PROGRESS_BAR = 5,
STATUS_BAR_MODE_COUNT
};

View File

@ -80,9 +80,20 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
&vieweableMarginLeft);
const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight;
const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT;
const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - PROGRESS_BAR_HEIGHT;
const int barWidth = progressBarMaxWidth * bookProgress / 100;
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, PROGRESS_BAR_HEIGHT, true);
}
void ScreenComponents::drawChapterProgressBar(const GfxRenderer& renderer, const size_t chapterProgress) {
int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft;
renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom,
&vieweableMarginLeft);
const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight;
const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - PROGRESS_BAR_HEIGHT;
const int barWidth = progressBarMaxWidth * chapterProgress / 100;
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, PROGRESS_BAR_HEIGHT, true);
}
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) {

View File

@ -13,7 +13,14 @@ struct TabInfo {
class ScreenComponents {
public:
static const int BOOK_PROGRESS_BAR_HEIGHT = 4;
static const int PROGRESS_BAR_HEIGHT = 6;
struct PopupLayout {
int x;
int y;
int width;
int height;
};
struct PopupLayout {
int x;
@ -24,6 +31,11 @@ class ScreenComponents {
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress);
static void drawChapterProgressBar(const GfxRenderer& renderer, size_t chapterProgress);
static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message);
static void fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, int progress);
static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message);

View File

@ -4,6 +4,7 @@
#include <Epub.h>
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <Utf8.h>
#include <Xtc.h>
#include <cstring>
@ -366,7 +367,7 @@ void HomeActivity::render() {
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
lines.back().resize(lines.back().size() - 3); // Remove "..."
StringUtils::utf8RemoveLastChar(lines.back());
utf8RemoveLastChar(lines.back());
lines.back().append("...");
}
break;
@ -375,7 +376,7 @@ void HomeActivity::render() {
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
while (wordWidth > maxLineWidth && !i.empty()) {
// Word itself is too long, trim it (UTF-8 safe)
StringUtils::utf8RemoveLastChar(i);
utf8RemoveLastChar(i);
// Check if we have room for ellipsis
std::string withEllipsis = i + "...";
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
@ -428,7 +429,7 @@ void HomeActivity::render() {
if (!lastBookAuthor.empty()) {
std::string trimmedAuthor = lastBookAuthor;
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()) <
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
@ -462,14 +463,14 @@ void HomeActivity::render() {
// Trim author if too long (UTF-8 safe)
bool wasTrimmed = false;
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor);
utf8RemoveLastChar(trimmedAuthor);
wasTrimmed = true;
}
if (wasTrimmed && !trimmedAuthor.empty()) {
// Make room for ellipsis
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
!trimmedAuthor.empty()) {
StringUtils::utf8RemoveLastChar(trimmedAuthor);
utf8RemoveLastChar(trimmedAuthor);
}
trimmedAuthor.append("...");
}

View File

@ -351,9 +351,10 @@ void EpubReaderActivity::renderScreen() {
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
// Add additional margin for status bar if progress bar is shown
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
(showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
(showProgressBar ? (ScreenComponents::PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
}
if (!section) {
@ -493,16 +494,20 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
const int orientedMarginLeft) const {
// determine visible status bar elements
const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
const bool showBookProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
@ -515,7 +520,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
if (showProgressText || showProgressPercentage) {
if (showProgressText || showProgressPercentage || showBookPercentage) {
// Right aligned text for progress counter
char progressStr[32];
@ -523,6 +528,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
if (showProgressPercentage) {
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount,
bookProgress);
} else if (showBookPercentage) {
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
} else {
snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount);
}
@ -532,11 +539,18 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
progressStr);
}
if (showProgressBar) {
if (showBookProgressBar) {
// Draw progress bar at the very bottom of the screen, from edge to edge of viewable area
ScreenComponents::drawBookProgressBar(renderer, static_cast<size_t>(bookProgress));
}
if (showChapterProgressBar) {
// Draw chapter progress bar at the very bottom of the screen, from edge to edge of viewable area
const float chapterProgress =
(section->pageCount > 0) ? (static_cast<float>(section->currentPage + 1) / section->pageCount) * 100 : 0;
ScreenComponents::drawChapterProgressBar(renderer, static_cast<size_t>(chapterProgress));
}
if (showBattery) {
ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage);
}
@ -570,8 +584,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
titleMarginLeftAdjusted = titleMarginLeft;
}
while (titleWidth > availableTitleSpace && title.length() > 11) {
title.replace(title.length() - 8, 8, "...");
if (titleWidth > availableTitleSpace) {
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
}
}

View File

@ -172,9 +172,10 @@ void TxtReaderActivity::initializeReader() {
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
// Add additional margin for status bar if progress bar is shown
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
orientedMarginBottom += statusBarMargin - cachedScreenMargin +
(showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
(showProgressBar ? (ScreenComponents::PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
}
viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
@ -487,27 +488,34 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
const auto screenHeight = renderer.getScreenHeight();
// Adjust text position upward when progress bar is shown to avoid overlap
const auto textY = screenHeight - orientedMarginBottom - 4;
int progressTextWidth = 0;
const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0;
if (showProgressText || showProgressPercentage) {
if (showProgressText || showProgressPercentage || showBookPercentage) {
char progressStr[32];
if (showProgressPercentage) {
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage + 1, totalPages, progress);
} else if (showBookPercentage) {
snprintf(progressStr, sizeof(progressStr), "%.0f%%", progress);
} else {
snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage + 1, totalPages);
}
@ -522,6 +530,11 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
ScreenComponents::drawBookProgressBar(renderer, static_cast<size_t>(progress));
}
if (showChapterProgressBar) {
// For text mode, treat the entire book as one chapter, so chapter progress == book progress
ScreenComponents::drawChapterProgressBar(renderer, static_cast<size_t>(progress));
}
if (showBattery) {
ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY, showBatteryPercentage);
}
@ -533,8 +546,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
std::string title = txt->getTitle();
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
while (titleWidth > availableTextWidth && title.length() > 11) {
title.replace(title.length() - 8, 8, "...");
if (titleWidth > availableTextWidth) {
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
}

View File

@ -18,8 +18,9 @@ const SettingInfo displaySettings[displaySettingsCount] = {
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
{"None", "Contrast", "Inverted"}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar,
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum(
"Status Bar", &CrossPointSettings::statusBar,
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar", "Chapter Progress Bar"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};

View File

@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) {
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

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 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