mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 08:07:40 +03:00
Compare commits
19 Commits
7e67b76eb0
...
269bca1cc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
269bca1cc6 | ||
|
|
e5c0ddc9fa | ||
|
|
b1dcb7733b | ||
|
|
0d82b03981 | ||
|
|
5a97334ace | ||
|
|
66e69e750e | ||
|
|
45bba9a822 | ||
|
|
cebfc54266 | ||
|
|
fe119239ad | ||
|
|
a6b4ed5cf9 | ||
|
|
91313d8505 | ||
|
|
3c727dd8f9 | ||
|
|
bde92c288f | ||
|
|
3d9c3bf101 | ||
|
|
b7b36f02e2 | ||
|
|
73c5f05843 | ||
|
|
178c826b52 | ||
|
|
7aa21f2386 | ||
|
|
727186f208 |
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
|
||||||
|
|||||||
@ -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()
|
||||||
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int PAGE_ITEMS = 23;
|
constexpr int PAGE_ITEMS = 23;
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
|
||||||
@ -118,12 +117,6 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
|
|
||||||
// Handle browsing state
|
// Handle browsing state
|
||||||
if (state == BrowserState::BROWSING) {
|
if (state == BrowserState::BROWSING) {
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!entries.empty()) {
|
if (!entries.empty()) {
|
||||||
const auto& entry = entries[selectorIndex];
|
const auto& entry = entries[selectorIndex];
|
||||||
@ -135,20 +128,29 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else if (prevReleased && !entries.empty()) {
|
}
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size();
|
// Handle navigation
|
||||||
} else {
|
if (!entries.empty()) {
|
||||||
selectorIndex = (selectorIndex + entries.size() - 1) % entries.size();
|
buttonNavigator.onNextRelease([this] {
|
||||||
}
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased && !entries.empty()) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size();
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
||||||
selectorIndex = (selectorIndex + 1) % entries.size();
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
updateRequired = true;
|
|
||||||
|
buttonNavigator.onNextContinuous([this] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for browsing and downloading books from an OPDS server.
|
* Activity for browsing and downloading books from an OPDS server.
|
||||||
@ -37,6 +38,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
BrowserState state = BrowserState::LOADING;
|
BrowserState state = BrowserState::LOADING;
|
||||||
|
|||||||
@ -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>
|
||||||
@ -161,13 +162,18 @@ void HomeActivity::freeCoverBuffer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::loop() {
|
void HomeActivity::loop() {
|
||||||
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
|
||||||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const int menuCount = getMenuItemCount();
|
const int menuCount = getMenuItemCount();
|
||||||
|
|
||||||
|
buttonNavigator.onNext([this, menuCount] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this, menuCount] {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Calculate dynamic indices based on which options are available
|
// Calculate dynamic indices based on which options are available
|
||||||
int idx = 0;
|
int idx = 0;
|
||||||
@ -188,12 +194,6 @@ void HomeActivity::loop() {
|
|||||||
} else if (selectorIndex == settingsIdx) {
|
} else if (selectorIndex == settingsIdx) {
|
||||||
onSettingsOpen();
|
onSettingsOpen();
|
||||||
}
|
}
|
||||||
} else if (prevPressed) {
|
|
||||||
selectorIndex = (selectorIndex + menuCount - 1) % menuCount;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextPressed) {
|
|
||||||
selectorIndex = (selectorIndex + 1) % menuCount;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +366,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 +375,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 +428,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 +462,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("...");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,12 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class HomeActivity final : public Activity {
|
class HomeActivity final : public Activity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
bool hasContinueReading = false;
|
bool hasContinueReading = false;
|
||||||
|
|||||||
@ -21,7 +21,6 @@ constexpr int LEFT_MARGIN = 20;
|
|||||||
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
|
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
|
||||||
|
|
||||||
// Timing thresholds
|
// Timing thresholds
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
constexpr unsigned long GO_HOME_MS = 1000;
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
|
|
||||||
void sortFileList(std::vector<std::string>& strs) {
|
void sortFileList(std::vector<std::string>& strs) {
|
||||||
@ -178,13 +177,9 @@ void MyLibraryActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
|
||||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
|
||||||
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||||
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
|
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
|
|
||||||
// Confirm button - open selected item
|
// Confirm button - open selected item
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (currentTab == Tab::Recent) {
|
if (currentTab == Tab::Recent) {
|
||||||
@ -249,24 +244,28 @@ void MyLibraryActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigation: Up/Down moves through items only
|
// Navigation: Up/Down moves through items only
|
||||||
const bool prevReleased = upReleased;
|
constexpr auto upButton = MappedInputManager::Button::Up;
|
||||||
const bool nextReleased = downReleased;
|
constexpr auto downButton = MappedInputManager::Button::Down;
|
||||||
|
|
||||||
if (prevReleased && itemCount > 0) {
|
buttonNavigator.onRelease({downButton}, [this, itemCount] {
|
||||||
if (skipPage) {
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, itemCount);
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + itemCount - 1) % itemCount;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextReleased && itemCount > 0) {
|
});
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount;
|
buttonNavigator.onRelease({upButton}, [this, itemCount] {
|
||||||
} else {
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, itemCount);
|
||||||
selectorIndex = (selectorIndex + 1) % itemCount;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onContinuous({downButton}, [this, itemCount, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, itemCount, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onContinuous({upButton}, [this, itemCount, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, itemCount, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::displayTaskLoop() {
|
void MyLibraryActivity::displayTaskLoop() {
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class MyLibraryActivity final : public Activity {
|
class MyLibraryActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
@ -17,6 +18,7 @@ class MyLibraryActivity final : public Activity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
Tab currentTab = Tab::Recent;
|
Tab currentTab = Tab::Recent;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
|
|||||||
@ -72,18 +72,15 @@ void NetworkModeSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
buttonNavigator.onNext([this] {
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left);
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
updateRequired = true;
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right);
|
});
|
||||||
|
|
||||||
if (prevPressed) {
|
buttonNavigator.onPrevious([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (nextPressed) {
|
});
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkModeSelectionActivity::displayTaskLoop() {
|
void NetworkModeSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
// Enum for network mode selection
|
// Enum for network mode selection
|
||||||
enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
||||||
@ -22,6 +23,8 @@ enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
|
|||||||
class NetworkModeSelectionActivity final : public Activity {
|
class NetworkModeSelectionActivity final : public Activity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void(NetworkMode)> onModeSelected;
|
const std::function<void(NetworkMode)> onModeSelected;
|
||||||
|
|||||||
@ -419,20 +419,16 @@ void WifiSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle UP/DOWN navigation
|
// Handle navigation
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
buttonNavigator.onNext([this] {
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
|
||||||
if (selectedNetworkIndex > 0) {
|
updateRequired = true;
|
||||||
selectedNetworkIndex--;
|
});
|
||||||
updateRequired = true;
|
|
||||||
}
|
buttonNavigator.onPrevious([this] {
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size());
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
updateRequired = true;
|
||||||
if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) {
|
});
|
||||||
selectedNetworkIndex++;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
// Structure to hold WiFi network information
|
// Structure to hold WiFi network information
|
||||||
struct WifiNetworkInfo {
|
struct WifiNetworkInfo {
|
||||||
@ -45,6 +46,7 @@ enum class WifiSelectionState {
|
|||||||
class WifiSelectionActivity final : public ActivityWithSubactivity {
|
class WifiSelectionActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
WifiSelectionState state = WifiSelectionState::SCANNING;
|
WifiSelectionState state = WifiSelectionState::SCANNING;
|
||||||
int selectedNetworkIndex = 0;
|
int selectedNetworkIndex = 0;
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,11 +7,6 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Time threshold for treating a long press as a page-up/page-down
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
||||||
@ -119,12 +114,6 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
|
|
||||||
@ -145,21 +134,27 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextReleased) {
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + 1) % totalItems;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@ -7,12 +7,14 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
|
||||||
std::shared_ptr<Epub> epub;
|
std::shared_ptr<Epub> epub;
|
||||||
std::string epubPath;
|
std::string epubPath;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
int totalPagesInSpine = 0;
|
int totalPagesInSpine = 0;
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,6 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
int XtcReaderChapterSelectionActivity::getPageItems() const {
|
||||||
constexpr int startY = 60;
|
constexpr int startY = 60;
|
||||||
constexpr int lineHeight = 30;
|
constexpr int lineHeight = 30;
|
||||||
@ -75,13 +71,8 @@ void XtcReaderChapterSelectionActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::loop() {
|
void XtcReaderChapterSelectionActivity::loop() {
|
||||||
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Left);
|
|
||||||
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasReleased(MappedInputManager::Button::Right);
|
|
||||||
|
|
||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
|
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
@ -90,29 +81,27 @@ void XtcReaderChapterSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else if (prevReleased) {
|
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
|
||||||
if (total == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + total - 1) % total;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (nextReleased) {
|
|
||||||
const int total = static_cast<int>(xtc->getChapters().size());
|
|
||||||
if (total == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (skipPage) {
|
|
||||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total;
|
|
||||||
} else {
|
|
||||||
selectorIndex = (selectorIndex + 1) % total;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||||
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
|
||||||
|
|||||||
@ -7,11 +7,13 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class XtcReaderChapterSelectionActivity final : public Activity {
|
class XtcReaderChapterSelectionActivity final : public Activity {
|
||||||
std::shared_ptr<Xtc> xtc;
|
std::shared_ptr<Xtc> xtc;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
uint32_t currentPage = 0;
|
uint32_t currentPage = 0;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
@ -62,15 +62,16 @@ void CalibreSettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
// Handle navigation
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibreSettingsActivity::handleSelection() {
|
void CalibreSettingsActivity::handleSelection() {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submenu for OPDS Browser settings.
|
* Submenu for OPDS Browser settings.
|
||||||
@ -24,6 +25,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
|
|||||||
@ -62,15 +62,15 @@ void CategorySettingsActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
buttonNavigator.onNext([this] {
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
selectedSettingIndex = (selectedSettingIndex + 1) % settingsCount;
|
||||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
});
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedSettingIndex = (selectedSettingIndex + settingsCount - 1) % settingsCount;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CategorySettingsActivity::toggleCurrentSetting() {
|
void CategorySettingsActivity::toggleCurrentSetting() {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ struct SettingInfo {
|
|||||||
class CategorySettingsActivity final : public ActivityWithSubactivity {
|
class CategorySettingsActivity final : public ActivityWithSubactivity {
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedSettingIndex = 0;
|
int selectedSettingIndex = 0;
|
||||||
const char* categoryName;
|
const char* categoryName;
|
||||||
|
|||||||
@ -63,15 +63,16 @@ void KOReaderSettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
// Handle navigation
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPrevious([this] {
|
||||||
|
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSettingsActivity::handleSelection() {
|
void KOReaderSettingsActivity::handleSelection() {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submenu for KOReader Sync settings.
|
* Submenu for KOReader Sync settings.
|
||||||
@ -24,6 +25,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity {
|
|||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
|
|||||||
@ -111,17 +111,15 @@ void SettingsActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
buttonNavigator.onNext([this] {
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
selectedCategoryIndex = (selectedCategoryIndex + 1) % categoryCount;
|
||||||
// Move selection up (with wrap-around)
|
|
||||||
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
});
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
// Move selection down (with wrap around)
|
buttonNavigator.onPrevious([this] {
|
||||||
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
|
selectedCategoryIndex = (selectedCategoryIndex + categoryCount - 1) % categoryCount;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsActivity::enterCategory(int categoryIndex) {
|
void SettingsActivity::enterCategory(int categoryIndex) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
struct SettingInfo;
|
struct SettingInfo;
|
||||||
@ -15,6 +16,7 @@ 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;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedCategoryIndex = 0; // Currently selected category
|
int selectedCategoryIndex = 0; // Currently selected category
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|||||||
@ -138,37 +138,24 @@ void KeyboardEntryActivity::handleKeyPress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardEntryActivity::loop() {
|
void KeyboardEntryActivity::loop() {
|
||||||
// Navigation
|
// Handle navigation
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] {
|
||||||
if (selectedRow > 0) {
|
selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS);
|
||||||
selectedRow--;
|
|
||||||
// Clamp column to valid range for new row
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
} else {
|
|
||||||
// Wrap to bottom row
|
|
||||||
selectedRow = NUM_ROWS - 1;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
if (selectedRow < NUM_ROWS - 1) {
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
selectedRow++;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
} else {
|
|
||||||
// Wrap to top row
|
|
||||||
selectedRow = 0;
|
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
|
||||||
if (selectedCol > maxCol) selectedCol = maxCol;
|
|
||||||
}
|
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] {
|
||||||
|
selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS);
|
||||||
|
|
||||||
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
if (selectedCol > maxCol) selectedCol = maxCol;
|
||||||
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] {
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
|
||||||
// Special bottom row case
|
// Special bottom row case
|
||||||
@ -187,20 +174,14 @@ void KeyboardEntryActivity::loop() {
|
|||||||
// At done button, move to backspace
|
// At done button, move to backspace
|
||||||
selectedCol = BACKSPACE_COL;
|
selectedCol = BACKSPACE_COL;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol > 0) {
|
|
||||||
selectedCol--;
|
|
||||||
} else {
|
} else {
|
||||||
// Wrap to end of current row
|
selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1);
|
||||||
selectedCol = maxCol;
|
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
updateRequired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] {
|
||||||
const int maxCol = getRowLength(selectedRow) - 1;
|
const int maxCol = getRowLength(selectedRow) - 1;
|
||||||
|
|
||||||
// Special bottom row case
|
// Special bottom row case
|
||||||
@ -219,18 +200,11 @@ void KeyboardEntryActivity::loop() {
|
|||||||
// At done button, wrap to beginning of row
|
// At done button, wrap to beginning of row
|
||||||
selectedCol = SHIFT_COL;
|
selectedCol = SHIFT_COL;
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCol < maxCol) {
|
|
||||||
selectedCol++;
|
|
||||||
} else {
|
} else {
|
||||||
// Wrap to beginning of current row
|
selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1);
|
||||||
selectedCol = 0;
|
|
||||||
}
|
}
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
});
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "../Activity.h"
|
#include "../Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable keyboard entry activity for text input.
|
* Reusable keyboard entry activity for text input.
|
||||||
@ -65,6 +66,7 @@ class KeyboardEntryActivity : public Activity {
|
|||||||
bool isPassword;
|
bool isPassword;
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|
||||||
// Keyboard state
|
// Keyboard state
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
#include "activities/settings/SettingsActivity.h"
|
#include "activities/settings/SettingsActivity.h"
|
||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
HalDisplay display;
|
HalDisplay display;
|
||||||
HalGPIO gpio;
|
HalGPIO gpio;
|
||||||
@ -293,6 +294,7 @@ void setup() {
|
|||||||
|
|
||||||
SETTINGS.loadFromFile();
|
SETTINGS.loadFromFile();
|
||||||
KOREADER_STORE.loadFromFile();
|
KOREADER_STORE.loadFromFile();
|
||||||
|
ButtonNavigator::setMappedInputManager(mappedInputManager);
|
||||||
|
|
||||||
switch (gpio.getWakeupReason()) {
|
switch (gpio.getWakeupReason()) {
|
||||||
case HalGPIO::WakeupReason::PowerButton:
|
case HalGPIO::WakeupReason::PowerButton:
|
||||||
|
|||||||
124
src/util/ButtonNavigator.cpp
Normal file
124
src/util/ButtonNavigator.cpp
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
#include "ButtonNavigator.h"
|
||||||
|
|
||||||
|
const MappedInputManager* ButtonNavigator::mappedInput = nullptr;
|
||||||
|
|
||||||
|
void ButtonNavigator::onNext(const Callback& callback) {
|
||||||
|
onNextPress(callback);
|
||||||
|
onNextContinuous(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onPrevious(const Callback& callback) {
|
||||||
|
onPreviousPress(callback);
|
||||||
|
onPreviousContinuous(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onPressAndContinuous(const Buttons& buttons, const Callback& callback) {
|
||||||
|
onPress(buttons, callback);
|
||||||
|
onContinuous(buttons, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onNextRelease(const Callback& callback) { onRelease(getNextButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPreviousRelease(const Callback& callback) { onRelease(getPreviousButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); }
|
||||||
|
|
||||||
|
void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) {
|
||||||
|
const bool wasPressed = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) {
|
||||||
|
return mappedInput != nullptr && mappedInput->wasPressed(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasPressed) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) {
|
||||||
|
const bool wasReleased = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) {
|
||||||
|
return mappedInput != nullptr && mappedInput->wasReleased(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasReleased) {
|
||||||
|
if (lastContinuousNavTime == 0) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastContinuousNavTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) {
|
||||||
|
const bool isPressed = std::any_of(buttons.begin(), buttons.end(), [this](const MappedInputManager::Button button) {
|
||||||
|
return mappedInput != nullptr && mappedInput->isPressed(button) && shouldNavigateContinuously();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPressed) {
|
||||||
|
callback();
|
||||||
|
lastContinuousNavTime = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ButtonNavigator::shouldNavigateContinuously() const {
|
||||||
|
if (!mappedInput) return false;
|
||||||
|
|
||||||
|
const bool buttonHeldLongEnough = mappedInput->getHeldTime() > continuousStartMs;
|
||||||
|
const bool navigationIntervalElapsed = (millis() - lastContinuousNavTime) > continuousIntervalMs;
|
||||||
|
|
||||||
|
return buttonHeldLongEnough && navigationIntervalElapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) {
|
||||||
|
if (totalItems <= 0) return 0;
|
||||||
|
|
||||||
|
// Calculate the next index with wrap-around
|
||||||
|
return (currentIndex + 1) % totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) {
|
||||||
|
if (totalItems <= 0) return 0;
|
||||||
|
|
||||||
|
// Calculate the previous index with wrap-around
|
||||||
|
return (currentIndex + totalItems - 1) % totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) {
|
||||||
|
if (totalItems <= 0 || itemsPerPage <= 0) return 0;
|
||||||
|
|
||||||
|
// When items fit on one page, use index navigation instead
|
||||||
|
if (totalItems <= itemsPerPage) {
|
||||||
|
return nextIndex(currentIndex, totalItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lastPageIndex = (totalItems - 1) / itemsPerPage;
|
||||||
|
const int currentPageIndex = currentIndex / itemsPerPage;
|
||||||
|
|
||||||
|
if (currentPageIndex < lastPageIndex) {
|
||||||
|
return (currentPageIndex + 1) * itemsPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ButtonNavigator::previousPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) {
|
||||||
|
if (totalItems <= 0 || itemsPerPage <= 0) return 0;
|
||||||
|
|
||||||
|
// When items fit on one page, use index navigation instead
|
||||||
|
if (totalItems <= itemsPerPage) {
|
||||||
|
return previousIndex(currentIndex, totalItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lastPageIndex = (totalItems - 1) / itemsPerPage;
|
||||||
|
const int currentPageIndex = currentIndex / itemsPerPage;
|
||||||
|
|
||||||
|
if (currentPageIndex > 0) {
|
||||||
|
return (currentPageIndex - 1) * itemsPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastPageIndex * itemsPerPage;
|
||||||
|
}
|
||||||
53
src/util/ButtonNavigator.h
Normal file
53
src/util/ButtonNavigator.h
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
|
||||||
|
class ButtonNavigator final {
|
||||||
|
using Callback = std::function<void()>;
|
||||||
|
using Buttons = std::vector<MappedInputManager::Button>;
|
||||||
|
|
||||||
|
const uint16_t continuousStartMs;
|
||||||
|
const uint16_t continuousIntervalMs;
|
||||||
|
uint32_t lastContinuousNavTime = 0;
|
||||||
|
static const MappedInputManager* mappedInput;
|
||||||
|
|
||||||
|
[[nodiscard]] bool shouldNavigateContinuously() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ButtonNavigator(const uint16_t continuousIntervalMs = 500, const uint16_t continuousStartMs = 500)
|
||||||
|
: continuousStartMs(continuousStartMs), continuousIntervalMs(continuousIntervalMs) {}
|
||||||
|
|
||||||
|
static void setMappedInputManager(const MappedInputManager& mappedInputManager) { mappedInput = &mappedInputManager; }
|
||||||
|
|
||||||
|
void onNext(const Callback& callback);
|
||||||
|
void onPrevious(const Callback& callback);
|
||||||
|
void onPressAndContinuous(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
void onNextPress(const Callback& callback);
|
||||||
|
void onPreviousPress(const Callback& callback);
|
||||||
|
void onPress(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
void onNextRelease(const Callback& callback);
|
||||||
|
void onPreviousRelease(const Callback& callback);
|
||||||
|
void onRelease(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
void onNextContinuous(const Callback& callback);
|
||||||
|
void onPreviousContinuous(const Callback& callback);
|
||||||
|
void onContinuous(const Buttons& buttons, const Callback& callback);
|
||||||
|
|
||||||
|
[[nodiscard]] static int nextIndex(int currentIndex, int totalItems);
|
||||||
|
[[nodiscard]] static int previousIndex(int currentIndex, int totalItems);
|
||||||
|
|
||||||
|
[[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage);
|
||||||
|
[[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage);
|
||||||
|
|
||||||
|
[[nodiscard]] static Buttons getNextButtons() {
|
||||||
|
return {MappedInputManager::Button::Down, MappedInputManager::Button::Right};
|
||||||
|
}
|
||||||
|
[[nodiscard]] static Buttons getPreviousButtons() {
|
||||||
|
return {MappedInputManager::Button::Up, MappedInputManager::Button::Left};
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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