mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
Compare commits
6 Commits
a3cc1b9ca3
...
8bfadaca45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bfadaca45 | ||
|
|
e5c0ddc9fa | ||
|
|
b1dcb7733b | ||
|
|
0d82b03981 | ||
|
|
5a97334ace | ||
|
|
5ae10a7eb6 |
14
README.md
14
README.md
@ -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, 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
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -49,4 +49,12 @@ static void readString(FsFile& file, std::string& s) {
|
||||
s.resize(len);
|
||||
file.read(&s[0], len);
|
||||
}
|
||||
|
||||
static void readString(FsFile& file, char* buffer, size_t maxLen) {
|
||||
uint32_t len;
|
||||
readPod(file, len);
|
||||
const uint32_t bytesToRead = (len < maxLen - 1) ? len : (maxLen - 1);
|
||||
file.read(reinterpret_cast<uint8_t*>(buffer), bytesToRead);
|
||||
buffer[bytesToRead] = '\0';
|
||||
}
|
||||
} // namespace serialization
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
214
scripts/debugging_monitor.py
Executable 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()
|
||||
@ -11,21 +11,132 @@
|
||||
// Initialize the static instance
|
||||
CrossPointSettings CrossPointSettings::instance;
|
||||
|
||||
void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
||||
uint8_t tempValue;
|
||||
serialization::readPod(file, tempValue);
|
||||
if (tempValue < maxValue) {
|
||||
member = tempValue;
|
||||
// SettingDescriptor implementations
|
||||
bool SettingDescriptor::validate(const CrossPointSettings& settings) const {
|
||||
if (type == SettingType::STRING) {
|
||||
return true; // Strings are always valid
|
||||
}
|
||||
if (!validator) {
|
||||
return true; // No validator means always valid
|
||||
}
|
||||
const uint8_t value = settings.*(memberPtr);
|
||||
return validator(value);
|
||||
}
|
||||
|
||||
uint8_t SettingDescriptor::getValue(const CrossPointSettings& settings) const { return settings.*(memberPtr); }
|
||||
|
||||
void SettingDescriptor::setValue(CrossPointSettings& settings, uint8_t value) const { settings.*(memberPtr) = value; }
|
||||
|
||||
void SettingDescriptor::resetToDefault(CrossPointSettings& settings) const {
|
||||
if (type == SettingType::STRING) {
|
||||
strncpy(stringPtr, stringData.defaultString, stringData.maxSize - 1);
|
||||
stringPtr[stringData.maxSize - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
setValue(settings, defaultValue);
|
||||
}
|
||||
|
||||
void SettingDescriptor::save(FsFile& file, const CrossPointSettings& settings) const {
|
||||
if (type == SettingType::STRING) {
|
||||
serialization::writeString(file, std::string(stringPtr));
|
||||
return;
|
||||
}
|
||||
serialization::writePod(file, settings.*(memberPtr));
|
||||
}
|
||||
|
||||
void SettingDescriptor::load(FsFile& file, CrossPointSettings& settings) const {
|
||||
if (type == SettingType::STRING) {
|
||||
serialization::readString(file, stringPtr, stringData.maxSize);
|
||||
return;
|
||||
}
|
||||
uint8_t value;
|
||||
serialization::readPod(file, value);
|
||||
settings.*(memberPtr) = value;
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
// Increment this when adding new persisted settings fields
|
||||
constexpr uint8_t SETTINGS_COUNT = 23;
|
||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||
} // namespace
|
||||
|
||||
// Define enum value arrays
|
||||
namespace {
|
||||
constexpr const char* sleepScreenValues[] = {"Dark", "Light", "Custom", "Cover", "None"};
|
||||
constexpr const char* shortPwrBtnValues[] = {"Ignore", "Sleep", "Page Turn"};
|
||||
constexpr const char* statusBarValues[] = {"None", "No Progress", "Full"};
|
||||
constexpr const char* orientationValues[] = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
||||
constexpr const char* frontButtonLayoutValues[] = {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm",
|
||||
"Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"};
|
||||
constexpr const char* sideButtonLayoutValues[] = {"Prev/Next", "Next/Prev"};
|
||||
constexpr const char* fontFamilyValues[] = {"Bookerly", "Noto Sans", "Open Dyslexic"};
|
||||
constexpr const char* fontSizeValues[] = {"Small", "Medium", "Large", "X Large"};
|
||||
constexpr const char* lineSpacingValues[] = {"Tight", "Normal", "Wide"};
|
||||
constexpr const char* paragraphAlignmentValues[] = {"Justify", "Left", "Center", "Right"};
|
||||
constexpr const char* sleepTimeoutValues[] = {"1 min", "5 min", "10 min", "15 min", "30 min"};
|
||||
constexpr const char* refreshFrequencyValues[] = {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"};
|
||||
constexpr const char* sleepScreenCoverModeValues[] = {"Fit", "Crop"};
|
||||
constexpr const char* hideBatteryPercentageValues[] = {"Never", "In Reader", "Always"};
|
||||
constexpr const char* sleepScreenCoverFilterValues[] = {"None", "Contrast", "Inverted"};
|
||||
|
||||
// Helper function template to deduce array size automatically
|
||||
template <size_t N>
|
||||
constexpr SettingDescriptor makeEnumDescriptor(const char* name, uint8_t CrossPointSettings::* ptr,
|
||||
uint8_t defaultValue, const char* const (&enumValues)[N]) {
|
||||
return SettingDescriptor(name, SettingType::ENUM, ptr, defaultValue, validateEnum<N>, enumValues, N);
|
||||
}
|
||||
|
||||
// Helper macro to create STRING descriptors without repetition
|
||||
#define makeStringDescriptor(name, member, defStr) \
|
||||
SettingDescriptor(name, SettingType::STRING, CrossPointSettings::instance.member, defStr, \
|
||||
sizeof(CrossPointSettings::member))
|
||||
} // namespace
|
||||
|
||||
// Define static constexpr members (required in C++14 and earlier)
|
||||
constexpr size_t CrossPointSettings::DESCRIPTOR_COUNT;
|
||||
|
||||
// Define the static constexpr array of all setting descriptors
|
||||
// Order must match current serialization order for file format compatibility!
|
||||
const std::array<SettingDescriptor, CrossPointSettings::DESCRIPTOR_COUNT> CrossPointSettings::descriptors = {{
|
||||
makeEnumDescriptor("Sleep Screen", &CrossPointSettings::sleepScreen, CrossPointSettings::DARK, sleepScreenValues),
|
||||
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, 1, validateToggle,
|
||||
nullptr, 0},
|
||||
makeEnumDescriptor("Short Power Button Click", &CrossPointSettings::shortPwrBtn, CrossPointSettings::IGNORE,
|
||||
shortPwrBtnValues),
|
||||
makeEnumDescriptor("Status Bar", &CrossPointSettings::statusBar, CrossPointSettings::FULL, statusBarValues),
|
||||
makeEnumDescriptor("Reading Orientation", &CrossPointSettings::orientation, CrossPointSettings::PORTRAIT,
|
||||
orientationValues),
|
||||
makeEnumDescriptor("Front Button Layout", &CrossPointSettings::frontButtonLayout,
|
||||
CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT, frontButtonLayoutValues),
|
||||
makeEnumDescriptor("Side Button Layout", &CrossPointSettings::sideButtonLayout, CrossPointSettings::PREV_NEXT,
|
||||
sideButtonLayoutValues),
|
||||
makeEnumDescriptor("Reader Font Family", &CrossPointSettings::fontFamily, CrossPointSettings::BOOKERLY,
|
||||
fontFamilyValues),
|
||||
makeEnumDescriptor("Reader Font Size", &CrossPointSettings::fontSize, CrossPointSettings::MEDIUM, fontSizeValues),
|
||||
makeEnumDescriptor("Reader Line Spacing", &CrossPointSettings::lineSpacing, CrossPointSettings::NORMAL,
|
||||
lineSpacingValues),
|
||||
makeEnumDescriptor("Reader Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
||||
CrossPointSettings::JUSTIFIED, paragraphAlignmentValues),
|
||||
makeEnumDescriptor("Time to Sleep", &CrossPointSettings::sleepTimeout, CrossPointSettings::SLEEP_10_MIN,
|
||||
sleepTimeoutValues),
|
||||
makeEnumDescriptor("Refresh Frequency", &CrossPointSettings::refreshFrequency, CrossPointSettings::REFRESH_15,
|
||||
refreshFrequencyValues),
|
||||
{"Reader Screen Margin", SettingType::VALUE, &CrossPointSettings::screenMargin, 5, validateRange<5, 40>,
|
||||
ValueRange{5, 40, 5}},
|
||||
makeEnumDescriptor("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, CrossPointSettings::FIT,
|
||||
sleepScreenCoverModeValues),
|
||||
makeStringDescriptor("OPDS Server URL", opdsServerUrl, ""),
|
||||
{"Text Anti-Aliasing", SettingType::TOGGLE, &CrossPointSettings::textAntiAliasing, 1, validateToggle, nullptr, 0},
|
||||
makeEnumDescriptor("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, CrossPointSettings::HIDE_NEVER,
|
||||
hideBatteryPercentageValues),
|
||||
{"Long-press Chapter Skip", SettingType::TOGGLE, &CrossPointSettings::longPressChapterSkip, 1, validateToggle,
|
||||
nullptr, 0},
|
||||
{"Hyphenation", SettingType::TOGGLE, &CrossPointSettings::hyphenationEnabled, 0, validateToggle, nullptr, 0},
|
||||
makeStringDescriptor("Username", opdsUsername, ""),
|
||||
makeStringDescriptor("Password", opdsPassword, ""),
|
||||
makeEnumDescriptor("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
|
||||
CrossPointSettings::NO_FILTER, sleepScreenCoverFilterValues),
|
||||
}};
|
||||
|
||||
bool CrossPointSettings::saveToFile() const {
|
||||
// Make sure the directory exists
|
||||
SdMan.mkdir("/.crosspoint");
|
||||
@ -36,31 +147,15 @@ bool CrossPointSettings::saveToFile() const {
|
||||
}
|
||||
|
||||
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
||||
serialization::writePod(outputFile, SETTINGS_COUNT);
|
||||
serialization::writePod(outputFile, sleepScreen);
|
||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
||||
serialization::writePod(outputFile, shortPwrBtn);
|
||||
serialization::writePod(outputFile, statusBar);
|
||||
serialization::writePod(outputFile, orientation);
|
||||
serialization::writePod(outputFile, frontButtonLayout);
|
||||
serialization::writePod(outputFile, sideButtonLayout);
|
||||
serialization::writePod(outputFile, fontFamily);
|
||||
serialization::writePod(outputFile, fontSize);
|
||||
serialization::writePod(outputFile, lineSpacing);
|
||||
serialization::writePod(outputFile, paragraphAlignment);
|
||||
serialization::writePod(outputFile, sleepTimeout);
|
||||
serialization::writePod(outputFile, refreshFrequency);
|
||||
serialization::writePod(outputFile, screenMargin);
|
||||
serialization::writePod(outputFile, sleepScreenCoverMode);
|
||||
serialization::writeString(outputFile, std::string(opdsServerUrl));
|
||||
serialization::writePod(outputFile, textAntiAliasing);
|
||||
serialization::writePod(outputFile, hideBatteryPercentage);
|
||||
serialization::writePod(outputFile, longPressChapterSkip);
|
||||
serialization::writePod(outputFile, hyphenationEnabled);
|
||||
serialization::writeString(outputFile, std::string(opdsUsername));
|
||||
serialization::writeString(outputFile, std::string(opdsPassword));
|
||||
serialization::writePod(outputFile, sleepScreenCoverFilter);
|
||||
// New fields added at end for backward compatibility
|
||||
serialization::writePod(outputFile, static_cast<uint8_t>(CrossPointSettings::DESCRIPTOR_COUNT));
|
||||
|
||||
// Use descriptors to automatically serialize all uint8_t settings
|
||||
uint8_t descriptorIndex = 0;
|
||||
for (const auto& desc : descriptors) {
|
||||
desc.save(outputFile, *this);
|
||||
descriptorIndex++;
|
||||
}
|
||||
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
@ -68,8 +163,10 @@ bool CrossPointSettings::saveToFile() const {
|
||||
}
|
||||
|
||||
bool CrossPointSettings::loadFromFile() {
|
||||
Serial.printf("[%lu] [CPS] Loading settings from file\n", millis());
|
||||
FsFile inputFile;
|
||||
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
||||
Serial.printf("[%lu] [CPS] Deserialization failed: Could not open settings file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -84,74 +181,26 @@ bool CrossPointSettings::loadFromFile() {
|
||||
uint8_t fileSettingsCount = 0;
|
||||
serialization::readPod(inputFile, fileSettingsCount);
|
||||
|
||||
// load settings that exist (support older files with fewer fields)
|
||||
uint8_t settingsRead = 0;
|
||||
do {
|
||||
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, extraParagraphSpacing);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, fontSize, FONT_SIZE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, lineSpacing, LINE_COMPRESSION_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, paragraphAlignment, PARAGRAPH_ALIGNMENT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sleepTimeout, SLEEP_TIMEOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, refreshFrequency, REFRESH_FREQUENCY_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, screenMargin);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sleepScreenCoverMode, SLEEP_SCREEN_COVER_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string urlStr;
|
||||
serialization::readString(inputFile, urlStr);
|
||||
strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1);
|
||||
opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, textAntiAliasing);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, longPressChapterSkip);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, hyphenationEnabled);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string usernameStr;
|
||||
serialization::readString(inputFile, usernameStr);
|
||||
strncpy(opdsUsername, usernameStr.c_str(), sizeof(opdsUsername) - 1);
|
||||
opdsUsername[sizeof(opdsUsername) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string passwordStr;
|
||||
serialization::readString(inputFile, passwordStr);
|
||||
strncpy(opdsPassword, passwordStr.c_str(), sizeof(opdsPassword) - 1);
|
||||
opdsPassword[sizeof(opdsPassword) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
// New fields added at end for backward compatibility
|
||||
} while (false);
|
||||
// Use descriptors to automatically deserialize all uint8_t settings
|
||||
uint8_t descriptorIndex = 0;
|
||||
uint8_t filePosition = 0;
|
||||
|
||||
for (const auto& desc : descriptors) {
|
||||
if (filePosition >= fileSettingsCount) {
|
||||
break; // File has fewer settings than current version
|
||||
}
|
||||
|
||||
desc.load(inputFile, *this);
|
||||
if (!desc.validate(*this)) {
|
||||
Serial.printf("[%lu] [CPS] Invalid value (0x%X) for %s, resetting to default\n", millis(), desc.getValue(*this),
|
||||
desc.name);
|
||||
desc.resetToDefault(*this);
|
||||
}
|
||||
descriptorIndex++;
|
||||
filePosition++;
|
||||
}
|
||||
inputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,6 +1,112 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Setting descriptor infrastructure
|
||||
enum class SettingType { TOGGLE, ENUM, VALUE, STRING };
|
||||
|
||||
// Validator function pointer (not std::function to save memory)
|
||||
using SettingValidator = bool (*)(uint8_t);
|
||||
|
||||
// Forward declare for descriptors
|
||||
class CrossPointSettings;
|
||||
|
||||
// Forward declare file type
|
||||
class FsFile;
|
||||
|
||||
// Base descriptor for all settings (non-virtual for constexpr)
|
||||
struct SettingDescriptorBase {
|
||||
const char* name; // Display name
|
||||
SettingType type;
|
||||
};
|
||||
|
||||
// Value range for VALUE type settings
|
||||
struct ValueRange {
|
||||
uint8_t min, max, step;
|
||||
};
|
||||
|
||||
// Concrete descriptor for uint8_t settings (constexpr-compatible)
|
||||
struct SettingDescriptor : public SettingDescriptorBase {
|
||||
union {
|
||||
uint8_t CrossPointSettings::* memberPtr; // For TOGGLE/ENUM/VALUE types
|
||||
char* stringPtr; // For STRING type
|
||||
};
|
||||
uint8_t defaultValue;
|
||||
SettingValidator validator; // Optional validator function
|
||||
|
||||
union {
|
||||
// For ENUM types
|
||||
struct {
|
||||
const char* const* values;
|
||||
uint8_t count;
|
||||
} enumData;
|
||||
|
||||
// For VALUE types
|
||||
ValueRange valueRange;
|
||||
|
||||
// For STRING types
|
||||
struct {
|
||||
const char* defaultString; // Default string value
|
||||
size_t maxSize; // Max size of the string buffer
|
||||
} stringData;
|
||||
};
|
||||
|
||||
// Constexpr constructors for different setting types
|
||||
// TOGGLE/ENUM constructor
|
||||
constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal,
|
||||
SettingValidator val, const char* const* enumVals, uint8_t enumCnt)
|
||||
: SettingDescriptorBase{name_, type_},
|
||||
memberPtr(ptr),
|
||||
defaultValue(defVal),
|
||||
validator(val),
|
||||
enumData{enumVals, enumCnt} {}
|
||||
|
||||
// VALUE constructor
|
||||
constexpr SettingDescriptor(const char* name_, SettingType type_, uint8_t CrossPointSettings::* ptr, uint8_t defVal,
|
||||
SettingValidator val, ValueRange valRange)
|
||||
: SettingDescriptorBase{name_, type_},
|
||||
memberPtr(ptr),
|
||||
defaultValue(defVal),
|
||||
validator(val),
|
||||
valueRange(valRange) {}
|
||||
|
||||
// STRING constructor
|
||||
constexpr SettingDescriptor(const char* name_, SettingType type_, char* strPtr, const char* defStr, size_t maxSz)
|
||||
: SettingDescriptorBase{name_, type_},
|
||||
stringPtr(strPtr),
|
||||
defaultValue(0),
|
||||
validator(nullptr),
|
||||
stringData{defStr, maxSz} {}
|
||||
|
||||
bool validate(const CrossPointSettings& settings) const;
|
||||
uint8_t getValue(const CrossPointSettings& settings) const;
|
||||
void setValue(CrossPointSettings& settings, uint8_t value) const;
|
||||
void resetToDefault(CrossPointSettings& settings) const;
|
||||
void save(FsFile& file, const CrossPointSettings& settings) const;
|
||||
void load(FsFile& file, CrossPointSettings& settings) const;
|
||||
|
||||
// Helper to get enum value as string
|
||||
const char* getEnumValueString(uint8_t index) const {
|
||||
if (index < enumData.count && enumData.values) {
|
||||
return enumData.values[index];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Validator functions (constexpr for compile-time optimization)
|
||||
constexpr bool validateToggle(uint8_t v) { return v <= 1; }
|
||||
template <uint8_t MAX>
|
||||
constexpr bool validateEnum(uint8_t v) {
|
||||
return v < MAX;
|
||||
}
|
||||
template <uint8_t MIN, uint8_t MAX>
|
||||
constexpr bool validateRange(uint8_t v) {
|
||||
return v >= MIN && v <= MAX;
|
||||
}
|
||||
|
||||
class CrossPointSettings {
|
||||
private:
|
||||
@ -15,13 +121,17 @@ class CrossPointSettings {
|
||||
CrossPointSettings(const CrossPointSettings&) = delete;
|
||||
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
|
||||
|
||||
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4, SLEEP_SCREEN_MODE_COUNT };
|
||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
|
||||
// Static constexpr array of all setting descriptors
|
||||
static constexpr size_t DESCRIPTOR_COUNT = 23;
|
||||
static const std::array<SettingDescriptor, DESCRIPTOR_COUNT> descriptors;
|
||||
|
||||
// Should match with SettingsActivity text
|
||||
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 };
|
||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 };
|
||||
enum SLEEP_SCREEN_COVER_FILTER {
|
||||
NO_FILTER = 0,
|
||||
BLACK_AND_WHITE = 1,
|
||||
INVERTED_BLACK_AND_WHITE = 2,
|
||||
SLEEP_SCREEN_COVER_FILTER_COUNT
|
||||
};
|
||||
|
||||
// Status bar display type enum
|
||||
@ -31,7 +141,6 @@ class CrossPointSettings {
|
||||
FULL = 2,
|
||||
FULL_WITH_PROGRESS_BAR = 3,
|
||||
ONLY_PROGRESS_BAR = 4,
|
||||
STATUS_BAR_MODE_COUNT
|
||||
};
|
||||
|
||||
enum ORIENTATION {
|
||||
@ -39,7 +148,6 @@ class CrossPointSettings {
|
||||
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||
INVERTED = 2, // 480x800 logical coordinates, inverted
|
||||
LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation
|
||||
ORIENTATION_COUNT
|
||||
};
|
||||
|
||||
// Front button layout options
|
||||
@ -50,25 +158,23 @@ class CrossPointSettings {
|
||||
LEFT_RIGHT_BACK_CONFIRM = 1,
|
||||
LEFT_BACK_CONFIRM_RIGHT = 2,
|
||||
BACK_CONFIRM_RIGHT_LEFT = 3,
|
||||
FRONT_BUTTON_LAYOUT_COUNT
|
||||
};
|
||||
|
||||
// Side button layout options
|
||||
// Default: Previous, Next
|
||||
// Swapped: Next, Previous
|
||||
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT };
|
||||
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
|
||||
|
||||
// Font family options
|
||||
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
|
||||
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 };
|
||||
// Font size options
|
||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
|
||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
|
||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
||||
enum PARAGRAPH_ALIGNMENT {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
RIGHT_ALIGN = 3,
|
||||
PARAGRAPH_ALIGNMENT_COUNT
|
||||
};
|
||||
|
||||
// Auto-sleep timeout options (in minutes)
|
||||
@ -78,7 +184,6 @@ class CrossPointSettings {
|
||||
SLEEP_10_MIN = 2,
|
||||
SLEEP_15_MIN = 3,
|
||||
SLEEP_30_MIN = 4,
|
||||
SLEEP_TIMEOUT_COUNT
|
||||
};
|
||||
|
||||
// E-ink refresh frequency (pages between full refreshes)
|
||||
@ -88,14 +193,13 @@ class CrossPointSettings {
|
||||
REFRESH_10 = 2,
|
||||
REFRESH_15 = 3,
|
||||
REFRESH_30 = 4,
|
||||
REFRESH_FREQUENCY_COUNT
|
||||
};
|
||||
|
||||
// Short power button press actions
|
||||
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT };
|
||||
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 };
|
||||
|
||||
// Hide battery percentage
|
||||
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
|
||||
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 };
|
||||
|
||||
// Sleep screen settings
|
||||
uint8_t sleepScreen = DARK;
|
||||
|
||||
@ -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("...");
|
||||
}
|
||||
|
||||
@ -570,8 +570,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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -533,8 +533,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());
|
||||
}
|
||||
|
||||
|
||||
@ -62,77 +62,84 @@ void CategorySettingsActivity::loop() {
|
||||
}
|
||||
|
||||
// Handle navigation
|
||||
const int totalItemsCount = descriptors.size() + actionItems.size();
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (totalItemsCount - 1);
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||
selectedSettingIndex = (selectedSettingIndex < totalItemsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void CategorySettingsActivity::toggleCurrentSetting() {
|
||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
||||
const int totalItemsCount = descriptors.size() + actionItems.size();
|
||||
|
||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= totalItemsCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& setting = settingsList[selectedSettingIndex];
|
||||
// Check if it's a descriptor or an action item
|
||||
if (selectedSettingIndex < static_cast<int>(descriptors.size())) {
|
||||
// Handle descriptor
|
||||
const auto* desc = descriptors[selectedSettingIndex];
|
||||
|
||||
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
||||
// Toggle the boolean value using the member pointer
|
||||
const bool currentValue = SETTINGS.*(setting.valuePtr);
|
||||
SETTINGS.*(setting.valuePtr) = !currentValue;
|
||||
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
||||
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
||||
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
|
||||
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
|
||||
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
|
||||
if (desc->type == SettingType::TOGGLE) {
|
||||
uint8_t currentValue = desc->getValue(SETTINGS);
|
||||
desc->setValue(SETTINGS, !currentValue);
|
||||
} else if (desc->type == SettingType::ENUM) {
|
||||
uint8_t currentValue = desc->getValue(SETTINGS);
|
||||
desc->setValue(SETTINGS, (currentValue + 1) % desc->enumData.count);
|
||||
} else if (desc->type == SettingType::VALUE) {
|
||||
uint8_t currentValue = desc->getValue(SETTINGS);
|
||||
if (currentValue + desc->valueRange.step > desc->valueRange.max) {
|
||||
desc->setValue(SETTINGS, desc->valueRange.min);
|
||||
} else {
|
||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||
desc->setValue(SETTINGS, currentValue + desc->valueRange.step);
|
||||
}
|
||||
} else if (setting.type == SettingType::ACTION) {
|
||||
if (strcmp(setting.name, "KOReader Sync") == 0) {
|
||||
}
|
||||
|
||||
SETTINGS.saveToFile();
|
||||
} else {
|
||||
// Handle action item
|
||||
const int actionIndex = selectedSettingIndex - descriptors.size();
|
||||
const auto& action = actionItems[actionIndex];
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
|
||||
switch (action.type) {
|
||||
case ActionItem::Type::KOREADER_SYNC:
|
||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "OPDS Browser") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
break;
|
||||
case ActionItem::Type::CALIBRE_SETTINGS:
|
||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "Clear Cache") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
break;
|
||||
case ActionItem::Type::CLEAR_CACHE:
|
||||
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "Check for updates") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
break;
|
||||
case ActionItem::Type::CHECK_UPDATES:
|
||||
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
SETTINGS.saveToFile();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
}
|
||||
|
||||
void CategorySettingsActivity::displayTaskLoop() {
|
||||
@ -153,29 +160,31 @@ void CategorySettingsActivity::render() const {
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Draw header with category name
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
|
||||
|
||||
// Draw selection highlight
|
||||
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
||||
|
||||
// Draw all settings
|
||||
for (int i = 0; i < settingsCount; i++) {
|
||||
const int settingY = 60 + i * 30; // 30 pixels between settings
|
||||
// Draw all descriptors
|
||||
for (size_t i = 0; i < descriptors.size(); i++) {
|
||||
const auto* desc = descriptors[i];
|
||||
const int settingY = 60 + i * 30;
|
||||
const bool isSelected = (i == selectedSettingIndex);
|
||||
|
||||
// Draw setting name
|
||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
|
||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, desc->name, !isSelected);
|
||||
|
||||
// Draw value based on setting type
|
||||
std::string valueText;
|
||||
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
|
||||
const bool value = SETTINGS.*(settingsList[i].valuePtr);
|
||||
if (desc->type == SettingType::TOGGLE) {
|
||||
const bool value = desc->getValue(SETTINGS);
|
||||
valueText = value ? "ON" : "OFF";
|
||||
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
||||
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
||||
valueText = settingsList[i].enumValues[value];
|
||||
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
|
||||
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
||||
} else if (desc->type == SettingType::ENUM) {
|
||||
const uint8_t value = desc->getValue(SETTINGS);
|
||||
valueText = desc->getEnumValueString(value);
|
||||
} else if (desc->type == SettingType::VALUE) {
|
||||
valueText = std::to_string(desc->getValue(SETTINGS));
|
||||
}
|
||||
if (!valueText.empty()) {
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||
@ -183,9 +192,22 @@ void CategorySettingsActivity::render() const {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw all action items
|
||||
for (size_t i = 0; i < actionItems.size(); i++) {
|
||||
const auto& action = actionItems[i];
|
||||
const int itemIndex = descriptors.size() + i;
|
||||
const int settingY = 60 + itemIndex * 30;
|
||||
const bool isSelected = (itemIndex == selectedSettingIndex);
|
||||
|
||||
// Draw action name
|
||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, action.name, !isSelected);
|
||||
}
|
||||
|
||||
// Draw version text above button hints
|
||||
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||
pageHeight - 60, CROSSPOINT_VERSION);
|
||||
|
||||
// Draw help text
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
|
||||
@ -7,38 +7,14 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
class CrossPointSettings;
|
||||
|
||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
|
||||
|
||||
struct SettingInfo {
|
||||
// Action items for the System category
|
||||
struct ActionItem {
|
||||
const char* name;
|
||||
SettingType type;
|
||||
uint8_t CrossPointSettings::* valuePtr;
|
||||
std::vector<std::string> enumValues;
|
||||
|
||||
struct ValueRange {
|
||||
uint8_t min;
|
||||
uint8_t max;
|
||||
uint8_t step;
|
||||
};
|
||||
ValueRange valueRange;
|
||||
|
||||
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) {
|
||||
return {name, SettingType::TOGGLE, ptr};
|
||||
}
|
||||
|
||||
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
|
||||
return {name, SettingType::ENUM, ptr, std::move(values)};
|
||||
}
|
||||
|
||||
static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; }
|
||||
|
||||
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
|
||||
return {name, SettingType::VALUE, ptr, {}, valueRange};
|
||||
}
|
||||
enum class Type { KOREADER_SYNC, CALIBRE_SETTINGS, CLEAR_CACHE, CHECK_UPDATES };
|
||||
Type type;
|
||||
};
|
||||
|
||||
class CategorySettingsActivity final : public ActivityWithSubactivity {
|
||||
@ -47,8 +23,8 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
|
||||
bool updateRequired = false;
|
||||
int selectedSettingIndex = 0;
|
||||
const char* categoryName;
|
||||
const SettingInfo* settingsList;
|
||||
int settingsCount;
|
||||
const std::vector<const SettingDescriptor*> descriptors;
|
||||
const std::vector<ActionItem> actionItems;
|
||||
const std::function<void()> onGoBack;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
@ -58,11 +34,12 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
|
||||
|
||||
public:
|
||||
CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName,
|
||||
const SettingInfo* settingsList, int settingsCount, const std::function<void()>& onGoBack)
|
||||
const std::vector<const SettingDescriptor*>& descriptors,
|
||||
const std::vector<ActionItem>& actionItems, const std::function<void()>& onGoBack)
|
||||
: ActivityWithSubactivity("CategorySettings", renderer, mappedInput),
|
||||
categoryName(categoryName),
|
||||
settingsList(settingsList),
|
||||
settingsCount(settingsCount),
|
||||
descriptors(descriptors),
|
||||
actionItems(actionItems),
|
||||
onGoBack(onGoBack) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "CategorySettingsActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
@ -10,51 +12,12 @@
|
||||
|
||||
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
||||
|
||||
namespace {
|
||||
constexpr int displaySettingsCount = 6;
|
||||
const SettingInfo displaySettings[displaySettingsCount] = {
|
||||
// Should match with SLEEP_SCREEN_MODE
|
||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
|
||||
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
|
||||
SettingInfo::Enum("Sleep Screen Cover 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("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
|
||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
|
||||
|
||||
constexpr int readerSettingsCount = 9;
|
||||
const SettingInfo readerSettings[readerSettingsCount] = {
|
||||
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
|
||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
||||
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
||||
{"Justify", "Left", "Center", "Right"}),
|
||||
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
|
||||
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
||||
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
|
||||
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
|
||||
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
|
||||
|
||||
constexpr int controlsSettingsCount = 4;
|
||||
const SettingInfo controlsSettings[controlsSettingsCount] = {
|
||||
SettingInfo::Enum(
|
||||
"Front Button Layout", &CrossPointSettings::frontButtonLayout,
|
||||
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}),
|
||||
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
|
||||
{"Prev, Next", "Next, Prev"}),
|
||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
|
||||
|
||||
constexpr int systemSettingsCount = 5;
|
||||
const SettingInfo systemSettings[systemSettingsCount] = {
|
||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"),
|
||||
SettingInfo::Action("Check for updates")};
|
||||
} // namespace
|
||||
// Helper function to find descriptor by member pointer
|
||||
static const SettingDescriptor* findDescriptor(uint8_t CrossPointSettings::* memberPtr) {
|
||||
auto it = std::find_if(CrossPointSettings::descriptors.begin(), CrossPointSettings::descriptors.end(),
|
||||
[memberPtr](const SettingDescriptor& desc) { return desc.memberPtr == memberPtr; });
|
||||
return (it != CrossPointSettings::descriptors.end()) ? &(*it) : nullptr;
|
||||
}
|
||||
|
||||
void SettingsActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<SettingsActivity*>(param);
|
||||
@ -125,37 +88,51 @@ void SettingsActivity::loop() {
|
||||
}
|
||||
|
||||
void SettingsActivity::enterCategory(int categoryIndex) {
|
||||
if (categoryIndex < 0 || categoryIndex >= categoryCount) {
|
||||
return;
|
||||
std::vector<const SettingDescriptor*> descriptors;
|
||||
std::vector<ActionItem> actionItems;
|
||||
|
||||
switch (categoryIndex) {
|
||||
case 0: // Display
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreen));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverMode));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepScreenCoverFilter));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::statusBar));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::hideBatteryPercentage));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::refreshFrequency));
|
||||
break;
|
||||
|
||||
case 1: // Reader
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::fontFamily));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::fontSize));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::lineSpacing));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::screenMargin));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::paragraphAlignment));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::hyphenationEnabled));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::orientation));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::extraParagraphSpacing));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::textAntiAliasing));
|
||||
break;
|
||||
|
||||
case 2: // Controls
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::frontButtonLayout));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::sideButtonLayout));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::longPressChapterSkip));
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::shortPwrBtn));
|
||||
break;
|
||||
|
||||
case 3: // System
|
||||
descriptors.push_back(findDescriptor(&CrossPointSettings::sleepTimeout));
|
||||
actionItems.push_back({"KOReader Sync", ActionItem::Type::KOREADER_SYNC});
|
||||
actionItems.push_back({"Calibre Settings", ActionItem::Type::CALIBRE_SETTINGS});
|
||||
actionItems.push_back({"Clear Cache", ActionItem::Type::CLEAR_CACHE});
|
||||
actionItems.push_back({"Check for updates", ActionItem::Type::CHECK_UPDATES});
|
||||
break;
|
||||
}
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
|
||||
const SettingInfo* settingsList = nullptr;
|
||||
int settingsCount = 0;
|
||||
|
||||
switch (categoryIndex) {
|
||||
case 0: // Display
|
||||
settingsList = displaySettings;
|
||||
settingsCount = displaySettingsCount;
|
||||
break;
|
||||
case 1: // Reader
|
||||
settingsList = readerSettings;
|
||||
settingsCount = readerSettingsCount;
|
||||
break;
|
||||
case 2: // Controls
|
||||
settingsList = controlsSettings;
|
||||
settingsCount = controlsSettingsCount;
|
||||
break;
|
||||
case 3: // System
|
||||
settingsList = systemSettings;
|
||||
settingsCount = systemSettingsCount;
|
||||
break;
|
||||
}
|
||||
|
||||
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
|
||||
settingsCount, [this] {
|
||||
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], descriptors,
|
||||
actionItems, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
|
||||
@ -9,9 +9,6 @@
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
class CrossPointSettings;
|
||||
struct SettingInfo;
|
||||
|
||||
class SettingsActivity final : public ActivityWithSubactivity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user