Compare commits

...
44 Commits
Author SHA1 Message Date
Justin Mitchell 757c0b6d9e Add 16px icon size and remove hardcoded bookmark icon
Generate 16px variants for all icons and remove the old hardcoded bookmark icon in favor of using the generated icon system. This provides better consistency across icon sizes and simplifies icon management.
2026-06-28 02:34:10 -04:00
Justin Mitchell 2c8ef01894 Merge remote-tracking branch 'origin/develop' into feat-touch
# Conflicts:
#	freeink-sdk
#	lib/I18n/translations/slovak.yaml
#	platformio.ini
#	src/components/icons/bookmark.h
2026-06-28 02:20:24 -04:00
Bastian cbaa498ccc docs: adding quick resume option and quick resume on timeout to userguide (#2425) 2026-06-27 22:40:21 +03:00
Justin Mitchell ebebc6f202 chore: migrate from open-x4-sdk to freeink-sdk (#2449)
This PR moves us from the xteink openx4 SDK to the freeink sdk from
https://freeink.org. Out of the box there are NO changes needed in the
firmware to support this swap, it all magically works as is. However as
we support more than just the x3/x4 devices, this sdk allows us to pass
env vars into the build commands to include support for other devices.
As support for new hardware such as touch screens and bluetooth are
added the xteink builds decide at compile time if the libraries are used
or not. For example right now the freeinkui and icons libraries are in
the platform.io file but as they are not used anywhere, they won't be
included in the final build. Once the touch branch and sd themes branch
are merged in this sdk is required for them to function correctly. All
the docs for freeink are available at freeink.org/docs. x4/x3 is a
single binary build unlike other devices that will build unique binaries
for each device. Eventually we will want to remove a lot of the manual
isx3 type stuff from our firmware and go through the boardsupport api
the sdk provides as it will generalize everything into one common system
that any device can support. The upcoming touch branch does a lot of
this for us but this initial PR is JUST to get the sdk swapped over
without any code changes to show seamless integration without any
regressions.
2026-06-27 21:22:58 +03:00
Justin Mitchell 1bd718fad9 Update freeink-sdk submodule
Update freeink-sdk submodule from 4b30a86 to c3b3ab3.
2026-06-21 18:29:48 -04:00
Justin Mitchell 61cb4946d6 Fix I2C initialization for dual X3+X4 binary
Sync the runtime board profile to the detected device before I2C consumers initialize. This ensures Wire.begin() is called when needed for X3 devices. Previously, the X4 profile remained active through initialization, causing Wire to never reinitialize after detection probe, leading to X3 I2C read failures with 'could not acquire lock' errors.
2026-06-21 02:06:01 -04:00
Justin Mitchell 4e352b9894 Fix code formatting and include order
Break long line in BookMetadataCache.cpp for better readability and move include directive to top of file in Logging.cpp to follow style guidelines.
2026-06-21 01:24:22 -04:00
Justin Mitchell 4e156e1617 Scale preview padding in UITheme metrics
Apply scaling to previewPadding metric while keeping previewHeightPercent intentionally unscaled. Adds clarifying comment about the different scaling behavior between these two preview-related metrics.
2026-06-21 01:23:56 -04:00
Justin Mitchell 3e0ba4488b Merge remote-tracking branch 'origin/master' into feat-touch 2026-06-21 01:18:18 -04:00
Justin Mitchell e42503f1a4 Preserve touch held time across gesture detection
Touch held time was being lost when gestures were detected because the gesture detection happens before the touch release event. Now remembers the held time at the moment of gesture detection and returns it within a 250ms window, allowing UI elements to properly respond to long-press gestures. Also refactors deep sleep code to use PowerManager methods.
2026-06-21 01:14:40 -04:00
Justin Mitchell c812091a4a Make USB detection pin configurable
Replace hardcoded UART0_RXD pin with configurable BoardConfig::ACTIVE.usbDetect. Also simplify wakeup reason detection logic to prioritize GPIO/EXT1 wakeup from deep sleep as PowerButton event.
2026-06-20 12:15:45 -04:00
Justin Mitchell beb4e82c9c Add extra bar indicator for near-complete progress
Draws a small additional bar segment when progress reaches 95% or higher, positioned at x+14 within the progress bar cavity. The extra bar is capped at 3 pixels wide to fit within the available space.
2026-06-20 00:14:48 -04:00
Justin Mitchell a242397fa0 Update freeink-sdk submodule
Updates freeink-sdk submodule from 5ea178c to 4b30a86.
2026-06-19 16:15:24 -04:00
Justin Mitchell 1511ab35d5 Remove obsolete TODO comments and clarify code
Clean up outdated TODO comments that are no longer relevant and improve comment clarity in activity classes and parsers.
2026-06-19 15:54:44 -04:00
Justin Mitchell 57c389c0b6 Add touch-down visual feedback to settings menus
Handle touch-down events separately from tap events in settings activities to provide immediate visual feedback when menu items are touched, before the tap is completed. This improves UI responsiveness by updating the selected index on touch-down.
2026-06-19 14:41:21 -04:00
Justin Mitchell 06ff5aa44a Add SDK RTC and IMU hardware abstraction support
Integrate SDK-based RTC and IMU backends as alternatives to direct hardware access. HalClock now attempts SDK RTC initialization and caches time values for reliability. HalTiltSensor adds SDK IMU backend with fallback logic. ClockOffsetActivity gains touch and swipe gesture support for field navigation.
2026-06-19 14:25:51 -04:00
Justin Mitchell 526cf1b6f9 Add touch gesture support and LilyGo T5 S3 board
Implements bottom-edge swipe-up gesture detection and delayed touch-select with tracking state. Adds isTouchTapCandidate() to distinguish taps from swipes. Includes LilyGo T5 S3 board configuration with USB CDC logging transport for boards where Serial operator bool reports false incorrectly.
2026-06-19 13:39:10 -04:00
Justin Mitchell 6648a980cb Center row icons between title and subtitle
When a row has a subtitle, position the icon at the midpoint between the title and subtitle text centers instead of aligning only with the title. This prevents icons from appearing too high when subtitles are present.
2026-06-17 01:41:10 -04:00
Justin Mitchell e567620003 Change icon library reference from library-big to library
Updates the icon manifest to use the standard library instead of library-big
2026-06-17 01:37:08 -04:00
Justin Mitchell 0b575d1965 Updates icons to match original Lyra
Adds a unique icon for firmware bins in the file browser
2026-06-17 01:31:07 -04:00
Justin Mitchell 4f55444f67 Add 2-bit compressed rendering for large UI fonts
Refactor UI font generation to support different rendering modes based on size. Small UI fonts (12pt) use 1-bit rendering for crisp display and reduced flash usage. Large UI fonts (14pt) now use 2-bit compressed rendering to stay smooth when scaled up on touch/high-density boards. Extract font generation logic into reusable generate_ui_font() function.
2026-06-17 01:18:17 -04:00
Justin Mitchell 4a17d21d80 Add icon generation script and list scroll helper
Introduce gen_icons.sh to regenerate UI icons from a manifest file using the FreeInk SDK's Lucide icon set. Add icons.manifest mapping UI icons to Lucide names. Refactor vertical swipe list scrolling into a reusable wasListScroll() helper method used by both reader chapter selection and settings activities. Add static assertion to ensure ThemeMetrics scaling stays in sync with struct changes.
2026-06-16 00:15:18 -04:00
Justin Mitchell 1c63847631 Cleanup pass
Removed the getFontXHeight method from GfxRenderer interface. Updated documentation to clarify that getFontCapHeight returns the 'H' glyph height for optical alignment, and improved the comment for getTextVisualCenterOffset to be more concise.
2026-06-15 23:55:19 -04:00
Justin Mitchell 4a5b45409f Add icon rendering and list scroll gesture support
Implement orientation-aware icon rendering using freeink::Icon format that applies rotation transforms per-pixel. Add wasListScroll helper for touch-based list navigation with swipe gestures. Switch vertical centering from x-height to cap-height for better visual alignment with capital-led UI labels.
2026-06-15 23:37:49 -04:00
Justin Mitchell e103cdfd11 Add UI scaling support for touch vs button devices
Introduces uiScale() method to apply per-board UI scaling based on device type (1.0 for button devices, >1 for touch devices to make UI elements finger-sized). Adds scaledMetrics member to store scaled theme metrics that are returned by getMetrics().
2026-06-15 22:42:33 -04:00
Justin Mitchell cef0d267b7 Clarify touch gesture and input API comments
Simplify and clarify documentation for touch gesture detection (back gesture, item taps, long-press) and board detection flags. No functional changes, just improved comment readability.
2026-06-15 19:42:09 -04:00
Justin Mitchell 9f3dddabe6 Add touch gesture to open reader menu
Implements a press-and-hold gesture in the center touch zone (400ms threshold) that opens the reader menu, mirroring the Confirm button behavior. The center third of the screen is now reserved for this menu gesture, while left and right thirds continue to handle page turns. Applied to both EPUB and XTC readers.
2026-06-15 17:16:59 -04:00
Justin Mitchell 350fc8472a Add touch reader controls translation string
Adds STR_TOUCH_READER_CONTROLS translation key across all supported languages.
2026-06-15 17:02:48 -04:00
Justin Mitchell 79ef9d6a35 Add touch reader controls for page navigation
Implements tap zones on touch devices for page back/forward navigation and press-and-hold actions, mirroring physical button behavior. Adds isXteinkDevice() helper to distinguish Xteink X3/X4 boards from touch devices. The sunlight fading fix setting is now exclusive to Xteink devices, while touch devices get the new touch reader controls toggle instead.
2026-06-15 17:01:42 -04:00
Justin Mitchell 0036a220be Split Rect constructor into default and explicit ctors
Separate the default constructor from the parameterized constructor to allow value-initialization of Rect arrays without routing through an explicit constructor. The parameterized constructor remains explicit to prevent implicit int-to-Rect conversions. Member variables now use in-class initializers.
2026-06-15 16:39:32 -04:00
Justin Mitchell 8ad538c98b Fix back gesture to use logical screen coordinates
Changed the fallback corner gesture detection to use logical (orientation-mapped) coordinates instead of native panel coordinates. This ensures the gesture area remains in the visual top-left corner across all screen orientations, particularly for screens without a header or Back button target like the reader.
2026-06-15 16:28:23 -04:00
Justin Mitchell f1b5da25b0 Expand continue-reading card clickable area
Make the entire continue-reading card (cover, title, and gray area) clickable instead of just the cover photo. The clickable area now spans the full tile width rather than just the cover width plus padding.
2026-06-15 16:16:25 -04:00
Justin Mitchell d739827db2 Add touch-down and long-press input detection
Implement touch-down event detection to provide immediate visual feedback when touching list items, mirroring button navigation behavior. Add long-press detection (500ms threshold) to distinguish between tap and hold gestures. Apply touch-down selection updates to file browser, home menu, and recent books activities.
2026-06-15 16:12:07 -04:00
Justin Mitchell 9176e76f5a Add Tab and Cover touch kinds to TouchRegistry
Extends the Kind enum with two new touch interaction types: Tab (2) and Cover (3), enabling support for additional UI element touch handling.
2026-06-15 15:47:33 -04:00
Justin Mitchell 7294610894 Add touch-to-logical coordinate mapping for UI
Implements tapToLogical() to convert normalized touch coordinates to orientation-aware logical screen coordinates. Adds TouchRegistry hit testing for interactive UI elements and header back button. Touch gestures now work correctly across all screen orientations (Portrait, Landscape, etc.) by inverting the rotateCoordinates transform.
2026-06-15 15:32:22 -04:00
Justin Mitchell a51098725d Support multi-I2C and fix ESP32-S3 USB logging
Add dynamic I2C bus selection for battery gauge to support boards with multiple I2C controllers (e.g., Sticky uses Wire1 to avoid conflicts with GT911 touch). Fix logging on ESP32-S3 USB-Serial-JTAG by using esp_rom_printf instead of Serial, which incorrectly reports disconnected state.
2026-06-15 13:52:16 -04:00
Justin Mitchell 6acecd8ea6 Use board config for battery monitoring setup
Replace hardcoded X3-specific I2C pins and frequencies with values from the active board profile. Add support for boards with I2C fuel gauges (X3, LilyGo, Sticky) and ADC-based battery sensing (X4, M5Paper). Skip ADC setup when batteryAdc is unassigned to prevent GPIO mux faults.
2026-06-15 12:29:24 -04:00
Justin Mitchell 9b7b9e9094 Skip X3 fingerprint probe on non-C3 boards
Prevent I2C pin collision on S3/ESP32 boards by skipping the X3/X4 fingerprint probe. The probe's I2C pins (GPIO20/0) conflict with onboard peripherals like the BQ27220 gauge and reconfigure strapping pins. Force deviceType to X4 for non-C3 builds.
2026-06-15 12:26:21 -04:00
Justin Mitchell 204aff6be6 Merge remote-tracking branch 'origin/master' into feat-touch 2026-06-15 12:16:39 -04:00
Justin Mitchell 28d8ad565f Fix SPI bus initialization for non-C3 boards
Change SPI pre-claim logic to only apply on C3/Xteink hardware where EPD_* pins are hardcoded. Other boards (M5Paper, Sticky) need to initialize SPI from BoardConfig::ACTIVE pins to avoid incorrect pin assignments for display and SD card.
2026-06-15 12:14:19 -04:00
Justin Mitchell ed6690c0cc Suppress cppcheck warnings for build-time switch
Add cppcheck-suppress comments for knownConditionTrueFalse warnings related to CROSSPOINT_SHOW_BUTTON_HINTS, which is a build-time configuration switch that may appear as a constant condition during static analysis.
2026-06-08 14:31:55 -04:00
Justin Mitchell 1bc154da8b swap SDK submodule from open-x4 to freeink + add m5paper_v11 env
Adds driver support for the m5paper via the new freeink sdk
2026-06-08 14:18:08 -04:00
Justin Mitchell c308520f9c Add touch tap gesture for back navigation
Implements a reusable touch tap gesture in the top-left corner that maps to the Back button. The gesture is automatically integrated into wasPressed/wasReleased for Back button, making it available across all screens without per-activity code. Also fixes low-power CPU frequency floor to 80 MHz on PSRAM boards to prevent SD write and PSRAM instability caused by APB clock dropping below safe threshold.
2026-06-08 14:01:30 -04:00
Justin Mitchell 38ad4f9036 Battery, EPD, and SD support for m5paper
Also hide button hints
2026-06-08 12:47:43 -04:00
138 changed files with 10177 additions and 1114 deletions
+4 -3
View File
@@ -1,3 +1,4 @@
[submodule "open-x4-sdk"]
path = open-x4-sdk
url = https://github.com/crosspoint-reader/community-sdk.git
[submodule "freeink-sdk"]
path = freeink-sdk
url = https://github.com/Free-Ink/freeink-sdk.git
branch = main
+2 -2
View File
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
### Hardware Abstraction Layer (HAL)
+3
View File
@@ -174,6 +174,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
@@ -186,6 +187,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
+2 -2
View File
@@ -4,7 +4,7 @@
.DESCRIPTION
Formats all C/C++ source and header files in the repository, excluding
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
generated, vendored, and build directories (freeink-sdk, builtinFonts,
hyphenation tries, uzlib, .pio, *.generated.h).
The clang-format binary path is resolved once and cached in
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
$clangFormat = Resolve-ClangFormat
$exclude = @(
'open-x4-sdk'
'freeink-sdk'
'lib\EpdFont\builtinFonts'
'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib'
+3 -3
View File
@@ -8,7 +8,7 @@ At a high level, it is firmware that uses an activity-driven application archite
```mermaid
graph TD
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk]
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
B --> C[lib/hal wrappers]
C --> D[src/main.cpp runtime loop]
D --> E[Activities layer]
@@ -195,10 +195,10 @@ When editing related source assets, regenerate via normal build steps/scripts.
- `src/`: app orchestration, settings/state, and activity implementations
- `src/network/`: web server and OTA/update networking
- `src/components/`: theming and shared UI components
- `lib/hal/`: hardware abstraction wrappers around open-x4-sdk
- `lib/hal/`: hardware abstraction wrappers around freeink-sdk
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
- `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
- `docs/`: user and technical documentation
## Embedded constraints that shape design
Submodule
+1
Submodule freeink-sdk added at 329a4bebef
+2
View File
@@ -37,3 +37,5 @@
#include <builtinFonts/ubuntu_10_regular.h>
#include <builtinFonts/ubuntu_12_bold.h>
#include <builtinFonts/ubuntu_12_regular.h>
#include <builtinFonts/ubuntu_14_bold.h>
#include <builtinFonts/ubuntu_14_regular.h>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -94,6 +94,13 @@ ruby -rdigest -e 'puts [
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
))"
echo "#define UI_14_FONT_ID ($(
ruby -rdigest -e 'puts [
"./ubuntu_14_regular.h",
"./ubuntu_14_bold.h",
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
))"
echo "#define SMALL_FONT_ID ($(
ruby -rdigest -e 'puts [
"./notosans_8_regular.h",
+31 -12
View File
@@ -28,23 +28,42 @@ for size in ${NOTOSANS_FONT_SIZES[@]}; do
done
done
# Small UI chrome (button devices, uiScale 1.0). Rendered 1-bit: crisp at these
# sizes and half the flash of 2-bit.
UI_FONT_SIZES=(10 12)
# Larger UI chrome substituted in on touch/high-density boards via the uiScale
# remap (see src/main.cpp setupFonts). Rendered 2-bit so it stays smooth when
# enlarged. Touch boards use uiScale 1.2, so UI_12 -> 14.4 -> 14 is the size the
# remap actually lands on; add larger sizes here if a board adopts a higher scale.
UI_FONT_SIZES_LARGE=(14)
UI_FONT_STYLES=("Regular" "Bold")
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs are
# filled from it while every glyph Ubuntu already has stays unchanged (fontstack
# is ordered by descending priority). NotoSansHebrew fills U+05D0-U+05EA so the
# Hebrew UI translation renders in menus and settings.
generate_ui_font() {
local size="$1" style="$2" extra_flags="$3"
local font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
local font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
local hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
local viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
local output_path="../builtinFonts/${font_name}.h"
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
--additional-intervals 0x05D0,0x05EA $extra_flags > $output_path
echo "Generated $output_path"
}
for size in ${UI_FONT_SIZES[@]}; do
for style in ${UI_FONT_STYLES[@]}; do
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
# are filled from it while every glyph Ubuntu already has stays unchanged
# (fontstack is ordered by descending priority).
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
output_path="../builtinFonts/${font_name}.h"
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
--additional-intervals 0x05D0,0x05EA > $output_path
echo "Generated $output_path"
generate_ui_font $size $style ""
done
done
for size in ${UI_FONT_SIZES_LARGE[@]}; do
for style in ${UI_FONT_STYLES[@]}; do
generate_ui_font $size $style "--2bit --compress"
done
done
+71 -13
View File
@@ -14,8 +14,47 @@ constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-com
constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
constexpr uint32_t MAX_CACHE_STRING_LEN = 4096;
bool readStringBounded(HalFile& file, std::string& out, const uint32_t maxLen = MAX_CACHE_STRING_LEN) {
uint32_t len = 0;
if (file.read(&len, sizeof(len)) != static_cast<int>(sizeof(len))) {
return false;
}
if (len > maxLen || len > static_cast<uint32_t>(file.available())) {
LOG_ERR("BMC", "Invalid cache string length: %lu (max=%lu available=%d)", static_cast<unsigned long>(len),
static_cast<unsigned long>(maxLen), file.available());
return false;
}
out.clear();
if (len == 0) {
return true;
}
out.resize(len);
return file.read(out.data(), len) == static_cast<int>(len);
}
class CacheIoLock {
public:
explicit CacheIoLock(SemaphoreHandle_t mutex) : mutex(mutex) {
if (mutex) xSemaphoreTakeRecursive(mutex, portMAX_DELAY);
}
~CacheIoLock() {
if (mutex) xSemaphoreGiveRecursive(mutex);
}
private:
SemaphoreHandle_t mutex;
};
} // namespace
BookMetadataCache::~BookMetadataCache() {
if (ioMutex) {
vSemaphoreDelete(ioMutex);
ioMutex = nullptr;
}
}
/* ============= WRITING / BUILDING FUNCTIONS ================ */
bool BookMetadataCache::beginWrite() {
@@ -375,6 +414,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
/* ============= READING / LOADING FUNCTIONS ================ */
bool BookMetadataCache::load() {
CacheIoLock ioLock(ioMutex);
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
return false;
}
@@ -392,11 +432,13 @@ bool BookMetadataCache::load() {
serialization::readPod(bookFile, spineCount);
serialization::readPod(bookFile, tocCount);
serialization::readString(bookFile, coreMetadata.title);
serialization::readString(bookFile, coreMetadata.author);
serialization::readString(bookFile, coreMetadata.language);
serialization::readString(bookFile, coreMetadata.coverItemHref);
serialization::readString(bookFile, coreMetadata.textReferenceHref);
if (!readStringBounded(bookFile, coreMetadata.title) || !readStringBounded(bookFile, coreMetadata.author) ||
!readStringBounded(bookFile, coreMetadata.language) || !readStringBounded(bookFile, coreMetadata.coverItemHref) ||
!readStringBounded(bookFile, coreMetadata.textReferenceHref)) {
LOG_ERR("BMC", "Invalid cache metadata strings");
bookFile.close();
return false;
}
loaded = true;
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
@@ -404,6 +446,7 @@ bool BookMetadataCache::load() {
}
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
CacheIoLock ioLock(ioMutex);
if (!loaded) {
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
return {};
@@ -418,11 +461,16 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
uint32_t spineEntryPos;
serialization::readPod(bookFile, spineEntryPos);
if (spineEntryPos >= bookFile.size()) {
LOG_ERR("BMC", "Spine entry offset out of range: %lu", static_cast<unsigned long>(spineEntryPos));
return {};
}
bookFile.seek(spineEntryPos);
return readSpineEntry(bookFile);
}
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
CacheIoLock ioLock(ioMutex);
if (!loaded) {
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
return {};
@@ -437,24 +485,34 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
uint32_t tocEntryPos;
serialization::readPod(bookFile, tocEntryPos);
if (tocEntryPos >= bookFile.size()) {
LOG_ERR("BMC", "TOC entry offset out of range: %lu", static_cast<unsigned long>(tocEntryPos));
return {};
}
bookFile.seek(tocEntryPos);
return readTocEntry(bookFile);
}
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
SpineEntry entry;
serialization::readString(file, entry.href);
serialization::readPod(file, entry.cumulativeSize);
serialization::readPod(file, entry.tocIndex);
if (!readStringBounded(file, entry.href) ||
file.read(&entry.cumulativeSize, sizeof(entry.cumulativeSize)) !=
static_cast<int>(sizeof(entry.cumulativeSize)) ||
file.read(&entry.tocIndex, sizeof(entry.tocIndex)) != static_cast<int>(sizeof(entry.tocIndex))) {
LOG_ERR("BMC", "Invalid spine cache entry");
return {};
}
return entry;
}
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
TocEntry entry;
serialization::readString(file, entry.title);
serialization::readString(file, entry.href);
serialization::readString(file, entry.anchor);
serialization::readPod(file, entry.level);
serialization::readPod(file, entry.spineIndex);
if (!readStringBounded(file, entry.title) || !readStringBounded(file, entry.href) ||
!readStringBounded(file, entry.anchor) ||
file.read(&entry.level, sizeof(entry.level)) != static_cast<int>(sizeof(entry.level)) ||
file.read(&entry.spineIndex, sizeof(entry.spineIndex)) != static_cast<int>(sizeof(entry.spineIndex))) {
LOG_ERR("BMC", "Invalid TOC cache entry");
return {};
}
return entry;
}
+10 -2
View File
@@ -1,6 +1,7 @@
#pragma once
#include <HalStorage.h>
#include <freertos/semphr.h>
#include <algorithm>
#include <deque>
@@ -54,6 +55,7 @@ class BookMetadataCache {
// Temp file handles during build
HalFile spineFile;
HalFile tocFile;
SemaphoreHandle_t ioMutex;
// Index for fast href→spineIndex lookup (used only for large EPUBs)
struct SpineHrefIndexEntry {
@@ -85,8 +87,14 @@ class BookMetadataCache {
BookMetadata coreMetadata;
explicit BookMetadataCache(std::string cachePath)
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
~BookMetadataCache() = default;
: cachePath(std::move(cachePath)),
lutOffset(0),
spineCount(0),
tocCount(0),
loaded(false),
buildMode(false),
ioMutex(xSemaphoreCreateRecursiveMutex()) {}
~BookMetadataCache();
// Building phase (stream to disk immediately)
bool beginWrite();
@@ -150,7 +150,6 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
self->state = IN_GUIDE;
// TODO Remove print
LOG_DBG("COF", "Entering guide state.");
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
+96 -10
View File
@@ -3,6 +3,7 @@
#include <BidiUtils.h>
#include <FontDecompressor.h>
#include <HalGPIO.h>
#include <Icon.h>
#include <Logging.h>
#include <SdCardFont.h>
#include <Utf8.h>
@@ -100,6 +101,23 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
}
}
void GfxRenderer::setUiFontRemap(const int* from, const int* to, int count) {
if (count < 0) count = 0;
if (count > MAX_UI_FONT_REMAP) count = MAX_UI_FONT_REMAP;
for (int i = 0; i < count; ++i) {
uiFontFrom_[i] = from[i];
uiFontTo_[i] = to[i];
}
uiFontRemapCount_ = count;
}
int GfxRenderer::remapUiFont(const int fontId) const {
for (int i = 0; i < uiFontRemapCount_; ++i) {
if (uiFontFrom_[i] == fontId) return uiFontTo_[i];
}
return fontId;
}
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
// This should always be inlined for better performance
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
@@ -365,7 +383,7 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontF
return 0;
}
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
@@ -407,7 +425,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
return;
}
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return;
@@ -1058,6 +1076,21 @@ void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, con
display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width);
}
void GfxRenderer::drawIcon(const freeink::Icon& icon, const int x, const int y, const bool black) const {
// Bits are un-rotated, so each drawn pixel goes through drawPixel (which applies
// the orientation transform) — correct in every orientation, unlike the legacy
// uint8_t* overload that pre-rotates for one orientation.
const int rowBytes = (icon.w + 7) / 8;
for (int row = 0; row < icon.h; ++row) {
const uint8_t* r = icon.bits + static_cast<int>(row) * rowBytes;
for (int col = 0; col < icon.w; ++col) {
if (((r[col >> 3] >> (7 - (col & 7))) & 1) == 0) { // 0 = drawn
drawPixel(x + col, y + row, black);
}
}
}
}
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
const float cropX, const float cropY) const {
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
@@ -1466,6 +1499,38 @@ int GfxRenderer::getScreenHeight() const {
return panelWidth;
}
void GfxRenderer::tapToLogical(float nx, float ny, int& outX, int& outY) const {
// Native panel pixel of the tap (wasTouchTap normalizes over the native panel).
int phyX = static_cast<int>(nx * panelWidth);
int phyY = static_cast<int>(ny * panelHeight);
if (phyX < 0) phyX = 0;
if (phyX > panelWidth - 1) phyX = panelWidth - 1;
if (phyY < 0) phyY = 0;
if (phyY > panelHeight - 1) phyY = panelHeight - 1;
// Inverse of rotateCoordinates() (see the forward transform above): map a
// physical/native point back into the current orientation's logical frame.
switch (orientation) {
case Portrait: // forward: phyX=logY, phyY=panelHeight-1-logX
outX = panelHeight - 1 - phyY;
outY = phyX;
break;
case PortraitInverted: // forward: phyX=panelWidth-1-logY, phyY=logX
outX = phyY;
outY = panelWidth - 1 - phyX;
break;
case LandscapeClockwise: // forward: phyX=panelWidth-1-logX, phyY=panelHeight-1-logY
outX = panelWidth - 1 - phyX;
outY = panelHeight - 1 - phyY;
break;
case LandscapeCounterClockwise: // forward: identity
default:
outX = phyX;
outY = phyY;
break;
}
}
// Translate a logical rect through rotateCoordinates and take the bounding
// box of its four corners on the physical panel. Output coords are inclusive
// and clamped. Returns false if the rect ends up fully off-panel.
@@ -1557,7 +1622,7 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
}
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
@@ -1578,7 +1643,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
}
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) return 0;
const auto& font = fontIt->second;
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
@@ -1592,7 +1657,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
const EpdFontFamily::Style style) const {
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) return 0;
const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
@@ -1624,7 +1689,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
return fp4::toPixel(widthFP);
}
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
@@ -1660,7 +1725,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
}
int GfxRenderer::getFontAscenderSize(const int fontId) const {
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
@@ -1670,7 +1735,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
}
int GfxRenderer::getLineHeight(const int fontId) const {
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
@@ -1680,7 +1745,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
}
int GfxRenderer::getTextHeight(const int fontId) const {
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return 0;
@@ -1688,6 +1753,27 @@ int GfxRenderer::getTextHeight(const int fontId) const {
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
}
// Height of a reference glyph above the baseline (glyph->top): 'H' gives the real
// cap height, the actual visible font extent for vertical centering.
int GfxRenderer::glyphTop(const int fontId, const uint32_t cp) const {
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) return 0;
const EpdGlyph* g = fontIt->second.getGlyph(cp, EpdFontFamily::REGULAR);
return g ? g->top : 0;
}
int GfxRenderer::getFontCapHeight(const int fontId) const { return glyphTop(fontId, 'H'); }
int GfxRenderer::getTextVisualCenterOffset(const int fontId) const {
// Cap-height middle: baseline is at top + ascender, caps reach capHeight above it,
// so the optical center is ascender - capHeight/2 below the top. Cap (not x) height
// because UI labels are capital-led. Falls back to ascender*0.65 without an 'H'.
const int ascender = getFontAscenderSize(fontId);
const int capHeight = getFontCapHeight(fontId);
if (capHeight <= 0) return (ascender * 65) / 100;
return ascender - capHeight / 2;
}
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
const EpdFontFamily::Style style) const {
// Cannot draw a NULL / empty string
@@ -1695,7 +1781,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
return;
}
const auto fontIt = fontMap.find(fontId);
const auto fontIt = fontMap.find(remapUiFont(fontId));
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return;
+29
View File
@@ -13,6 +13,9 @@ enum class BidiBaseDir : signed char { AUTO = -1, LTR = 0, RTL = 1 };
class FontCacheManager;
class SdCardFont;
namespace freeink {
struct Icon;
}
#include <cstring>
#include <map>
@@ -51,6 +54,13 @@ class GfxRenderer {
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
std::vector<uint8_t*> bwBufferChunks;
std::map<int, EpdFontFamily> fontMap;
// UI chrome font remap table (see setUiFontRemap). Empty = identity.
static constexpr int MAX_UI_FONT_REMAP = 8;
int uiFontFrom_[MAX_UI_FONT_REMAP] = {0};
int uiFontTo_[MAX_UI_FONT_REMAP] = {0};
int uiFontRemapCount_ = 0;
int remapUiFont(int fontId) const;
int glyphTop(int fontId, uint32_t cp) const;
// Mutable because ensureSdCardFontReady() is const (called from layout code
// that holds a const GfxRenderer&) but triggers SD card reads and heap
// allocation inside the SdCardFont objects. Same pragmatic compromise as
@@ -101,6 +111,14 @@ class GfxRenderer {
// Setup
void begin(); // must be called right after display.begin()
void insertFont(int fontId, EpdFontFamily font);
// Orientation-correct icon blit for the freeink::Icon format (un-rotated bits +
// optical-center metadata). Prefer this over the legacy raw-bitmap drawIcon.
void drawIcon(const freeink::Icon& icon, int x, int y, bool black = true) const;
// UI chrome font scaling: firmware supplies a small remap table (from the board
// uiScale) that substitutes a larger font for each scaled UI font id at lookup
// time, so layout and drawing stay consistent with no call-site changes. Reader
// body fonts aren't in the table, so book text is unaffected. count <= 8.
void setUiFontRemap(const int* from, const int* to, int count);
// Clears both the flash-font map and any SD-font registration for fontId.
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
// the underlying SdCardFont and forget the SD-side unregister.
@@ -141,6 +159,11 @@ class GfxRenderer {
void clearScreen(uint8_t color = 0xFF) const;
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
// Map a touch tap (normalized 0..1 in panel-native orientation, from
// InputManager::wasTouchTap) to logical screen coordinates matching the Rects the
// UI draws in. Inverse of rotateCoordinates() for the current orientation.
void tapToLogical(float nx, float ny, int& outX, int& outY) const;
// Tiled grayscale strip target. While active, drawPixel() and clearScreen()
// operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical
// rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels
@@ -208,6 +231,12 @@ class GfxRenderer {
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const;
// Cap height (from the 'H' glyph), the real visible font extent for vertical
// alignment that follows the font instead of a guessed ascender fraction.
int getFontCapHeight(int fontId) const;
// Offset from text top (drawText's y) to the text's optical center; align an
// element to a line via centerY = textTop + getTextVisualCenterOffset(fontId).
int getTextVisualCenterOffset(int fontId) const;
std::string truncatedText(int fontId, const char* text, int maxWidth,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
+1
View File
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Згладжванне тэксту"
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
STR_ORIENTATION: "Арыентацыя чытання"
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
STR_TOUCH_READER_CONTROLS: "Сэнсарнае кіраванне чытаннем"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Suprimir"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
+1
View File
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Vyhlazování textu"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
STR_TOUCH_READER_CONTROLS: "Dotykové ovládání čtečky"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientovat přední tlačítka"
STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Skjul"
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
STR_ORIENTATION: "Læseretning"
STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)"
STR_TOUCH_READER_CONTROLS: "Touch-betjening (læser)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientér forreste knapper"
STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Verbergen"
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
STR_ORIENTATION: "Leesstand"
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
STR_TOUCH_READER_CONTROLS: "Aanraakbediening lezer"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Richt voorste knoppen"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Suppress"
STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_TOUCH_READER_CONTROLS: "Touch Reader Controls"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orient front buttons"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
+1
View File
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Tekstin reunanpehmennys"
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
STR_ORIENTATION: "Lukusuunta"
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
STR_TOUCH_READER_CONTROLS: "Kosketusohjaus (lukija)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Suuntaa etupainikkeet"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Masquer"
STR_SHORT_PWR_BTN: "Appui court alim."
STR_ORIENTATION: "Orientation de lecture"
STR_SIDE_BTN_LAYOUT: "Boutons latéraux"
STR_TOUCH_READER_CONTROLS: "Commandes tactiles (lecteur)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter boutons avant"
STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
+1
View File
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Schriftglättung"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_TOUCH_READER_CONTROLS: "Touch-Steuerung (Lesen)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
STR_LONG_PRESS_BEHAVIOR_OFF: "Aus"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "הסתר תמונות"
STR_SHORT_PWR_BTN: "לחיצה קצרה על כפתור ההפעלה"
STR_ORIENTATION: "כיוון קריאה (מסך)"
STR_SIDE_BTN_LAYOUT: "פריסת כפתורי צד (בקריאה)"
STR_TOUCH_READER_CONTROLS: "פקדי מגע בקריאה"
STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Elnyomás"
STR_SHORT_PWR_BTN: "Rövid bekapcsológomb nyomás"
STR_ORIENTATION: "Olvasási irány"
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
STR_TOUCH_READER_CONTROLS: "Érintőképernyős vezérlés (olvasó)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
STR_FONT_PREVIEW_TEXT: "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Nascondi"
STR_SHORT_PWR_BTN: "Press. breve pul. accensione"
STR_ORIENTATION: "Orientamento lettura"
STR_SIDE_BTN_LAYOUT: "Pul. laterali (lettore)"
STR_TOUCH_READER_CONTROLS: "Controlli touch (lettore)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienta pul. frontali"
STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
+1
View File
@@ -67,6 +67,7 @@ STR_TEXT_AA: "Мәтін сырғытпасы"
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
STR_ORIENTATION: "Оқу бағдары"
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
STR_TOUCH_READER_CONTROLS: "Сенсорлық басқару (оқырман)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Slėpti"
STR_SHORT_PWR_BTN: "Trumpas įjungimo pasp."
STR_ORIENTATION: "Orientacija"
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
STR_TOUCH_READER_CONTROLS: "Liečiamasis valdymas (skaityklė)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Pomijaj"
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
STR_ORIENTATION: "Układ czytania"
STR_SIDE_BTN_LAYOUT: "Układ przycisków bocznych"
STR_TOUCH_READER_CONTROLS: "Sterowanie dotykowe (czytnik)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuj przednie przyciski"
STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
+1
View File
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Suavização de texto"
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
STR_ORIENTATION: "Orientação de leitura"
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
STR_TOUCH_READER_CONTROLS: "Controlos táteis do leitor"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Suprimare"
STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător"
STR_ORIENTATION: "Orientare lectură"
STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (lectură)"
STR_TOUCH_READER_CONTROLS: "Control tactil (lectură)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientare butoane frontale"
STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Скрыть"
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
STR_ORIENTATION: "Ориентация чтения"
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
STR_TOUCH_READER_CONTROLS: "Сенсорное управление чтением"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентировать передние кнопки"
STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
+374 -373
View File
@@ -1,381 +1,382 @@
_language_name: "Slovenčina"
_language_code: "SK"
_order: "24"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPÚŠŤANIE"
STR_SLEEPING: "SPÁNOK"
STR_ENTERING_SLEEP: "Prechod do režimu spánku"
STR_BROWSE_FILES: "Prehliadať súbory"
STR_FILE_TRANSFER: "Prenos súborov"
STR_SETTINGS_TITLE: "Nastavenia"
STR_CONTINUE_READING: "Pokračovať v čítaní"
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
STR_START_READING: "Začnite čítať nižšie"
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
STR_SELECT_CHAPTER: "Vybrať kapitolu"
STR_NO_CHAPTERS: "Žiadne kapitoly"
STR_END_OF_BOOK: "Koniec knihy"
STR_EMPTY_CHAPTER: "Prázdna kapitola"
STR_INDEXING: "Indexovanie"
STR_MEMORY_ERROR: "Chyba pamäte"
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
STR_EMPTY_FILE: "Prázdny súbor"
STR_OUT_OF_BOUNDS: "Mimo rozsahu"
STR_LOADING: "Načítava sa..."
STR_LOADING_POPUP: "Načítavanie"
STR_WIFI_NETWORKS: "Wi-Fi siete"
STR_NO_NETWORKS: "Nenašli sa žiadne siete"
STR_NETWORKS_FOUND: "Nájdených %zu sietí"
STR_SCANNING: "Skenovanie..."
STR_CONNECTING: "Pripájanie..."
STR_CONNECTED: "Pripojené!"
STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
STR_FORGET_NETWORK: "Zabudnúť sieť?"
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
STR_JOIN_NETWORK: "Pripojiť sa k sieti"
STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti"
STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní"
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti"
STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači"
STR_OR_HTTP_PREFIX: "alebo http://"
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
STR_TO_PREFIX: "pre"
STR_CALIBRE_RECEIVING: "Prijímanie:"
STR_CALIBRE_RECEIVED: "Prijaté:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čítačka"
STR_CAT_CONTROLS: "Ovládanie"
STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
STR_SLEEP_COVER_MODE: "Obrazovka spánku režim krytu"
STR_HIDE_BATTERY: "Skryť % batérie"
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
STR_TEXT_AA: "Vyhladzovanie textu"
STR_IMAGES: "Obrázky"
STR_IMAGES_DISPLAY: "Zobraz"
STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
STR_IMAGES_SUPPRESS: "Potlačiť"
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
STR_ORIENTATION: "Orientácia čítania"
STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii"
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
STR_FONT_FAMILY: "Rodina písiem čítačky"
STR_FONT_SIZE: "Veľkosť písma rozhrania"
STR_LINE_SPACING: "Riadkovanie čítačky"
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
STR_HYPHENATION: "Delenie slov"
STR_TIME_TO_SLEEP: "Čas do uspania"
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
STR_REFRESH_FREQ: "Frekvencia obnovovania"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
STR_LANGUAGE: "Jazyk"
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
STR_USERNAME: "Používateľské meno"
STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
STR_AUTHENTICATE: "Overiť"
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
STR_KOREADER_PASSWORD: "Heslo KOReader"
STR_FILENAME: "Názov súboru"
STR_BINARY: "Binárny"
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
STR_AUTHENTICATING: "Overovanie..."
STR_AUTH_SUCCESS: "Overenie úspešné!"
STR_KOREADER_AUTH: "Overenie KOReader"
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
STR_AUTH_FAILED: "Overenie zlyhalo"
STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
STR_ITEMS_REMOVED: "položiek odstránených"
STR_FAILED_LOWER: "zlyhalo"
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
STR_DARK: "Tmavý"
STR_LIGHT: "Svetlý"
STR_CUSTOM: "Vlastný"
STR_COVER: "Obálka"
STR_NONE_OPT: "Žiadny"
STR_FIT: "Prispôsobiť"
STR_CROP: "Orezať"
STR_NEVER: "Nikdy"
STR_IN_READER: "V čítačke"
STR_ALWAYS: "Vždy"
STR_IGNORE: "Ignorovať"
STR_SLEEP: "Spánok"
STR_PAGE_TURN: "Otáčanie stránok"
STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
_language_name: "Slovenčina"
_language_code: "SK"
_order: "24"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPÚŠŤANIE"
STR_SLEEPING: "SPÁNOK"
STR_ENTERING_SLEEP: "Prechod do režimu spánku"
STR_BROWSE_FILES: "Prehliadať súbory"
STR_FILE_TRANSFER: "Prenos súborov"
STR_SETTINGS_TITLE: "Nastavenia"
STR_CONTINUE_READING: "Pokračovať v čítaní"
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
STR_START_READING: "Začnite čítať nižšie"
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
STR_SELECT_CHAPTER: "Vybrať kapitolu"
STR_NO_CHAPTERS: "Žiadne kapitoly"
STR_END_OF_BOOK: "Koniec knihy"
STR_EMPTY_CHAPTER: "Prázdna kapitola"
STR_INDEXING: "Indexovanie"
STR_MEMORY_ERROR: "Chyba pamäte"
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
STR_EMPTY_FILE: "Prázdny súbor"
STR_OUT_OF_BOUNDS: "Mimo rozsahu"
STR_LOADING: "Načítava sa..."
STR_LOADING_POPUP: "Načítavanie"
STR_WIFI_NETWORKS: "Wi-Fi siete"
STR_NO_NETWORKS: "Nenašli sa žiadne siete"
STR_NETWORKS_FOUND: "Nájdených %zu sietí"
STR_SCANNING: "Skenovanie..."
STR_CONNECTING: "Pripájanie..."
STR_CONNECTED: "Pripojené!"
STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
STR_FORGET_NETWORK: "Zabudnúť sieť?"
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
STR_JOIN_NETWORK: "Pripojiť sa k sieti"
STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti"
STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní"
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti"
STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači"
STR_OR_HTTP_PREFIX: "alebo http://"
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
STR_TO_PREFIX: "pre"
STR_CALIBRE_RECEIVING: "Prijímanie:"
STR_CALIBRE_RECEIVED: "Prijaté:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čítačka"
STR_CAT_CONTROLS: "Ovládanie"
STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
STR_SLEEP_COVER_MODE: "Obrazovka spánku režim krytu"
STR_HIDE_BATTERY: "Skryť % batérie"
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
STR_TEXT_AA: "Vyhladzovanie textu"
STR_IMAGES: "Obrázky"
STR_IMAGES_DISPLAY: "Zobraz"
STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
STR_IMAGES_SUPPRESS: "Potlačiť"
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
STR_ORIENTATION: "Orientácia čítania"
STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)"
STR_TOUCH_READER_CONTROLS: "Dotykové ovládanie čítačky"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii"
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
STR_FONT_FAMILY: "Rodina písiem čítačky"
STR_FONT_SIZE: "Veľkosť písma rozhrania"
STR_LINE_SPACING: "Riadkovanie čítačky"
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
STR_HYPHENATION: "Delenie slov"
STR_TIME_TO_SLEEP: "Čas do uspania"
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
STR_REFRESH_FREQ: "Frekvencia obnovovania"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
STR_LANGUAGE: "Jazyk"
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
STR_USERNAME: "Používateľské meno"
STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
STR_AUTHENTICATE: "Overiť"
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
STR_KOREADER_PASSWORD: "Heslo KOReader"
STR_FILENAME: "Názov súboru"
STR_BINARY: "Binárny"
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
STR_AUTHENTICATING: "Overovanie..."
STR_AUTH_SUCCESS: "Overenie úspešné!"
STR_KOREADER_AUTH: "Overenie KOReader"
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
STR_AUTH_FAILED: "Overenie zlyhalo"
STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
STR_ITEMS_REMOVED: "položiek odstránených"
STR_FAILED_LOWER: "zlyhalo"
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
STR_DARK: "Tmavý"
STR_LIGHT: "Svetlý"
STR_CUSTOM: "Vlastný"
STR_COVER: "Obálka"
STR_NONE_OPT: "Žiadny"
STR_FIT: "Prispôsobiť"
STR_CROP: "Orezať"
STR_NEVER: "Nikdy"
STR_IN_READER: "V čítačke"
STR_ALWAYS: "Vždy"
STR_IGNORE: "Ignorovať"
STR_SLEEP: "Spánok"
STR_PAGE_TURN: "Otáčanie stránok"
STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
STR_INVERTED: "Invertovaný"
STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
STR_DISABLED: "Vypnuté"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malý"
STR_MEDIUM: "Stredný"
STR_LARGE: "Veľký"
STR_X_LARGE: "Obrovský"
STR_TIGHT: "Tesný"
STR_NORMAL: "Normálny"
STR_WIDE: "Široký"
STR_JUSTIFY: "Zarovnať do bloku"
STR_ALIGN_LEFT: "Vľavo"
STR_CENTER: "Na stred"
STR_ALIGN_RIGHT: "Vpravo"
STR_PAGES_1: "1 strana"
STR_PAGES_5: "5 strán"
STR_PAGES_10: "10 strán"
STR_PAGES_15: "15 strán"
STR_PAGES_30: "30 strán"
STR_UPDATE: "Aktualizácia"
STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
STR_CURRENT_VERSION: "Aktuálna verzia:"
STR_NEW_VERSION: "Nová verzia:"
STR_UPDATING: "Aktualizácia..."
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
STR_UPDATE_FAILED: "Aktualizácia zlyhala"
STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
STR_DOWNLOADING: "Sťahovanie..."
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepomenované"
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
STR_DISABLED: "Vypnuté"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malý"
STR_MEDIUM: "Stredný"
STR_LARGE: "Veľký"
STR_X_LARGE: "Obrovský"
STR_TIGHT: "Tesný"
STR_NORMAL: "Normálny"
STR_WIDE: "Široký"
STR_JUSTIFY: "Zarovnať do bloku"
STR_ALIGN_LEFT: "Vľavo"
STR_CENTER: "Na stred"
STR_ALIGN_RIGHT: "Vpravo"
STR_PAGES_1: "1 strana"
STR_PAGES_5: "5 strán"
STR_PAGES_10: "10 strán"
STR_PAGES_15: "15 strán"
STR_PAGES_30: "30 strán"
STR_UPDATE: "Aktualizácia"
STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
STR_CURRENT_VERSION: "Aktuálna verzia:"
STR_NEW_VERSION: "Nová verzia:"
STR_UPDATING: "Aktualizácia..."
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
STR_UPDATE_FAILED: "Aktualizácia zlyhala"
STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
STR_DOWNLOADING: "Sťahovanie..."
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepomenované"
STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
STR_NEXT_PAGE: "Ďaľsia strana »"
STR_PREV_PAGE: "« Predchádzajúca str."
STR_NETWORK_PREFIX: "Sieť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
STR_SD_CARD: "SD karta"
STR_BACK: "« Späť"
STR_EXIT: "« Koniec"
STR_HOME: "« Domov"
STR_SELECT: "Vybrať"
STR_SELECTED: "Vybrané"
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
STR_NEXT_PAGE: "Ďaľsia strana »"
STR_PREV_PAGE: "« Predchádzajúca str."
STR_NETWORK_PREFIX: "Sieť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
STR_SD_CARD: "SD karta"
STR_BACK: "« Späť"
STR_EXIT: "« Koniec"
STR_HOME: "« Domov"
STR_SELECT: "Vybrať"
STR_SELECTED: "Vybrané"
STR_TOGGLE: "Prepnúť"
STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
STR_CONFIRM: "Potvrdiť"
STR_CANCEL: "Zrušiť"
STR_CONNECT: "Pripojiť"
STR_OPEN: "Otvoriť"
STR_DOWNLOAD: "Stiahnuť"
STR_RETRY: "Skúsiť znova"
STR_YES: "Áno"
STR_NO: "Nie"
STR_SHOW: "Zobraz"
STR_HIDE: "Skryť"
STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP"
STR_NOT_SET: "Nenastavené"
STR_DIR_LEFT: "Vľavo"
STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Hore"
STR_DIR_DOWN: "Dole"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
STR_PROGRESS_BAR: "Ukazovateľ čítania"
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
STR_PROGRESS_BAR_THIN: "Tenký"
STR_PROGRESS_BAR_MEDIUM: "Stredný"
STR_PROGRESS_BAR_THICK: "Hrubý"
STR_BOOK: "Kniha"
STR_CHAPTER: "Kapitola"
STR_EXAMPLE_CHAPTER: "Kapitola 21"
STR_EXAMPLE_BOOK: "Názov knihy"
STR_PREVIEW: "Ukážka"
STR_TITLE: "Názov"
STR_BATTERY: "Batéria"
STR_XTC_STATUS_BAR: "Stavový panel XTC"
STR_BOTTOM: "Dole"
STR_TOP: "Hore"
STR_CLOCK: "Hodiny"
STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
STR_CLOCK_FORMAT: "Formát času"
STR_CLOCK_FORMAT_24H: "24-hodinový"
STR_CLOCK_FORMAT_12H: "12-hodinový"
STR_CURRENT_TIME: "Aktuálny čas:"
STR_NEXT_FIELD: "Ďalej"
STR_CLOCK_SYNC: "Synchronizácia hodín"
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova."
STR_CLOCK_SYNCED: "Hodiny synchronizované"
STR_UI_THEME: "Téma rozhrania"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
STR_CONFIRM: "Potvrdiť"
STR_CANCEL: "Zrušiť"
STR_CONNECT: "Pripojiť"
STR_OPEN: "Otvoriť"
STR_DOWNLOAD: "Stiahnuť"
STR_RETRY: "Skúsiť znova"
STR_YES: "Áno"
STR_NO: "Nie"
STR_SHOW: "Zobraz"
STR_HIDE: "Skryť"
STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP"
STR_NOT_SET: "Nenastavené"
STR_DIR_LEFT: "Vľavo"
STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Hore"
STR_DIR_DOWN: "Dole"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
STR_PROGRESS_BAR: "Ukazovateľ čítania"
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
STR_PROGRESS_BAR_THIN: "Tenký"
STR_PROGRESS_BAR_MEDIUM: "Stredný"
STR_PROGRESS_BAR_THICK: "Hrubý"
STR_BOOK: "Kniha"
STR_CHAPTER: "Kapitola"
STR_EXAMPLE_CHAPTER: "Kapitola 21"
STR_EXAMPLE_BOOK: "Názov knihy"
STR_PREVIEW: "Ukážka"
STR_TITLE: "Názov"
STR_BATTERY: "Batéria"
STR_XTC_STATUS_BAR: "Stavový panel XTC"
STR_BOTTOM: "Dole"
STR_TOP: "Hore"
STR_CLOCK: "Hodiny"
STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
STR_CLOCK_FORMAT: "Formát času"
STR_CLOCK_FORMAT_24H: "24-hodinový"
STR_CLOCK_FORMAT_12H: "12-hodinový"
STR_CURRENT_TIME: "Aktuálny čas:"
STR_NEXT_FIELD: "Ďalej"
STR_CLOCK_SYNC: "Synchronizácia hodín"
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova."
STR_CLOCK_SYNCED: "Hodiny synchronizované"
STR_UI_THEME: "Téma rozhrania"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
STR_BOOKMARKS: "Záložky"
STR_BOOKMARK_ADDED: "Záložka pridaná."
STR_BOOKMARK_REMOVED: "Záložka odstránená."
STR_OPDS_BROWSER: "Prehliadač OPDS"
STR_SEARCH: "Hľadať"
STR_COVER_CUSTOM: "Obálka + Vlastné"
STR_QUICK_RESUME: "Rýchle obnovenie"
STR_MENU_RECENT_BOOKS: "Nedávne knihy"
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
STR_FORGET_BUTTON: "Zabudnúť"
STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
STR_CALIBRE_SETUP: "Nastavenie"
STR_CALIBRE_STATUS: "Stav"
STR_CLEAR_BUTTON: "Vymazať"
STR_DEFAULT_VALUE: "Predvolené"
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
STR_UNASSIGNED: "Nepriradené"
STR_ALREADY_ASSIGNED: "Už priradené"
STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie"
STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie"
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
STR_GO_TO_PERCENT: "Prejsť na %"
STR_GO_HOME_BUTTON: "Prejsť na Domov"
STR_SYNC_PROGRESS: "Priebeh synchronizácie"
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
STR_DELETE: "Vymazať"
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
STR_DISPLAY_QR: "Zobraz stránku ako QR"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "strán |"
STR_BOOK_PREFIX: "Kniha:"
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
STR_SYNCING_TIME: "Čas synchronizácie..."
STR_CALC_HASH: "Výpočet hashu dokumentu..."
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
STR_PROGRESS_FOUND: "Priebeh nájdený!"
STR_REMOTE_LABEL: "Vzdialený:"
STR_LOCAL_LABEL: "Lokálny:"
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
STR_SECTION_PREFIX: "Sekcia"
STR_UPLOAD: "Nahrať"
STR_BOOK_S_STYLE: "Štýl knihy"
STR_EMBEDDED_STYLE: "Vložený štýl"
STR_FOCUS_READING: "Sústredené čítanie"
STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
STR_SET_SLEEP_COVER: "Nastav obal"
STR_FOOTNOTES: "Poznámky pod čiarou"
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
STR_ADD_SERVER: "Pridať server"
STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem"
STR_FONT_BROWSER: "Prehliadač písiem"
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
STR_FONT_INSTALLED: "Písmo nainštalované!"
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
STR_INSTALLED: "Nainštalované"
STR_DOWNLOAD_ALL: "Stiahnuť všetko"
STR_UPDATE_ALL: "Aktualizovať všetko"
STR_UPDATE_AVAILABLE: "Aktualizovať"
STR_CRASH_TITLE: "Zlyhanie systému"
STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby."
STR_CRASH_REASON: "Dôvod zlyhania:"
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
STR_TILT_PAGE_TURN: "Otáčanie strán naklonením"
STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora"
STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora"
STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla"
STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky"
STR_KB_TIPS: "Tipy:"
STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici"
STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL"
STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu"
STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak"
STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak"
STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak"
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru"
STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu"
STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý"
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
STR_RECOVERY_MODE: "Režim obnovenia"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
STR_OPDS_BROWSER: "Prehliadač OPDS"
STR_SEARCH: "Hľadať"
STR_COVER_CUSTOM: "Obálka + Vlastné"
STR_QUICK_RESUME: "Rýchle obnovenie"
STR_MENU_RECENT_BOOKS: "Nedávne knihy"
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
STR_FORGET_BUTTON: "Zabudnúť"
STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
STR_CALIBRE_SETUP: "Nastavenie"
STR_CALIBRE_STATUS: "Stav"
STR_CLEAR_BUTTON: "Vymazať"
STR_DEFAULT_VALUE: "Predvolené"
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
STR_UNASSIGNED: "Nepriradené"
STR_ALREADY_ASSIGNED: "Už priradené"
STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie"
STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie"
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
STR_GO_TO_PERCENT: "Prejsť na %"
STR_GO_HOME_BUTTON: "Prejsť na Domov"
STR_SYNC_PROGRESS: "Priebeh synchronizácie"
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
STR_DELETE: "Vymazať"
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
STR_DISPLAY_QR: "Zobraz stránku ako QR"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "strán |"
STR_BOOK_PREFIX: "Kniha:"
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
STR_SYNCING_TIME: "Čas synchronizácie..."
STR_CALC_HASH: "Výpočet hashu dokumentu..."
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
STR_PROGRESS_FOUND: "Priebeh nájdený!"
STR_REMOTE_LABEL: "Vzdialený:"
STR_LOCAL_LABEL: "Lokálny:"
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
STR_SECTION_PREFIX: "Sekcia"
STR_UPLOAD: "Nahrať"
STR_BOOK_S_STYLE: "Štýl knihy"
STR_EMBEDDED_STYLE: "Vložený štýl"
STR_FOCUS_READING: "Sústredené čítanie"
STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
STR_SET_SLEEP_COVER: "Nastav obal"
STR_FOOTNOTES: "Poznámky pod čiarou"
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
STR_ADD_SERVER: "Pridať server"
STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem"
STR_FONT_BROWSER: "Prehliadač písiem"
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
STR_FONT_INSTALLED: "Písmo nainštalované!"
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
STR_INSTALLED: "Nainštalované"
STR_DOWNLOAD_ALL: "Stiahnuť všetko"
STR_UPDATE_ALL: "Aktualizovať všetko"
STR_UPDATE_AVAILABLE: "Aktualizovať"
STR_CRASH_TITLE: "Zlyhanie systému"
STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby."
STR_CRASH_REASON: "Dôvod zlyhania:"
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
STR_TILT_PAGE_TURN: "Otáčanie strán naklonením"
STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora"
STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora"
STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla"
STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky"
STR_KB_TIPS: "Tipy:"
STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici"
STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL"
STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu"
STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak"
STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak"
STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak"
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru"
STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu"
STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý"
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
STR_RECOVERY_MODE: "Režim obnovenia"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Zatdi"
STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop"
STR_ORIENTATION: "Orientacija branja"
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
STR_TOUCH_READER_CONTROLS: "Dotikalni nadzor (bralnik)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
+1
View File
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Ocultar"
STR_SHORT_PWR_BTN: "Toque corto del encendido"
STR_ORIENTATION: "Orientación"
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
STR_TOUCH_READER_CONTROLS: "Controles táctiles (lector)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botones frontales"
STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Dölj"
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
STR_ORIENTATION: "Läsrikting"
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
STR_TOUCH_READER_CONTROLS: "Pekkontroller (läsare)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Rikta främre knappar"
STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
+1
View File
@@ -67,6 +67,7 @@ STR_TEXT_AA: "Metin Yumuşatma (AA)"
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
STR_TOUCH_READER_CONTROLS: "Dokunmatik okuyucu kontrolleri"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Приховати"
STR_SHORT_PWR_BTN: "Короткий натиск кн. живл."
STR_ORIENTATION: "Орієнтація читання"
STR_SIDE_BTN_LAYOUT: "Схема бічних кнопок"
STR_TOUCH_READER_CONTROLS: "Сенсорне керування читанням"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Орієнтувати передні кнопки"
STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настику"
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Eliminar"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
+1
View File
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Ẩn đi"
STR_SHORT_PWR_BTN: "Nhấn nhanh nút nguồn"
STR_ORIENTATION: "Hướng đọc"
STR_SIDE_BTN_LAYOUT: "Bố trí nút bên (trình đọc)"
STR_TOUCH_READER_CONTROLS: "Điều khiển cảm ứng (trình đọc)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Xoay nút trước theo hướng"
STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ nút"
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
+13
View File
@@ -1,5 +1,8 @@
#include "Logging.h"
#include <BoardConfig.h>
#include <esp_rom_sys.h>
#include <string>
#define MAX_ENTRY_LEN 256
@@ -59,9 +62,19 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
}
}
va_end(args);
#if FREEINK_LOG_TRANSPORT == FREEINK_LOG_TRANSPORT_USB_CDC_WRITE
// Native USB CDC can report false while PlatformIO monitor is attached on
// boards like LilyGo T5 S3, so write directly to the CDC object.
logSerial.write(reinterpret_cast<const uint8_t*>(buf), strnlen(buf, sizeof(buf)));
#elif FREEINK_LOG_TRANSPORT == FREEINK_LOG_TRANSPORT_ROM_PRINTF
// IDF/ROM console path for boards whose monitor is attached there during
// bring-up, e.g. Sticky.
esp_rom_printf("%s", buf);
#else
if (logSerial) {
logSerial.print(buf);
}
#endif
addToLogRingBuffer(buf);
}
+4 -1
View File
@@ -27,7 +27,10 @@ won't trigger deprecation warnings.
#define LOG_LEVEL 0
#endif
static HWCDC& logSerial = Serial;
// The concrete Serial type differs by MCU: HWCDC (native USB CDC on C3/S3) vs
// HardwareSerial (UART0 on the classic ESP32 / M5Paper). Bind to whatever the
// real Serial is here — this is before the `#define Serial` shim below.
static decltype(Serial)& logSerial = Serial;
void logPrintf(const char* level, const char* origin, const char* format, ...);
+62 -1
View File
@@ -18,8 +18,11 @@ static uint8_t bcdToDec(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F);
static uint8_t decToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }
void HalClock::begin() {
_usesSdkRtc = false;
if (!gpio.deviceIsX3()) {
_available = false;
_available = _sdkRtc.begin();
_usesSdkRtc = _available;
LOG_INF("CLK", _available ? "SDK RTC found" : "RTC not found");
return;
}
@@ -57,6 +60,24 @@ bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const {
return true;
}
if (_usesSdkRtc) {
Rtc::DateTime dt;
if (!_sdkRtc.now(dt)) {
if (!_hasCachedTime) return false;
_lastPollMs = now;
hour = _cachedHour;
minute = _cachedMinute;
return true;
}
_cachedHour = dt.hour;
_cachedMinute = dt.minute;
_lastPollMs = now;
_hasCachedTime = true;
hour = _cachedHour;
minute = _cachedMinute;
return true;
}
// Read 3 bytes starting at register 0x00: seconds, minutes, hours
Wire.beginTransmission(I2C_ADDR_DS3231);
Wire.write(DS3231_SEC_REG);
@@ -131,6 +152,25 @@ bool HalClock::writeTimeToRTC(uint8_t hour, uint8_t minute, uint8_t second) {
assert(hour < 24);
assert(minute < 60);
assert(second < 60);
if (_usesSdkRtc) {
Rtc::DateTime dt;
dt.hour = hour;
dt.minute = minute;
dt.second = second;
dt.year = 2000;
dt.month = 1;
dt.day = 1;
if (!_sdkRtc.set(dt)) {
LOG_ERR("CLK", "Failed to write time to SDK RTC");
return false;
}
_lastPollMs = 0;
_cachedHour = hour;
_cachedMinute = minute;
_hasCachedTime = true;
return true;
}
Wire.beginTransmission(I2C_ADDR_DS3231);
Wire.write(DS3231_SEC_REG); // Start at register 0x00
Wire.write(decToBcd(second)); // 0x00: Seconds
@@ -168,6 +208,27 @@ bool HalClock::syncFromNTP() {
struct tm timeinfo;
gmtime_r(&now, &timeinfo);
if (_usesSdkRtc) {
Rtc::DateTime dt;
dt.year = static_cast<uint16_t>(timeinfo.tm_year + 1900);
dt.month = static_cast<uint8_t>(timeinfo.tm_mon + 1);
dt.day = static_cast<uint8_t>(timeinfo.tm_mday);
dt.hour = static_cast<uint8_t>(timeinfo.tm_hour);
dt.minute = static_cast<uint8_t>(timeinfo.tm_min);
dt.second = static_cast<uint8_t>(timeinfo.tm_sec);
dt.weekday = static_cast<uint8_t>(timeinfo.tm_wday);
if (_sdkRtc.set(dt)) {
_lastPollMs = 0;
_cachedHour = dt.hour;
_cachedMinute = dt.minute;
_hasCachedTime = true;
LOG_INF("CLK", "RTC set to %04u-%02u-%02u %02u:%02u:%02u UTC", dt.year, dt.month, dt.day, dt.hour, dt.minute,
dt.second);
return true;
}
return false;
}
if (writeTimeToRTC(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)) {
LOG_INF("CLK", "RTC set to %02d:%02d:%02d UTC", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
return true;
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include <Arduino.h>
#include <Rtc.h>
#include <Wire.h>
#include "HalGPIO.h"
@@ -10,6 +11,8 @@ extern HalClock halClock; // Singleton
class HalClock {
bool _available = false;
bool _usesSdkRtc = false;
mutable Rtc _sdkRtc;
mutable uint8_t _cachedHour = 0;
mutable uint8_t _cachedMinute = 0;
mutable bool _hasCachedTime = false;
+79 -17
View File
@@ -1,5 +1,6 @@
#include <HalGPIO.h>
#include <Logging.h>
#include <PowerManager.h>
#include <Preferences.h>
#include <SPI.h>
#include <Wire.h>
@@ -192,14 +193,44 @@ HalGPIO::DeviceType detectDeviceTypeWithFingerprint() {
void HalGPIO::begin() {
inputMgr.begin();
#if FREEINK_MCU_C3
// Claim the shared SPI bus with the X4/X3 display+SD pins. These EPD_* pin
// macros are hardcoded C3/Xteink values, so this pre-claim is only valid on the
// C3 family. On other boards (M5Paper's IT8951, Sticky's SSD1677, ...) the SDK
// driver and SDCardManager bring up SPI from BoardConfig::ACTIVE pins; pre-
// claiming here would stick (SPIClass::begin early-returns once the bus is
// started) and leave the display/SD on the wrong pins.
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
#endif
#if FREEINK_MCU_C3
_deviceType = detectDeviceTypeWithFingerprint();
// Sync the runtime board profile to the detected device NOW, before any I2C
// consumer begins. powerManager/clock/tilt begin() run immediately after this
// in setup() and rely on BoardConfig::ACTIVE: HalPowerManager only calls
// Wire.begin() when the active profile has an I2C gauge (gaugeAddr != 0), and
// the X3 clock/tilt raw-Wire paths assume that begin happened. The dual X3+X4
// binary boots as X4 (gaugeAddr 0), so without this the X4 profile stays active
// through those begins, Wire is never (re)initialised after the detection probe
// end()s it, and every X3 read fails with lock==NULL ("could not acquire lock"
// / "NULL TX buffer pointer"). FreeInkDisplay also calls selectDevice() later,
// but that is after the I2C consumers — too late. selectDevice() is idempotent.
BoardConfig::selectDevice(deviceIsX3() ? BoardConfig::Board::XteinkX3 : BoardConfig::Board::XteinkX4);
if (deviceIsX4()) {
pinMode(BAT_GPIO0, INPUT);
pinMode(UART0_RXD, INPUT);
}
#else
// Non-C3 boards (S3/ESP32) are single-device builds; their pins, panel, and
// battery backend all come from BoardConfig::ACTIVE. The X3/X4 fingerprint probe
// targets Xteink C3 hardware and drives Wire on X3_I2C_SDA/SCL (GPIO20/0) — which
// collides with this board's own I2C (e.g. Sticky's BQ27220 gauge shares GPIO0)
// and reconfigures strapping pins. Skip it; _deviceType stays X4 ("not X3"),
// the correct non-X3 branch for every deviceIsX3() consumer.
_deviceType = DeviceType::X4;
#endif
}
void HalGPIO::update() {
@@ -225,30 +256,55 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouchTap(nx, ny); }
bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); }
bool HalGPIO::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const {
return inputMgr.isTouchTapCandidate(nx, ny, heldMs);
}
unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); }
bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const {
return inputMgr.wasSwipe(nxStart, nyStart, nxEnd, nyEnd);
}
bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); }
bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); }
bool HalGPIO::isXteinkDevice() const {
const auto board = BoardConfig::ACTIVE.board;
return board == BoardConfig::Board::XteinkX3 || board == BoardConfig::Board::XteinkX4;
}
void HalGPIO::startDeepSleep() {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (inputMgr.isPressed(BTN_POWER)) {
delay(50);
inputMgr.update();
}
// Arm the wakeup trigger *after* the button is released
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
freeink::PowerManager::armPowerButtonWakeup();
freeink::PowerManager::deepSleep();
}
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
if (BoardConfig::ACTIVE.input.power < 0) {
// No readable power-button input pin: can't verify a hold, so don't sleep.
return;
}
#if defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER
// M5Paper: power-on is a hardware latch and the "power button" is the rotary
// push (G38), shared with Confirm. A USB/flash cold boot is indistinguishable
// from an intentional power-on hold here, so skip the X4-style anti-accidental-
// wake check and always boot. G38 still serves as the deep-sleep wake source.
return;
#endif
if (shortPressAllowed) {
// Fast path - no duration check needed
return;
}
// TODO: Intermittent edge case remains: a single tap followed by another single tap
// can still power on the device. Tighten wake debounce/state handling here.
// Calibrate: subtract boot time already elapsed, assuming button held since boot
const uint16_t calibration = millis();
const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
const auto start = millis();
inputMgr.update();
// inputMgr.isPressed() may take up to ~500ms to return correct state
@@ -257,11 +313,12 @@ void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
inputMgr.update();
}
if (inputMgr.isPressed(BTN_POWER)) {
const auto holdStart = millis();
do {
delay(10);
inputMgr.update();
} while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration);
if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) {
} while (inputMgr.isPressed(BTN_POWER) && millis() - holdStart < requiredDurationMs);
if (millis() - holdStart < requiredDurationMs) {
startDeepSleep();
}
} else {
@@ -282,8 +339,10 @@ bool HalGPIO::isUsbConnected() const {
}
return false;
}
// U0RXD/GPIO20 reads HIGH when USB is connected
return digitalRead(UART0_RXD) == HIGH;
if (BoardConfig::ACTIVE.usbDetect < 0) {
return false;
}
return digitalRead(BoardConfig::ACTIVE.usbDetect) == HIGH;
}
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
@@ -292,8 +351,11 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
const bool usbConnected = isUsbConnected();
if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) ||
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) {
if (resetReason == ESP_RST_DEEPSLEEP &&
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO || wakeupCause == ESP_SLEEP_WAKEUP_EXT1)) {
return WakeupReason::PowerButton;
}
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) {
return WakeupReason::PowerButton;
}
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) {
+33
View File
@@ -59,6 +59,11 @@ class HalGPIO {
inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; }
inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; }
// True on the Xteink X3/X4 boards. Unlike deviceIsX3/X4 (which both stay "X4"
// on non-C3 boards), this keys off BoardConfig::ACTIVE.board, so it reliably
// gates Xteink-only features (sunlight fix) and non-Xteink-only ones (touch).
bool isXteinkDevice() const;
// Start button GPIO and setup SPI for screen and SD card
void begin();
@@ -72,6 +77,34 @@ class HalGPIO {
unsigned long getHeldTime() const;
unsigned long getPowerButtonHeldTime() const;
// Touch: one-shot tap with the release position normalized 0..1 in the panel's
// native orientation. Returns false on non-touch devices. (Reusable gesture
// primitive; see MappedInputManager for the top-left = Back mapping.)
bool wasTouchTap(float& nx, float& ny) const;
// Press-edge of a touch: true on touch-down with the down position normalized
// 0..1 (panel native). For showing the pressed/selected element before release.
bool wasTouchDown(float& nx, float& ny) const;
// True while a touch remains within tap slop; writes touch-down position and held time.
bool isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const;
// Duration (ms) of the last touch contact, latched on release. Valid on the
// release frame (alongside wasTouchTap). For tap-vs-long-press decisions.
unsigned long lastTouchHeldMs() const;
// Swipe (flick) gesture on the release frame: writes the start/end positions
// normalized 0..1 in the panel's native frame (map via GfxRenderer::tapToLogical).
// A swipe also raises wasTouchTap(), so check this first. False on non-touch.
bool wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const;
// True if a touch controller is present/active (runtime gate; false on the C3).
bool hasTouch() const;
// True if a touch press or release happened this frame (the touch analogue of
// wasAnyPressed/Released), for resetting idle/sleep timers and CPU frequency.
bool wasTouchActivity() const;
// Setup wake up GPIO and enter deep sleep
void startDeepSleep();
+45 -25
View File
@@ -1,6 +1,7 @@
#include "HalPowerManager.h"
#include <Logging.h>
#include <PowerManager.h>
#include <WiFi.h>
#include <esp_sleep.h>
@@ -10,15 +11,32 @@
HalPowerManager powerManager; // Singleton instance
namespace {
// The fuel gauge's I2C controller (Wire or Wire1) per the active board profile.
// On single-bus SoCs (ESP32-C3, SOC_I2C_NUM == 1) Wire1 doesn't exist, so always
// use Wire. On Sticky the gauge is on Wire1 so it doesn't fight the GT911 touch,
// which owns Wire.
TwoWire& gaugeWire() {
#if SOC_I2C_NUM > 1
if (BoardConfig::ACTIVE.batteryGauge.i2cBus == 1) return Wire1;
#endif
return Wire;
}
} // namespace
void HalPowerManager::begin() {
if (gpio.deviceIsX3()) {
// X3 uses an I2C fuel gauge for battery monitoring.
// I2C init must come AFTER gpio.begin() so early hardware detection/probes are finished.
Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ);
Wire.setTimeOut(4);
const auto& gauge = BoardConfig::ACTIVE.batteryGauge;
if (gauge.gaugeAddr != 0) {
// Board has an I2C fuel gauge (X3, LilyGo, Sticky, ...). Pins/freq come from
// the active board profile, not hardcoded X3 values. I2C init must come AFTER
// gpio.begin() so early hardware detection/probes are finished.
gaugeWire().begin(gauge.i2cSda, gauge.i2cScl, gauge.i2cHz);
gaugeWire().setTimeOut(4);
_batteryUseI2C = true;
} else {
pinMode(BAT_GPIO0, INPUT);
} else if (BoardConfig::ACTIVE.batteryAdc >= 0) {
// ADC-sensed board (X4: GPIO0, M5Paper: GPIO35). Skip when the profile leaves
// batteryAdc unassigned (PIN_UNASSIGNED) — pinMode(255) faults the GPIO mux.
pinMode(BoardConfig::ACTIVE.batteryAdc, INPUT);
}
normalFreq = getCpuFrequencyMhz();
modeMutex = xSemaphoreCreateMutex();
@@ -76,22 +94,19 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
#endif
// Pre-sleep routines from the original firmware
#if !SOC_PM_SUPPORT_EXT1_WAKEUP // RISC-V (C3 / X4)
// GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep
// Note that this means the MCU will be completely powered off during sleep, including RTC
// (X4-specific: on classic ESP32/M5Paper GPIO13 is SD MISO, so this is skipped.)
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
gpio_set_level(GPIO_SPIWP, 0);
esp_sleep_config_gpio_isolate();
gpio_deep_sleep_hold_en();
gpio_hold_en(GPIO_SPIWP);
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
// Arm the wakeup trigger *after* the button is released
// Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the
// power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source
// configuration
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
#endif
freeink::PowerManager::armPowerButtonWakeup();
freeink::PowerManager::deepSleep();
}
uint16_t HalPowerManager::getBatteryPercentage() const {
@@ -101,27 +116,32 @@ uint16_t HalPowerManager::getBatteryPercentage() const {
return _batteryCachedPercent;
}
// Read SOC directly from I2C fuel gauge (16-bit LE register).
// On I2C error, keep last known value to avoid UI jitter/slowdowns.
Wire.beginTransmission(I2C_ADDR_BQ27220);
Wire.write(BQ27220_SOC_REG);
if (Wire.endTransmission(false) != 0) {
// Read SOC directly from the I2C fuel gauge (16-bit LE register). Gauge
// address comes from the active board profile (all current gauges are
// BQ27220-class, SOC at 0x2C). On I2C error, keep last known value to avoid
// UI jitter/slowdowns.
const uint8_t gaugeAddr = BoardConfig::ACTIVE.batteryGauge.gaugeAddr;
TwoWire& w = gaugeWire();
w.beginTransmission(gaugeAddr);
w.write(BQ27220_SOC_REG);
if (w.endTransmission(false) != 0) {
_batteryLastPollMs = now;
return _batteryCachedPercent;
}
Wire.requestFrom(I2C_ADDR_BQ27220, (uint8_t)2);
if (Wire.available() < 2) {
w.requestFrom(gaugeAddr, (uint8_t)2);
if (w.available() < 2) {
_batteryLastPollMs = now;
return _batteryCachedPercent;
}
const uint8_t lo = Wire.read();
const uint8_t hi = Wire.read();
const uint8_t lo = w.read();
const uint8_t hi = w.read();
const uint16_t soc = (hi << 8) | lo;
_batteryCachedPercent = soc > 100 ? 100 : soc;
_batteryLastPollMs = now;
return _batteryCachedPercent;
}
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
// ADC pin from the active profile (X4 GPIO0 / M5Paper GPIO35); default 2:1 divider.
static const BatteryMonitor battery = BatteryMonitor(BoardConfig::ACTIVE.batteryAdc);
// smooth the battery %.
if (_batteryCachedPercent == 0) {
+10 -1
View File
@@ -28,7 +28,16 @@ class HalPowerManager {
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
public:
static constexpr int LOW_POWER_FREQ = 10; // MHz
// On PSRAM boards (classic ESP32 / M5Paper) the APB clock follows the CPU below
// 80 MHz, and APB clocks both the SD SPI bus and the 80 MHz PSRAM (which holds
// the framebuffer). Dropping to 10 MHz breaks SD writes and PSRAM, so floor the
// low-power CPU at 80 MHz (APB stays pinned at 80 for CPU 80/160/240, so power
// still drops from the 240 MHz default while SD/PSRAM stay stable).
#if defined(BOARD_HAS_PSRAM)
static constexpr int LOW_POWER_FREQ = 80; // MHz (APB-safe floor for PSRAM/SD)
#else
static constexpr int LOW_POWER_FREQ = 10; // MHz
#endif
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
+8
View File
@@ -38,6 +38,13 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
__real_panic_print_backtrace(frame, core);
return;
}
#if !__riscv
// RvExcFrame and the flat SP scan below are RISC-V-only (C3). On Xtensa
// (classic ESP32 / S3) the exception frame is XtExcFrame with windowed-register
// unwinding, so fall back to the default backtrace.
__real_panic_print_backtrace(frame, core);
return;
#else
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
@@ -65,6 +72,7 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
}
__real_panic_print_backtrace(frame, core);
#endif // __riscv
}
}
+36 -1
View File
@@ -26,6 +26,15 @@ bool HalTiltSensor::readReg(uint8_t reg, uint8_t* val) const {
}
bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const {
if (_backend == Backend::SdkImu) {
Imu::Sample sample;
if (!_sdkImu.read(sample)) return false;
gx = sample.gx;
gy = sample.gy;
gz = sample.gz;
return true;
}
Wire.beginTransmission(_i2cAddr);
Wire.write(REG_GX_L); // Start reading at Gyro X Low
if (Wire.endTransmission(false) != 0) {
@@ -52,11 +61,22 @@ bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const {
}
void HalTiltSensor::begin() {
_backend = Backend::None;
if (!gpio.deviceIsX3()) {
_available = false;
_available = _sdkImu.begin();
if (!_available) {
LOG_ERR("GYR", "SDK IMU not found");
return;
}
_backend = Backend::SdkImu;
_initMs = millis();
_lastPollMs = millis();
LOG_INF("GYR", "SDK IMU initialized");
return;
}
_backend = Backend::Qmi8658;
// Try primary address, then alternate
uint8_t whoami = 0;
_i2cAddr = I2C_ADDR_QMI8658;
@@ -89,6 +109,14 @@ bool HalTiltSensor::wake() {
return false;
}
if (_backend == Backend::SdkImu) {
_lastPollMs = millis();
_lastTiltMs = millis();
_wakeMs = millis();
_isAwake = true;
return true;
}
// Wait for init to complete before waking
if ((millis() - _initMs) < SLEEP_STABILIZE_MS) {
return false;
@@ -111,6 +139,13 @@ bool HalTiltSensor::deepSleep() {
return false;
}
if (_backend == Backend::SdkImu) {
clearPendingEvents();
_inTilt = false;
_isAwake = false;
return true;
}
if ((millis() - _wakeMs) < SLEEP_STABILIZE_MS) {
return false;
}
+5
View File
@@ -1,6 +1,7 @@
#pragma once
#include <Arduino.h>
#include <Imu.h>
#include <Wire.h>
#include "HalGPIO.h"
@@ -18,8 +19,12 @@ class HalTiltSensor;
extern HalTiltSensor halTiltSensor; // Singleton
class HalTiltSensor {
enum class Backend : uint8_t { None, Qmi8658, SdkImu };
bool _available = false;
Backend _backend = Backend::None;
uint8_t _i2cAddr = 0;
mutable Imu _sdkImu;
// Tilt gesture state machine
bool _tiltForwardEvent = false; // Consumed by wasTiltedForward()
Submodule open-x4-sdk deleted from 198ad26721
+86 -12
View File
@@ -55,47 +55,121 @@ extra_scripts =
pre:scripts/patch_jpegdec.py
post:scripts/register_unit_tests_target.py
; Libraries
; Libraries — FreeInk SDK (git submodule at ./freeink-sdk). The left-hand names
; are the include paths the firmware already uses (<EInkDisplay.h>, etc.); FreeInk's
; compat shim preserves them. BoardConfig + PowerManager are FreeInk-only and
; required (device profiles + the SoC-correct deep-sleep wakeup the HAL relies on).
lib_deps =
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
BatteryMonitor=symlink://freeink-sdk/libs/hardware/BatteryMonitor
InputManager=symlink://freeink-sdk/libs/hardware/InputManager
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
Rtc=symlink://freeink-sdk/libs/hardware/Rtc
Imu=symlink://freeink-sdk/libs/hardware/Imu
FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI
Icons=symlink://freeink-sdk/libs/assets/Icons
bblanchon/ArduinoJson @ 7.4.2
ricmoo/QRCode @ 0.0.1
bitbank2/PNGdec @ 1.1.6
https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b
links2004/WebSockets @ 2.7.3
[env:default]
; ESP32-C3 device set: X3 + X4 compiled into one binary, runtime-selected after
; the I2C fingerprint. FreeInk requires an explicit device selection, so the C3
; envs extend this instead of [base] directly. (M5Paper is a different MCU family
; and selects its own device below.)
[c3]
extends = base
build_flags =
${base.build_flags}
-DFREEINK_DEVICE_X3=1
-DFREEINK_DEVICE_X4=1
[env:default]
extends = c3
build_flags =
${c3.build_flags}
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=2 ; Set log level to debug for development builds
[env:gh_release]
extends = base
extends = c3
build_flags =
${base.build_flags}
${c3.build_flags}
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release builds
[env:gh_release_rc]
extends = base
extends = c3
build_flags =
${base.build_flags}
${c3.build_flags}
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
[env:slim]
extends = base
extends = c3
build_flags =
${base.build_flags}
${c3.build_flags}
-DCROSSPOINT_VERSION=\"${crosspoint.version}-slim\"
; serial output is disabled in slim builds to save space
-UENABLE_SERIAL_LOG
; --- LilyGo T5 S3 (ESP32-S3 N16R8, 4.7" ED047TC1 / raw-parallel EPD) ---------
; Uses FreeInk's LilyGo board-support library for the PCA9535/TPS65185 display
; power hooks and expander button.
; pio run -e lilygo_t5s3 -t upload --upload-port /dev/tty.usbmodemXXXX
[env:lilygo_t5s3]
extends = base
board = esp32-s3-devkitc1-n16r8
board_build.mcu = esp32s3
board_build.flash_mode = qio
board_build.arduino.memory_type = qio_opi
build_flags =
${base.build_flags}
-DBOARD_HAS_PSRAM
-DFREEINK_DEVICE_LILYGO=1
-DCROSSPOINT_VERSION=\"${crosspoint.version}-lilygo_t5s3\"
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=2
-DTOUCH_PROBE_DEBUG=1
lib_deps =
${base.lib_deps}
BoardT5S3=symlink://freeink-sdk/libs/hardware/BoardT5S3
m5stack/M5GFX @ 0.2.20
; --- M5Paper v1.1 (classic ESP32-D0WDQ6, 4.7" 540x960 / IT8951E) -------------
; Different MCU family than the C3 base: override board/mcu/flash, disable the
; C3-only native-USB CDC (logs over UART0), and select the M5Paper device.
; pio run -e m5paper_v11 -t upload --upload-port /dev/tty.usbserial-XXXX
[env:m5paper_v11]
extends = base
board = esp32dev
board_build.mcu = esp32
board_build.flash_mode = qio
; CP2104 bridge: 460800 is ~4x the 115200 fallback and reliable; drop if a flash
; stalls. (921600 usually works too.)
upload_speed = 460800
build_flags =
${base.build_flags}
-UARDUINO_USB_MODE
-UARDUINO_USB_CDC_ON_BOOT
-DBOARD_HAS_PSRAM
-DFREEINK_DEVICE_M5PAPER=1
; git_branch.py only injects CROSSPOINT_VERSION for the default env; set it here.
-DCROSSPOINT_VERSION=\"${crosspoint.version}-m5paper_v11\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=2
; Touch + rotary: no physical button row, so hide on-screen button hints.
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
; Archive-order workarounds for the classic-ESP32 link (see freeink-sdk
; platformio.crosspoint.sample.ini): force Arduino startup + the I2C HAL members.
-Wl,.pio/build/m5paper_v11/FrameworkArduino/main.cpp.o
-Wl,-u,i2cInit
-Wl,-u,i2cSlaveInit
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Regenerate src/components/icons/generated_icons.h from icons.manifest, using the
# FreeInk SDK icon generator and its vendored Lucide submodule. generated_icons.h is
# a committed build product of these inputs — re-run this after editing the manifest.
#
# Requires: rsvg-convert (librsvg) + Pillow, and the Lucide submodule fetched
# (git submodule update --init in the SDK).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SDK="$ROOT/freeink-sdk"
SVGDIR="$SDK/libs/assets/Icons/lucide/icons"
if [ ! -d "$SVGDIR" ]; then
echo "Lucide SVGs not found at $SVGDIR" >&2
echo "Fetch the submodule: git -C \"$SDK\" submodule update --init libs/assets/Icons/lucide" >&2
exit 1
fi
python3 "$SDK/libs/assets/Icons/tools/gen_icons.py" \
--manifest "$ROOT/src/components/icons/icons.manifest" \
--svgdir "$SVGDIR" \
--sizes 16,24,32,40,48 \
--out "$ROOT/src/components/icons/generated_icons.h"
echo "Regenerated src/components/icons/generated_icons.h"
+3
View File
@@ -246,6 +246,9 @@ class CrossPointSettings {
uint8_t uiTheme = LYRA;
// Sunlight fading compensation
uint8_t fadingFix = 0;
// Touch reader controls: tap zones for page back/forward + press-and-hold,
// mirroring the physical buttons. Touch devices only (hidden elsewhere).
uint8_t touchReaderControls = 1;
// Power button return from footnotes (1 = enabled, 0 = disabled)
uint8_t pwrBtnFootnoteBack = 1;
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
+187 -3
View File
@@ -1,8 +1,12 @@
#include "MappedInputManager.h"
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <cstdlib>
#include "CrossPointSettings.h"
#include "components/TouchRegistry.h"
bool MappedInputManager::isNavDirectionSwapped() const {
// Key the swap on the orientation the screen is *actually* rendered at, not the persisted reader
@@ -74,9 +78,182 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
return false;
}
bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
// Top-left corner fallback, as a fraction of the logical screen. Generous to hit.
static constexpr float BACK_GESTURE_FRAC_X = 0.22f;
static constexpr float BACK_GESTURE_FRAC_Y = 0.12f;
static constexpr float BOTTOM_EDGE_BACK_GESTURE_FRAC_Y = 0.14f;
static constexpr unsigned long TOUCH_DOWN_SELECT_DELAY_MS = 90;
static constexpr unsigned long TOUCH_HELD_OVERRIDE_WINDOW_MS = 250;
bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
void MappedInputManager::rememberTouchHeldTime() const {
touchHeldOverrideValid = true;
touchHeldOverrideMs = gpio.lastTouchHeldMs();
touchHeldOverrideAt = millis();
}
bool MappedInputManager::wasBottomEdgeSwipeUp() const {
float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f;
if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return false;
int sx = 0, sy = 0, ex = 0, ey = 0;
renderer.tapToLogical(nxs, nys, sx, sy);
renderer.tapToLogical(nxe, nye, ex, ey);
const int screenHeight = renderer.getScreenHeight();
const int bottomEdgeTop = screenHeight - static_cast<int>(screenHeight * BOTTOM_EDGE_BACK_GESTURE_FRAC_Y);
const bool isBackSwipe = sy >= bottomEdgeTop && ey < sy && std::abs(ey - sy) > std::abs(ex - sx);
if (isBackSwipe) rememberTouchHeldTime();
return isBackSwipe;
}
bool MappedInputManager::wasBackGesture() const {
if (wasBottomEdgeSwipeUp()) return true;
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false;
int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly);
// A tap on the theme's header Back target acts as Back.
int id = 0;
if (TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Back, id)) {
rememberTouchHeldTime();
return true;
}
// Else the top-left corner, for screens with no Back target (e.g. the reader).
const bool isTopLeftBack =
lx <= renderer.getScreenWidth() * BACK_GESTURE_FRAC_X && ly <= renderer.getScreenHeight() * BACK_GESTURE_FRAC_Y;
if (isTopLeftBack) rememberTouchHeldTime();
return isTopLeftBack;
}
bool MappedInputManager::wasItemTapped(int& id) const {
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false;
int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly);
const bool hit = TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id);
if (hit) rememberTouchHeldTime();
return hit;
}
bool MappedInputManager::wasItemTouchedDown(int& id) const {
float nx = 0.0f, ny = 0.0f;
unsigned long heldMs = 0;
if (!gpio.isTouchTapCandidate(nx, ny, heldMs)) {
touchSelectTracking = false;
touchSelectEmitted = false;
touchSelectId = -1;
return false;
}
if (!touchSelectTracking) {
float downNx = 0.0f, downNy = 0.0f;
if (!gpio.wasTouchDown(downNx, downNy)) return false;
int lx = 0, ly = 0;
renderer.tapToLogical(downNx, downNy, lx, ly);
int candidateId = -1;
if (!TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, candidateId)) {
touchSelectTracking = false;
touchSelectEmitted = false;
touchSelectId = -1;
return false;
}
touchSelectTracking = true;
touchSelectEmitted = false;
touchSelectId = candidateId;
}
if (touchSelectEmitted || heldMs < TOUCH_DOWN_SELECT_DELAY_MS) return false;
touchSelectEmitted = true;
id = touchSelectId;
return true;
}
bool MappedInputManager::wasItemLongPressed(int& id) const {
static constexpr unsigned long TOUCH_LONG_PRESS_MS = 500;
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false; // release frame
if (gpio.lastTouchHeldMs() < TOUCH_LONG_PRESS_MS) return false;
int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly);
const bool hit = TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id);
if (hit) rememberTouchHeldTime();
return hit;
}
bool MappedInputManager::wasTabTapped(int& id) const {
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false;
int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly);
const bool hit = TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Tab, id);
if (hit) rememberTouchHeldTime();
return hit;
}
bool MappedInputManager::wasCoverTapped(int& id) const {
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false;
int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly);
const bool hit = TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Cover, id);
if (hit) rememberTouchHeldTime();
return hit;
}
bool MappedInputManager::wasScreenTapped(int& x, int& y) const {
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false;
renderer.tapToLogical(nx, ny, x, y);
rememberTouchHeldTime();
return true;
}
bool MappedInputManager::wasListScroll(int& index, int count, int pageItems) const {
if (count <= 0) return false;
if (pageItems < 1) pageItems = 1;
if (wasBottomEdgeSwipeUp()) return false;
const SwipeDir swipe = wasSwipe();
if (swipe == SwipeDir::Up) {
return freeink::ui::listPageIndex(index, +1, count, pageItems);
}
if (swipe == SwipeDir::Down) {
return freeink::ui::listPageIndex(index, -1, count, pageItems);
}
return false;
}
MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const {
if (wasBottomEdgeSwipeUp()) return SwipeDir::None;
float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f;
if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return SwipeDir::None;
// Map both endpoints into the logical frame so the direction follows what the
// user sees regardless of panel mount/orientation.
int sx = 0, sy = 0, ex = 0, ey = 0;
renderer.tapToLogical(nxs, nys, sx, sy);
renderer.tapToLogical(nxe, nye, ex, ey);
const int dx = ex - sx;
const int dy = ey - sy;
if (std::abs(dx) >= std::abs(dy)) {
return dx < 0 ? SwipeDir::Left : SwipeDir::Right;
}
return dy < 0 ? SwipeDir::Up : SwipeDir::Down;
}
bool MappedInputManager::wasPressed(const Button button) const {
// A top-left tap fires on the release frame; expose it on Back's press edge too
// so menus that act on wasPressed(Back) also respond. Deliberately NOT folded
// into isPressed, so a quick tap never satisfies the readers' long-press-home.
if (button == Button::Back && wasBackGesture()) return true;
return mapButton(button, &HalGPIO::wasPressed);
}
bool MappedInputManager::wasReleased(const Button button) const {
if (button == Button::Back && wasBackGesture()) return true;
return mapButton(button, &HalGPIO::wasReleased);
}
bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
@@ -84,7 +261,14 @@ bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); }
unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); }
unsigned long MappedInputManager::getHeldTime() const {
if (!gpio.wasAnyPressed() && !gpio.wasAnyReleased() && touchHeldOverrideValid &&
millis() - touchHeldOverrideAt <= TOUCH_HELD_OVERRIDE_WINDOW_MS) {
return touchHeldOverrideMs;
}
touchHeldOverrideValid = false;
return gpio.getHeldTime();
}
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const {
+37
View File
@@ -7,6 +7,7 @@ class GfxRenderer;
class MappedInputManager {
public:
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
enum class SwipeDir { None, Left, Right, Up, Down };
struct Labels {
const char* btn1;
@@ -21,6 +22,33 @@ class MappedInputManager {
bool wasPressed(Button button) const;
bool wasReleased(Button button) const;
bool isPressed(Button button) const;
// Touch "back" gesture: a tap on the theme's header Back target, or in the
// top-left corner, or a swipe up from the visible bottom edge. Folded into
// Back's edges, so every screen gets it for free.
bool wasBackGesture() const;
// True (and writes the id) if a tap this frame hit a TouchRegistry item.
// Activities treat the id as "select + activate". False on non-touch devices.
bool wasItemTapped(int& id) const;
// Stable touch-down candidate: fires once when a touch remains over an item
// briefly without crossing tap slop, so swipes do not show row selection.
bool wasItemTouchedDown(int& id) const;
// Subset of wasItemTapped's releases held past the long-press threshold (check
// this first). Distinguishes tap vs press-and-hold.
bool wasItemLongPressed(int& id) const;
// wasItemTapped for tab-bar tabs (id = tab index) and cover/card targets
// (id = item index). Distinct kinds so a screen with both doesn't confuse them.
bool wasTabTapped(int& id) const;
bool wasCoverTapped(int& id) const;
// True on a touch release anywhere on screen, with logical/oriented coords.
bool wasScreenTapped(int& x, int& y) const;
// Swipe direction in the current logical (oriented) frame, or None. A swipe also
// raises the tap helpers above, so check this first and consume it.
SwipeDir wasSwipe() const;
// Page-scroll a list selection by a vertical swipe (touch nav without buttons):
// swipe up advances down the list, swipe down moves up, by pageItems, clamped to
// [0, count-1]. Updates index and returns true when a vertical swipe occurred, so
// callers can requestUpdate() and return before the tap handlers run.
bool wasListScroll(int& index, int count, int pageItems) const;
bool wasAnyPressed() const;
bool wasAnyReleased() const;
unsigned long getHeldTime() const;
@@ -44,4 +72,13 @@ class MappedInputManager {
const GfxRenderer& renderer;
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
void rememberTouchHeldTime() const;
bool wasBottomEdgeSwipeUp() const;
mutable bool touchSelectTracking = false;
mutable bool touchSelectEmitted = false;
mutable int touchSelectId = -1;
mutable bool touchHeldOverrideValid = false;
mutable unsigned long touchHeldOverrideMs = 0;
mutable unsigned long touchHeldOverrideAt = 0;
};
+22
View File
@@ -1,6 +1,8 @@
#pragma once
#include <BoardConfig.h>
#include <HalClock.h>
#include <HalGPIO.h>
#include <HalTiltSensor.h>
#include <I18n.h>
#include <SdCardFontRegistry.h>
@@ -258,6 +260,26 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced",
StrId::STR_CUSTOMISE_STATUS_BAR),
};
// Sunlight fading fix is Xteink-only (transflective panel); other devices get
// the touch reader controls toggle instead. Same gate as the reader runtime.
if (!gpio.isXteinkDevice()) {
v.erase(std::remove_if(v.begin(), v.end(),
[](const SettingInfo& s) { return s.nameId == StrId::STR_SUNLIGHT_FADING_FIX; }),
v.end());
for (auto it = v.begin(); it != v.end(); ++it) {
if (it->nameId == StrId::STR_SIDE_BTN_LAYOUT) {
v.insert(it, SettingInfo::Toggle(StrId::STR_TOUCH_READER_CONTROLS, &CrossPointSettings::touchReaderControls,
"touchReaderControls", StrId::STR_CAT_CONTROLS));
break;
}
}
}
if (BoardConfig::hasTouch()) {
v.erase(std::remove_if(v.begin(), v.end(),
[](const SettingInfo& s) { return s.nameId == StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION; }),
v.end());
}
// Only show tilt page turn setting when the QMI8658 IMU is present (X3)
if (halTiltSensor.isAvailable()) {
// Insert after the short power button setting (end of Controls section)
+1 -2
View File
@@ -56,8 +56,7 @@ class Activity {
// Finish this activity and return to the previous one on the stack (if any)
void finish();
// Convenience method to facilitate API transition to ActivityManager
// TODO: remove this in near future
// Convenience method to facilitate API transition to ActivityManager.
void onGoHome(HomeMenuItem item = HomeMenuItem::NONE);
void onSelectBook(const std::string& path);
};
+16 -4
View File
@@ -9,6 +9,7 @@
#include "boot_sleep/BootActivity.h"
#include "boot_sleep/SleepActivity.h"
#include "browser/OpdsBookBrowserActivity.h"
#include "components/TouchRegistry.h"
#include "home/CrashActivity.h"
#include "home/FileBrowserActivity.h"
#include "home/HomeActivity.h"
@@ -19,6 +20,13 @@
#include "settings/SettingsActivity.h"
#include "util/FullScreenMessageActivity.h"
// taskENTER_CRITICAL needs a real spinlock on dual-core targets (classic ESP32,
// e.g. M5Paper). On the single-core ESP32-C3 a nullptr mux was tolerated, but
// the dual-core port acquires an inter-core spinlock and asserts on a null
// pointer (spinlock_acquire, spinlock.h:84). One shared spinlock guards the
// short waitingTaskHandle critical sections below.
static portMUX_TYPE activityMux = portMUX_INITIALIZER_UNLOCKED;
void ActivityManager::begin() {
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
8192, // Stack size
@@ -42,14 +50,18 @@ void ActivityManager::renderTaskLoop() {
RenderLock lock;
if (currentActivity) {
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
// Touch targets are rebuilt every frame: clear before the activity draws, then
// publish so the next loop() hit-tests against exactly what's on screen.
TouchRegistry::getInstance().beginFrame();
currentActivity->render(std::move(lock));
TouchRegistry::getInstance().publish();
}
// Notify any task blocked in requestUpdateAndWait() that the render is done.
TaskHandle_t waiter = nullptr;
taskENTER_CRITICAL(nullptr);
taskENTER_CRITICAL(&activityMux);
waiter = waitingTaskHandle;
waitingTaskHandle = nullptr;
taskEXIT_CRITICAL(nullptr);
taskEXIT_CRITICAL(&activityMux);
if (waiter) {
xTaskNotify(waiter, 1, eIncrement);
}
@@ -280,7 +292,7 @@ void ActivityManager::requestUpdateAndWait() {
}
// Atomic section to perform checks
taskENTER_CRITICAL(nullptr);
taskENTER_CRITICAL(&activityMux);
auto currTaskHandler = xTaskGetCurrentTaskHandle();
auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex);
bool isRenderTask = (currTaskHandler == renderTaskHandle);
@@ -289,7 +301,7 @@ void ActivityManager::requestUpdateAndWait() {
if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) {
waitingTaskHandle = currTaskHandler;
}
taskEXIT_CRITICAL(nullptr);
taskEXIT_CRITICAL(&activityMux);
// Render task cannot call requestUpdateAndWait() or it will cause a deadlock
assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()");
@@ -1,5 +1,6 @@
#include "OpdsBookBrowserActivity.h"
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
@@ -10,6 +11,7 @@
#include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/TouchRegistry.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/HttpDownloader.h"
@@ -31,7 +33,6 @@ void OpdsBookBrowserActivity::onEnter() {
currentPath = "";
selectorIndex = 0;
consumeConfirm = false;
consumeBack = false;
errorMessage.clear();
statusMessage = tr(STR_CHECKING_WIFI);
requestUpdate();
@@ -60,10 +61,6 @@ void OpdsBookBrowserActivity::loop() {
consumeConfirm = false;
return;
}
if (consumeBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) {
consumeBack = false;
return;
}
if (state == BrowserState::ERROR) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
@@ -91,11 +88,28 @@ void OpdsBookBrowserActivity::loop() {
if (state == BrowserState::DOWNLOADING) return;
if (state == BrowserState::BROWSING) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (!entries.empty()) {
const auto& entry = entries[selectorIndex];
entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry);
if (!entries.empty()) {
if (mappedInput.wasListScroll(selectorIndex, static_cast<int>(entries.size()), PAGE_ITEMS)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) &&
freeink::ui::listSelectIndex(selectorIndex, downId, static_cast<int>(entries.size()))) {
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < static_cast<int>(entries.size())) {
selectorIndex = tappedId;
activateSelectedEntry();
return;
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
activateSelectedEntry();
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
navigateBack();
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
@@ -178,13 +192,20 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
std::string displayText = (entry.type == OpdsEntryType::NAVIGATION) ? "> " + entry.title : entry.title;
if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author;
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), pageWidth - 40);
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(),
i != static_cast<size_t>(selectorIndex));
const int itemY = 60 + (i % PAGE_ITEMS) * 30;
renderer.drawText(UI_10_FONT_ID, 20, itemY, item.c_str(), i != static_cast<size_t>(selectorIndex));
TouchRegistry::getInstance().add(Rect{0, itemY - 2, pageWidth, 30}, static_cast<int>(i), TouchRegistry::Item);
}
}
renderer.displayBuffer();
}
void OpdsBookBrowserActivity::activateSelectedEntry() {
if (entries.empty() || selectorIndex < 0 || selectorIndex >= static_cast<int>(entries.size())) return;
const auto& entry = entries[selectorIndex];
entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry);
}
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
if (server.url.empty()) {
state = BrowserState::ERROR;
@@ -33,7 +33,6 @@ class OpdsBookBrowserActivity final : public Activity {
std::string currentPath;
std::string searchTemplate;
bool consumeConfirm = false;
bool consumeBack = false; // Added missing member
int selectorIndex = 0;
std::string errorMessage;
std::string statusMessage;
@@ -51,5 +50,6 @@ class OpdsBookBrowserActivity final : public Activity {
void downloadBook(const OpdsEntry& book);
void launchSearch();
void performSearch(const std::string& query);
void activateSelectedEntry();
bool preventAutoSleep() override { return true; }
};
+3 -1
View File
@@ -20,7 +20,9 @@ void CrashActivity::onEnter() {
}
void CrashActivity::loop() {
if (mappedInput.isPressed(MappedInputManager::Button::Back)) {
int tapX = 0;
int tapY = 0;
if (mappedInput.isPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(tapX, tapY)) {
finish();
}
}
+23 -1
View File
@@ -206,7 +206,29 @@ void FileBrowserActivity::loop() {
const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing;
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved);
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
int scrollIdx = static_cast<int>(selectorIndex);
if (mappedInput.wasListScroll(scrollIdx, static_cast<int>(files.size()), pageItems)) {
selectorIndex = scrollIdx;
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(files.size())) {
selectorIndex = downId;
requestUpdate();
}
// A tap opens the tapped entry (held-time is 0 on a tap, so it takes the short-press
// open path below, never the long-press delete).
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(files.size())) {
selectorIndex = tappedId;
}
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (lockNextConfirmRelease) {
lockNextConfirmRelease = false;
return;
+30 -7
View File
@@ -179,7 +179,29 @@ void HomeActivity::loop() {
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Tap a menu button to select + activate it. The button menu registers
// menu-local ids (drawn with selectorIndex offset by recentBooks.size()), so map
// back into the global selector space.
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId)) {
selectorIndex = static_cast<int>(recentBooks.size()) + downId;
requestUpdate();
}
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped) {
selectorIndex = static_cast<int>(recentBooks.size()) + tappedId;
}
// A tap on the continue-reading cover selects that book (home selector index).
int coverId = -1;
const bool coverTapped = mappedInput.wasCoverTapped(coverId);
if (coverTapped && coverId >= 0 && coverId < static_cast<int>(recentBooks.size())) {
selectorIndex = coverId;
}
if (tapped || coverTapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectorIndex < recentBooks.size()) {
onSelectBook(recentBooks[selectorIndex].path);
} else {
@@ -233,7 +255,7 @@ void HomeActivity::render(RenderLock&&) {
// Build menu items dynamically
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Settings};
std::vector<UIIcon> menuIcons = {Folder, Book, Transfer, Settings};
if (hasOpdsServers) {
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
@@ -246,12 +268,13 @@ void HomeActivity::render(RenderLock&&) {
menuIcons.insert(menuIcons.begin(), Book);
}
// Menu fills the space between the cover and the bottom reserve (button hints
// when shown, else a margin). Sizing it to the real remaining height lets
// drawButtonMenu fit the items, so a scaled-up menu never runs off the bottom.
const int menuY = metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset;
const int bottomReserve = (BaseTheme::showButtonHints() ? metrics.buttonHintsHeight : 0) + metrics.verticalSpacing;
GUI.drawButtonMenu(
renderer,
Rect{0, metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset, pageWidth,
pageHeight - (metrics.headerHeight + metrics.homeTopPadding + metrics.verticalSpacing +
metrics.homeMenuTopOffset + metrics.buttonHintsHeight)},
static_cast<int>(menuItems.size()),
renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve}, static_cast<int>(menuItems.size()),
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
[&menuItems](int index) { return std::string(menuItems[index]); },
[&menuIcons](int index) { return menuIcons[index]; });
+18 -1
View File
@@ -44,6 +44,14 @@ void RecentBooksActivity::onExit() {
void RecentBooksActivity::loop() {
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
int scrollIdx = static_cast<int>(selectorIndex);
if (mappedInput.wasListScroll(scrollIdx, static_cast<int>(recentBooks.size()), pageItems)) {
selectorIndex = scrollIdx;
requestUpdate();
return;
}
// After a long-press has fired, swallow input until Confirm is physically released
// (so the release doesn't also open the book; re-arm only once the button is up).
if (longPressFired) {
@@ -63,7 +71,16 @@ void RecentBooksActivity::loop() {
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(recentBooks.size())) {
selectorIndex = downId;
requestUpdate();
}
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(recentBooks.size())) selectorIndex = tappedId;
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
onSelectBook(recentBooks[selectorIndex].path);
@@ -4,13 +4,13 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#include "MappedInputManager.h"
#include "SilentRestart.h"
#include "WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/TaskWatchdog.h"
namespace {
constexpr const char* HOSTNAME = "crosspoint";
@@ -110,12 +110,12 @@ void CalibreConnectActivity::loop() {
LOG_DBG("CAL", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient);
}
esp_task_wdt_reset();
feedTaskWatchdog();
constexpr int MAX_ITERATIONS = 80;
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
if ((i & 0x07) == 0x07) {
esp_task_wdt_reset();
feedTaskWatchdog();
}
if ((i & 0x0F) == 0x0F) {
yield();
@@ -5,7 +5,6 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#include <cstddef>
@@ -16,6 +15,7 @@
#include "activities/network/CalibreConnectActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/TaskWatchdog.h"
#include "util/QrUtils.h"
namespace {
@@ -328,7 +328,7 @@ void CrossPointWebServerActivity::loop() {
}
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
esp_task_wdt_reset();
feedTaskWatchdog();
// Process HTTP requests in tight loop for maximum throughput
// More iterations = more data processed per main loop cycle
@@ -337,7 +337,7 @@ void CrossPointWebServerActivity::loop() {
webServer->handleClient();
// Reset watchdog every 32 iterations
if ((i & 0x1F) == 0x1F) {
esp_task_wdt_reset();
feedTaskWatchdog();
}
// Yield and check for exit button every 64 iterations
if ((i & 0x3F) == 0x3F) {
@@ -30,8 +30,17 @@ void NetworkModeSelectionActivity::loop() {
return;
}
// Handle confirm button - select current option
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(MENU_ITEM_COUNT)) {
selectedIndex = downId;
requestUpdate();
}
// Handle confirm button (or a tap) - select current option
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(MENU_ITEM_COUNT)) selectedIndex = tappedId;
if (tapped || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
NetworkMode mode = NetworkMode::JOIN_NETWORK;
if (selectedIndex == 1) {
mode = NetworkMode::CONNECT_CALIBRE;
@@ -42,7 +51,6 @@ void NetworkModeSelectionActivity::loop() {
return;
}
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
requestUpdate();
@@ -77,7 +85,6 @@ void NetworkModeSelectionActivity::render(RenderLock&&) {
[](int index) { return std::string(I18N.get(menuItems[index])); },
[](int index) { return std::string(I18N.get(menuDescs[index])); }, [](int index) { return menuIcons[index]; });
// Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -409,6 +409,27 @@ void WifiSelectionActivity::loop() {
// Handle network list state
if (state == WifiSelectionState::NETWORK_LIST) {
// Vertical swipe page-scrolls the network list (touch nav without the side buttons).
int scrollIdx = static_cast<int>(selectedNetworkIndex);
if (mappedInput.wasListScroll(scrollIdx, static_cast<int>(networks.size()),
UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false))) {
selectedNetworkIndex = scrollIdx;
requestUpdate();
return;
}
// Touch: stable press highlights; tap selects it (like Confirm). Drag/scroll contacts do not preselect a row.
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(networks.size())) {
selectedNetworkIndex = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < static_cast<int>(networks.size())) {
selectedNetworkIndex = tappedId;
selectNetwork(selectedNetworkIndex);
return;
}
// Check for Back button to exit (cancel)
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onComplete(false);
@@ -442,7 +463,6 @@ void WifiSelectionActivity::loop() {
}
}
// Handle navigation
buttonNavigator.onNext([this] {
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
requestUpdate();
+13 -7
View File
@@ -298,10 +298,14 @@ void EpubReaderActivity::loop() {
requestUpdate();
}
// Enter reader menu activity on short-press Confirm. A long-press that fired a bound
// function (bookmark or KOReader sync) sets ignoreNextConfirmRelease so the release
// following the hold does not also open the menu.
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Touch page nav (idempotent within a frame, so read here and again below). The
// top-left Back corner is consumed by wasReleased(Back) above, never reaching here.
const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
// Enter reader menu activity on short-press Confirm release, or a center touch-and-hold. A
// long-press that fired a bound function (bookmark or KOReader sync) sets ignoreNextConfirmRelease
// so the release following the hold does not also open the menu.
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || ReaderUtils::isTouchMenuGesture(touch)) {
if (ignoreNextConfirmRelease) {
ignoreNextConfirmRelease = false;
} else {
@@ -400,7 +404,9 @@ void EpubReaderActivity::loop() {
return;
}
const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
prevTriggered = prevTriggered || touch.prev;
nextTriggered = nextTriggered || touch.next;
if (!prevTriggered && !nextTriggered) {
return;
}
@@ -418,7 +424,8 @@ void EpubReaderActivity::loop() {
return;
}
const bool longPress = !fromTilt && mappedInput.getHeldTime() > ReaderUtils::SKIP_HOLD_MS;
const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime();
const bool longPress = !fromTilt && heldMs > ReaderUtils::SKIP_HOLD_MS;
// Don't skip chapter after screenshot
if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) {
@@ -788,7 +795,6 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn) {
requestUpdate();
}
// TODO: Failure handling
void EpubReaderActivity::render(RenderLock&& lock) {
if (!epub) {
return;
@@ -103,7 +103,24 @@ void EpubReaderBookmarksActivity::loop() {
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Open
if (confirmingDelete < DELETE_MODE_DISPLAY) {
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
const int pageItems = GUI.getListPageItems(getListHeight(renderer), true);
if (mappedInput.wasListScroll(selectorIndex, static_cast<int>(bookmarks.size()), pageItems)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(bookmarks.size())) {
selectorIndex = downId;
requestUpdate();
}
}
int tappedId = -1;
const bool tapped = (confirmingDelete < DELETE_MODE_DISPLAY) && mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(bookmarks.size())) selectorIndex = tappedId;
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Open
if (bookmarks.empty()) {
return;
}
@@ -3,11 +3,39 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub ? epub->getTocItemsCount() : 0; }
void EpubReaderChapterSelectionActivity::cancelAndFinish() {
ActivityResult result;
result.isCancelled = true;
setResult(std::move(result));
finish();
}
void EpubReaderChapterSelectionActivity::activateSelection(int index) {
const int totalItems = getTotalItems();
if (!epub || totalItems <= 0) {
cancelAndFinish();
return;
}
index = std::clamp(index, 0, totalItems - 1);
const auto tocItem = epub->getTocItem(index);
if (tocItem.spineIndex < 0 || tocItem.spineIndex >= epub->getSpineItemsCount()) {
cancelAndFinish();
return;
}
selectorIndex = index;
setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor});
finish();
}
void EpubReaderChapterSelectionActivity::onEnter() {
Activity::onEnter();
@@ -30,23 +58,38 @@ void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
void EpubReaderChapterSelectionActivity::loop() {
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
const int totalItems = getTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto tocItem = epub->getTocItem(selectorIndex);
if (tocItem.spineIndex == -1) {
ActivityResult result;
result.isCancelled = true;
setResult(std::move(result));
finish();
} else {
setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor});
finish();
if (!epub || totalItems <= 0) {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
cancelAndFinish();
}
return;
}
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectorIndex, totalItems, pageItems)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < totalItems) {
selectorIndex = downId;
requestUpdate();
}
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
const bool validTap = tapped && tappedId >= 0 && tappedId < totalItems;
const bool confirm = mappedInput.wasReleased(MappedInputManager::Button::Confirm);
if (validTap) {
selectorIndex = tappedId;
}
if (validTap || confirm) {
activateSelection(selectorIndex);
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result;
result.isCancelled = true;
setResult(std::move(result));
finish();
cancelAndFinish();
return;
}
buttonNavigator.onNextRelease([this, totalItems] {
@@ -83,10 +126,12 @@ void EpubReaderChapterSelectionActivity::render(RenderLock&&) {
const int contentHeight = screen.height - contentTop - metrics.verticalSpacing;
const int totalItems = getTotalItems();
GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, selectorIndex,
const int displayIndex = totalItems > 0 ? std::clamp(selectorIndex, 0, totalItems - 1) : 0;
GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, displayIndex,
[this](int index) {
auto item = epub->getTocItem(index);
std::string indent((item.level - 1) * 2, ' ');
const int level = item.level > 0 ? item.level - 1 : 0;
std::string indent(level * 2, ' ');
return indent + item.title;
});
@@ -19,6 +19,8 @@ class EpubReaderChapterSelectionActivity final : public Activity {
// Total TOC items count
int getTotalItems() const;
void cancelAndFinish();
void activateSelection(int index);
public:
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
@@ -1,11 +1,13 @@
#include "EpubReaderFootnotesActivity.h"
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
#include "MappedInputManager.h"
#include "components/TouchRegistry.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -18,6 +20,30 @@ void EpubReaderFootnotesActivity::onEnter() {
void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); }
void EpubReaderFootnotesActivity::loop() {
constexpr int lineHeight = 36;
const auto orientation = renderer.getOrientation();
const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted;
const int contentY = isPortraitInverted ? 50 : 0;
const int visibleCount = std::max(1, (renderer.getScreenHeight() - contentY) / lineHeight);
if (mappedInput.wasListScroll(selectedIndex, static_cast<int>(footnotes.size()), visibleCount)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) &&
freeink::ui::listSelectIndex(selectedIndex, downId, static_cast<int>(footnotes.size()))) {
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < static_cast<int>(footnotes.size())) {
selectedIndex = tappedId;
setResult(FootnoteResult{footnotes[selectedIndex].href});
finish();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult result;
result.isCancelled = true;
@@ -102,6 +128,7 @@ void EpubReaderFootnotesActivity::render(RenderLock&&) {
label = tr(STR_LINK);
}
renderer.drawText(UI_10_FONT_ID, marginLeft, y + 4, label.c_str(), !isSelected);
TouchRegistry::getInstance().add(Rect{contentX, y, contentWidth, lineHeight}, i, TouchRegistry::Item);
}
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
@@ -50,7 +50,6 @@ void EpubReaderMenuActivity::onEnter() {
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
void EpubReaderMenuActivity::loop() {
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
requestUpdate();
@@ -61,7 +60,27 @@ void EpubReaderMenuActivity::loop() {
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Vertical swipe page-scrolls the menu (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectedIndex, static_cast<int>(menuItems.size()),
UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false))) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(menuItems.size())) {
selectedIndex = downId;
requestUpdate();
}
// A tap selects the item and activates it in one gesture (falls into Confirm below).
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(menuItems.size())) {
selectedIndex = tappedId;
}
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto selectedAction = menuItems[selectedIndex].action;
if (selectedAction == MenuAction::ROTATE_SCREEN) {
// Cycle orientation preview locally; actual rotation happens on menu exit.
@@ -3,6 +3,8 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -32,6 +34,22 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
requestUpdate();
}
bool EpubReaderPercentSelectionActivity::setPercentFromTouch(const int x, const int y) {
auto& theme = UITheme::getInstance();
const auto metrics = theme.getMetrics();
const Rect screen = theme.getScreenSafeArea(renderer, true, false);
const int contentTop = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing * 4;
constexpr int barWidth = 360;
constexpr int barHeight = 16;
const int barX = screen.x + (screen.width - barWidth) / 2;
const int barY = contentTop + metrics.verticalSpacing * 2;
if (y < barY - 28 || y > barY + barHeight + 36 || x < barX - 20 || x > barX + barWidth + 20) return false;
const int clampedX = std::max(barX, std::min(x, barX + barWidth));
percent = (clampedX - barX) * 100 / barWidth;
return true;
}
void EpubReaderPercentSelectionActivity::loop() {
// Back cancels, confirm selects, arrows adjust the percent.
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
@@ -48,6 +66,31 @@ void EpubReaderPercentSelectionActivity::loop() {
return;
}
switch (mappedInput.wasSwipe()) {
case MappedInputManager::SwipeDir::Left:
adjustPercent(-kSmallStep);
return;
case MappedInputManager::SwipeDir::Right:
adjustPercent(kSmallStep);
return;
case MappedInputManager::SwipeDir::Up:
adjustPercent(kLargeStep);
return;
case MappedInputManager::SwipeDir::Down:
adjustPercent(-kLargeStep);
return;
case MappedInputManager::SwipeDir::None:
break;
}
int tapX = 0;
int tapY = 0;
if (mappedInput.wasScreenTapped(tapX, tapY) && setPercentFromTouch(tapX, tapY)) {
setResult(PercentResult{percent});
finish();
return;
}
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); });
@@ -24,4 +24,5 @@ class EpubReaderPercentSelectionActivity final : public Activity {
// Change the current percent by a delta and clamp within bounds.
void adjustPercent(int delta);
bool setPercentFromTouch(int x, int y);
};
@@ -21,6 +21,13 @@ void QrDisplayActivity::loop() {
finish();
return;
}
int tapX = 0;
int tapY = 0;
if (mappedInput.wasScreenTapped(tapX, tapY)) {
finish();
return;
}
}
void QrDisplayActivity::render(RenderLock&&) {
+44
View File
@@ -2,6 +2,7 @@
#include <CrossPointSettings.h>
#include <GfxRenderer.h>
#include <HalGPIO.h>
#include <HalTiltSensor.h>
#include <Logging.h>
@@ -13,6 +14,9 @@ constexpr unsigned long GO_HOME_MS = 1000;
constexpr unsigned long SKIP_HOLD_MS = 700;
constexpr unsigned long BOOKMARK_HOLD_MS = 400;
constexpr unsigned long BOOKMARK_MESSAGE_DURATION_MS = 2500;
// Press-and-hold in the center touch zone (see detectTouchPageTurn) opens the
// reader menu, the touch analogue of releasing the Confirm button.
constexpr unsigned long TOUCH_MENU_HOLD_MS = 400;
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
switch (orientation) {
@@ -59,6 +63,46 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
return {prev, next, tiltPrev || tiltNext};
}
// Touch reader controls: left third = page back, right third = forward, center =
// open menu on press-and-hold (see isTouchMenuGesture). heldMs is the contact
// duration, for the same long-press behavior as the buttons (chapter skip).
// All-false on Xteink (no touch) or when the setting is off.
struct TouchPageTurn {
bool prev;
bool next;
bool center;
unsigned long heldMs;
};
inline TouchPageTurn detectTouchPageTurn(GfxRenderer& renderer) {
TouchPageTurn result{false, false, false, 0};
if (gpio.isXteinkDevice() || !SETTINGS.touchReaderControls) {
return result;
}
float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) {
return result;
}
int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly);
const int third = renderer.getScreenWidth() / 3;
if (lx < third) {
result.prev = true;
} else if (lx >= 2 * third) {
result.next = true;
} else {
result.center = true;
}
result.heldMs = gpio.lastTouchHeldMs();
return result;
}
// True when the center zone was pressed and held long enough to open the reader
// menu (touch analogue of a Confirm release).
inline bool isTouchMenuGesture(const TouchPageTurn& touch) {
return touch.center && touch.heldMs >= TOUCH_MENU_HOLD_MS;
}
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
+6 -1
View File
@@ -73,7 +73,12 @@ void TxtReaderActivity::loop() {
return;
}
const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
// Touch page nav (left third = back, right third = forward).
const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
prevTriggered = prevTriggered || touch.prev;
nextTriggered = nextTriggered || touch.next;
if (!prevTriggered && !nextTriggered) {
return;
}
+11 -5
View File
@@ -54,8 +54,11 @@ void XtcReaderActivity::onExit() {
}
void XtcReaderActivity::loop() {
// Enter chapter selection activity
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Touch page nav; center hold opens chapter selection (the Confirm analogue).
const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
// Enter chapter selection activity (Confirm release, or a center touch-and-hold).
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || ReaderUtils::isTouchMenuGesture(touch)) {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
startActivityForResult(
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
@@ -80,7 +83,9 @@ void XtcReaderActivity::loop() {
return;
}
const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
prevTriggered = prevTriggered || touch.prev;
nextTriggered = nextTriggered || touch.next;
if (!prevTriggered && !nextTriggered) {
return;
}
@@ -96,8 +101,9 @@ void XtcReaderActivity::loop() {
return;
}
const bool skipPages = !fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP &&
mappedInput.getHeldTime() > ReaderUtils::SKIP_HOLD_MS;
const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime();
const bool skipPages =
!fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP && heldMs > ReaderUtils::SKIP_HOLD_MS;
const int skipAmount = skipPages ? 10 : 1;
if (prevTriggered) {
@@ -1,11 +1,13 @@
#include "XtcReaderChapterSelectionActivity.h"
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
#include "MappedInputManager.h"
#include "components/TouchRegistry.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -56,6 +58,26 @@ void XtcReaderChapterSelectionActivity::loop() {
const int pageItems = getPageItems();
const int totalItems = static_cast<int>(xtc->getChapters().size());
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectorIndex, totalItems, pageItems)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && freeink::ui::listSelectIndex(selectorIndex, downId, totalItems)) {
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < totalItems) {
selectorIndex = tappedId;
const auto& chapters = xtc->getChapters();
setResult(PageResult{chapters[selectorIndex].startPage});
finish();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto& chapters = xtc->getChapters();
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
@@ -127,7 +149,9 @@ void XtcReaderChapterSelectionActivity::render(RenderLock&&) {
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
const auto& chapter = chapters[i];
const char* title = chapter.name.empty() ? tr(STR_UNNAMED) : chapter.name.c_str();
renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, i != selectorIndex);
const int itemY = 60 + contentY + (i % pageItems) * 30;
renderer.drawText(UI_10_FONT_ID, contentX + 20, itemY, title, i != selectorIndex);
TouchRegistry::getInstance().add(Rect{contentX, itemY - 2, contentWidth, 30}, i, TouchRegistry::Item);
}
// Skip button hints in landscape CW mode (they overlap content)
+24 -1
View File
@@ -122,6 +122,27 @@ void ClearCacheActivity::clearCache() {
void ClearCacheActivity::loop() {
if (state == WARNING) {
int tapX = 0;
int tapY = 0;
if (mappedInput.wasScreenTapped(tapX, tapY)) {
const int actionTop = renderer.getScreenHeight() - UITheme::getInstance().getMetrics().buttonHintsHeight - 12;
if (tapY >= actionTop) {
if (tapX < renderer.getScreenWidth() / 2) {
LOG_DBG("CLEAR_CACHE", "User cancelled via touch");
goBack();
} else {
LOG_DBG("CLEAR_CACHE", "User confirmed via touch, starting cache clear");
{
RenderLock lock(*this);
state = CLEARING;
}
requestUpdateAndWait();
clearCache();
}
return;
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear");
{
@@ -141,7 +162,9 @@ void ClearCacheActivity::loop() {
}
if (state == SUCCESS || state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
int tapX = 0;
int tapY = 0;
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(tapX, tapY)) {
goBack();
}
return;
@@ -9,6 +9,7 @@
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/TouchRegistry.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -108,14 +109,57 @@ void ClockOffsetActivity::adjustActiveField(int delta) {
}
}
void ClockOffsetActivity::selectRelativeField(const int delta) {
activeField = static_cast<Field>((activeField + delta + FIELD_COUNT) % FIELD_COUNT);
}
void ClockOffsetActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
switch (mappedInput.wasSwipe()) {
case MappedInputManager::SwipeDir::Up:
adjustActiveField(+1);
requestUpdate();
return;
case MappedInputManager::SwipeDir::Down:
adjustActiveField(-1);
requestUpdate();
return;
case MappedInputManager::SwipeDir::Left:
selectRelativeField(+1);
requestUpdate();
return;
case MappedInputManager::SwipeDir::Right:
selectRelativeField(-1);
requestUpdate();
return;
case MappedInputManager::SwipeDir::None:
break;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < FIELD_COUNT) {
activeField = static_cast<Field>(downId);
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < FIELD_COUNT) {
const Field tappedField = static_cast<Field>(tappedId);
if (activeField == tappedField) {
adjustActiveField(+1);
} else {
activeField = tappedField;
}
requestUpdate();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
activeField = static_cast<Field>((activeField + 1) % FIELD_COUNT);
selectRelativeField(+1);
requestUpdate();
return;
}
@@ -183,6 +227,8 @@ void ClockOffsetActivity::render(RenderLock&&) {
}
const int textX = boxX + (boxWidth - widthOf(text)) / 2;
renderer.drawText(UI_12_FONT_ID, textX, centreY, text, true, EpdFontFamily::BOLD);
TouchRegistry::getInstance().add(Rect{boxX - 4, centreY - 8, boxWidth + 8, fieldHeight + 16},
static_cast<int>(field), TouchRegistry::Item);
};
drawField(signStr, x, signBoxW, FIELD_SIGN);
@@ -34,4 +34,5 @@ class ClockOffsetActivity final : public Activity {
void saveToSettings() const;
void adjustActiveField(int delta);
void clampForSign();
void selectRelativeField(int delta);
};
@@ -95,6 +95,13 @@ void ClockSyncActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
int tapX = 0;
int tapY = 0;
if (mappedInput.wasScreenTapped(tapX, tapY)) {
finish();
}
}
@@ -1,6 +1,7 @@
#include "FontDownloadActivity.h"
#include <ArduinoJson.h>
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -420,6 +421,38 @@ bool FontDownloadActivity::isSelectedFamilyDeletable() const {
return family.installed && !family.hasUpdate;
}
void FontDownloadActivity::activateSelectedItem() {
if (families_.empty()) return;
if (isDownloadAllRow(selectedIndex_)) {
currentFileIndex_ = 0;
currentFileTotal_ = 0;
for (const auto& f : families_) {
if (!f.installed) currentFileTotal_ += f.files.size();
}
downloadAll();
} else if (isUpdateAllRow(selectedIndex_)) {
currentFileIndex_ = 0;
currentFileTotal_ = 0;
for (const auto& f : families_) {
if (f.hasUpdate) currentFileTotal_ += f.files.size();
}
updateAll();
} else {
auto& family = families_[familyIndexFromList(selectedIndex_)];
if (!family.installed || family.hasUpdate) {
currentFileIndex_ = 0;
currentFileTotal_ = family.files.size();
downloadFamily(family);
} else {
promptDeleteSelectedFamily();
return;
}
}
requestUpdateAndWait();
}
// --- Input handling ---
void FontDownloadActivity::loop() {
@@ -431,6 +464,23 @@ void FontDownloadActivity::loop() {
const int listSize = listItemCount();
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false);
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectedIndex_, listSize, pageItems)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && freeink::ui::listSelectIndex(selectedIndex_, downId, listSize)) {
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < listSize) {
selectedIndex_ = tappedId;
activateSelectedItem();
return;
}
buttonNavigator_.onNextRelease([this, listSize] {
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
@@ -453,36 +503,8 @@ void FontDownloadActivity::loop() {
});
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!families_.empty()) {
if (isDownloadAllRow(selectedIndex_)) {
currentFileIndex_ = 0;
currentFileTotal_ = 0;
for (const auto& f : families_) {
if (!f.installed) currentFileTotal_ += f.files.size();
}
downloadAll();
} else if (isUpdateAllRow(selectedIndex_)) {
currentFileIndex_ = 0;
currentFileTotal_ = 0;
for (const auto& f : families_) {
if (f.hasUpdate) currentFileTotal_ += f.files.size();
}
updateAll();
} else {
auto& family = families_[familyIndexFromList(selectedIndex_)];
if (!family.installed || family.hasUpdate) {
currentFileIndex_ = 0;
currentFileTotal_ = family.files.size();
downloadFamily(family);
} else {
promptDeleteSelectedFamily();
return;
}
}
requestUpdateAndWait();
return;
}
activateSelectedItem();
return;
}
} else if (state_ == COMPLETE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
@@ -99,6 +99,7 @@ class FontDownloadActivity : public Activity {
bool isDownloadAllRow(int index) const;
bool isUpdateAllRow(int index) const;
bool isSelectedFamilyDeletable() const;
void activateSelectedItem();
void promptDeleteSelectedFamily();
void onDeleteConfirmationResult(const ActivityResult& result);
int familyIndexFromList(int listIndex) const { return listIndex - specialRowCount(); }
@@ -80,33 +80,58 @@ void FontSelectionActivity::loop() {
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (selectedIndex_ == previewFontIndex_) {
handleSelection();
} else {
previewFontIndex_ = selectedIndex_;
const auto& font = fonts_[selectedIndex_];
if (font.isBuiltin) {
SETTINGS.fontFamily = font.settingIndex;
SETTINGS.sdFontFamilyName[0] = '\0';
} else if (registry_) {
const int sdIdx = font.settingIndex - CrossPointSettings::BUILTIN_FONT_COUNT;
const auto& families = registry_->getFamilies();
if (sdIdx < static_cast<int>(families.size())) {
strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1);
SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0';
sdFontSystem.ensureLoaded(renderer);
}
}
requestUpdate();
}
return;
}
const int listSize = static_cast<int>(fonts_.size());
const int pageItems =
UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, previewHeight + metrics_.verticalSpacing);
// First select of a font loads it into the preview pane; confirming the already-previewed
// font commits it. Shared by hardware Confirm and touch taps so both honour the preview step.
const auto confirmSelection = [this]() {
if (selectedIndex_ == previewFontIndex_) {
handleSelection();
return;
}
previewFontIndex_ = selectedIndex_;
const auto& font = fonts_[selectedIndex_];
if (font.isBuiltin) {
SETTINGS.fontFamily = font.settingIndex;
SETTINGS.sdFontFamilyName[0] = '\0';
} else if (registry_) {
const int sdIdx = font.settingIndex - CrossPointSettings::BUILTIN_FONT_COUNT;
const auto& families = registry_->getFamilies();
if (sdIdx < static_cast<int>(families.size())) {
strncpy(SETTINGS.sdFontFamilyName, families[sdIdx].name.c_str(), sizeof(SETTINGS.sdFontFamilyName) - 1);
SETTINGS.sdFontFamilyName[sizeof(SETTINGS.sdFontFamilyName) - 1] = '\0';
sdFontSystem.ensureLoaded(renderer);
}
}
requestUpdate();
};
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectedIndex_, listSize, pageItems)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) {
selectedIndex_ = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex_ = tappedId;
confirmSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
confirmSelection();
return;
}
buttonNavigator_.onNextRelease([this, listSize] {
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
requestUpdate();
@@ -101,8 +101,10 @@ void KOReaderAuthActivity::render(RenderLock&&) {
void KOReaderAuthActivity::loop() {
if (state == SUCCESS || state == FAILED) {
int tapX = 0;
int tapY = 0;
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
mappedInput.wasPressed(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(tapX, tapY)) {
finish();
}
}
@@ -1,5 +1,6 @@
#include "KOReaderSettingsActivity.h"
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <I18n.h>
@@ -33,12 +34,24 @@ void KOReaderSettingsActivity::loop() {
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && freeink::ui::listSelectIndex(selectedIndex, downId, MENU_ITEMS)) {
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < MENU_ITEMS) {
selectedIndex = tappedId;
handleSelection();
requestUpdate();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
requestUpdate();
@@ -142,7 +155,6 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
},
true);
// Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -32,14 +32,31 @@ void LanguageSelectActivity::loop() {
return;
}
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false);
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectedIndex, totalItems, pageItems)) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) {
selectedIndex = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex = tappedId;
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false);
// Handle navigation
buttonNavigator.onNextRelease([this] {
selectedIndex = ButtonNavigator::nextIndex(static_cast<int>(selectedIndex), totalItems);
requestUpdate();
@@ -41,6 +41,26 @@ void OpdsServerListActivity::loop() {
return;
}
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
if (mappedInput.wasListScroll(selectedIndex, getItemCount(),
UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false))) {
requestUpdate();
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) {
selectedIndex = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex = tappedId;
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
@@ -1,5 +1,6 @@
#include "OpdsSettingsActivity.h"
#include <FreeInkUI.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
@@ -53,12 +54,26 @@ void OpdsSettingsActivity::loop() {
return;
}
const int menuItems = getMenuItemCount();
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && freeink::ui::listSelectIndex(selectedIndex, downId, menuItems)) {
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < menuItems) {
selectedIndex = tappedId;
handleSelection();
requestUpdate();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
const int menuItems = getMenuItemCount();
buttonNavigator.onNext([this, menuItems] {
selectedIndex = (selectedIndex + 1) % menuItems;
requestUpdate();
+21 -3
View File
@@ -145,7 +145,21 @@ void OtaUpdateActivity::render(RenderLock&&) {
void OtaUpdateActivity::loop() {
if (state == WAITING_CONFIRMATION) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
int tapX = 0;
int tapY = 0;
bool touchUpdate = false;
if (mappedInput.wasScreenTapped(tapX, tapY)) {
const int actionTop = renderer.getScreenHeight() - UITheme::getInstance().getMetrics().buttonHintsHeight - 12;
if (tapY >= actionTop) {
if (tapX < renderer.getScreenWidth() / 2) {
finish();
return;
}
touchUpdate = true;
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm) || touchUpdate) {
LOG_DBG("OTA", "New update available, starting download...");
{
RenderLock lock(*this);
@@ -192,14 +206,18 @@ void OtaUpdateActivity::loop() {
}
if (state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
int tapX = 0;
int tapY = 0;
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(tapX, tapY)) {
finish();
}
return;
}
if (state == NO_UPDATE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
int tapX = 0;
int tapY = 0;
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasScreenTapped(tapX, tapY)) {
finish();
}
return;
@@ -186,8 +186,10 @@ void SdFirmwareUpdateActivity::performUpdate() {
void SdFirmwareUpdateActivity::loop() {
if (state == State::FAILED) {
int tapX = 0;
int tapY = 0;
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
mappedInput.wasPressed(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(tapX, tapY)) {
if (recoveryMode) {
// Go back to picker so user can try a different .bin
state = State::PICKING;
+50 -2
View File
@@ -115,6 +115,56 @@ void SettingsActivity::onExit() {
void SettingsActivity::loop() {
bool hasChangedCategory = false;
// A horizontal swipe pages categories (and the tab bar slides to keep the new one
// visible). Handled first, and it suppresses the row/tab tap handlers below so the
// swipe's release isn't also read as a tap.
const MappedInputManager::SwipeDir swipe = mappedInput.wasSwipe();
const bool swiped = swipe != MappedInputManager::SwipeDir::None;
if (swipe == MappedInputManager::SwipeDir::Left) {
selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount);
selectedSettingIndex = 0;
hasChangedCategory = true;
requestUpdate();
} else if (swipe == MappedInputManager::SwipeDir::Right) {
selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount);
selectedSettingIndex = 0;
hasChangedCategory = true;
requestUpdate();
} else if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) {
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
// count is settingsCount + 1 because row 0 is the category tab bar.
const int page =
std::max(1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false));
mappedInput.wasListScroll(selectedSettingIndex, settingsCount + 1, page);
requestUpdate();
}
// Tap a settings row to select + activate it. Row 0 is the tab bar, so the list
// is drawn at selectedSettingIndex - 1; map the tapped row back by +1.
int downId = -1;
if (!swiped && mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
selectedSettingIndex = downId + 1;
requestUpdate();
}
int tappedId = -1;
if (!swiped && mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
selectedSettingIndex = tappedId + 1;
toggleCurrentSetting();
requestUpdate();
return;
}
// A tap on a category tab switches to it (keeps focus on the tab row); the
// hasChangedCategory block below swaps in that category's settings list.
int tabId = -1;
if (!swiped && mappedInput.wasTabTapped(tabId) && tabId >= 0 && tabId < categoryCount) {
selectedCategoryIndex = tabId;
selectedSettingIndex = 0;
hasChangedCategory = true;
requestUpdate();
}
// Handle actions with early return
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (selectedSettingIndex == 0) {
@@ -139,7 +189,6 @@ void SettingsActivity::loop() {
return;
}
// Handle navigation
buttonNavigator.onNextRelease([this] {
selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1);
requestUpdate();
@@ -379,7 +428,6 @@ void SettingsActivity::render(RenderLock&&) {
},
true);
// Draw help text
const auto confirmLabel =
(selectedSettingIndex == 0)
? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])

Some files were not shown because too many files have changed in this diff Show More