mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 07:37:37 +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
|
```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
|
||||||
|
|
||||||
|
|||||||
@ -415,13 +415,21 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
|||||||
|
|
||||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
|
if (!text || maxWidth <= 0) return "";
|
||||||
|
|
||||||
std::string item = text;
|
std::string item = text;
|
||||||
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
const char* ellipsis = "...";
|
||||||
while (itemWidth > maxWidth && item.length() > 8) {
|
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
||||||
item.replace(item.length() - 5, 5, "...");
|
if (textWidth <= maxWidth) {
|
||||||
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
// Text fits, return as is
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
return item;
|
|
||||||
|
while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) {
|
||||||
|
utf8RemoveLastChar(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.empty() ? ellipsis : item + ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
||||||
|
|||||||
@ -49,4 +49,12 @@ static void readString(FsFile& file, std::string& s) {
|
|||||||
s.resize(len);
|
s.resize(len);
|
||||||
file.read(&s[0], 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
|
} // namespace serialization
|
||||||
|
|||||||
@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) {
|
|||||||
|
|
||||||
return cp;
|
return cp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t utf8RemoveLastChar(std::string& str) {
|
||||||
|
if (str.empty()) return 0;
|
||||||
|
size_t pos = str.size() - 1;
|
||||||
|
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
||||||
|
--pos;
|
||||||
|
}
|
||||||
|
str.resize(pos);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate string by removing N UTF-8 characters from the end
|
||||||
|
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
||||||
|
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
||||||
|
utf8RemoveLastChar(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#define REPLACEMENT_GLYPH 0xFFFD
|
#define REPLACEMENT_GLYPH 0xFFFD
|
||||||
|
|
||||||
uint32_t utf8NextCodepoint(const unsigned char** string);
|
uint32_t utf8NextCodepoint(const unsigned char** string);
|
||||||
|
// Remove the last UTF-8 codepoint from a std::string and return the new size.
|
||||||
|
size_t utf8RemoveLastChar(std::string& str);
|
||||||
|
// Truncate string by removing N UTF-8 codepoints from the end.
|
||||||
|
void utf8TruncateChars(std::string& str, size_t numChars);
|
||||||
|
|||||||
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
|
// Initialize the static instance
|
||||||
CrossPointSettings CrossPointSettings::instance;
|
CrossPointSettings CrossPointSettings::instance;
|
||||||
|
|
||||||
void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
// SettingDescriptor implementations
|
||||||
uint8_t tempValue;
|
bool SettingDescriptor::validate(const CrossPointSettings& settings) const {
|
||||||
serialization::readPod(file, tempValue);
|
if (type == SettingType::STRING) {
|
||||||
if (tempValue < maxValue) {
|
return true; // Strings are always valid
|
||||||
member = tempValue;
|
|
||||||
}
|
}
|
||||||
|
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 {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
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";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // 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 {
|
bool CrossPointSettings::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
SdMan.mkdir("/.crosspoint");
|
||||||
@ -36,31 +147,15 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
||||||
serialization::writePod(outputFile, SETTINGS_COUNT);
|
serialization::writePod(outputFile, static_cast<uint8_t>(CrossPointSettings::DESCRIPTOR_COUNT));
|
||||||
serialization::writePod(outputFile, sleepScreen);
|
|
||||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
// Use descriptors to automatically serialize all uint8_t settings
|
||||||
serialization::writePod(outputFile, shortPwrBtn);
|
uint8_t descriptorIndex = 0;
|
||||||
serialization::writePod(outputFile, statusBar);
|
for (const auto& desc : descriptors) {
|
||||||
serialization::writePod(outputFile, orientation);
|
desc.save(outputFile, *this);
|
||||||
serialization::writePod(outputFile, frontButtonLayout);
|
descriptorIndex++;
|
||||||
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
|
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||||
@ -68,8 +163,10 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CrossPointSettings::loadFromFile() {
|
bool CrossPointSettings::loadFromFile() {
|
||||||
|
Serial.printf("[%lu] [CPS] Loading settings from file\n", millis());
|
||||||
FsFile inputFile;
|
FsFile inputFile;
|
||||||
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
if (!SdMan.openFileForRead("CPS", SETTINGS_FILE, inputFile)) {
|
||||||
|
Serial.printf("[%lu] [CPS] Deserialization failed: Could not open settings file\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,74 +181,26 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
uint8_t fileSettingsCount = 0;
|
uint8_t fileSettingsCount = 0;
|
||||||
serialization::readPod(inputFile, fileSettingsCount);
|
serialization::readPod(inputFile, fileSettingsCount);
|
||||||
|
|
||||||
// load settings that exist (support older files with fewer fields)
|
// Use descriptors to automatically deserialize all uint8_t settings
|
||||||
uint8_t settingsRead = 0;
|
uint8_t descriptorIndex = 0;
|
||||||
do {
|
uint8_t filePosition = 0;
|
||||||
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);
|
|
||||||
|
|
||||||
|
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();
|
inputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings loaded from file\n", millis());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,112 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iosfwd>
|
#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 {
|
class CrossPointSettings {
|
||||||
private:
|
private:
|
||||||
@ -15,13 +121,17 @@ class CrossPointSettings {
|
|||||||
CrossPointSettings(const CrossPointSettings&) = delete;
|
CrossPointSettings(const CrossPointSettings&) = delete;
|
||||||
CrossPointSettings& operator=(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 };
|
// Static constexpr array of all setting descriptors
|
||||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
|
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 {
|
enum SLEEP_SCREEN_COVER_FILTER {
|
||||||
NO_FILTER = 0,
|
NO_FILTER = 0,
|
||||||
BLACK_AND_WHITE = 1,
|
BLACK_AND_WHITE = 1,
|
||||||
INVERTED_BLACK_AND_WHITE = 2,
|
INVERTED_BLACK_AND_WHITE = 2,
|
||||||
SLEEP_SCREEN_COVER_FILTER_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Status bar display type enum
|
// Status bar display type enum
|
||||||
@ -31,7 +141,6 @@ class CrossPointSettings {
|
|||||||
FULL = 2,
|
FULL = 2,
|
||||||
FULL_WITH_PROGRESS_BAR = 3,
|
FULL_WITH_PROGRESS_BAR = 3,
|
||||||
ONLY_PROGRESS_BAR = 4,
|
ONLY_PROGRESS_BAR = 4,
|
||||||
STATUS_BAR_MODE_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ORIENTATION {
|
enum ORIENTATION {
|
||||||
@ -39,7 +148,6 @@ class CrossPointSettings {
|
|||||||
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||||
INVERTED = 2, // 480x800 logical coordinates, inverted
|
INVERTED = 2, // 480x800 logical coordinates, inverted
|
||||||
LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation
|
LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation
|
||||||
ORIENTATION_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Front button layout options
|
// Front button layout options
|
||||||
@ -50,25 +158,23 @@ class CrossPointSettings {
|
|||||||
LEFT_RIGHT_BACK_CONFIRM = 1,
|
LEFT_RIGHT_BACK_CONFIRM = 1,
|
||||||
LEFT_BACK_CONFIRM_RIGHT = 2,
|
LEFT_BACK_CONFIRM_RIGHT = 2,
|
||||||
BACK_CONFIRM_RIGHT_LEFT = 3,
|
BACK_CONFIRM_RIGHT_LEFT = 3,
|
||||||
FRONT_BUTTON_LAYOUT_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Side button layout options
|
// Side button layout options
|
||||||
// Default: Previous, Next
|
// Default: Previous, Next
|
||||||
// Swapped: Next, Previous
|
// 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
|
// 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
|
// Font size options
|
||||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
|
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
||||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
|
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
||||||
enum PARAGRAPH_ALIGNMENT {
|
enum PARAGRAPH_ALIGNMENT {
|
||||||
JUSTIFIED = 0,
|
JUSTIFIED = 0,
|
||||||
LEFT_ALIGN = 1,
|
LEFT_ALIGN = 1,
|
||||||
CENTER_ALIGN = 2,
|
CENTER_ALIGN = 2,
|
||||||
RIGHT_ALIGN = 3,
|
RIGHT_ALIGN = 3,
|
||||||
PARAGRAPH_ALIGNMENT_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-sleep timeout options (in minutes)
|
// Auto-sleep timeout options (in minutes)
|
||||||
@ -78,7 +184,6 @@ class CrossPointSettings {
|
|||||||
SLEEP_10_MIN = 2,
|
SLEEP_10_MIN = 2,
|
||||||
SLEEP_15_MIN = 3,
|
SLEEP_15_MIN = 3,
|
||||||
SLEEP_30_MIN = 4,
|
SLEEP_30_MIN = 4,
|
||||||
SLEEP_TIMEOUT_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// E-ink refresh frequency (pages between full refreshes)
|
// E-ink refresh frequency (pages between full refreshes)
|
||||||
@ -88,14 +193,13 @@ class CrossPointSettings {
|
|||||||
REFRESH_10 = 2,
|
REFRESH_10 = 2,
|
||||||
REFRESH_15 = 3,
|
REFRESH_15 = 3,
|
||||||
REFRESH_30 = 4,
|
REFRESH_30 = 4,
|
||||||
REFRESH_FREQUENCY_COUNT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Short power button press actions
|
// 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
|
// 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
|
// Sleep screen settings
|
||||||
uint8_t sleepScreen = DARK;
|
uint8_t sleepScreen = DARK;
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
#include <Utf8.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -366,7 +367,7 @@ void HomeActivity::render() {
|
|||||||
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
||||||
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
// Remove "..." first, then remove one UTF-8 char, then add "..." back
|
||||||
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
lines.back().resize(lines.back().size() - 3); // Remove "..."
|
||||||
StringUtils::utf8RemoveLastChar(lines.back());
|
utf8RemoveLastChar(lines.back());
|
||||||
lines.back().append("...");
|
lines.back().append("...");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -375,7 +376,7 @@ void HomeActivity::render() {
|
|||||||
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
||||||
while (wordWidth > maxLineWidth && !i.empty()) {
|
while (wordWidth > maxLineWidth && !i.empty()) {
|
||||||
// Word itself is too long, trim it (UTF-8 safe)
|
// Word itself is too long, trim it (UTF-8 safe)
|
||||||
StringUtils::utf8RemoveLastChar(i);
|
utf8RemoveLastChar(i);
|
||||||
// Check if we have room for ellipsis
|
// Check if we have room for ellipsis
|
||||||
std::string withEllipsis = i + "...";
|
std::string withEllipsis = i + "...";
|
||||||
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
|
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str());
|
||||||
@ -428,7 +429,7 @@ void HomeActivity::render() {
|
|||||||
if (!lastBookAuthor.empty()) {
|
if (!lastBookAuthor.empty()) {
|
||||||
std::string trimmedAuthor = lastBookAuthor;
|
std::string trimmedAuthor = lastBookAuthor;
|
||||||
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
utf8RemoveLastChar(trimmedAuthor);
|
||||||
}
|
}
|
||||||
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
|
if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) <
|
||||||
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
|
renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) {
|
||||||
@ -462,14 +463,14 @@ void HomeActivity::render() {
|
|||||||
// Trim author if too long (UTF-8 safe)
|
// Trim author if too long (UTF-8 safe)
|
||||||
bool wasTrimmed = false;
|
bool wasTrimmed = false;
|
||||||
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
utf8RemoveLastChar(trimmedAuthor);
|
||||||
wasTrimmed = true;
|
wasTrimmed = true;
|
||||||
}
|
}
|
||||||
if (wasTrimmed && !trimmedAuthor.empty()) {
|
if (wasTrimmed && !trimmedAuthor.empty()) {
|
||||||
// Make room for ellipsis
|
// Make room for ellipsis
|
||||||
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
|
while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth &&
|
||||||
!trimmedAuthor.empty()) {
|
!trimmedAuthor.empty()) {
|
||||||
StringUtils::utf8RemoveLastChar(trimmedAuthor);
|
utf8RemoveLastChar(trimmedAuthor);
|
||||||
}
|
}
|
||||||
trimmedAuthor.append("...");
|
trimmedAuthor.append("...");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -570,8 +570,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
|||||||
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
||||||
titleMarginLeftAdjusted = titleMarginLeft;
|
titleMarginLeftAdjusted = titleMarginLeft;
|
||||||
}
|
}
|
||||||
while (titleWidth > availableTitleSpace && title.length() > 11) {
|
if (titleWidth > availableTitleSpace) {
|
||||||
title.replace(title.length() - 8, 8, "...");
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -533,8 +533,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
|
|||||||
|
|
||||||
std::string title = txt->getTitle();
|
std::string title = txt->getTitle();
|
||||||
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
while (titleWidth > availableTextWidth && title.length() > 11) {
|
if (titleWidth > availableTextWidth) {
|
||||||
title.replace(title.length() - 8, 8, "...");
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth);
|
||||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,77 +62,84 @@ void CategorySettingsActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
|
const int totalItemsCount = descriptors.size() + actionItems.size();
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
|
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (totalItemsCount - 1);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
selectedSettingIndex = (selectedSettingIndex < totalItemsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategorySettingsActivity::toggleCurrentSetting() {
|
void CategorySettingsActivity::toggleCurrentSetting() {
|
||||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
const int totalItemsCount = descriptors.size() + actionItems.size();
|
||||||
|
|
||||||
|
if (selectedSettingIndex < 0 || selectedSettingIndex >= totalItemsCount) {
|
||||||
return;
|
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) {
|
if (desc->type == SettingType::TOGGLE) {
|
||||||
// Toggle the boolean value using the member pointer
|
uint8_t currentValue = desc->getValue(SETTINGS);
|
||||||
const bool currentValue = SETTINGS.*(setting.valuePtr);
|
desc->setValue(SETTINGS, !currentValue);
|
||||||
SETTINGS.*(setting.valuePtr) = !currentValue;
|
} else if (desc->type == SettingType::ENUM) {
|
||||||
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
uint8_t currentValue = desc->getValue(SETTINGS);
|
||||||
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
desc->setValue(SETTINGS, (currentValue + 1) % desc->enumData.count);
|
||||||
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
} else if (desc->type == SettingType::VALUE) {
|
||||||
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
|
uint8_t currentValue = desc->getValue(SETTINGS);
|
||||||
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
|
if (currentValue + desc->valueRange.step > desc->valueRange.max) {
|
||||||
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
|
desc->setValue(SETTINGS, desc->valueRange.min);
|
||||||
SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
|
} else {
|
||||||
} else {
|
desc->setValue(SETTINGS, currentValue + desc->valueRange.step);
|
||||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
}
|
||||||
}
|
|
||||||
} else if (setting.type == SettingType::ACTION) {
|
|
||||||
if (strcmp(setting.name, "KOReader Sync") == 0) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "OPDS Browser") == 0) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "Clear Cache") == 0) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
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();
|
|
||||||
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SETTINGS.saveToFile();
|
||||||
} else {
|
} else {
|
||||||
return;
|
// Handle action item
|
||||||
}
|
const int actionIndex = selectedSettingIndex - descriptors.size();
|
||||||
|
const auto& action = actionItems[actionIndex];
|
||||||
|
|
||||||
SETTINGS.saveToFile();
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
exitActivity();
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionItem::Type::KOREADER_SYNC:
|
||||||
|
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
case ActionItem::Type::CALIBRE_SETTINGS:
|
||||||
|
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
case ActionItem::Type::CLEAR_CACHE:
|
||||||
|
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
case ActionItem::Type::CHECK_UPDATES:
|
||||||
|
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategorySettingsActivity::displayTaskLoop() {
|
void CategorySettingsActivity::displayTaskLoop() {
|
||||||
@ -153,29 +160,31 @@ void CategorySettingsActivity::render() const {
|
|||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Draw header with category name
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
||||||
|
|
||||||
// Draw all settings
|
// Draw all descriptors
|
||||||
for (int i = 0; i < settingsCount; i++) {
|
for (size_t i = 0; i < descriptors.size(); i++) {
|
||||||
const int settingY = 60 + i * 30; // 30 pixels between settings
|
const auto* desc = descriptors[i];
|
||||||
|
const int settingY = 60 + i * 30;
|
||||||
const bool isSelected = (i == selectedSettingIndex);
|
const bool isSelected = (i == selectedSettingIndex);
|
||||||
|
|
||||||
// Draw setting name
|
// Draw setting name
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
|
renderer.drawText(UI_10_FONT_ID, 20, settingY, desc->name, !isSelected);
|
||||||
|
|
||||||
// Draw value based on setting type
|
// Draw value based on setting type
|
||||||
std::string valueText;
|
std::string valueText;
|
||||||
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
|
if (desc->type == SettingType::TOGGLE) {
|
||||||
const bool value = SETTINGS.*(settingsList[i].valuePtr);
|
const bool value = desc->getValue(SETTINGS);
|
||||||
valueText = value ? "ON" : "OFF";
|
valueText = value ? "ON" : "OFF";
|
||||||
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
} else if (desc->type == SettingType::ENUM) {
|
||||||
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
const uint8_t value = desc->getValue(SETTINGS);
|
||||||
valueText = settingsList[i].enumValues[value];
|
valueText = desc->getEnumValueString(value);
|
||||||
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
|
} else if (desc->type == SettingType::VALUE) {
|
||||||
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
valueText = std::to_string(desc->getValue(SETTINGS));
|
||||||
}
|
}
|
||||||
if (!valueText.empty()) {
|
if (!valueText.empty()) {
|
||||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||||
@ -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),
|
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||||
pageHeight - 60, CROSSPOINT_VERSION);
|
pageHeight - 60, CROSSPOINT_VERSION);
|
||||||
|
|
||||||
|
// Draw help text
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
|
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
|||||||
@ -7,38 +7,14 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
// Action items for the System category
|
||||||
|
struct ActionItem {
|
||||||
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
|
|
||||||
|
|
||||||
struct SettingInfo {
|
|
||||||
const char* name;
|
const char* name;
|
||||||
SettingType type;
|
enum class Type { KOREADER_SYNC, CALIBRE_SETTINGS, CLEAR_CACHE, CHECK_UPDATES };
|
||||||
uint8_t CrossPointSettings::* valuePtr;
|
Type type;
|
||||||
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};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CategorySettingsActivity final : public ActivityWithSubactivity {
|
class CategorySettingsActivity final : public ActivityWithSubactivity {
|
||||||
@ -47,8 +23,8 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
|
|||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedSettingIndex = 0;
|
int selectedSettingIndex = 0;
|
||||||
const char* categoryName;
|
const char* categoryName;
|
||||||
const SettingInfo* settingsList;
|
const std::vector<const SettingDescriptor*> descriptors;
|
||||||
int settingsCount;
|
const std::vector<ActionItem> actionItems;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
@ -58,11 +34,12 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
CategorySettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* categoryName,
|
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),
|
: ActivityWithSubactivity("CategorySettings", renderer, mappedInput),
|
||||||
categoryName(categoryName),
|
categoryName(categoryName),
|
||||||
settingsList(settingsList),
|
descriptors(descriptors),
|
||||||
settingsCount(settingsCount),
|
actionItems(actionItems),
|
||||||
onGoBack(onGoBack) {}
|
onGoBack(onGoBack) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "CategorySettingsActivity.h"
|
#include "CategorySettingsActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
@ -10,51 +12,12 @@
|
|||||||
|
|
||||||
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
||||||
|
|
||||||
namespace {
|
// Helper function to find descriptor by member pointer
|
||||||
constexpr int displaySettingsCount = 6;
|
static const SettingDescriptor* findDescriptor(uint8_t CrossPointSettings::* memberPtr) {
|
||||||
const SettingInfo displaySettings[displaySettingsCount] = {
|
auto it = std::find_if(CrossPointSettings::descriptors.begin(), CrossPointSettings::descriptors.end(),
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
[memberPtr](const SettingDescriptor& desc) { return desc.memberPtr == memberPtr; });
|
||||||
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
|
return (it != CrossPointSettings::descriptors.end()) ? &(*it) : nullptr;
|
||||||
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
|
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<SettingsActivity*>(param);
|
auto* self = static_cast<SettingsActivity*>(param);
|
||||||
@ -125,37 +88,51 @@ void SettingsActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::enterCategory(int categoryIndex) {
|
void SettingsActivity::enterCategory(int categoryIndex) {
|
||||||
if (categoryIndex < 0 || categoryIndex >= categoryCount) {
|
std::vector<const SettingDescriptor*> descriptors;
|
||||||
return;
|
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);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], descriptors,
|
||||||
const SettingInfo* settingsList = nullptr;
|
actionItems, [this] {
|
||||||
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] {
|
|
||||||
exitActivity();
|
exitActivity();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -9,9 +9,6 @@
|
|||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
|
||||||
struct SettingInfo;
|
|
||||||
|
|
||||||
class SettingsActivity final : public ActivityWithSubactivity {
|
class SettingsActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
|||||||
@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) {
|
|||||||
return localFile.endsWith(localExtension);
|
return localFile.endsWith(localExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t utf8RemoveLastChar(std::string& str) {
|
|
||||||
if (str.empty()) return 0;
|
|
||||||
size_t pos = str.size() - 1;
|
|
||||||
// Walk back to find the start of the last UTF-8 character
|
|
||||||
// UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF)
|
|
||||||
while (pos > 0 && (static_cast<unsigned char>(str[pos]) & 0xC0) == 0x80) {
|
|
||||||
--pos;
|
|
||||||
}
|
|
||||||
str.resize(pos);
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate string by removing N UTF-8 characters from the end
|
|
||||||
void utf8TruncateChars(std::string& str, const size_t numChars) {
|
|
||||||
for (size_t i = 0; i < numChars && !str.empty(); ++i) {
|
|
||||||
utf8RemoveLastChar(str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace StringUtils
|
} // namespace StringUtils
|
||||||
|
|||||||
@ -19,10 +19,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
|
|||||||
bool checkFileExtension(const std::string& fileName, const char* extension);
|
bool checkFileExtension(const std::string& fileName, const char* extension);
|
||||||
bool checkFileExtension(const String& fileName, const char* extension);
|
bool checkFileExtension(const String& fileName, const char* extension);
|
||||||
|
|
||||||
// UTF-8 safe string truncation - removes one character from the end
|
|
||||||
// Returns the new size after removing one UTF-8 character
|
|
||||||
size_t utf8RemoveLastChar(std::string& str);
|
|
||||||
|
|
||||||
// Truncate string by removing N UTF-8 characters from the end
|
|
||||||
void utf8TruncateChars(std::string& str, size_t numChars);
|
|
||||||
} // namespace StringUtils
|
} // namespace StringUtils
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user