mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-08 08:37:38 +03:00
Compare commits
1 Commits
a1843e1196
...
2750238fc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2750238fc4 |
14
README.md
14
README.md
@ -95,20 +95,6 @@ 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, it’s 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
|
||||||
|
|
||||||
|
|||||||
@ -428,7 +428,6 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
|
||||||
std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||||
|
|
||||||
bool Epub::generateThumbBmp(int height) const {
|
bool Epub::generateThumbBmp(int height) const {
|
||||||
|
|||||||
@ -47,7 +47,6 @@ class Epub {
|
|||||||
const std::string& getLanguage() const;
|
const std::string& getLanguage() const;
|
||||||
std::string getCoverBmpPath(bool cropped = false) const;
|
std::string getCoverBmpPath(bool cropped = false) const;
|
||||||
bool generateCoverBmp(bool cropped = false) const;
|
bool generateCoverBmp(bool cropped = false) const;
|
||||||
std::string getThumbBmpPath() const;
|
|
||||||
std::string getThumbBmpPath(int height) const;
|
std::string getThumbBmpPath(int height) const;
|
||||||
bool generateThumbBmp(int height) const;
|
bool generateThumbBmp(int height) const;
|
||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
|
|||||||
@ -247,10 +247,10 @@ static constexpr int matrixSize = 4;
|
|||||||
static constexpr int matrixLevels = matrixSize * matrixSize;
|
static constexpr int matrixLevels = matrixSize * matrixSize;
|
||||||
|
|
||||||
void GfxRenderer::drawPixelDither(const int x, const int y, Color color) const {
|
void GfxRenderer::drawPixelDither(const int x, const int y, Color color) const {
|
||||||
if (color == Color::Clear) {
|
if (color == COLOR_CLEAR) {
|
||||||
} else if (color == Color::Black) {
|
} else if (color == COLOR_BLACK) {
|
||||||
drawPixel(x, y, true);
|
drawPixel(x, y, true);
|
||||||
} else if (color == Color::White) {
|
} else if (color == COLOR_WHITE) {
|
||||||
drawPixel(x, y, false);
|
drawPixel(x, y, false);
|
||||||
} else {
|
} else {
|
||||||
// Use dithering
|
// Use dithering
|
||||||
@ -269,10 +269,10 @@ void GfxRenderer::drawPixelDither(const int x, const int y, Color color) const {
|
|||||||
|
|
||||||
// Use Bayer matrix 4x4 dithering to fill the rectangle with a grey level
|
// Use Bayer matrix 4x4 dithering to fill the rectangle with a grey level
|
||||||
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
||||||
if (color == Color::Clear) {
|
if (color == COLOR_CLEAR) {
|
||||||
} else if (color == Color::Black) {
|
} else if (color == COLOR_BLACK) {
|
||||||
fillRect(x, y, width, height, true);
|
fillRect(x, y, width, height, true);
|
||||||
} else if (color == Color::White) {
|
} else if (color == COLOR_WHITE) {
|
||||||
fillRect(x, y, width, height, false);
|
fillRect(x, y, width, height, false);
|
||||||
} else {
|
} else {
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
for (int fillY = y; fillY < y + height; fillY++) {
|
||||||
@ -628,23 +628,15 @@ 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;
|
||||||
const char* ellipsis = "...";
|
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||||
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
while (itemWidth > maxWidth && item.length() > 8) {
|
||||||
if (textWidth <= maxWidth) {
|
item.replace(item.length() - 5, 5, "...");
|
||||||
// Text fits, return as is
|
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
||||||
|
}
|
||||||
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
|
||||||
int GfxRenderer::getScreenWidth() const {
|
int GfxRenderer::getScreenWidth() const {
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
|
|||||||
@ -9,7 +9,13 @@
|
|||||||
|
|
||||||
// Color representation: uint8_t mapped to 4x4 Bayer matrix dithering levels
|
// Color representation: uint8_t mapped to 4x4 Bayer matrix dithering levels
|
||||||
// 0 = transparent, 1-16 = gray levels (white to black)
|
// 0 = transparent, 1-16 = gray levels (white to black)
|
||||||
enum Color : uint8_t { Clear = 0x00, White = 0x01, LightGray = 0x05, DarkGray = 0x0A, Black = 0x10 };
|
using Color = uint8_t;
|
||||||
|
|
||||||
|
constexpr Color COLOR_CLEAR = 0x00;
|
||||||
|
constexpr Color COLOR_WHITE = 0x01;
|
||||||
|
constexpr Color COLOR_LIGHT_GRAY = 0x05;
|
||||||
|
constexpr Color COLOR_DARK_GRAY = 0x0A;
|
||||||
|
constexpr Color COLOR_BLACK = 0x10;
|
||||||
|
|
||||||
class GfxRenderer {
|
class GfxRenderer {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@ -29,20 +29,3 @@ 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
#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);
|
|
||||||
|
|||||||
@ -301,7 +301,6 @@ bool Xtc::generateCoverBmp() const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
|
||||||
std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||||
|
|
||||||
bool Xtc::generateThumbBmp(int height) const {
|
bool Xtc::generateThumbBmp(int height) const {
|
||||||
|
|||||||
@ -64,7 +64,6 @@ class Xtc {
|
|||||||
std::string getCoverBmpPath() const;
|
std::string getCoverBmpPath() const;
|
||||||
bool generateCoverBmp() const;
|
bool generateCoverBmp() const;
|
||||||
// Thumbnail support (for Continue Reading card)
|
// Thumbnail support (for Continue Reading card)
|
||||||
std::string getThumbBmpPath() const;
|
|
||||||
std::string getThumbBmpPath(int height) const;
|
std::string getThumbBmpPath(int height) const;
|
||||||
bool generateThumbBmp(int height) const;
|
bool generateThumbBmp(int height) const;
|
||||||
|
|
||||||
|
|||||||
@ -1,214 +0,0 @@
|
|||||||
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()
|
|
||||||
@ -14,8 +14,7 @@ constexpr int MAX_RECENT_BOOKS = 10;
|
|||||||
|
|
||||||
RecentBooksStore RecentBooksStore::instance;
|
RecentBooksStore RecentBooksStore::instance;
|
||||||
|
|
||||||
void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author,
|
void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author) {
|
||||||
const std::string& coverBmpPath) {
|
|
||||||
// Remove existing entry if present
|
// Remove existing entry if present
|
||||||
auto it =
|
auto it =
|
||||||
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; });
|
||||||
@ -24,7 +23,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add to front
|
// Add to front
|
||||||
recentBooks.insert(recentBooks.begin(), {path, title, author, coverBmpPath});
|
recentBooks.insert(recentBooks.begin(), {path, title, author});
|
||||||
|
|
||||||
// Trim to max size
|
// Trim to max size
|
||||||
if (recentBooks.size() > MAX_RECENT_BOOKS) {
|
if (recentBooks.size() > MAX_RECENT_BOOKS) {
|
||||||
@ -51,7 +50,6 @@ bool RecentBooksStore::saveToFile() const {
|
|||||||
serialization::writeString(outputFile, book.path);
|
serialization::writeString(outputFile, book.path);
|
||||||
serialization::writeString(outputFile, book.title);
|
serialization::writeString(outputFile, book.title);
|
||||||
serialization::writeString(outputFile, book.author);
|
serialization::writeString(outputFile, book.author);
|
||||||
serialization::writeString(outputFile, book.coverBmpPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
@ -79,7 +77,7 @@ bool RecentBooksStore::loadFromFile() {
|
|||||||
serialization::readString(inputFile, path);
|
serialization::readString(inputFile, path);
|
||||||
// Title and author will be empty, they will be filled when the book is
|
// Title and author will be empty, they will be filled when the book is
|
||||||
// opened again
|
// opened again
|
||||||
recentBooks.push_back({path, "", "", ""});
|
recentBooks.push_back({path, "", ""});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version);
|
Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
@ -94,12 +92,11 @@ bool RecentBooksStore::loadFromFile() {
|
|||||||
recentBooks.reserve(count);
|
recentBooks.reserve(count);
|
||||||
|
|
||||||
for (uint8_t i = 0; i < count; i++) {
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
std::string path, title, author, coverBmpPath;
|
std::string path, title, author;
|
||||||
serialization::readString(inputFile, path);
|
serialization::readString(inputFile, path);
|
||||||
serialization::readString(inputFile, title);
|
serialization::readString(inputFile, title);
|
||||||
serialization::readString(inputFile, author);
|
serialization::readString(inputFile, author);
|
||||||
serialization::readString(inputFile, coverBmpPath);
|
recentBooks.push_back({path, title, author});
|
||||||
recentBooks.push_back({path, title, author, coverBmpPath});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,15 @@ struct RecentBook {
|
|||||||
std::string path;
|
std::string path;
|
||||||
std::string title;
|
std::string title;
|
||||||
std::string author;
|
std::string author;
|
||||||
std::string coverBmpPath;
|
|
||||||
|
|
||||||
bool operator==(const RecentBook& other) const { return path == other.path; }
|
bool operator==(const RecentBook& other) const { return path == other.path; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RecentBookWithCover {
|
||||||
|
RecentBook book;
|
||||||
|
std::string coverBmpPath;
|
||||||
|
};
|
||||||
|
|
||||||
class RecentBooksStore {
|
class RecentBooksStore {
|
||||||
// Static instance
|
// Static instance
|
||||||
static RecentBooksStore instance;
|
static RecentBooksStore instance;
|
||||||
@ -24,8 +28,7 @@ class RecentBooksStore {
|
|||||||
static RecentBooksStore& getInstance() { return instance; }
|
static RecentBooksStore& getInstance() { return instance; }
|
||||||
|
|
||||||
// Add a book to the recent list (moves to front if already exists)
|
// Add a book to the recent list (moves to front if already exists)
|
||||||
void addBook(const std::string& path, const std::string& title, const std::string& author,
|
void addBook(const std::string& path, const std::string& title, const std::string& author);
|
||||||
const std::string& coverBmpPath);
|
|
||||||
|
|
||||||
// Get the list of recent books (most recent first)
|
// Get the list of recent books (most recent first)
|
||||||
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
#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>
|
||||||
@ -35,10 +34,8 @@ int HomeActivity::getMenuItemCount() const {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) {
|
void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight, Rect& popupRect) {
|
||||||
recentsLoading = true;
|
recentsLoading = true;
|
||||||
bool showingLoading = false;
|
|
||||||
Rect popupRect;
|
|
||||||
|
|
||||||
recentBooks.clear();
|
recentBooks.clear();
|
||||||
const auto& books = RECENT_BOOKS.getBooks();
|
const auto& books = RECENT_BOOKS.getBooks();
|
||||||
@ -46,57 +43,57 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) {
|
|||||||
|
|
||||||
int progress = 0;
|
int progress = 0;
|
||||||
for (const RecentBook& book : books) {
|
for (const RecentBook& book : books) {
|
||||||
|
const std::string& path = book.path;
|
||||||
|
|
||||||
// Limit to maximum number of recent books
|
// Limit to maximum number of recent books
|
||||||
if (recentBooks.size() >= maxBooks) {
|
if (recentBooks.size() >= maxBooks) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if file no longer exists
|
// Skip if file no longer exists
|
||||||
if (!SdMan.exists(book.path.c_str())) {
|
if (!SdMan.exists(path.c_str())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!book.coverBmpPath.empty()) {
|
std::string coverBmpPath = "";
|
||||||
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
|
|
||||||
if (!SdMan.exists(coverPath.c_str())) {
|
|
||||||
std::string lastBookFileName = "";
|
std::string lastBookFileName = "";
|
||||||
const size_t lastSlash = book.path.find_last_of('/');
|
const size_t lastSlash = path.find_last_of('/');
|
||||||
if (lastSlash != std::string::npos) {
|
if (lastSlash != std::string::npos) {
|
||||||
lastBookFileName = book.path.substr(lastSlash + 1);
|
lastBookFileName = path.substr(lastSlash + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("Loading recent book: %s\n", book.path.c_str());
|
Serial.printf("Loading recent book: %s\n", path.c_str());
|
||||||
|
|
||||||
// If epub, try to load the metadata for title/author and cover
|
// If epub, try to load the metadata for title/author and cover
|
||||||
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
||||||
Epub epub(book.path, "/.crosspoint");
|
Epub epub(path, "/.crosspoint");
|
||||||
epub.load(false);
|
epub.load(false);
|
||||||
|
|
||||||
// Try to generate thumbnail image for Continue Reading card
|
// Try to generate thumbnail image for Continue Reading card
|
||||||
if (!showingLoading) {
|
coverBmpPath = epub.getThumbBmpPath(coverHeight);
|
||||||
showingLoading = true;
|
if (!SdMan.exists(coverBmpPath.c_str())) {
|
||||||
popupRect = UITheme::drawPopup(renderer, "Loading...");
|
|
||||||
}
|
|
||||||
UITheme::fillPopupProgress(renderer, popupRect, progress * 30);
|
UITheme::fillPopupProgress(renderer, popupRect, progress * 30);
|
||||||
epub.generateThumbBmp(coverHeight);
|
if (!epub.generateThumbBmp(coverHeight)) {
|
||||||
|
coverBmpPath = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") ||
|
} else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") ||
|
||||||
StringUtils::checkFileExtension(lastBookFileName, ".xtc")) {
|
StringUtils::checkFileExtension(lastBookFileName, ".xtc")) {
|
||||||
// Handle XTC file
|
// Handle XTC file
|
||||||
Xtc xtc(book.path, "/.crosspoint");
|
Xtc xtc(path, "/.crosspoint");
|
||||||
if (xtc.load()) {
|
if (xtc.load()) {
|
||||||
// Try to generate thumbnail image for Continue Reading card
|
// Try to generate thumbnail image for Continue Reading card
|
||||||
if (!showingLoading) {
|
coverBmpPath = xtc.getThumbBmpPath(coverHeight);
|
||||||
showingLoading = true;
|
if (!SdMan.exists(coverBmpPath.c_str())) {
|
||||||
popupRect = UITheme::drawPopup(renderer, "Loading...");
|
|
||||||
}
|
|
||||||
UITheme::fillPopupProgress(renderer, popupRect, progress * 30);
|
UITheme::fillPopupProgress(renderer, popupRect, progress * 30);
|
||||||
xtc.generateThumbBmp(coverHeight);
|
if (!xtc.generateThumbBmp(coverHeight)) {
|
||||||
|
coverBmpPath = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recentBooks.push_back(book);
|
recentBooks.push_back(RecentBookWithCover{book, coverBmpPath});
|
||||||
progress++;
|
progress++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +204,7 @@ void HomeActivity::loop() {
|
|||||||
const int settingsIdx = idx;
|
const int settingsIdx = idx;
|
||||||
|
|
||||||
if (selectorIndex < recentBooks.size()) {
|
if (selectorIndex < recentBooks.size()) {
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].book.path);
|
||||||
} else if (menuSelectedIndex == myLibraryIdx) {
|
} else if (menuSelectedIndex == myLibraryIdx) {
|
||||||
onMyLibraryOpen();
|
onMyLibraryOpen();
|
||||||
} else if (menuSelectedIndex == recentsIdx) {
|
} else if (menuSelectedIndex == recentsIdx) {
|
||||||
@ -260,7 +257,8 @@ void HomeActivity::render() {
|
|||||||
std::bind(&HomeActivity::storeCoverBuffer, this));
|
std::bind(&HomeActivity::storeCoverBuffer, this));
|
||||||
} else if (!recentsLoading && firstRenderDone) {
|
} else if (!recentsLoading && firstRenderDone) {
|
||||||
recentsLoading = true;
|
recentsLoading = true;
|
||||||
loadRecentBooks(metrics.homeRecentBooksCount, metrics.homeCoverHeight);
|
Rect popupRect = UITheme::drawPopup(renderer, "Loading...");
|
||||||
|
loadRecentBooks(metrics.homeRecentBooksCount, metrics.homeCoverHeight, popupRect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "./MyLibraryActivity.h"
|
#include "./MyLibraryActivity.h"
|
||||||
|
|
||||||
struct RecentBook;
|
struct RecentBookWithCover;
|
||||||
struct Rect;
|
struct Rect;
|
||||||
|
|
||||||
class HomeActivity final : public Activity {
|
class HomeActivity final : public Activity {
|
||||||
@ -26,7 +26,7 @@ class HomeActivity final : public Activity {
|
|||||||
bool coverRendered = false; // Track if cover has been rendered once
|
bool coverRendered = false; // Track if cover has been rendered once
|
||||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
||||||
std::vector<RecentBook> recentBooks;
|
std::vector<RecentBookWithCover> recentBooks;
|
||||||
const std::function<void(const std::string& path)> onSelectBook;
|
const std::function<void(const std::string& path)> onSelectBook;
|
||||||
const std::function<void()> onMyLibraryOpen;
|
const std::function<void()> onMyLibraryOpen;
|
||||||
const std::function<void()> onRecentsOpen;
|
const std::function<void()> onRecentsOpen;
|
||||||
@ -41,7 +41,7 @@ class HomeActivity final : public Activity {
|
|||||||
bool storeCoverBuffer(); // Store frame buffer for cover image
|
bool storeCoverBuffer(); // Store frame buffer for cover image
|
||||||
bool restoreCoverBuffer(); // Restore frame buffer from stored cover
|
bool restoreCoverBuffer(); // Restore frame buffer from stored cover
|
||||||
void freeCoverBuffer(); // Free the stored cover buffer
|
void freeCoverBuffer(); // Free the stored cover buffer
|
||||||
void loadRecentBooks(int maxBooks, int coverHeight);
|
void loadRecentBooks(int maxBooks, int coverHeight, Rect& popupRect);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
|||||||
@ -114,7 +114,7 @@ void MyLibraryActivity::loop() {
|
|||||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, true);
|
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, true, true);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
@ -201,7 +201,7 @@ void MyLibraryActivity::render() const {
|
|||||||
} else {
|
} else {
|
||||||
UITheme::drawList(
|
UITheme::drawList(
|
||||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
||||||
[this](int index) { return files[index]; }, false, nullptr, false, nullptr, false, nullptr);
|
[this](int index) { return files[index]; }, false, nullptr, false, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help text
|
// Help text
|
||||||
|
|||||||
@ -75,7 +75,7 @@ void RecentBooksActivity::loop() {
|
|||||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, true);
|
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, true, true);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
@ -137,8 +137,7 @@ void RecentBooksActivity::render() const {
|
|||||||
} else {
|
} else {
|
||||||
UITheme::drawList(
|
UITheme::drawList(
|
||||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
|
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
|
||||||
[this](int index) { return recentBooks[index].title; }, true,
|
[this](int index) { return recentBooks[index].title; }, false, nullptr, false, nullptr);
|
||||||
[this](int index) { return recentBooks[index].author; }, false, nullptr, false, nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help text
|
// Help text
|
||||||
|
|||||||
@ -85,7 +85,7 @@ void EpubReaderActivity::onEnter() {
|
|||||||
// Save current epub as last opened epub and add to recent books
|
// Save current epub as last opened epub and add to recent books
|
||||||
APP_STATE.openEpubPath = epub->getPath();
|
APP_STATE.openEpubPath = epub->getPath();
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
|
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor());
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@ -575,8 +575,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
|||||||
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
||||||
titleMarginLeftAdjusted = titleMarginLeft;
|
titleMarginLeftAdjusted = titleMarginLeft;
|
||||||
}
|
}
|
||||||
if (titleWidth > availableTitleSpace) {
|
while (titleWidth > availableTitleSpace && title.length() > 11) {
|
||||||
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
title.replace(title.length() - 8, 8, "...");
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,7 +60,7 @@ void TxtReaderActivity::onEnter() {
|
|||||||
// Save current txt as last opened file and add to recent books
|
// Save current txt as last opened file and add to recent books
|
||||||
APP_STATE.openEpubPath = txt->getPath();
|
APP_STATE.openEpubPath = txt->getPath();
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
RECENT_BOOKS.addBook(txt->getPath(), "", "", "");
|
RECENT_BOOKS.addBook(txt->getPath(), "", "");
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@ -537,8 +537,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());
|
||||||
if (titleWidth > availableTextWidth) {
|
while (titleWidth > availableTextWidth && title.length() > 11) {
|
||||||
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth);
|
title.replace(title.length() - 8, 8, "...");
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "XtcReaderChapterSelectionActivity.h"
|
#include "XtcReaderChapterSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -46,7 +45,7 @@ void XtcReaderActivity::onEnter() {
|
|||||||
// Save current XTC as last opened book and add to recent books
|
// Save current XTC as last opened book and add to recent books
|
||||||
APP_STATE.openEpubPath = xtc->getPath();
|
APP_STATE.openEpubPath = xtc->getPath();
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor(), xtc->getThumbBmpPath());
|
RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor());
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|||||||
@ -277,7 +277,7 @@ void SettingsActivity::render() const {
|
|||||||
pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.buttonHintsHeight +
|
pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.buttonHintsHeight +
|
||||||
metrics.verticalSpacing * 2)},
|
metrics.verticalSpacing * 2)},
|
||||||
settingsCount, selectedSettingIndex - 1, [this](int index) { return std::string(settingsList[index].name); },
|
settingsCount, selectedSettingIndex - 1, [this](int index) { return std::string(settingsList[index].name); },
|
||||||
false, nullptr, false, nullptr, true,
|
false, nullptr, true,
|
||||||
[this](int i) {
|
[this](int i) {
|
||||||
const auto& setting = settingsList[i];
|
const auto& setting = settingsList[i];
|
||||||
std::string valueText = "";
|
std::string valueText = "";
|
||||||
|
|||||||
@ -32,8 +32,7 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints) {
|
||||||
bool hasSubtitle) {
|
|
||||||
const ThemeMetrics& metrics = UITheme::getMetrics();
|
const ThemeMetrics& metrics = UITheme::getMetrics();
|
||||||
int reservedHeight = metrics.topPadding;
|
int reservedHeight = metrics.topPadding;
|
||||||
if (hasHeader) {
|
if (hasHeader) {
|
||||||
@ -46,16 +45,7 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader
|
|||||||
reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight;
|
reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight;
|
||||||
}
|
}
|
||||||
const int availableHeight = renderer.getScreenHeight() - reservedHeight;
|
const int availableHeight = renderer.getScreenHeight() - reservedHeight;
|
||||||
int rowHeight = hasSubtitle ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight;
|
return availableHeight / metrics.listRowHeight;
|
||||||
return availableHeight / rowHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) {
|
|
||||||
size_t pos = coverBmpPath.find("[HEIGHT]", 0);
|
|
||||||
if (pos != std::string::npos) {
|
|
||||||
coverBmpPath.replace(pos, 8, std::to_string(coverHeight));
|
|
||||||
}
|
|
||||||
return coverBmpPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward all component methods to the current theme
|
// Forward all component methods to the current theme
|
||||||
@ -85,13 +75,11 @@ void UITheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBt
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UITheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
void UITheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& rowTitle, bool hasSubtitle,
|
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowSubtitle, bool hasIcon,
|
|
||||||
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
||||||
const std::function<std::string(int index)>& rowValue) {
|
const std::function<std::string(int index)>& rowValue) {
|
||||||
if (currentTheme != nullptr) {
|
if (currentTheme != nullptr) {
|
||||||
currentTheme->drawList(renderer, rect, itemCount, selectedIndex, rowTitle, hasSubtitle, rowSubtitle, hasIcon,
|
currentTheme->drawList(renderer, rect, itemCount, selectedIndex, rowTitle, hasIcon, rowIcon, hasValue, rowValue);
|
||||||
rowIcon, hasValue, rowValue);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +96,7 @@ void UITheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
void UITheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBookWithCover>& recentBooks,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) {
|
bool& bufferRestored, std::function<bool()> storeCoverBuffer) {
|
||||||
if (currentTheme != nullptr) {
|
if (currentTheme != nullptr) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
struct RecentBook;
|
struct RecentBookWithCover;
|
||||||
|
|
||||||
struct Rect {
|
struct Rect {
|
||||||
int x;
|
int x;
|
||||||
@ -34,7 +34,6 @@ struct ThemeMetrics {
|
|||||||
|
|
||||||
int contentSidePadding;
|
int contentSidePadding;
|
||||||
int listRowHeight;
|
int listRowHeight;
|
||||||
int listWithSubtitleRowHeight;
|
|
||||||
int menuRowHeight;
|
int menuRowHeight;
|
||||||
int menuSpacing;
|
int menuSpacing;
|
||||||
|
|
||||||
@ -71,22 +70,20 @@ class UITheme {
|
|||||||
static void initialize();
|
static void initialize();
|
||||||
static void setTheme(CrossPointSettings::UI_THEME type);
|
static void setTheme(CrossPointSettings::UI_THEME type);
|
||||||
static const ThemeMetrics& getMetrics() { return *currentMetrics; }
|
static const ThemeMetrics& getMetrics() { return *currentMetrics; }
|
||||||
static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints);
|
||||||
bool hasSubtitle);
|
|
||||||
static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight);
|
|
||||||
static void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total);
|
static void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total);
|
||||||
static void drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage = true);
|
static void drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage = true);
|
||||||
static void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
static void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||||
const char* btn4);
|
const char* btn4);
|
||||||
static void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn);
|
static void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn);
|
||||||
static void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
static void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& rowTitle, bool hasSubtitle,
|
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowSubtitle, bool hasIcon,
|
|
||||||
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
||||||
const std::function<std::string(int index)>& rowValue);
|
const std::function<std::string(int index)>& rowValue);
|
||||||
static void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title);
|
static void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title);
|
||||||
static void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, bool selected);
|
static void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, bool selected);
|
||||||
static void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
static void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBookWithCover>& recentBooks,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer);
|
bool& bufferRestored, std::function<bool()> storeCoverBuffer);
|
||||||
static void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
static void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
#include <Utf8.h>
|
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -10,6 +9,7 @@
|
|||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
// Internal constants
|
// Internal constants
|
||||||
namespace {
|
namespace {
|
||||||
@ -156,12 +156,11 @@ void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& rowTitle, bool hasSubtitle,
|
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowSubtitle, bool hasIcon,
|
|
||||||
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
||||||
const std::function<std::string(int index)>& rowValue) {
|
const std::function<std::string(int index)>& rowValue) {
|
||||||
int rowHeight = hasSubtitle ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
int pageItems = rect.height / BaseMetrics::values.listRowHeight;
|
||||||
int pageItems = rect.height / rowHeight;
|
const int rowHeight = BaseMetrics::values.listRowHeight;
|
||||||
|
|
||||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||||
if (totalPages > 1) {
|
if (totalPages > 1) {
|
||||||
@ -190,36 +189,29 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection
|
// Draw selection
|
||||||
int contentWidth = rect.width - 5;
|
int contentWidth = rect.width - BaseMetrics::values.sideButtonHintsWidth - 5;
|
||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight);
|
renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, contentWidth, rowHeight);
|
||||||
}
|
}
|
||||||
// Draw all items
|
// Draw all items
|
||||||
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
||||||
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||||
int textWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - (hasValue ? 60 : 0);
|
|
||||||
|
|
||||||
// Draw name
|
// Draw name
|
||||||
auto itemName = rowTitle(i);
|
auto itemName = rowTitle(i);
|
||||||
auto font = hasSubtitle ? UI_12_FONT_ID : UI_10_FONT_ID;
|
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(),
|
||||||
auto item = renderer.truncatedText(font, itemName.c_str(), textWidth);
|
contentWidth - BaseMetrics::values.contentSidePadding * 2 -
|
||||||
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex);
|
(hasValue ? 60 : 0)); // TODO truncate according to value width?
|
||||||
|
renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(),
|
||||||
if (hasSubtitle) {
|
|
||||||
// Draw subtitle
|
|
||||||
std::string subtitleText = rowSubtitle(i);
|
|
||||||
auto subtitle = renderer.truncatedText(UI_10_FONT_ID, subtitleText.c_str(), textWidth);
|
|
||||||
renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY + 30, subtitle.c_str(),
|
|
||||||
i != selectedIndex);
|
i != selectedIndex);
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue) {
|
if (hasValue) {
|
||||||
// Draw value
|
// Draw value
|
||||||
std::string valueText = rowValue(i);
|
std::string valueText = rowValue(i);
|
||||||
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
const auto textWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||||
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth,
|
renderer.drawText(UI_10_FONT_ID, contentWidth - BaseMetrics::values.contentSidePadding - textWidth, itemY,
|
||||||
itemY, valueText.c_str(), i != selectedIndex);
|
valueText.c_str(), i != selectedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,11 +229,7 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
int padding = rect.width - batteryX;
|
renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, title, true, EpdFontFamily::BOLD);
|
||||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
|
||||||
rect.width - padding * 2 - BaseMetrics::values.contentSidePadding * 2,
|
|
||||||
EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,9 +265,10 @@ void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const s
|
|||||||
|
|
||||||
// Draw the "Recent Book" cover card on the home screen
|
// Draw the "Recent Book" cover card on the home screen
|
||||||
// TODO: Refactor method to make it cleaner, split into smaller methods
|
// TODO: Refactor method to make it cleaner, split into smaller methods
|
||||||
void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const std::vector<RecentBookWithCover>& recentBooks, const int selectorIndex,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) {
|
bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||||
|
std::function<bool()> storeCoverBuffer) {
|
||||||
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
||||||
const int bookWidth = rect.width / 2;
|
const int bookWidth = rect.width / 2;
|
||||||
const int bookHeight = rect.height;
|
const int bookHeight = rect.height;
|
||||||
@ -299,8 +288,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
// Draw cover image as background if available (inside the box)
|
// Draw cover image as background if available (inside the box)
|
||||||
// Only load from SD on first render, then use stored buffer
|
// Only load from SD on first render, then use stored buffer
|
||||||
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) {
|
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) {
|
||||||
const std::string coverBmpPath =
|
const std::string& coverBmpPath = recentBooks[0].coverBmpPath;
|
||||||
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
|
|
||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
FsFile file;
|
FsFile file;
|
||||||
@ -390,8 +378,8 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasContinueReading) {
|
if (hasContinueReading) {
|
||||||
const std::string& lastBookTitle = recentBooks[0].title;
|
const std::string& lastBookTitle = recentBooks[0].book.title;
|
||||||
const std::string& lastBookAuthor = recentBooks[0].author;
|
const std::string& lastBookAuthor = recentBooks[0].book.author;
|
||||||
|
|
||||||
// Invert text colors based on selection state:
|
// Invert text colors based on selection state:
|
||||||
// - With cover: selected = white text on black box, unselected = black text on white box
|
// - With cover: selected = white text on black box, unselected = black text on white box
|
||||||
@ -431,7 +419,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
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 "..."
|
||||||
utf8RemoveLastChar(lines.back());
|
StringUtils::utf8RemoveLastChar(lines.back());
|
||||||
lines.back().append("...");
|
lines.back().append("...");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -440,7 +428,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
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)
|
||||||
utf8RemoveLastChar(i);
|
StringUtils::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());
|
||||||
@ -493,7 +481,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
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()) {
|
||||||
utf8RemoveLastChar(trimmedAuthor);
|
StringUtils::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())) {
|
||||||
@ -527,14 +515,14 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
// 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()) {
|
||||||
utf8RemoveLastChar(trimmedAuthor);
|
StringUtils::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()) {
|
||||||
utf8RemoveLastChar(trimmedAuthor);
|
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
||||||
}
|
}
|
||||||
trimmedAuthor.append("...");
|
trimmedAuthor.append("...");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
|
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
|
struct RecentBookInfo;
|
||||||
|
|
||||||
// Default theme implementation (Classic Theme)
|
// Default theme implementation (Classic Theme)
|
||||||
// Additional themes can inherit from this and override methods as needed
|
// Additional themes can inherit from this and override methods as needed
|
||||||
@ -20,7 +21,6 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
|
|||||||
.verticalSpacing = 10,
|
.verticalSpacing = 10,
|
||||||
.contentSidePadding = 20,
|
.contentSidePadding = 20,
|
||||||
.listRowHeight = 30,
|
.listRowHeight = 30,
|
||||||
.listWithSubtitleRowHeight = 65,
|
|
||||||
.menuRowHeight = 45,
|
.menuRowHeight = 45,
|
||||||
.menuSpacing = 8,
|
.menuSpacing = 8,
|
||||||
.tabSpacing = 10,
|
.tabSpacing = 10,
|
||||||
@ -49,16 +49,16 @@ class BaseTheme {
|
|||||||
const char* btn4);
|
const char* btn4);
|
||||||
virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn);
|
virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn);
|
||||||
virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& rowTitle, bool hasSubtitle,
|
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowSubtitle, bool hasIcon,
|
|
||||||
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
||||||
const std::function<std::string(int index)>& rowValue);
|
const std::function<std::string(int index)>& rowValue);
|
||||||
|
|
||||||
virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title);
|
virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title);
|
||||||
virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, bool selected);
|
virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, bool selected);
|
||||||
virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const std::vector<RecentBookWithCover>& recentBooks, const int selectorIndex,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer);
|
bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||||
|
std::function<bool()> storeCoverBuffer);
|
||||||
virtual void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
virtual void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& buttonLabel, bool hasIcon,
|
const std::function<std::string(int index)>& buttonLabel, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowIcon);
|
const std::function<std::string(int index)>& rowIcon);
|
||||||
|
|||||||
@ -72,11 +72,8 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
auto truncatedTitle = renderer.truncatedText(
|
|
||||||
UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding,
|
renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding,
|
||||||
rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true,
|
rect.y + LyraMetrics::values.batteryBarHeight + 3, title, true, EpdFontFamily::BOLD);
|
||||||
EpdFontFamily::BOLD);
|
|
||||||
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width, rect.y + rect.height - 3, 3, true);
|
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width, rect.y + rect.height - 3, 3, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,7 +82,7 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve
|
|||||||
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray);
|
renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, COLOR_LIGHT_GRAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& tab : tabs) {
|
for (const auto& tab : tabs) {
|
||||||
@ -94,10 +91,10 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve
|
|||||||
if (tab.selected) {
|
if (tab.selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderer.fillRoundedRect(currentX, rect.y + 1, textWidth + 2 * hPaddingInSelection, rect.height - 4,
|
renderer.fillRoundedRect(currentX, rect.y + 1, textWidth + 2 * hPaddingInSelection, rect.height - 4,
|
||||||
cornerRadius, Color::Black);
|
cornerRadius, COLOR_BLACK);
|
||||||
} else {
|
} else {
|
||||||
renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3,
|
renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3,
|
||||||
Color::LightGray);
|
COLOR_LIGHT_GRAY);
|
||||||
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection,
|
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection,
|
||||||
rect.y + rect.height - 3, 2, true);
|
rect.y + rect.height - 3, 2, true);
|
||||||
}
|
}
|
||||||
@ -113,12 +110,11 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& rowTitle, bool hasSubtitle,
|
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowSubtitle, bool hasIcon,
|
|
||||||
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
||||||
const std::function<std::string(int index)>& rowValue) {
|
const std::function<std::string(int index)>& rowValue) {
|
||||||
int rowHeight = hasSubtitle ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
int pageItems = rect.height / LyraMetrics::values.listRowHeight;
|
||||||
int pageItems = rect.height / rowHeight;
|
const int rowHeight = LyraMetrics::values.listRowHeight;
|
||||||
|
|
||||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||||
if (totalPages > 1) {
|
if (totalPages > 1) {
|
||||||
@ -141,7 +137,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
|||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
||||||
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius,
|
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius,
|
||||||
Color::LightGray);
|
COLOR_LIGHT_GRAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw all items
|
// Draw all items
|
||||||
@ -150,35 +146,28 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
|||||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||||
|
|
||||||
// Draw name
|
// Draw name
|
||||||
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 -
|
|
||||||
(hasValue ? 60 : 0); // TODO truncate according to value width?
|
|
||||||
auto itemName = rowTitle(i);
|
auto itemName = rowTitle(i);
|
||||||
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), textWidth);
|
auto item =
|
||||||
|
renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(),
|
||||||
|
contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 -
|
||||||
|
(hasValue ? 60 : 0)); // TODO truncate according to value width?
|
||||||
renderer.drawText(UI_10_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2,
|
renderer.drawText(UI_10_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2,
|
||||||
itemY + 6, item.c_str(), true);
|
itemY + 6, item.c_str(), true);
|
||||||
|
|
||||||
if (hasSubtitle) {
|
|
||||||
// Draw subtitle
|
|
||||||
std::string subtitleText = rowSubtitle(i);
|
|
||||||
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), textWidth);
|
|
||||||
renderer.drawText(SMALL_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2,
|
|
||||||
itemY + 30, subtitle.c_str(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue) {
|
if (hasValue) {
|
||||||
// Draw value
|
// Draw value
|
||||||
std::string valueText = rowValue(i);
|
std::string valueText = rowValue(i);
|
||||||
if (!valueText.empty()) {
|
if (!valueText.empty()) {
|
||||||
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
const auto textWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||||
|
|
||||||
if (i == selectedIndex) {
|
if (i == selectedIndex) {
|
||||||
renderer.fillRoundedRect(
|
renderer.fillRoundedRect(
|
||||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection * 2 - valueTextWidth, itemY,
|
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection * 2 - textWidth, itemY,
|
||||||
valueTextWidth + hPaddingInSelection * 2, rowHeight, cornerRadius, Color::Black);
|
textWidth + hPaddingInSelection * 2, rowHeight, cornerRadius, COLOR_BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID,
|
renderer.drawText(UI_10_FONT_ID,
|
||||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueTextWidth,
|
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - textWidth,
|
||||||
itemY + 6, valueText.c_str(), i != selectedIndex);
|
itemY + 6, valueText.c_str(), i != selectedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,9 +243,10 @@ void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const std::vector<RecentBookWithCover>& recentBooks, const int selectorIndex,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) {
|
bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||||
|
std::function<bool()> storeCoverBuffer) {
|
||||||
const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / 3;
|
const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / 3;
|
||||||
const int tileHeight = rect.height;
|
const int tileHeight = rect.height;
|
||||||
const int bookTitleHeight = tileHeight - LyraMetrics::values.homeCoverHeight - hPaddingInSelection;
|
const int bookTitleHeight = tileHeight - LyraMetrics::values.homeCoverHeight - hPaddingInSelection;
|
||||||
@ -270,13 +260,12 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
if (!coverRendered) {
|
if (!coverRendered) {
|
||||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount);
|
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount);
|
||||||
i++) {
|
i++) {
|
||||||
std::string coverPath = recentBooks[i].coverBmpPath;
|
const std::string& coverBmpPath = recentBooks[i].coverBmpPath;
|
||||||
if (coverPath.empty()) {
|
|
||||||
|
if (coverBmpPath.empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight);
|
|
||||||
|
|
||||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
@ -307,18 +296,18 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
|
|
||||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||||
auto title =
|
auto title =
|
||||||
renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].title.c_str(), tileWidth - 2 * hPaddingInSelection);
|
renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].book.title.c_str(), tileWidth - 2 * hPaddingInSelection);
|
||||||
|
|
||||||
if (bookSelected) {
|
if (bookSelected) {
|
||||||
// Draw selection box
|
// Draw selection box
|
||||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||||
Color::LightGray);
|
COLOR_LIGHT_GRAY);
|
||||||
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
||||||
LyraMetrics::values.homeCoverHeight, Color::LightGray);
|
LyraMetrics::values.homeCoverHeight, COLOR_LIGHT_GRAY);
|
||||||
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
hPaddingInSelection, LyraMetrics::values.homeCoverHeight, Color::LightGray);
|
hPaddingInSelection, LyraMetrics::values.homeCoverHeight, COLOR_LIGHT_GRAY);
|
||||||
renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth,
|
renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth,
|
||||||
bookTitleHeight, cornerRadius, false, false, true, true, Color::LightGray);
|
bookTitleHeight, cornerRadius, false, false, true, true, COLOR_LIGHT_GRAY);
|
||||||
}
|
}
|
||||||
renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection,
|
renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection,
|
||||||
tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true);
|
tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true);
|
||||||
@ -339,7 +328,7 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount
|
|||||||
const bool selected = selectedIndex == i;
|
const bool selected = selectedIndex == i;
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderer.fillRoundedRect(tileRect.x, tileRect.y, tileRect.width, tileRect.height, cornerRadius, Color::LightGray);
|
renderer.fillRoundedRect(tileRect.x, tileRect.y, tileRect.width, tileRect.height, cornerRadius, COLOR_LIGHT_GRAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* label = buttonLabel(i).c_str();
|
const char* label = buttonLabel(i).c_str();
|
||||||
|
|||||||
@ -14,7 +14,6 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
|
|||||||
.verticalSpacing = 16,
|
.verticalSpacing = 16,
|
||||||
.contentSidePadding = 20,
|
.contentSidePadding = 20,
|
||||||
.listRowHeight = 40,
|
.listRowHeight = 40,
|
||||||
.listWithSubtitleRowHeight = 60,
|
|
||||||
.menuRowHeight = 64,
|
.menuRowHeight = 64,
|
||||||
.menuSpacing = 8,
|
.menuSpacing = 8,
|
||||||
.tabSpacing = 8,
|
.tabSpacing = 8,
|
||||||
@ -40,8 +39,7 @@ class LyraTheme : public BaseTheme {
|
|||||||
void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) override;
|
void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) override;
|
||||||
void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, bool selected) override;
|
void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, bool selected) override;
|
||||||
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& rowTitle, bool hasSubtitle,
|
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowSubtitle, bool hasIcon,
|
|
||||||
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
const std::function<std::string(int index)>& rowIcon, bool hasValue,
|
||||||
const std::function<std::string(int index)>& rowValue) override;
|
const std::function<std::string(int index)>& rowValue) override;
|
||||||
void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||||
@ -50,7 +48,7 @@ class LyraTheme : public BaseTheme {
|
|||||||
void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& buttonLabel, bool hasIcon,
|
const std::function<std::string(int index)>& buttonLabel, bool hasIcon,
|
||||||
const std::function<std::string(int index)>& rowIcon) override;
|
const std::function<std::string(int index)>& rowIcon) override;
|
||||||
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBookWithCover>& recentBooks,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||||
std::function<bool()> storeCoverBuffer) override;
|
std::function<bool()> storeCoverBuffer) override;
|
||||||
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) override;
|
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) override;
|
||||||
|
|||||||
@ -61,4 +61,23 @@ 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
|
||||||
|
|||||||
@ -19,4 +19,10 @@ 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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user