Compare commits
60
Commits
1.4.0
...
feat-touch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
757c0b6d9e | ||
|
|
2c8ef01894 | ||
|
|
cbaa498ccc | ||
|
|
ebebc6f202 | ||
|
|
970b2c6ca1 | ||
|
|
b6ce599b20 | ||
|
|
f54eab2725 | ||
|
|
0d4c9ab91b | ||
|
|
0daa9db243 | ||
|
|
a2f2eea79e | ||
|
|
86a9b9c4a2 | ||
|
|
ba44978ac4 | ||
|
|
555f76da88 | ||
|
|
0a57c0a5a7 | ||
|
|
a09aef0889 | ||
|
|
8626d69f46 | ||
|
|
fc89e57e69 | ||
|
|
487613b082 | ||
|
|
8d5b119644 | ||
|
|
7271c00d35 | ||
|
|
1bd718fad9 | ||
|
|
61cb4946d6 | ||
|
|
4e352b9894 | ||
|
|
4e156e1617 | ||
|
|
3e0ba4488b | ||
|
|
e42503f1a4 | ||
|
|
c812091a4a | ||
|
|
beb4e82c9c | ||
|
|
a242397fa0 | ||
|
|
1511ab35d5 | ||
|
|
57c389c0b6 | ||
|
|
06ff5aa44a | ||
|
|
526cf1b6f9 | ||
|
|
6648a980cb | ||
|
|
e567620003 | ||
|
|
0b575d1965 | ||
|
|
4f55444f67 | ||
|
|
4a17d21d80 | ||
|
|
1c63847631 | ||
|
|
4a5b45409f | ||
|
|
e103cdfd11 | ||
|
|
cef0d267b7 | ||
|
|
9f3dddabe6 | ||
|
|
350fc8472a | ||
|
|
79ef9d6a35 | ||
|
|
0036a220be | ||
|
|
8ad538c98b | ||
|
|
f1b5da25b0 | ||
|
|
d739827db2 | ||
|
|
9176e76f5a | ||
|
|
7294610894 | ||
|
|
a51098725d | ||
|
|
6acecd8ea6 | ||
|
|
9b7b9e9094 | ||
|
|
204aff6be6 | ||
|
|
28d8ad565f | ||
|
|
ed6690c0cc | ||
|
|
1bc154da8b | ||
|
|
c308520f9c | ||
|
|
38ad4f9036 |
+4
-3
@@ -1,3 +1,4 @@
|
|||||||
[submodule "open-x4-sdk"]
|
[submodule "freeink-sdk"]
|
||||||
path = open-x4-sdk
|
path = freeink-sdk
|
||||||
url = https://github.com/crosspoint-reader/community-sdk.git
|
url = https://github.com/Free-Ink/freeink-sdk.git
|
||||||
|
branch = main
|
||||||
|
|||||||
+2
-2
@@ -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).
|
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
||||||
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
* 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.
|
* 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).
|
* 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.
|
* 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).
|
* 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/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
||||||
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
||||||
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
* 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
|
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
||||||
|
|
||||||
### Hardware Abstraction Layer (HAL)
|
### Hardware Abstraction Layer (HAL)
|
||||||
|
|||||||
+37
-10
@@ -122,19 +122,43 @@ A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined
|
|||||||
|
|
||||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||||
|
|
||||||
1. Install the plugin in Calibre:
|
#### Installing the Plugin in Calibre
|
||||||
|
|
||||||
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
|
||||||
|
|
||||||
- Download the zip file.
|
|
||||||
|
|
||||||
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
|
||||||
|
|
||||||
2. On the device: File Transfer -> Calibre Wireless, then join a network.
|
If you don't already have the plugin installed:
|
||||||
|
|
||||||
3. Make sure your computer is on the same Wi-Fi network.
|
1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||||
|
2. Download the zip file.
|
||||||
|
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||||
|
4. Restart Calibre.
|
||||||
|
|
||||||
4. In Calibre, click "Send to device" to transfer books.
|
#### Configuring the CrossPoint Plugin in Calibre
|
||||||
|
1. In Calibre select Preferences.
|
||||||
|
2. In the Preferences dialog select Plugins.
|
||||||
|
3. In Plugins search for "crosspoint".
|
||||||
|
4. Click on "Customize plugin".
|
||||||
|
5. Update the value for "Host" to match the IP for your device.
|
||||||
|
6. Leave the other settings as they are.
|
||||||
|
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
|
||||||
|
8. Restart Calibre.
|
||||||
|
|
||||||
|
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
|
||||||
|
|
||||||
|
#### Uploading Books
|
||||||
|
|
||||||
|
To upload a book using the CrossPoint plugin in Calibre:
|
||||||
|
|
||||||
|
1. On the device: File Transfer -> Calibre Wireless, then join a network.
|
||||||
|
2. Select one or more books.
|
||||||
|
3. Right-click on that selection.
|
||||||
|
4. Select "Send to Device" > "Send to main memory"
|
||||||
|
|
||||||
|
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
|
||||||
|
|
||||||
|
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
|
||||||
|
|
||||||
|
#### Removing a Book
|
||||||
|
|
||||||
|
Books cannot be removed from your device through Calibre. Use the web interface instead.
|
||||||
|
|
||||||
### 3.6 Settings
|
### 3.6 Settings
|
||||||
|
|
||||||
@@ -150,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)
|
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||||
- "None" - A blank screen
|
- "None" - A blank screen
|
||||||
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
|
- "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:
|
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||||
|
|
||||||
@@ -162,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
|
- "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
|
- "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:
|
- **Status Bar**: Configure the status bar displayed while reading:
|
||||||
|
|
||||||
- "None" - No status bar
|
- "None" - No status bar
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Formats all C/C++ source and header files in the repository, excluding
|
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).
|
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||||
|
|
||||||
The clang-format binary path is resolved once and cached in
|
The clang-format binary path is resolved once and cached in
|
||||||
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
|
|||||||
$clangFormat = Resolve-ClangFormat
|
$clangFormat = Resolve-ClangFormat
|
||||||
|
|
||||||
$exclude = @(
|
$exclude = @(
|
||||||
'open-x4-sdk'
|
'freeink-sdk'
|
||||||
'lib\EpdFont\builtinFonts'
|
'lib\EpdFont\builtinFonts'
|
||||||
'lib\Epub\Epub\hyphenation\generated'
|
'lib\Epub\Epub\hyphenation\generated'
|
||||||
'lib\uzlib'
|
'lib\uzlib'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ At a high level, it is firmware that uses an activity-driven application archite
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
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]
|
B --> C[lib/hal wrappers]
|
||||||
C --> D[src/main.cpp runtime loop]
|
C --> D[src/main.cpp runtime loop]
|
||||||
D --> E[Activities layer]
|
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/`: app orchestration, settings/state, and activity implementations
|
||||||
- `src/network/`: web server and OTA/update networking
|
- `src/network/`: web server and OTA/update networking
|
||||||
- `src/components/`: theming and shared UI components
|
- `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/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
||||||
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
- `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
|
- `docs/`: user and technical documentation
|
||||||
|
|
||||||
## Embedded constraints that shape design
|
## Embedded constraints that shape design
|
||||||
|
|||||||
Submodule
+1
Submodule freeink-sdk added at 329a4bebef
@@ -37,3 +37,5 @@
|
|||||||
#include <builtinFonts/ubuntu_10_regular.h>
|
#include <builtinFonts/ubuntu_10_regular.h>
|
||||||
#include <builtinFonts/ubuntu_12_bold.h>
|
#include <builtinFonts/ubuntu_12_bold.h>
|
||||||
#include <builtinFonts/ubuntu_12_regular.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
@@ -94,6 +94,13 @@ ruby -rdigest -e 'puts [
|
|||||||
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
].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 ($(
|
echo "#define SMALL_FONT_ID ($(
|
||||||
ruby -rdigest -e 'puts [
|
ruby -rdigest -e 'puts [
|
||||||
"./notosans_8_regular.h",
|
"./notosans_8_regular.h",
|
||||||
|
|||||||
@@ -28,23 +28,42 @@ for size in ${NOTOSANS_FONT_SIZES[@]}; do
|
|||||||
done
|
done
|
||||||
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)
|
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")
|
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 size in ${UI_FONT_SIZES[@]}; do
|
||||||
for style in ${UI_FONT_STYLES[@]}; do
|
for style in ${UI_FONT_STYLES[@]}; do
|
||||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
generate_ui_font $size $style ""
|
||||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
done
|
||||||
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
done
|
||||||
# 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
|
for size in ${UI_FONT_SIZES_LARGE[@]}; do
|
||||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
for style in ${UI_FONT_STYLES[@]}; do
|
||||||
# (fontstack is ordered by descending priority).
|
generate_ui_font $size $style "--2bit --compress"
|
||||||
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"
|
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -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 bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.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
|
} // namespace
|
||||||
|
|
||||||
|
BookMetadataCache::~BookMetadataCache() {
|
||||||
|
if (ioMutex) {
|
||||||
|
vSemaphoreDelete(ioMutex);
|
||||||
|
ioMutex = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
|
|
||||||
bool BookMetadataCache::beginWrite() {
|
bool BookMetadataCache::beginWrite() {
|
||||||
@@ -375,6 +414,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
|||||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
bool BookMetadataCache::load() {
|
bool BookMetadataCache::load() {
|
||||||
|
CacheIoLock ioLock(ioMutex);
|
||||||
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -392,11 +432,13 @@ bool BookMetadataCache::load() {
|
|||||||
serialization::readPod(bookFile, spineCount);
|
serialization::readPod(bookFile, spineCount);
|
||||||
serialization::readPod(bookFile, tocCount);
|
serialization::readPod(bookFile, tocCount);
|
||||||
|
|
||||||
serialization::readString(bookFile, coreMetadata.title);
|
if (!readStringBounded(bookFile, coreMetadata.title) || !readStringBounded(bookFile, coreMetadata.author) ||
|
||||||
serialization::readString(bookFile, coreMetadata.author);
|
!readStringBounded(bookFile, coreMetadata.language) || !readStringBounded(bookFile, coreMetadata.coverItemHref) ||
|
||||||
serialization::readString(bookFile, coreMetadata.language);
|
!readStringBounded(bookFile, coreMetadata.textReferenceHref)) {
|
||||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
LOG_ERR("BMC", "Invalid cache metadata strings");
|
||||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
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) {
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
|
CacheIoLock ioLock(ioMutex);
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
@@ -418,11 +461,16 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
|
|||||||
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
|
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
|
||||||
uint32_t spineEntryPos;
|
uint32_t spineEntryPos;
|
||||||
serialization::readPod(bookFile, 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);
|
bookFile.seek(spineEntryPos);
|
||||||
return readSpineEntry(bookFile);
|
return readSpineEntry(bookFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
|
CacheIoLock ioLock(ioMutex);
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||||
return {};
|
return {};
|
||||||
@@ -437,24 +485,34 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
|||||||
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
|
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
|
||||||
uint32_t tocEntryPos;
|
uint32_t tocEntryPos;
|
||||||
serialization::readPod(bookFile, 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);
|
bookFile.seek(tocEntryPos);
|
||||||
return readTocEntry(bookFile);
|
return readTocEntry(bookFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||||
SpineEntry entry;
|
SpineEntry entry;
|
||||||
serialization::readString(file, entry.href);
|
if (!readStringBounded(file, entry.href) ||
|
||||||
serialization::readPod(file, entry.cumulativeSize);
|
file.read(&entry.cumulativeSize, sizeof(entry.cumulativeSize)) !=
|
||||||
serialization::readPod(file, entry.tocIndex);
|
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;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
||||||
TocEntry entry;
|
TocEntry entry;
|
||||||
serialization::readString(file, entry.title);
|
if (!readStringBounded(file, entry.title) || !readStringBounded(file, entry.href) ||
|
||||||
serialization::readString(file, entry.href);
|
!readStringBounded(file, entry.anchor) ||
|
||||||
serialization::readString(file, entry.anchor);
|
file.read(&entry.level, sizeof(entry.level)) != static_cast<int>(sizeof(entry.level)) ||
|
||||||
serialization::readPod(file, entry.level);
|
file.read(&entry.spineIndex, sizeof(entry.spineIndex)) != static_cast<int>(sizeof(entry.spineIndex))) {
|
||||||
serialization::readPod(file, entry.spineIndex);
|
LOG_ERR("BMC", "Invalid TOC cache entry");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
@@ -54,6 +55,7 @@ class BookMetadataCache {
|
|||||||
// Temp file handles during build
|
// Temp file handles during build
|
||||||
HalFile spineFile;
|
HalFile spineFile;
|
||||||
HalFile tocFile;
|
HalFile tocFile;
|
||||||
|
SemaphoreHandle_t ioMutex;
|
||||||
|
|
||||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||||
struct SpineHrefIndexEntry {
|
struct SpineHrefIndexEntry {
|
||||||
@@ -85,8 +87,14 @@ class BookMetadataCache {
|
|||||||
BookMetadata coreMetadata;
|
BookMetadata coreMetadata;
|
||||||
|
|
||||||
explicit BookMetadataCache(std::string cachePath)
|
explicit BookMetadataCache(std::string cachePath)
|
||||||
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
: cachePath(std::move(cachePath)),
|
||||||
~BookMetadataCache() = default;
|
lutOffset(0),
|
||||||
|
spineCount(0),
|
||||||
|
tocCount(0),
|
||||||
|
loaded(false),
|
||||||
|
buildMode(false),
|
||||||
|
ioMutex(xSemaphoreCreateRecursiveMutex()) {}
|
||||||
|
~BookMetadataCache();
|
||||||
|
|
||||||
// Building phase (stream to disk immediately)
|
// Building phase (stream to disk immediately)
|
||||||
bool beginWrite();
|
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)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
self->state = IN_GUIDE;
|
self->state = IN_GUIDE;
|
||||||
// TODO Remove print
|
|
||||||
LOG_DBG("COF", "Entering guide state.");
|
LOG_DBG("COF", "Entering guide state.");
|
||||||
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
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.");
|
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||||
|
|||||||
+22
-14
@@ -3,6 +3,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace FsHelpers {
|
namespace FsHelpers {
|
||||||
@@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string normalisePath(const std::string& path) {
|
std::string normalisePath(const std::string& path) {
|
||||||
std::vector<std::string> components;
|
std::vector<std::string_view> components;
|
||||||
std::string component;
|
components.reserve(8); // Eight nested folders is more than we might expect
|
||||||
|
|
||||||
for (const auto c : path) {
|
size_t start = 0;
|
||||||
if (c == '/') {
|
for (size_t i = 0; i <= path.length(); ++i) {
|
||||||
if (!component.empty()) {
|
if (i == path.length() || path[i] == '/') {
|
||||||
|
if (i > start) {
|
||||||
|
std::string_view component(path.data() + start, i - start);
|
||||||
if (component == "..") {
|
if (component == "..") {
|
||||||
if (!components.empty()) {
|
if (!components.empty()) {
|
||||||
components.pop_back();
|
components.pop_back();
|
||||||
@@ -49,23 +52,28 @@ std::string normalisePath(const std::string& path) {
|
|||||||
} else {
|
} else {
|
||||||
components.push_back(component);
|
components.push_back(component);
|
||||||
}
|
}
|
||||||
component.clear();
|
|
||||||
}
|
}
|
||||||
} else {
|
start = i + 1;
|
||||||
component += c;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!component.empty()) {
|
if (components.empty()) {
|
||||||
components.push_back(component);
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total_len = 0;
|
||||||
|
for (const auto& c : components) {
|
||||||
|
total_len += c.length() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string result;
|
std::string result;
|
||||||
for (const auto& c : components) {
|
result.reserve(total_len - 1);
|
||||||
if (!result.empty()) {
|
|
||||||
result += "/";
|
for (size_t i = 0; i < components.size(); ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
result += '/';
|
||||||
}
|
}
|
||||||
result += c;
|
result.append(components[i].data(), components[i].length());
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+281
-25
@@ -3,6 +3,7 @@
|
|||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
|
#include <Icon.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <SdCardFont.h>
|
#include <SdCardFont.h>
|
||||||
#include <Utf8.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
|
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||||
// This should always be inlined for better performance
|
// This should always be inlined for better performance
|
||||||
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -407,7 +425,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
@@ -661,8 +679,10 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
if (state) {
|
||||||
drawLine(x, fillY, x + width - 1, fillY, state);
|
fillRectImpl<Color::Black>(x, y, width, height);
|
||||||
|
} else {
|
||||||
|
fillRectImpl<Color::White>(x, y, width, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,26 +714,194 @@ void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) con
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
||||||
if (color == Color::Clear) {
|
switch (color) {
|
||||||
} else if (color == Color::Black) {
|
case Color::Clear:
|
||||||
fillRect(x, y, width, height, true);
|
break;
|
||||||
} else if (color == Color::White) {
|
case Color::Black:
|
||||||
fillRect(x, y, width, height, false);
|
fillRectImpl<Color::Black>(x, y, width, height);
|
||||||
} else if (color == Color::LightGray) {
|
break;
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
case Color::White:
|
||||||
for (int fillX = x; fillX < x + width; fillX++) {
|
fillRectImpl<Color::White>(x, y, width, height);
|
||||||
drawPixelDither<Color::LightGray>(fillX, fillY);
|
break;
|
||||||
|
case Color::LightGray:
|
||||||
|
fillRectImpl<Color::LightGray>(x, y, width, height);
|
||||||
|
break;
|
||||||
|
case Color::DarkGray:
|
||||||
|
fillRectImpl<Color::DarkGray>(x, y, width, height);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <Color C>
|
||||||
|
void GfxRenderer::fillRectImpl(const int x, const int y, const int width, const int height) const {
|
||||||
|
if constexpr (C == Color::Clear) return;
|
||||||
|
if (width <= 0 || height <= 0) return;
|
||||||
|
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||||
|
|
||||||
|
// Clip in logical space.
|
||||||
|
const int screenW = getScreenWidth();
|
||||||
|
const int screenH = getScreenHeight();
|
||||||
|
const int lx0 = std::max(0, x);
|
||||||
|
const int ly0 = std::max(0, y);
|
||||||
|
const int lx1 = std::min(screenW, x + width);
|
||||||
|
const int ly1 = std::min(screenH, y + height);
|
||||||
|
if (lx0 >= lx1 || ly0 >= ly1) return;
|
||||||
|
|
||||||
|
// Rotate the two opposing logical corners into physical-framebuffer space.
|
||||||
|
// The bounding rect in physical space is the rect we need to fill — rotation
|
||||||
|
// is rigid (no shear/stretch) so the bbox of the two corners IS the rect.
|
||||||
|
int paX, paY, pbX, pbY;
|
||||||
|
rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight);
|
||||||
|
rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight);
|
||||||
|
|
||||||
|
const int phyX0 = std::min(paX, pbX);
|
||||||
|
const int phyX1 = std::max(paX, pbX); // inclusive
|
||||||
|
int phyY0 = std::min(paY, pbY);
|
||||||
|
int phyY1 = std::max(paY, pbY);
|
||||||
|
|
||||||
|
// Strip mode: clip Y range to the active band and redirect writes.
|
||||||
|
uint8_t* target = getWriteTarget();
|
||||||
|
const int originY = getWriteOriginY();
|
||||||
|
const int writeRows = getWriteRows();
|
||||||
|
phyY0 = std::max(phyY0, originY);
|
||||||
|
phyY1 = std::min(phyY1, originY + writeRows - 1);
|
||||||
|
if (phyY0 > phyY1) return;
|
||||||
|
|
||||||
|
// Bit/byte layout: MSB-first within a byte, so phyX → bit (7 - (phyX & 7)).
|
||||||
|
// Head and tail masks cover only the in-rect bits of the first/last byte.
|
||||||
|
const int byteStart = phyX0 >> 3;
|
||||||
|
const int byteEnd = phyX1 >> 3; // inclusive
|
||||||
|
const uint8_t headMask = static_cast<uint8_t>(0xFFu >> (phyX0 & 7));
|
||||||
|
const uint8_t tailMask = static_cast<uint8_t>(0xFFu << (7 - (phyX1 & 7)));
|
||||||
|
const int32_t panelStride = static_cast<int32_t>(panelWidthBytes);
|
||||||
|
|
||||||
|
if constexpr (C == Color::Black || C == Color::White) {
|
||||||
|
// Solid fill. Framebuffer: 0 = black, 1 = white.
|
||||||
|
const uint8_t fillByte = (C == Color::Black) ? 0x00u : 0xFFu;
|
||||||
|
for (int py = phyY0; py <= phyY1; ++py) {
|
||||||
|
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
|
||||||
|
if (byteStart == byteEnd) {
|
||||||
|
const uint8_t mask = headMask & tailMask;
|
||||||
|
if constexpr (C == Color::Black) {
|
||||||
|
row[byteStart] &= static_cast<uint8_t>(~mask);
|
||||||
|
} else {
|
||||||
|
row[byteStart] |= mask;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if constexpr (C == Color::Black) {
|
||||||
|
row[byteStart] &= static_cast<uint8_t>(~headMask);
|
||||||
|
if (byteEnd > byteStart + 1) {
|
||||||
|
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
|
||||||
|
}
|
||||||
|
row[byteEnd] &= static_cast<uint8_t>(~tailMask);
|
||||||
|
} else {
|
||||||
|
row[byteStart] |= headMask;
|
||||||
|
if (byteEnd > byteStart + 1) {
|
||||||
|
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
|
||||||
|
}
|
||||||
|
row[byteEnd] |= tailMask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (color == Color::DarkGray) {
|
} else {
|
||||||
for (int fillY = y; fillY < y + height; fillY++) {
|
// Dither (LightGray / DarkGray). Both patterns have period 2 in logical
|
||||||
for (int fillX = x; fillX < x + width; fillX++) {
|
// (x, y), so per physical row we precompute one byte that represents the
|
||||||
drawPixelDither<Color::DarkGray>(fillX, fillY);
|
// pattern across an 8-pixel stretch — every full byte in the row uses
|
||||||
|
// that same value.
|
||||||
|
//
|
||||||
|
// dlxPerPhyX / dlyPerPhyX: how logical (x, y) change as phyX increments
|
||||||
|
// along a physical row. Derived from inverting rotateCoordinates.
|
||||||
|
int dlxPerPhyX = 0, dlyPerPhyX = 0;
|
||||||
|
switch (orientation) {
|
||||||
|
case Portrait:
|
||||||
|
dlxPerPhyX = 0;
|
||||||
|
dlyPerPhyX = 1;
|
||||||
|
break;
|
||||||
|
case PortraitInverted:
|
||||||
|
dlxPerPhyX = 0;
|
||||||
|
dlyPerPhyX = -1;
|
||||||
|
break;
|
||||||
|
case LandscapeClockwise:
|
||||||
|
dlxPerPhyX = -1;
|
||||||
|
dlyPerPhyX = 0;
|
||||||
|
break;
|
||||||
|
case LandscapeCounterClockwise:
|
||||||
|
dlxPerPhyX = 1;
|
||||||
|
dlyPerPhyX = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The dither pattern has period 2 in logical space, and each orientation
|
||||||
|
// maps py to logical coords with a fixed parity relationship. The
|
||||||
|
// blackMask byte therefore repeats with period 2 in py. Precompute both
|
||||||
|
// variants outside the row loop to eliminate the per-row switch + 8-bit
|
||||||
|
// construction loop.
|
||||||
|
uint8_t blackMasks[2];
|
||||||
|
for (int parityIdx = 0; parityIdx < 2; ++parityIdx) {
|
||||||
|
const int samplePy = phyY0 + parityIdx;
|
||||||
|
int lxBase = 0, lyBase = 0;
|
||||||
|
switch (orientation) {
|
||||||
|
case Portrait:
|
||||||
|
lxBase = panelHeight - 1 - samplePy;
|
||||||
|
lyBase = byteStart * 8;
|
||||||
|
break;
|
||||||
|
case PortraitInverted:
|
||||||
|
lxBase = samplePy;
|
||||||
|
lyBase = panelWidth - 1 - byteStart * 8;
|
||||||
|
break;
|
||||||
|
case LandscapeClockwise:
|
||||||
|
lxBase = panelWidth - 1 - byteStart * 8;
|
||||||
|
lyBase = panelHeight - 1 - samplePy;
|
||||||
|
break;
|
||||||
|
case LandscapeCounterClockwise:
|
||||||
|
lxBase = byteStart * 8;
|
||||||
|
lyBase = samplePy;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t mask = 0;
|
||||||
|
for (int b = 0; b < 8; ++b) {
|
||||||
|
const int lx = lxBase + b * dlxPerPhyX;
|
||||||
|
const int ly = lyBase + b * dlyPerPhyX;
|
||||||
|
bool isBlack;
|
||||||
|
if constexpr (C == Color::LightGray) {
|
||||||
|
isBlack = ((lx & 1) == 0) && ((ly & 1) == 0);
|
||||||
|
} else { // DarkGray
|
||||||
|
isBlack = (((lx + ly) & 1) == 0);
|
||||||
|
}
|
||||||
|
if (isBlack) mask |= static_cast<uint8_t>(1u << (7 - b));
|
||||||
|
}
|
||||||
|
blackMasks[samplePy & 1] = mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int py = phyY0; py <= phyY1; ++py) {
|
||||||
|
const uint8_t blackMask = blackMasks[py & 1];
|
||||||
|
const uint8_t whiteMask = static_cast<uint8_t>(~blackMask);
|
||||||
|
|
||||||
|
// Dither writes BOTH inks (the slow path called drawPixel for every
|
||||||
|
// pixel — setting or clearing — so we must do the same). Inside the
|
||||||
|
// rect mask: write whiteMask (1s where white, 0s where black). Outside
|
||||||
|
// the rect mask: leave the framebuffer untouched.
|
||||||
|
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
|
||||||
|
if (byteStart == byteEnd) {
|
||||||
|
const uint8_t rectMask = headMask & tailMask;
|
||||||
|
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~rectMask) | (rectMask & whiteMask));
|
||||||
|
} else {
|
||||||
|
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~headMask) | (headMask & whiteMask));
|
||||||
|
if (byteEnd > byteStart + 1) {
|
||||||
|
// Period 2, so every full byte in this row is exactly whiteMask.
|
||||||
|
memset(row + byteStart + 1, whiteMask, byteEnd - byteStart - 1);
|
||||||
|
}
|
||||||
|
row[byteEnd] = static_cast<uint8_t>((row[byteEnd] & ~tailMask) | (tailMask & whiteMask));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::Black>(int, int, int, int) const;
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::White>(int, int, int, int) const;
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::LightGray>(int, int, int, int) const;
|
||||||
|
template void GfxRenderer::fillRectImpl<Color::DarkGray>(int, int, int, int) const;
|
||||||
|
|
||||||
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
|
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
|
||||||
const int radius, const Color color) const {
|
const int radius, const Color color) const {
|
||||||
if (radius <= 0 || color == Color::Clear) {
|
if (radius <= 0 || color == Color::Clear) {
|
||||||
@@ -888,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);
|
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,
|
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 {
|
const float cropX, const float cropY) const {
|
||||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||||
@@ -1296,6 +1499,38 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
return panelWidth;
|
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
|
// Translate a logical rect through rotateCoordinates and take the bounding
|
||||||
// box of its four corners on the physical panel. Output coords are inclusive
|
// 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.
|
// and clamped. Returns false if the rect ends up fully off-panel.
|
||||||
@@ -1387,7 +1622,7 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
|||||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1408,7 +1643,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
|
|||||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
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;
|
if (fontIt == fontMap.end()) return 0;
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
|
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
|
||||||
@@ -1422,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,
|
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) return 0;
|
if (fontIt == fontMap.end()) return 0;
|
||||||
const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
|
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
|
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
|
||||||
@@ -1454,7 +1689,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
return fp4::toPixel(widthFP);
|
return fp4::toPixel(widthFP);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1490,7 +1725,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1500,7 +1735,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getLineHeight(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()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1510,7 +1745,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextHeight(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()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1518,6 +1753,27 @@ int GfxRenderer::getTextHeight(const int fontId) const {
|
|||||||
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
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,
|
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
// Cannot draw a NULL / empty string
|
// Cannot draw a NULL / empty string
|
||||||
@@ -1525,7 +1781,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ enum class BidiBaseDir : signed char { AUTO = -1, LTR = 0, RTL = 1 };
|
|||||||
|
|
||||||
class FontCacheManager;
|
class FontCacheManager;
|
||||||
class SdCardFont;
|
class SdCardFont;
|
||||||
|
namespace freeink {
|
||||||
|
struct Icon;
|
||||||
|
}
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -51,6 +54,13 @@ class GfxRenderer {
|
|||||||
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
||||||
std::vector<uint8_t*> bwBufferChunks;
|
std::vector<uint8_t*> bwBufferChunks;
|
||||||
std::map<int, EpdFontFamily> fontMap;
|
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
|
// Mutable because ensureSdCardFontReady() is const (called from layout code
|
||||||
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
||||||
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
||||||
@@ -81,6 +91,12 @@ class GfxRenderer {
|
|||||||
void drawPixelDither(int x, int y) const;
|
void drawPixelDither(int x, int y) const;
|
||||||
template <Color color>
|
template <Color color>
|
||||||
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
||||||
|
// Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's
|
||||||
|
// two opposing corners into physical-framebuffer space once, then walks each
|
||||||
|
// physical row with head-mask / middle memset / tail-mask byte writes — no
|
||||||
|
// per-pixel rotation, no per-pixel RMW.
|
||||||
|
template <Color color>
|
||||||
|
void fillRectImpl(int x, int y, int width, int height) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GfxRenderer(HalDisplay& halDisplay)
|
explicit GfxRenderer(HalDisplay& halDisplay)
|
||||||
@@ -95,6 +111,14 @@ class GfxRenderer {
|
|||||||
// Setup
|
// Setup
|
||||||
void begin(); // must be called right after display.begin()
|
void begin(); // must be called right after display.begin()
|
||||||
void insertFont(int fontId, EpdFontFamily font);
|
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.
|
// Clears both the flash-font map and any SD-font registration for fontId.
|
||||||
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
||||||
// the underlying SdCardFont and forget the SD-side unregister.
|
// the underlying SdCardFont and forget the SD-side unregister.
|
||||||
@@ -135,6 +159,11 @@ class GfxRenderer {
|
|||||||
void clearScreen(uint8_t color = 0xFF) const;
|
void clearScreen(uint8_t color = 0xFF) const;
|
||||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) 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()
|
// Tiled grayscale strip target. While active, drawPixel() and clearScreen()
|
||||||
// operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical
|
// operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical
|
||||||
// rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels
|
// rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels
|
||||||
@@ -202,6 +231,12 @@ class GfxRenderer {
|
|||||||
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
||||||
int getFontAscenderSize(int fontId) const;
|
int getFontAscenderSize(int fontId) const;
|
||||||
int getLineHeight(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,
|
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||||
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
|
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Згладжванне тэксту"
|
|||||||
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
|
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
|
||||||
STR_ORIENTATION: "Арыентацыя чытання"
|
STR_ORIENTATION: "Арыентацыя чытання"
|
||||||
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
|
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Сэнсарнае кіраванне чытаннем"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
|
||||||
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
|
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
|
||||||
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
|
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
|
||||||
@@ -126,6 +127,7 @@ STR_PAGE_TURN: "Перагортванне"
|
|||||||
STR_PORTRAIT: "Партрэт"
|
STR_PORTRAIT: "Партрэт"
|
||||||
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||||
STR_INVERTED: "Інверсія"
|
STR_INVERTED: "Інверсія"
|
||||||
|
STR_ORIENTATION_INVERTED: "Партрэт 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||||
STR_PREV_NEXT: "Назад/Наперад"
|
STR_PREV_NEXT: "Назад/Наперад"
|
||||||
STR_NEXT_PREV: "Наперад/Назад"
|
STR_NEXT_PREV: "Наперад/Назад"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Suprimir"
|
|||||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
||||||
STR_ORIENTATION: "Orientació de lectura"
|
STR_ORIENTATION: "Orientació de lectura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||||
@@ -136,6 +137,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Horitzontal horari"
|
STR_LANDSCAPE_CW: "Horitzontal horari"
|
||||||
STR_INVERTED: "Invertit"
|
STR_INVERTED: "Invertit"
|
||||||
|
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||||
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
||||||
STR_PREV_NEXT: "Anterior/Següent"
|
STR_PREV_NEXT: "Anterior/Següent"
|
||||||
STR_NEXT_PREV: "Següent/Anterior"
|
STR_NEXT_PREV: "Següent/Anterior"
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Vyhlazování textu"
|
|||||||
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
|
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
|
||||||
STR_ORIENTATION: "Orientace čtení"
|
STR_ORIENTATION: "Orientace čtení"
|
||||||
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
|
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_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: "Chování při dlouhém stisknutí tlačítka"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
||||||
@@ -131,6 +132,7 @@ STR_PAGE_TURN: "Otáčení stránek"
|
|||||||
STR_PORTRAIT: "Na výšku"
|
STR_PORTRAIT: "Na výšku"
|
||||||
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
||||||
STR_INVERTED: "Invertovaný"
|
STR_INVERTED: "Invertovaný"
|
||||||
|
STR_ORIENTATION_INVERTED: "Na výšku 180°"
|
||||||
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
||||||
STR_PREV_NEXT: "Předchozí/Další"
|
STR_PREV_NEXT: "Předchozí/Další"
|
||||||
STR_NEXT_PREV: "Další/Předchozí"
|
STR_NEXT_PREV: "Další/Předchozí"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Skjul"
|
|||||||
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
|
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
|
||||||
STR_ORIENTATION: "Læseretning"
|
STR_ORIENTATION: "Læseretning"
|
||||||
STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientér forreste knapper"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
|
STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
|
||||||
@@ -136,6 +137,7 @@ STR_PAGE_TURN: "Sideskift"
|
|||||||
STR_PORTRAIT: "Portræt"
|
STR_PORTRAIT: "Portræt"
|
||||||
STR_LANDSCAPE_CW: "Liggende med uret"
|
STR_LANDSCAPE_CW: "Liggende med uret"
|
||||||
STR_INVERTED: "Inverteret"
|
STR_INVERTED: "Inverteret"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portræt 180°"
|
||||||
STR_LANDSCAPE_CCW: "Liggende mod uret"
|
STR_LANDSCAPE_CCW: "Liggende mod uret"
|
||||||
STR_PREV_NEXT: "Forrige/Næste"
|
STR_PREV_NEXT: "Forrige/Næste"
|
||||||
STR_NEXT_PREV: "Næste/Forrige"
|
STR_NEXT_PREV: "Næste/Forrige"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Verbergen"
|
|||||||
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
|
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
|
||||||
STR_ORIENTATION: "Leesstand"
|
STR_ORIENTATION: "Leesstand"
|
||||||
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
|
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Aanraakbediening lezer"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Richt voorste knoppen"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Richt voorste knoppen"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
@@ -135,7 +136,8 @@ STR_SLEEP: "Slaap"
|
|||||||
STR_PAGE_TURN: "Pagina omslaan"
|
STR_PAGE_TURN: "Pagina omslaan"
|
||||||
STR_PORTRAIT: "Staand"
|
STR_PORTRAIT: "Staand"
|
||||||
STR_LANDSCAPE_CW: "Liggend (rechtsom)"
|
STR_LANDSCAPE_CW: "Liggend (rechtsom)"
|
||||||
STR_INVERTED: "Omgekeerd"
|
STR_INVERTED: "Geïnverteerd"
|
||||||
|
STR_ORIENTATION_INVERTED: "Staand 180°"
|
||||||
STR_LANDSCAPE_CCW: "Liggend (linksom)"
|
STR_LANDSCAPE_CCW: "Liggend (linksom)"
|
||||||
STR_PREV_NEXT: "Vorige/Volgende"
|
STR_PREV_NEXT: "Vorige/Volgende"
|
||||||
STR_NEXT_PREV: "Volgende/Vorige"
|
STR_NEXT_PREV: "Volgende/Vorige"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Suppress"
|
|||||||
STR_SHORT_PWR_BTN: "Short Power Button Click"
|
STR_SHORT_PWR_BTN: "Short Power Button Click"
|
||||||
STR_ORIENTATION: "Reading Orientation"
|
STR_ORIENTATION: "Reading Orientation"
|
||||||
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Touch Reader Controls"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orient front buttons"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orient front buttons"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
@@ -139,6 +140,7 @@ STR_FORCE_REFRESH: "Refresh Screen"
|
|||||||
STR_PORTRAIT: "Portrait"
|
STR_PORTRAIT: "Portrait"
|
||||||
STR_LANDSCAPE_CW: "Landscape CW"
|
STR_LANDSCAPE_CW: "Landscape CW"
|
||||||
STR_INVERTED: "Inverted"
|
STR_INVERTED: "Inverted"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portrait 180°"
|
||||||
STR_LANDSCAPE_CCW: "Landscape CCW"
|
STR_LANDSCAPE_CCW: "Landscape CCW"
|
||||||
STR_PREV_NEXT: "Prev/Next"
|
STR_PREV_NEXT: "Prev/Next"
|
||||||
STR_NEXT_PREV: "Next/Prev"
|
STR_NEXT_PREV: "Next/Prev"
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Tekstin reunanpehmennys"
|
|||||||
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
|
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
|
||||||
STR_ORIENTATION: "Lukusuunta"
|
STR_ORIENTATION: "Lukusuunta"
|
||||||
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
|
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Kosketusohjaus (lukija)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Suuntaa etupainikkeet"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Suuntaa etupainikkeet"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
@@ -130,7 +131,8 @@ STR_SLEEP: "Lepotila"
|
|||||||
STR_PAGE_TURN: "Sivunkääntö"
|
STR_PAGE_TURN: "Sivunkääntö"
|
||||||
STR_PORTRAIT: "Pysty"
|
STR_PORTRAIT: "Pysty"
|
||||||
STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
|
STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
|
||||||
STR_INVERTED: "Käännetty"
|
STR_INVERTED: "Käänteinen"
|
||||||
|
STR_ORIENTATION_INVERTED: "Pysty 180°"
|
||||||
STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
|
STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
|
||||||
STR_PREV_NEXT: "Edell/Seur"
|
STR_PREV_NEXT: "Edell/Seur"
|
||||||
STR_NEXT_PREV: "Seur/Edell"
|
STR_NEXT_PREV: "Seur/Edell"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Masquer"
|
|||||||
STR_SHORT_PWR_BTN: "Appui court alim."
|
STR_SHORT_PWR_BTN: "Appui court alim."
|
||||||
STR_ORIENTATION: "Orientation de lecture"
|
STR_ORIENTATION: "Orientation de lecture"
|
||||||
STR_SIDE_BTN_LAYOUT: "Boutons latéraux"
|
STR_SIDE_BTN_LAYOUT: "Boutons latéraux"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Commandes tactiles (lecteur)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter boutons avant"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter boutons avant"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
|
STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
|
||||||
@@ -136,6 +137,7 @@ STR_PAGE_TURN: "Page suivante"
|
|||||||
STR_PORTRAIT: "Portrait"
|
STR_PORTRAIT: "Portrait"
|
||||||
STR_LANDSCAPE_CW: "Paysage"
|
STR_LANDSCAPE_CW: "Paysage"
|
||||||
STR_INVERTED: "Inversé"
|
STR_INVERTED: "Inversé"
|
||||||
|
STR_ORIENTATION_INVERTED: "Portrait 180°"
|
||||||
STR_LANDSCAPE_CCW: "Paysage inversé"
|
STR_LANDSCAPE_CCW: "Paysage inversé"
|
||||||
STR_PREV_NEXT: "Préc/Suiv"
|
STR_PREV_NEXT: "Préc/Suiv"
|
||||||
STR_NEXT_PREV: "Suiv/Préc"
|
STR_NEXT_PREV: "Suiv/Préc"
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ STR_TEXT_AA: "Schriftglättung"
|
|||||||
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
|
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
|
||||||
STR_ORIENTATION: "Leseausrichtung"
|
STR_ORIENTATION: "Leseausrichtung"
|
||||||
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Touch-Steuerung (Lesen)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
|
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AUS"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Aus"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
|
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
|
||||||
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"
|
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"
|
||||||
@@ -79,8 +80,9 @@ STR_FONT_SIZE: "Schriftgröße"
|
|||||||
STR_LINE_SPACING: "Lese-Zeilenabstand"
|
STR_LINE_SPACING: "Lese-Zeilenabstand"
|
||||||
STR_SCREEN_MARGIN: "Lese-Seitenränder"
|
STR_SCREEN_MARGIN: "Lese-Seitenränder"
|
||||||
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
|
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
|
||||||
|
STR_LONG_PRESS_MENU: "Menütaste lang drücken"
|
||||||
STR_HYPHENATION: "Silbentrennung"
|
STR_HYPHENATION: "Silbentrennung"
|
||||||
STR_TIME_TO_SLEEP: "Standby nach"
|
STR_TIME_TO_SLEEP: "Standby-Modus nach"
|
||||||
STR_REFRESH_FREQ: "Anti-Ghosting nach"
|
STR_REFRESH_FREQ: "Anti-Ghosting nach"
|
||||||
STR_KOREADER_SYNC: "KOReader-Synchr."
|
STR_KOREADER_SYNC: "KOReader-Synchr."
|
||||||
STR_CHECK_UPDATES: "Nach Updates suchen"
|
STR_CHECK_UPDATES: "Nach Updates suchen"
|
||||||
@@ -129,7 +131,8 @@ STR_PAGE_TURN: "Umblättern"
|
|||||||
STR_FORCE_REFRESH: "Bildschirm regenerieren"
|
STR_FORCE_REFRESH: "Bildschirm regenerieren"
|
||||||
STR_PORTRAIT: "Hochformat"
|
STR_PORTRAIT: "Hochformat"
|
||||||
STR_LANDSCAPE_CW: "Querformat rechts"
|
STR_LANDSCAPE_CW: "Querformat rechts"
|
||||||
STR_INVERTED: "Hochformat 180°"
|
STR_INVERTED: "Invertiert"
|
||||||
|
STR_ORIENTATION_INVERTED: "Hochformat 180°"
|
||||||
STR_LANDSCAPE_CCW: "Querformat links"
|
STR_LANDSCAPE_CCW: "Querformat links"
|
||||||
STR_PREV_NEXT: "Zurück/Weiter"
|
STR_PREV_NEXT: "Zurück/Weiter"
|
||||||
STR_NEXT_PREV: "Weiter/Zurück"
|
STR_NEXT_PREV: "Weiter/Zurück"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "הסתר תמונות"
|
|||||||
STR_SHORT_PWR_BTN: "לחיצה קצרה על כפתור ההפעלה"
|
STR_SHORT_PWR_BTN: "לחיצה קצרה על כפתור ההפעלה"
|
||||||
STR_ORIENTATION: "כיוון קריאה (מסך)"
|
STR_ORIENTATION: "כיוון קריאה (מסך)"
|
||||||
STR_SIDE_BTN_LAYOUT: "פריסת כפתורי צד (בקריאה)"
|
STR_SIDE_BTN_LAYOUT: "פריסת כפתורי צד (בקריאה)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "פקדי מגע בקריאה"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
|
STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
|
||||||
@@ -134,7 +135,8 @@ STR_PAGE_TURN: "העברת דף"
|
|||||||
STR_FORCE_REFRESH: "רענון מסך מלא"
|
STR_FORCE_REFRESH: "רענון מסך מלא"
|
||||||
STR_PORTRAIT: "לאורך"
|
STR_PORTRAIT: "לאורך"
|
||||||
STR_LANDSCAPE_CW: "לרוחב (ימינה)"
|
STR_LANDSCAPE_CW: "לרוחב (ימינה)"
|
||||||
STR_INVERTED: "הפוך"
|
STR_INVERTED: "היפוך צבעים"
|
||||||
|
STR_ORIENTATION_INVERTED: "לאורך 180°"
|
||||||
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
|
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
|
||||||
STR_PREV_NEXT: "הקודם/הבא"
|
STR_PREV_NEXT: "הקודם/הבא"
|
||||||
STR_NEXT_PREV: "הבא/הקודם"
|
STR_NEXT_PREV: "הבא/הקודם"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Elnyomás"
|
|||||||
STR_SHORT_PWR_BTN: "Rövid bekapcsológomb nyomás"
|
STR_SHORT_PWR_BTN: "Rövid bekapcsológomb nyomás"
|
||||||
STR_ORIENTATION: "Olvasási irány"
|
STR_ORIENTATION: "Olvasási irány"
|
||||||
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
|
||||||
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
|
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"
|
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"
|
||||||
@@ -132,7 +133,8 @@ STR_SLEEP: "Alvás"
|
|||||||
STR_PAGE_TURN: "Lapozás"
|
STR_PAGE_TURN: "Lapozás"
|
||||||
STR_PORTRAIT: "Álló"
|
STR_PORTRAIT: "Álló"
|
||||||
STR_LANDSCAPE_CW: "Fekvő jobbra"
|
STR_LANDSCAPE_CW: "Fekvő jobbra"
|
||||||
STR_INVERTED: "Fordított"
|
STR_INVERTED: "Invertált"
|
||||||
|
STR_ORIENTATION_INVERTED: "Álló 180°"
|
||||||
STR_LANDSCAPE_CCW: "Fekvő balra"
|
STR_LANDSCAPE_CCW: "Fekvő balra"
|
||||||
STR_PREV_NEXT: "Előző/Következő"
|
STR_PREV_NEXT: "Előző/Következő"
|
||||||
STR_NEXT_PREV: "Következő/Előző"
|
STR_NEXT_PREV: "Következő/Előző"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Nascondi"
|
|||||||
STR_SHORT_PWR_BTN: "Press. breve pul. accensione"
|
STR_SHORT_PWR_BTN: "Press. breve pul. accensione"
|
||||||
STR_ORIENTATION: "Orientamento lettura"
|
STR_ORIENTATION: "Orientamento lettura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Pul. laterali (lettore)"
|
STR_SIDE_BTN_LAYOUT: "Pul. laterali (lettore)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Controlli touch (lettore)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienta pul. frontali"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienta pul. frontali"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
|
STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
@@ -137,7 +138,8 @@ STR_PAGE_TURN: "Cambio pagina"
|
|||||||
STR_FORCE_REFRESH: "Refresh"
|
STR_FORCE_REFRESH: "Refresh"
|
||||||
STR_PORTRAIT: "Verticale"
|
STR_PORTRAIT: "Verticale"
|
||||||
STR_LANDSCAPE_CW: "Orizzontale Dx"
|
STR_LANDSCAPE_CW: "Orizzontale Dx"
|
||||||
STR_INVERTED: "Capovolto"
|
STR_INVERTED: "Invertito"
|
||||||
|
STR_ORIENTATION_INVERTED: "Verticale 180°"
|
||||||
STR_LANDSCAPE_CCW: "Orizzontale Sx"
|
STR_LANDSCAPE_CCW: "Orizzontale Sx"
|
||||||
STR_PREV_NEXT: "Prec/Succ"
|
STR_PREV_NEXT: "Prec/Succ"
|
||||||
STR_NEXT_PREV: "Succ/Prec"
|
STR_NEXT_PREV: "Succ/Prec"
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ STR_TEXT_AA: "Мәтін сырғытпасы"
|
|||||||
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
|
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
|
||||||
STR_ORIENTATION: "Оқу бағдары"
|
STR_ORIENTATION: "Оқу бағдары"
|
||||||
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
|
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Сенсорлық басқару (оқырман)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
|
||||||
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
|
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
|
||||||
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
|
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
|
||||||
@@ -126,7 +127,8 @@ STR_SLEEP: "Ұйқы"
|
|||||||
STR_PAGE_TURN: "Бет аудару"
|
STR_PAGE_TURN: "Бет аудару"
|
||||||
STR_PORTRAIT: "Тік бағдар"
|
STR_PORTRAIT: "Тік бағдар"
|
||||||
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
|
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
|
||||||
STR_INVERTED: "Төңкерілген"
|
STR_INVERTED: "Инверсия"
|
||||||
|
STR_ORIENTATION_INVERTED: "Тік бағдар 180°"
|
||||||
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
|
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
|
||||||
STR_PREV_NEXT: "Алдыңғы/Келесі"
|
STR_PREV_NEXT: "Алдыңғы/Келесі"
|
||||||
STR_NEXT_PREV: "Келесі/Алдыңғы"
|
STR_NEXT_PREV: "Келесі/Алдыңғы"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Slėpti"
|
|||||||
STR_SHORT_PWR_BTN: "Trumpas įjungimo pasp."
|
STR_SHORT_PWR_BTN: "Trumpas įjungimo pasp."
|
||||||
STR_ORIENTATION: "Orientacija"
|
STR_ORIENTATION: "Orientacija"
|
||||||
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
|
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Liečiamasis valdymas (skaityklė)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
|
||||||
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
|
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
|
||||||
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
|
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
|
||||||
@@ -132,7 +133,8 @@ STR_SLEEP: "Miegas"
|
|||||||
STR_PAGE_TURN: "Versti psl."
|
STR_PAGE_TURN: "Versti psl."
|
||||||
STR_PORTRAIT: "Stačias"
|
STR_PORTRAIT: "Stačias"
|
||||||
STR_LANDSCAPE_CW: "Gulsčias (P)"
|
STR_LANDSCAPE_CW: "Gulsčias (P)"
|
||||||
STR_INVERTED: "Apverstas"
|
STR_INVERTED: "Invertuotas"
|
||||||
|
STR_ORIENTATION_INVERTED: "Stačias 180°"
|
||||||
STR_LANDSCAPE_CCW: "Gulsčias (A)"
|
STR_LANDSCAPE_CCW: "Gulsčias (A)"
|
||||||
STR_PREV_NEXT: "Atgal/Pirmyn"
|
STR_PREV_NEXT: "Atgal/Pirmyn"
|
||||||
STR_NEXT_PREV: "Pirmyn/Atgal"
|
STR_NEXT_PREV: "Pirmyn/Atgal"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Pomijaj"
|
|||||||
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
|
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
|
||||||
STR_ORIENTATION: "Układ czytania"
|
STR_ORIENTATION: "Układ czytania"
|
||||||
STR_SIDE_BTN_LAYOUT: "Układ przycisków bocznych"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuj przednie przyciski"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
|
STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
|
||||||
@@ -136,7 +137,8 @@ STR_PAGE_TURN: "Nast. str."
|
|||||||
STR_FORCE_REFRESH: "Odśwież ekran"
|
STR_FORCE_REFRESH: "Odśwież ekran"
|
||||||
STR_PORTRAIT: "Pionowo"
|
STR_PORTRAIT: "Pionowo"
|
||||||
STR_LANDSCAPE_CW: "Poziomo P"
|
STR_LANDSCAPE_CW: "Poziomo P"
|
||||||
STR_INVERTED: "Odwrócony"
|
STR_INVERTED: "Inwersja"
|
||||||
|
STR_ORIENTATION_INVERTED: "Pionowo 180°"
|
||||||
STR_LANDSCAPE_CCW: "Poziomo L"
|
STR_LANDSCAPE_CCW: "Poziomo L"
|
||||||
STR_PREV_NEXT: "Poprz./Nast."
|
STR_PREV_NEXT: "Poprz./Nast."
|
||||||
STR_NEXT_PREV: "Nast./Poprz."
|
STR_NEXT_PREV: "Nast./Poprz."
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Suavização de texto"
|
|||||||
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
|
||||||
STR_ORIENTATION: "Orientação de leitura"
|
STR_ORIENTATION: "Orientação de leitura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
|
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
|
||||||
@@ -131,6 +132,7 @@ STR_PAGE_TURN: "Virar página"
|
|||||||
STR_PORTRAIT: "Retrato"
|
STR_PORTRAIT: "Retrato"
|
||||||
STR_LANDSCAPE_CW: "Paisagem H"
|
STR_LANDSCAPE_CW: "Paisagem H"
|
||||||
STR_INVERTED: "Invertido"
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_ORIENTATION_INVERTED: "Retrato 180°"
|
||||||
STR_LANDSCAPE_CCW: "Paisagem AH"
|
STR_LANDSCAPE_CCW: "Paisagem AH"
|
||||||
STR_PREV_NEXT: "Ant/Próx"
|
STR_PREV_NEXT: "Ant/Próx"
|
||||||
STR_NEXT_PREV: "Próx/Ant"
|
STR_NEXT_PREV: "Próx/Ant"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Suprimare"
|
|||||||
STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător"
|
STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător"
|
||||||
STR_ORIENTATION: "Orientare lectură"
|
STR_ORIENTATION: "Orientare lectură"
|
||||||
STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientare butoane frontale"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
|
STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
|
||||||
@@ -136,6 +137,7 @@ STR_PAGE_TURN: "Răsfoire pagină"
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Orizontal dreapta"
|
STR_LANDSCAPE_CW: "Orizontal dreapta"
|
||||||
STR_INVERTED: "Invers"
|
STR_INVERTED: "Invers"
|
||||||
|
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||||
STR_LANDSCAPE_CCW: "Orizontal stânga"
|
STR_LANDSCAPE_CCW: "Orizontal stânga"
|
||||||
STR_PREV_NEXT: "Înainte/Înapoi"
|
STR_PREV_NEXT: "Înainte/Înapoi"
|
||||||
STR_NEXT_PREV: "Înapoi/Înainte"
|
STR_NEXT_PREV: "Înapoi/Înainte"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Скрыть"
|
|||||||
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
|
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
|
||||||
STR_ORIENTATION: "Ориентация чтения"
|
STR_ORIENTATION: "Ориентация чтения"
|
||||||
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
|
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Сенсорное управление чтением"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентировать передние кнопки"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентировать передние кнопки"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
|
STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
|
||||||
@@ -140,6 +141,7 @@ STR_FORCE_REFRESH: "Обновление экрана"
|
|||||||
STR_PORTRAIT: "Портрет"
|
STR_PORTRAIT: "Портрет"
|
||||||
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||||
STR_INVERTED: "Инверсия"
|
STR_INVERTED: "Инверсия"
|
||||||
|
STR_ORIENTATION_INVERTED: "Портрет 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||||
STR_PREV_NEXT: "Назад/Вперёд"
|
STR_PREV_NEXT: "Назад/Вперёд"
|
||||||
STR_NEXT_PREV: "Вперёд/Назад"
|
STR_NEXT_PREV: "Вперёд/Назад"
|
||||||
|
|||||||
+376
-374
@@ -1,380 +1,382 @@
|
|||||||
_language_name: "Slovenčina"
|
_language_name: "Slovenčina"
|
||||||
_language_code: "SK"
|
_language_code: "SK"
|
||||||
_order: "24"
|
_order: "24"
|
||||||
|
|
||||||
STR_CROSSPOINT: "CrossPoint"
|
STR_CROSSPOINT: "CrossPoint"
|
||||||
STR_BOOTING: "SPÚŠŤANIE"
|
STR_BOOTING: "SPÚŠŤANIE"
|
||||||
STR_SLEEPING: "SPÁNOK"
|
STR_SLEEPING: "SPÁNOK"
|
||||||
STR_ENTERING_SLEEP: "Prechod do režimu spánku"
|
STR_ENTERING_SLEEP: "Prechod do režimu spánku"
|
||||||
STR_BROWSE_FILES: "Prehliadať súbory"
|
STR_BROWSE_FILES: "Prehliadať súbory"
|
||||||
STR_FILE_TRANSFER: "Prenos súborov"
|
STR_FILE_TRANSFER: "Prenos súborov"
|
||||||
STR_SETTINGS_TITLE: "Nastavenia"
|
STR_SETTINGS_TITLE: "Nastavenia"
|
||||||
STR_CONTINUE_READING: "Pokračovať v čítaní"
|
STR_CONTINUE_READING: "Pokračovať v čítaní"
|
||||||
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
|
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
|
||||||
STR_START_READING: "Začnite čítať nižšie"
|
STR_START_READING: "Začnite čítať nižšie"
|
||||||
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
|
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
|
||||||
STR_SELECT_CHAPTER: "Vybrať kapitolu"
|
STR_SELECT_CHAPTER: "Vybrať kapitolu"
|
||||||
STR_NO_CHAPTERS: "Žiadne kapitoly"
|
STR_NO_CHAPTERS: "Žiadne kapitoly"
|
||||||
STR_END_OF_BOOK: "Koniec knihy"
|
STR_END_OF_BOOK: "Koniec knihy"
|
||||||
STR_EMPTY_CHAPTER: "Prázdna kapitola"
|
STR_EMPTY_CHAPTER: "Prázdna kapitola"
|
||||||
STR_INDEXING: "Indexovanie"
|
STR_INDEXING: "Indexovanie"
|
||||||
STR_MEMORY_ERROR: "Chyba pamäte"
|
STR_MEMORY_ERROR: "Chyba pamäte"
|
||||||
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
|
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
|
||||||
STR_EMPTY_FILE: "Prázdny súbor"
|
STR_EMPTY_FILE: "Prázdny súbor"
|
||||||
STR_OUT_OF_BOUNDS: "Mimo rozsahu"
|
STR_OUT_OF_BOUNDS: "Mimo rozsahu"
|
||||||
STR_LOADING: "Načítava sa..."
|
STR_LOADING: "Načítava sa..."
|
||||||
STR_LOADING_POPUP: "Načítavanie"
|
STR_LOADING_POPUP: "Načítavanie"
|
||||||
STR_WIFI_NETWORKS: "Wi-Fi siete"
|
STR_WIFI_NETWORKS: "Wi-Fi siete"
|
||||||
STR_NO_NETWORKS: "Nenašli sa žiadne siete"
|
STR_NO_NETWORKS: "Nenašli sa žiadne siete"
|
||||||
STR_NETWORKS_FOUND: "Nájdených %zu sietí"
|
STR_NETWORKS_FOUND: "Nájdených %zu sietí"
|
||||||
STR_SCANNING: "Skenovanie..."
|
STR_SCANNING: "Skenovanie..."
|
||||||
STR_CONNECTING: "Pripájanie..."
|
STR_CONNECTING: "Pripájanie..."
|
||||||
STR_CONNECTED: "Pripojené!"
|
STR_CONNECTED: "Pripojené!"
|
||||||
STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
|
STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
|
||||||
STR_FORGET_NETWORK: "Zabudnúť sieť?"
|
STR_FORGET_NETWORK: "Zabudnúť sieť?"
|
||||||
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
|
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
|
||||||
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
|
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
|
||||||
STR_JOIN_NETWORK: "Pripojiť sa k sieti"
|
STR_JOIN_NETWORK: "Pripojiť sa k sieti"
|
||||||
STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
|
STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
|
||||||
STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti"
|
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_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní"
|
||||||
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
|
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
|
||||||
STR_HOTSPOT_MODE: "Režim hotspotu"
|
STR_HOTSPOT_MODE: "Režim hotspotu"
|
||||||
STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti"
|
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_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači"
|
||||||
STR_OR_HTTP_PREFIX: "alebo http://"
|
STR_OR_HTTP_PREFIX: "alebo http://"
|
||||||
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
|
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
|
||||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||||
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
|
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
|
||||||
STR_MAC_ADDRESS: "MAC adresa:"
|
STR_MAC_ADDRESS: "MAC adresa:"
|
||||||
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
||||||
STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
|
STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
|
||||||
STR_TO_PREFIX: "pre"
|
STR_TO_PREFIX: "pre"
|
||||||
STR_CALIBRE_RECEIVING: "Prijímanie:"
|
STR_CALIBRE_RECEIVING: "Prijímanie:"
|
||||||
STR_CALIBRE_RECEIVED: "Prijaté:"
|
STR_CALIBRE_RECEIVED: "Prijaté:"
|
||||||
STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader"
|
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_2: "2) Buďte v rovnakej Wi-Fi sieti"
|
||||||
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
|
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
|
||||||
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
|
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
|
||||||
STR_CAT_DISPLAY: "Displej"
|
STR_CAT_DISPLAY: "Displej"
|
||||||
STR_CAT_READER: "Čítačka"
|
STR_CAT_READER: "Čítačka"
|
||||||
STR_CAT_CONTROLS: "Ovládanie"
|
STR_CAT_CONTROLS: "Ovládanie"
|
||||||
STR_CAT_SYSTEM: "Systém"
|
STR_CAT_SYSTEM: "Systém"
|
||||||
STR_SLEEP_SCREEN: "Obrazovka spánku"
|
STR_SLEEP_SCREEN: "Obrazovka spánku"
|
||||||
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
|
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
|
||||||
STR_SLEEP_COVER_MODE: "Obrazovka spánku – režim krytu"
|
STR_SLEEP_COVER_MODE: "Obrazovka spánku – režim krytu"
|
||||||
STR_HIDE_BATTERY: "Skryť % batérie"
|
STR_HIDE_BATTERY: "Skryť % batérie"
|
||||||
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
|
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
|
||||||
STR_TEXT_AA: "Vyhladzovanie textu"
|
STR_TEXT_AA: "Vyhladzovanie textu"
|
||||||
STR_IMAGES: "Obrázky"
|
STR_IMAGES: "Obrázky"
|
||||||
STR_IMAGES_DISPLAY: "Zobraz"
|
STR_IMAGES_DISPLAY: "Zobraz"
|
||||||
STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
|
STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
|
||||||
STR_IMAGES_SUPPRESS: "Potlačiť"
|
STR_IMAGES_SUPPRESS: "Potlačiť"
|
||||||
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
|
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
|
||||||
STR_ORIENTATION: "Orientácia čítania"
|
STR_ORIENTATION: "Orientácia čítania"
|
||||||
STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)"
|
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_TOUCH_READER_CONTROLS: "Dotykové ovládanie čítačky"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
|
||||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
||||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
|
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
|
||||||
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_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
|
||||||
STR_FONT_FAMILY: "Rodina písiem čítačky"
|
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_SIZE: "Veľkosť písma rozhrania"
|
STR_FONT_FAMILY: "Rodina písiem čítačky"
|
||||||
STR_LINE_SPACING: "Riadkovanie čítačky"
|
STR_FONT_SIZE: "Veľkosť písma rozhrania"
|
||||||
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
|
STR_LINE_SPACING: "Riadkovanie čítačky"
|
||||||
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
|
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
|
||||||
STR_HYPHENATION: "Delenie slov"
|
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
|
||||||
STR_TIME_TO_SLEEP: "Čas do uspania"
|
STR_HYPHENATION: "Delenie slov"
|
||||||
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
|
STR_TIME_TO_SLEEP: "Čas do uspania"
|
||||||
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
|
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
|
||||||
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
|
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
|
||||||
STR_REFRESH_FREQ: "Frekvencia obnovovania"
|
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
|
||||||
STR_KOREADER_SYNC: "KOReader Sync"
|
STR_REFRESH_FREQ: "Frekvencia obnovovania"
|
||||||
STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
|
STR_KOREADER_SYNC: "KOReader Sync"
|
||||||
STR_LANGUAGE: "Jazyk"
|
STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
|
||||||
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
|
STR_LANGUAGE: "Jazyk"
|
||||||
STR_USERNAME: "Používateľské meno"
|
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
|
||||||
STR_PASSWORD: "Heslo"
|
STR_USERNAME: "Používateľské meno"
|
||||||
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
|
STR_PASSWORD: "Heslo"
|
||||||
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
|
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
|
||||||
STR_AUTHENTICATE: "Overiť"
|
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
|
||||||
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
STR_AUTHENTICATE: "Overiť"
|
||||||
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
||||||
STR_FILENAME: "Názov súboru"
|
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
||||||
STR_BINARY: "Binárny"
|
STR_FILENAME: "Názov súboru"
|
||||||
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
|
STR_BINARY: "Binárny"
|
||||||
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
|
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
|
||||||
STR_AUTHENTICATING: "Overovanie..."
|
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
|
||||||
STR_AUTH_SUCCESS: "Overenie úspešné!"
|
STR_AUTHENTICATING: "Overovanie..."
|
||||||
STR_KOREADER_AUTH: "Overenie KOReader"
|
STR_AUTH_SUCCESS: "Overenie úspešné!"
|
||||||
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
|
STR_KOREADER_AUTH: "Overenie KOReader"
|
||||||
STR_AUTH_FAILED: "Overenie zlyhalo"
|
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
|
||||||
STR_DONE: "Hotovo"
|
STR_AUTH_FAILED: "Overenie zlyhalo"
|
||||||
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
|
STR_DONE: "Hotovo"
|
||||||
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
|
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
|
||||||
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
|
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
|
||||||
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
|
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
|
||||||
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
|
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
|
||||||
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
|
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
|
||||||
STR_ITEMS_REMOVED: "položiek odstránených"
|
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
|
||||||
STR_FAILED_LOWER: "zlyhalo"
|
STR_ITEMS_REMOVED: "položiek odstránených"
|
||||||
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
|
STR_FAILED_LOWER: "zlyhalo"
|
||||||
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
|
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
|
||||||
STR_DARK: "Tmavý"
|
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
|
||||||
STR_LIGHT: "Svetlý"
|
STR_DARK: "Tmavý"
|
||||||
STR_CUSTOM: "Vlastný"
|
STR_LIGHT: "Svetlý"
|
||||||
STR_COVER: "Obálka"
|
STR_CUSTOM: "Vlastný"
|
||||||
STR_NONE_OPT: "Žiadny"
|
STR_COVER: "Obálka"
|
||||||
STR_FIT: "Prispôsobiť"
|
STR_NONE_OPT: "Žiadny"
|
||||||
STR_CROP: "Orezať"
|
STR_FIT: "Prispôsobiť"
|
||||||
STR_NEVER: "Nikdy"
|
STR_CROP: "Orezať"
|
||||||
STR_IN_READER: "V čítačke"
|
STR_NEVER: "Nikdy"
|
||||||
STR_ALWAYS: "Vždy"
|
STR_IN_READER: "V čítačke"
|
||||||
STR_IGNORE: "Ignorovať"
|
STR_ALWAYS: "Vždy"
|
||||||
STR_SLEEP: "Spánok"
|
STR_IGNORE: "Ignorovať"
|
||||||
STR_PAGE_TURN: "Otáčanie stránok"
|
STR_SLEEP: "Spánok"
|
||||||
STR_FORCE_REFRESH: "Obnoviť obrazovku"
|
STR_PAGE_TURN: "Otáčanie stránok"
|
||||||
STR_PORTRAIT: "Na výšku"
|
STR_FORCE_REFRESH: "Obnoviť obrazovku"
|
||||||
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
|
STR_PORTRAIT: "Na výšku"
|
||||||
STR_INVERTED: "Obrátený"
|
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
|
||||||
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
|
STR_INVERTED: "Invertovaný"
|
||||||
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
|
STR_ORIENTATION_INVERTED: "Na výšku 180°"
|
||||||
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
|
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
|
||||||
STR_DISABLED: "Vypnuté"
|
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
|
||||||
STR_NOTO_SERIF: "Noto Serif"
|
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
|
||||||
STR_NOTO_SANS: "Noto Sans"
|
STR_DISABLED: "Vypnuté"
|
||||||
STR_SMALL: "Malý"
|
STR_NOTO_SERIF: "Noto Serif"
|
||||||
STR_MEDIUM: "Stredný"
|
STR_NOTO_SANS: "Noto Sans"
|
||||||
STR_LARGE: "Veľký"
|
STR_SMALL: "Malý"
|
||||||
STR_X_LARGE: "Obrovský"
|
STR_MEDIUM: "Stredný"
|
||||||
STR_TIGHT: "Tesný"
|
STR_LARGE: "Veľký"
|
||||||
STR_NORMAL: "Normálny"
|
STR_X_LARGE: "Obrovský"
|
||||||
STR_WIDE: "Široký"
|
STR_TIGHT: "Tesný"
|
||||||
STR_JUSTIFY: "Zarovnať do bloku"
|
STR_NORMAL: "Normálny"
|
||||||
STR_ALIGN_LEFT: "Vľavo"
|
STR_WIDE: "Široký"
|
||||||
STR_CENTER: "Na stred"
|
STR_JUSTIFY: "Zarovnať do bloku"
|
||||||
STR_ALIGN_RIGHT: "Vpravo"
|
STR_ALIGN_LEFT: "Vľavo"
|
||||||
STR_PAGES_1: "1 strana"
|
STR_CENTER: "Na stred"
|
||||||
STR_PAGES_5: "5 strán"
|
STR_ALIGN_RIGHT: "Vpravo"
|
||||||
STR_PAGES_10: "10 strán"
|
STR_PAGES_1: "1 strana"
|
||||||
STR_PAGES_15: "15 strán"
|
STR_PAGES_5: "5 strán"
|
||||||
STR_PAGES_30: "30 strán"
|
STR_PAGES_10: "10 strán"
|
||||||
STR_UPDATE: "Aktualizácia"
|
STR_PAGES_15: "15 strán"
|
||||||
STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
|
STR_PAGES_30: "30 strán"
|
||||||
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
|
STR_UPDATE: "Aktualizácia"
|
||||||
STR_CURRENT_VERSION: "Aktuálna verzia:"
|
STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
|
||||||
STR_NEW_VERSION: "Nová verzia:"
|
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
|
||||||
STR_UPDATING: "Aktualizácia..."
|
STR_CURRENT_VERSION: "Aktuálna verzia:"
|
||||||
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
|
STR_NEW_VERSION: "Nová verzia:"
|
||||||
STR_UPDATE_FAILED: "Aktualizácia zlyhala"
|
STR_UPDATING: "Aktualizácia..."
|
||||||
STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
|
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
|
||||||
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
|
STR_UPDATE_FAILED: "Aktualizácia zlyhala"
|
||||||
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
|
STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
|
||||||
STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
|
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
|
||||||
STR_DOWNLOADING: "Sťahovanie..."
|
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
|
||||||
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
|
STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
|
||||||
STR_ERROR_MSG: "Chyba:"
|
STR_DOWNLOADING: "Sťahovanie..."
|
||||||
STR_UNNAMED: "Nepomenované"
|
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
|
||||||
|
STR_ERROR_MSG: "Chyba:"
|
||||||
|
STR_UNNAMED: "Nepomenované"
|
||||||
STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
|
STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
|
||||||
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
|
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
|
||||||
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
|
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
|
||||||
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
|
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
|
||||||
STR_NEXT_PAGE: "Ďaľsia strana »"
|
STR_NEXT_PAGE: "Ďaľsia strana »"
|
||||||
STR_PREV_PAGE: "« Predchádzajúca str."
|
STR_PREV_PAGE: "« Predchádzajúca str."
|
||||||
STR_NETWORK_PREFIX: "Sieť:"
|
STR_NETWORK_PREFIX: "Sieť:"
|
||||||
STR_IP_ADDRESS_PREFIX: "IP adresa:"
|
STR_IP_ADDRESS_PREFIX: "IP adresa:"
|
||||||
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
|
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
|
||||||
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
|
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
|
||||||
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
|
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
|
||||||
STR_SD_CARD: "SD karta"
|
STR_SD_CARD: "SD karta"
|
||||||
STR_BACK: "« Späť"
|
STR_BACK: "« Späť"
|
||||||
STR_EXIT: "« Koniec"
|
STR_EXIT: "« Koniec"
|
||||||
STR_HOME: "« Domov"
|
STR_HOME: "« Domov"
|
||||||
STR_SELECT: "Vybrať"
|
STR_SELECT: "Vybrať"
|
||||||
STR_SELECTED: "Vybrané"
|
STR_SELECTED: "Vybrané"
|
||||||
STR_TOGGLE: "Prepnúť"
|
STR_TOGGLE: "Prepnúť"
|
||||||
STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
|
STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
|
||||||
STR_CONFIRM: "Potvrdiť"
|
STR_CONFIRM: "Potvrdiť"
|
||||||
STR_CANCEL: "Zrušiť"
|
STR_CANCEL: "Zrušiť"
|
||||||
STR_CONNECT: "Pripojiť"
|
STR_CONNECT: "Pripojiť"
|
||||||
STR_OPEN: "Otvoriť"
|
STR_OPEN: "Otvoriť"
|
||||||
STR_DOWNLOAD: "Stiahnuť"
|
STR_DOWNLOAD: "Stiahnuť"
|
||||||
STR_RETRY: "Skúsiť znova"
|
STR_RETRY: "Skúsiť znova"
|
||||||
STR_YES: "Áno"
|
STR_YES: "Áno"
|
||||||
STR_NO: "Nie"
|
STR_NO: "Nie"
|
||||||
STR_SHOW: "Zobraz"
|
STR_SHOW: "Zobraz"
|
||||||
STR_HIDE: "Skryť"
|
STR_HIDE: "Skryť"
|
||||||
STR_STATE_ON: "ZAP"
|
STR_STATE_ON: "ZAP"
|
||||||
STR_STATE_OFF: "VYP"
|
STR_STATE_OFF: "VYP"
|
||||||
STR_NOT_SET: "Nenastavené"
|
STR_NOT_SET: "Nenastavené"
|
||||||
STR_DIR_LEFT: "Vľavo"
|
STR_DIR_LEFT: "Vľavo"
|
||||||
STR_DIR_RIGHT: "Vpravo"
|
STR_DIR_RIGHT: "Vpravo"
|
||||||
STR_DIR_UP: "Hore"
|
STR_DIR_UP: "Hore"
|
||||||
STR_DIR_DOWN: "Dole"
|
STR_DIR_DOWN: "Dole"
|
||||||
STR_OK_BUTTON: "OK"
|
STR_OK_BUTTON: "OK"
|
||||||
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
|
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
|
||||||
STR_FILTER_CONTRAST: "Kontrast"
|
STR_FILTER_CONTRAST: "Kontrast"
|
||||||
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
|
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
|
||||||
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
|
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
|
||||||
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
|
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
|
||||||
STR_PROGRESS_BAR: "Ukazovateľ čítania"
|
STR_PROGRESS_BAR: "Ukazovateľ čítania"
|
||||||
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
|
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
|
||||||
STR_PROGRESS_BAR_THIN: "Tenký"
|
STR_PROGRESS_BAR_THIN: "Tenký"
|
||||||
STR_PROGRESS_BAR_MEDIUM: "Stredný"
|
STR_PROGRESS_BAR_MEDIUM: "Stredný"
|
||||||
STR_PROGRESS_BAR_THICK: "Hrubý"
|
STR_PROGRESS_BAR_THICK: "Hrubý"
|
||||||
STR_BOOK: "Kniha"
|
STR_BOOK: "Kniha"
|
||||||
STR_CHAPTER: "Kapitola"
|
STR_CHAPTER: "Kapitola"
|
||||||
STR_EXAMPLE_CHAPTER: "Kapitola 21"
|
STR_EXAMPLE_CHAPTER: "Kapitola 21"
|
||||||
STR_EXAMPLE_BOOK: "Názov knihy"
|
STR_EXAMPLE_BOOK: "Názov knihy"
|
||||||
STR_PREVIEW: "Ukážka"
|
STR_PREVIEW: "Ukážka"
|
||||||
STR_TITLE: "Názov"
|
STR_TITLE: "Názov"
|
||||||
STR_BATTERY: "Batéria"
|
STR_BATTERY: "Batéria"
|
||||||
STR_XTC_STATUS_BAR: "Stavový panel XTC"
|
STR_XTC_STATUS_BAR: "Stavový panel XTC"
|
||||||
STR_BOTTOM: "Dole"
|
STR_BOTTOM: "Dole"
|
||||||
STR_TOP: "Hore"
|
STR_TOP: "Hore"
|
||||||
STR_CLOCK: "Hodiny"
|
STR_CLOCK: "Hodiny"
|
||||||
STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
|
STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
|
||||||
STR_CLOCK_FORMAT: "Formát času"
|
STR_CLOCK_FORMAT: "Formát času"
|
||||||
STR_CLOCK_FORMAT_24H: "24-hodinový"
|
STR_CLOCK_FORMAT_24H: "24-hodinový"
|
||||||
STR_CLOCK_FORMAT_12H: "12-hodinový"
|
STR_CLOCK_FORMAT_12H: "12-hodinový"
|
||||||
STR_CURRENT_TIME: "Aktuálny čas:"
|
STR_CURRENT_TIME: "Aktuálny čas:"
|
||||||
STR_NEXT_FIELD: "Ďalej"
|
STR_NEXT_FIELD: "Ďalej"
|
||||||
STR_CLOCK_SYNC: "Synchronizácia hodín"
|
STR_CLOCK_SYNC: "Synchronizácia hodín"
|
||||||
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
|
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
|
||||||
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
|
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
|
||||||
STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
|
STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
|
||||||
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
|
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
|
||||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená"
|
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_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova."
|
||||||
STR_CLOCK_SYNCED: "Hodiny synchronizované"
|
STR_CLOCK_SYNCED: "Hodiny synchronizované"
|
||||||
STR_UI_THEME: "Téma rozhrania"
|
STR_UI_THEME: "Téma rozhrania"
|
||||||
STR_THEME_CLASSIC: "Klasická"
|
STR_THEME_CLASSIC: "Klasická"
|
||||||
STR_THEME_LYRA: "Lyra"
|
STR_THEME_LYRA: "Lyra"
|
||||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||||
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
|
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
|
||||||
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
|
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
|
||||||
STR_BOOKMARKS: "Záložky"
|
STR_BOOKMARKS: "Záložky"
|
||||||
STR_BOOKMARK_ADDED: "Záložka pridaná."
|
STR_BOOKMARK_ADDED: "Záložka pridaná."
|
||||||
STR_BOOKMARK_REMOVED: "Záložka odstránená."
|
STR_BOOKMARK_REMOVED: "Záložka odstránená."
|
||||||
STR_OPDS_BROWSER: "Prehliadač OPDS"
|
STR_OPDS_BROWSER: "Prehliadač OPDS"
|
||||||
STR_SEARCH: "Hľadať"
|
STR_SEARCH: "Hľadať"
|
||||||
STR_COVER_CUSTOM: "Obálka + Vlastné"
|
STR_COVER_CUSTOM: "Obálka + Vlastné"
|
||||||
STR_QUICK_RESUME: "Rýchle obnovenie"
|
STR_QUICK_RESUME: "Rýchle obnovenie"
|
||||||
STR_MENU_RECENT_BOOKS: "Nedávne knihy"
|
STR_MENU_RECENT_BOOKS: "Nedávne knihy"
|
||||||
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
|
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
|
||||||
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
|
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
|
||||||
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
|
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
|
||||||
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
|
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
|
||||||
STR_FORGET_BUTTON: "Zabudnúť"
|
STR_FORGET_BUTTON: "Zabudnúť"
|
||||||
STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
|
STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
|
||||||
STR_CALIBRE_SETUP: "Nastavenie"
|
STR_CALIBRE_SETUP: "Nastavenie"
|
||||||
STR_CALIBRE_STATUS: "Stav"
|
STR_CALIBRE_STATUS: "Stav"
|
||||||
STR_CLEAR_BUTTON: "Vymazať"
|
STR_CLEAR_BUTTON: "Vymazať"
|
||||||
STR_DEFAULT_VALUE: "Predvolené"
|
STR_DEFAULT_VALUE: "Predvolené"
|
||||||
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
|
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
|
||||||
STR_UNASSIGNED: "Nepriradené"
|
STR_UNASSIGNED: "Nepriradené"
|
||||||
STR_ALREADY_ASSIGNED: "Už priradené"
|
STR_ALREADY_ASSIGNED: "Už priradené"
|
||||||
STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie"
|
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_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie"
|
||||||
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
|
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
|
||||||
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
|
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
|
||||||
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
|
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
|
||||||
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
|
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
|
||||||
STR_GO_TO_PERCENT: "Prejsť na %"
|
STR_GO_TO_PERCENT: "Prejsť na %"
|
||||||
STR_GO_HOME_BUTTON: "Prejsť na Domov"
|
STR_GO_HOME_BUTTON: "Prejsť na Domov"
|
||||||
STR_SYNC_PROGRESS: "Priebeh synchronizácie"
|
STR_SYNC_PROGRESS: "Priebeh synchronizácie"
|
||||||
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
|
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
|
||||||
STR_DELETE: "Vymazať"
|
STR_DELETE: "Vymazať"
|
||||||
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
|
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
|
||||||
STR_DISPLAY_QR: "Zobraz stránku ako QR"
|
STR_DISPLAY_QR: "Zobraz stránku ako QR"
|
||||||
STR_CHAPTER_PREFIX: "Kapitola:"
|
STR_CHAPTER_PREFIX: "Kapitola:"
|
||||||
STR_PAGES_SEPARATOR: "strán |"
|
STR_PAGES_SEPARATOR: "strán |"
|
||||||
STR_BOOK_PREFIX: "Kniha:"
|
STR_BOOK_PREFIX: "Kniha:"
|
||||||
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
|
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
|
||||||
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
|
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
|
||||||
STR_SYNCING_TIME: "Čas synchronizácie..."
|
STR_SYNCING_TIME: "Čas synchronizácie..."
|
||||||
STR_CALC_HASH: "Výpočet hashu dokumentu..."
|
STR_CALC_HASH: "Výpočet hashu dokumentu..."
|
||||||
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
|
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
|
||||||
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
|
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
|
||||||
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
|
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
|
||||||
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
|
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
|
||||||
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
|
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
|
||||||
STR_PROGRESS_FOUND: "Priebeh nájdený!"
|
STR_PROGRESS_FOUND: "Priebeh nájdený!"
|
||||||
STR_REMOTE_LABEL: "Vzdialený:"
|
STR_REMOTE_LABEL: "Vzdialený:"
|
||||||
STR_LOCAL_LABEL: "Lokálny:"
|
STR_LOCAL_LABEL: "Lokálny:"
|
||||||
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
|
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
|
||||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
|
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
|
||||||
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
|
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
|
||||||
STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
|
STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
|
||||||
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
|
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
|
||||||
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
|
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
|
||||||
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
|
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
|
||||||
STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
|
STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
|
||||||
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
|
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
|
||||||
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
|
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
|
||||||
STR_SECTION_PREFIX: "Sekcia"
|
STR_SECTION_PREFIX: "Sekcia"
|
||||||
STR_UPLOAD: "Nahrať"
|
STR_UPLOAD: "Nahrať"
|
||||||
STR_BOOK_S_STYLE: "Štýl knihy"
|
STR_BOOK_S_STYLE: "Štýl knihy"
|
||||||
STR_EMBEDDED_STYLE: "Vložený štýl"
|
STR_EMBEDDED_STYLE: "Vložený štýl"
|
||||||
STR_FOCUS_READING: "Sústredené čítanie"
|
STR_FOCUS_READING: "Sústredené čítanie"
|
||||||
STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
|
STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
|
||||||
STR_SET_SLEEP_COVER: "Nastav obal"
|
STR_SET_SLEEP_COVER: "Nastav obal"
|
||||||
STR_FOOTNOTES: "Poznámky pod čiarou"
|
STR_FOOTNOTES: "Poznámky pod čiarou"
|
||||||
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
|
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
|
||||||
STR_LINK: "[link]"
|
STR_LINK: "[link]"
|
||||||
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
|
||||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||||
STR_SLEEP_NEVER: "Nikdy"
|
STR_SLEEP_NEVER: "Nikdy"
|
||||||
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
|
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
|
||||||
STR_ADD_SERVER: "Pridať server"
|
STR_ADD_SERVER: "Pridať server"
|
||||||
STR_SERVER_NAME: "Názov servera"
|
STR_SERVER_NAME: "Názov servera"
|
||||||
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
|
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
|
||||||
STR_DELETE_SERVER: "Odstrániť server"
|
STR_DELETE_SERVER: "Odstrániť server"
|
||||||
STR_OPDS_SERVERS: "OPDS servery"
|
STR_OPDS_SERVERS: "OPDS servery"
|
||||||
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
|
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_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
|
||||||
STR_MANAGE_FONTS: "Správa písiem"
|
STR_MANAGE_FONTS: "Správa písiem"
|
||||||
STR_FONT_BROWSER: "Prehliadač písiem"
|
STR_FONT_BROWSER: "Prehliadač písiem"
|
||||||
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
|
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
|
||||||
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
|
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
|
||||||
STR_FONT_INSTALLED: "Písmo nainštalované!"
|
STR_FONT_INSTALLED: "Písmo nainštalované!"
|
||||||
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
|
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
|
||||||
STR_INSTALLED: "Nainštalované"
|
STR_INSTALLED: "Nainštalované"
|
||||||
STR_DOWNLOAD_ALL: "Stiahnuť všetko"
|
STR_DOWNLOAD_ALL: "Stiahnuť všetko"
|
||||||
STR_UPDATE_ALL: "Aktualizovať všetko"
|
STR_UPDATE_ALL: "Aktualizovať všetko"
|
||||||
STR_UPDATE_AVAILABLE: "Aktualizovať"
|
STR_UPDATE_AVAILABLE: "Aktualizovať"
|
||||||
STR_CRASH_TITLE: "Zlyhanie systému"
|
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_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_REASON: "Dôvod zlyhania:"
|
||||||
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
|
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
|
||||||
STR_TILT_PAGE_TURN: "Otáčanie strán naklonením"
|
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_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_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_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_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_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla"
|
||||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie 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_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky"
|
||||||
STR_KB_TIPS: "Tipy:"
|
STR_KB_TIPS: "Tipy:"
|
||||||
STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici"
|
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_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_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_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_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_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak"
|
||||||
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
|
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
|
||||||
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
|
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
|
||||||
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
|
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
|
||||||
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
|
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
|
||||||
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
|
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
|
||||||
STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru"
|
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_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_TOO_SMALL: "Súbor firmvéru je príliš malý"
|
||||||
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
|
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
|
||||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
|
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
|
||||||
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
|
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
|
||||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
|
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
|
||||||
STR_RECOVERY_MODE: "Režim obnovenia"
|
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_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Zatdi"
|
|||||||
STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop"
|
STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop"
|
||||||
STR_ORIENTATION: "Orientacija branja"
|
STR_ORIENTATION: "Orientacija branja"
|
||||||
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
|
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Dotikalni nadzor (bralnik)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
|
||||||
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
|
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
|
||||||
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
|
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
|
||||||
@@ -132,7 +133,8 @@ STR_SLEEP: "Spanje"
|
|||||||
STR_PAGE_TURN: "Obračanje strani"
|
STR_PAGE_TURN: "Obračanje strani"
|
||||||
STR_PORTRAIT: "Pokončno"
|
STR_PORTRAIT: "Pokončno"
|
||||||
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
|
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
|
||||||
STR_INVERTED: "Obrnjeno"
|
STR_INVERTED: "Invertirano"
|
||||||
|
STR_ORIENTATION_INVERTED: "Pokončno 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
|
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
|
||||||
STR_PREV_NEXT: "Nazaj/Naprej"
|
STR_PREV_NEXT: "Nazaj/Naprej"
|
||||||
STR_NEXT_PREV: "Naprej/Nazaj"
|
STR_NEXT_PREV: "Naprej/Nazaj"
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Ocultar"
|
|||||||
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
||||||
STR_ORIENTATION: "Orientación"
|
STR_ORIENTATION: "Orientación"
|
||||||
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botones frontales"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
|
STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
|
||||||
@@ -138,6 +139,7 @@ STR_FORCE_REFRESH: "Refrescar pant."
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Horizontal (horario)"
|
STR_LANDSCAPE_CW: "Horizontal (horario)"
|
||||||
STR_INVERTED: "Invertido"
|
STR_INVERTED: "Invertido"
|
||||||
|
STR_ORIENTATION_INVERTED: "Al revés"
|
||||||
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
|
STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
|
||||||
STR_PREV_NEXT: "Ant./Sig."
|
STR_PREV_NEXT: "Ant./Sig."
|
||||||
STR_NEXT_PREV: "Sig./Ant."
|
STR_NEXT_PREV: "Sig./Ant."
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Dölj"
|
|||||||
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||||
STR_ORIENTATION: "Läsrikting"
|
STR_ORIENTATION: "Läsrikting"
|
||||||
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Pekkontroller (läsare)"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Rikta främre knappar"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Rikta främre knappar"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
|
STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||||
@@ -138,6 +139,7 @@ STR_FORCE_REFRESH: "Uppdatera skärmen"
|
|||||||
STR_PORTRAIT: "Porträtt"
|
STR_PORTRAIT: "Porträtt"
|
||||||
STR_LANDSCAPE_CW: "Landskap medurs"
|
STR_LANDSCAPE_CW: "Landskap medurs"
|
||||||
STR_INVERTED: "Inverterad"
|
STR_INVERTED: "Inverterad"
|
||||||
|
STR_ORIENTATION_INVERTED: "Porträtt 180°"
|
||||||
STR_LANDSCAPE_CCW: "Landskap moturs"
|
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||||
STR_PREV_NEXT: "Förra/Nästa"
|
STR_PREV_NEXT: "Förra/Nästa"
|
||||||
STR_NEXT_PREV: "Nästa/Förra"
|
STR_NEXT_PREV: "Nästa/Förra"
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ STR_TEXT_AA: "Metin Yumuşatma (AA)"
|
|||||||
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
|
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
|
||||||
STR_ORIENTATION: "Okuma Yönü"
|
STR_ORIENTATION: "Okuma Yönü"
|
||||||
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||||
@@ -130,7 +131,8 @@ STR_SLEEP: "Uyku"
|
|||||||
STR_PAGE_TURN: "Sayfa Çevirme"
|
STR_PAGE_TURN: "Sayfa Çevirme"
|
||||||
STR_PORTRAIT: "Dikey"
|
STR_PORTRAIT: "Dikey"
|
||||||
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
|
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
|
||||||
STR_INVERTED: "Ters"
|
STR_INVERTED: "Negatif"
|
||||||
|
STR_ORIENTATION_INVERTED: "Dikey 180°"
|
||||||
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
|
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
|
||||||
STR_PREV_NEXT: "Önceki/Sonraki"
|
STR_PREV_NEXT: "Önceki/Sonraki"
|
||||||
STR_NEXT_PREV: "Sonraki/Önceki"
|
STR_NEXT_PREV: "Sonraki/Önceki"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Приховати"
|
|||||||
STR_SHORT_PWR_BTN: "Короткий натиск кн. живл."
|
STR_SHORT_PWR_BTN: "Короткий натиск кн. живл."
|
||||||
STR_ORIENTATION: "Орієнтація читання"
|
STR_ORIENTATION: "Орієнтація читання"
|
||||||
STR_SIDE_BTN_LAYOUT: "Схема бічних кнопок"
|
STR_SIDE_BTN_LAYOUT: "Схема бічних кнопок"
|
||||||
|
STR_TOUCH_READER_CONTROLS: "Сенсорне керування читанням"
|
||||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Орієнтувати передні кнопки"
|
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Орієнтувати передні кнопки"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настику"
|
STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настику"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
|
||||||
@@ -137,7 +138,8 @@ STR_PAGE_TURN: "Наст. сторінка"
|
|||||||
STR_FORCE_REFRESH: "Оновити екран"
|
STR_FORCE_REFRESH: "Оновити екран"
|
||||||
STR_PORTRAIT: "Книжкова"
|
STR_PORTRAIT: "Книжкова"
|
||||||
STR_LANDSCAPE_CW: "Альбом. за год."
|
STR_LANDSCAPE_CW: "Альбом. за год."
|
||||||
STR_INVERTED: "Перевернутий"
|
STR_INVERTED: "Інверсія"
|
||||||
|
STR_ORIENTATION_INVERTED: "Книжкова 180°"
|
||||||
STR_LANDSCAPE_CCW: "Альбом. проти год."
|
STR_LANDSCAPE_CCW: "Альбом. проти год."
|
||||||
STR_PREV_NEXT: "Попер/Наст"
|
STR_PREV_NEXT: "Попер/Наст"
|
||||||
STR_NEXT_PREV: "Наст/Попер"
|
STR_NEXT_PREV: "Наст/Попер"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Eliminar"
|
|||||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
||||||
STR_ORIENTATION: "Orientació de lectura"
|
STR_ORIENTATION: "Orientació de lectura"
|
||||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
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_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||||
@@ -141,6 +142,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
|
|||||||
STR_PORTRAIT: "Vertical"
|
STR_PORTRAIT: "Vertical"
|
||||||
STR_LANDSCAPE_CW: "Horitzontal horari"
|
STR_LANDSCAPE_CW: "Horitzontal horari"
|
||||||
STR_INVERTED: "Invertit"
|
STR_INVERTED: "Invertit"
|
||||||
|
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||||
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
||||||
STR_PREV_NEXT: "Anterior/Següent"
|
STR_PREV_NEXT: "Anterior/Següent"
|
||||||
STR_NEXT_PREV: "Següent/Anterior"
|
STR_NEXT_PREV: "Següent/Anterior"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Ẩn đi"
|
|||||||
STR_SHORT_PWR_BTN: "Nhấn nhanh nút nguồn"
|
STR_SHORT_PWR_BTN: "Nhấn nhanh nút nguồn"
|
||||||
STR_ORIENTATION: "Hướng đọc"
|
STR_ORIENTATION: "Hướng đọc"
|
||||||
STR_SIDE_BTN_LAYOUT: "Bố trí nút bên (trình đọ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_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: "Hành vi nhấn giữ nút"
|
||||||
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
|
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
|
||||||
@@ -137,7 +138,8 @@ STR_PAGE_TURN: "Lật trang"
|
|||||||
STR_FORCE_REFRESH: "Làm tươi màn hình"
|
STR_FORCE_REFRESH: "Làm tươi màn hình"
|
||||||
STR_PORTRAIT: "Dọc"
|
STR_PORTRAIT: "Dọc"
|
||||||
STR_LANDSCAPE_CW: "Ngang (thuận)"
|
STR_LANDSCAPE_CW: "Ngang (thuận)"
|
||||||
STR_INVERTED: "Lật ngược"
|
STR_INVERTED: "Đảo màu"
|
||||||
|
STR_ORIENTATION_INVERTED: "Dọc 180°"
|
||||||
STR_LANDSCAPE_CCW: "Ngang (ngược)"
|
STR_LANDSCAPE_CCW: "Ngang (ngược)"
|
||||||
STR_PREV_NEXT: "Trước/Sau"
|
STR_PREV_NEXT: "Trước/Sau"
|
||||||
STR_NEXT_PREV: "Sau/Trước"
|
STR_NEXT_PREV: "Sau/Trước"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
|
|
||||||
|
#include <BoardConfig.h>
|
||||||
|
#include <esp_rom_sys.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#define MAX_ENTRY_LEN 256
|
#define MAX_ENTRY_LEN 256
|
||||||
@@ -59,9 +62,19 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
va_end(args);
|
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) {
|
if (logSerial) {
|
||||||
logSerial.print(buf);
|
logSerial.print(buf);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
addToLogRingBuffer(buf);
|
addToLogRingBuffer(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ won't trigger deprecation warnings.
|
|||||||
#define LOG_LEVEL 0
|
#define LOG_LEVEL 0
|
||||||
#endif
|
#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, ...);
|
void logPrintf(const char* level, const char* origin, const char* format, ...);
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -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); }
|
static uint8_t decToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }
|
||||||
|
|
||||||
void HalClock::begin() {
|
void HalClock::begin() {
|
||||||
|
_usesSdkRtc = false;
|
||||||
if (!gpio.deviceIsX3()) {
|
if (!gpio.deviceIsX3()) {
|
||||||
_available = false;
|
_available = _sdkRtc.begin();
|
||||||
|
_usesSdkRtc = _available;
|
||||||
|
LOG_INF("CLK", _available ? "SDK RTC found" : "RTC not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,24 @@ bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const {
|
|||||||
return true;
|
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
|
// Read 3 bytes starting at register 0x00: seconds, minutes, hours
|
||||||
Wire.beginTransmission(I2C_ADDR_DS3231);
|
Wire.beginTransmission(I2C_ADDR_DS3231);
|
||||||
Wire.write(DS3231_SEC_REG);
|
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(hour < 24);
|
||||||
assert(minute < 60);
|
assert(minute < 60);
|
||||||
assert(second < 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.beginTransmission(I2C_ADDR_DS3231);
|
||||||
Wire.write(DS3231_SEC_REG); // Start at register 0x00
|
Wire.write(DS3231_SEC_REG); // Start at register 0x00
|
||||||
Wire.write(decToBcd(second)); // 0x00: Seconds
|
Wire.write(decToBcd(second)); // 0x00: Seconds
|
||||||
@@ -168,6 +208,27 @@ bool HalClock::syncFromNTP() {
|
|||||||
struct tm timeinfo;
|
struct tm timeinfo;
|
||||||
gmtime_r(&now, &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)) {
|
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);
|
LOG_INF("CLK", "RTC set to %02d:%02d:%02d UTC", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <Rtc.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
|
||||||
#include "HalGPIO.h"
|
#include "HalGPIO.h"
|
||||||
@@ -10,6 +11,8 @@ extern HalClock halClock; // Singleton
|
|||||||
|
|
||||||
class HalClock {
|
class HalClock {
|
||||||
bool _available = false;
|
bool _available = false;
|
||||||
|
bool _usesSdkRtc = false;
|
||||||
|
mutable Rtc _sdkRtc;
|
||||||
mutable uint8_t _cachedHour = 0;
|
mutable uint8_t _cachedHour = 0;
|
||||||
mutable uint8_t _cachedMinute = 0;
|
mutable uint8_t _cachedMinute = 0;
|
||||||
mutable bool _hasCachedTime = false;
|
mutable bool _hasCachedTime = false;
|
||||||
|
|||||||
@@ -82,6 +82,17 @@ void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* m
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) {
|
void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) {
|
||||||
|
// X3: a HALF fallback means the caller wants a clean base (e.g. the sleep
|
||||||
|
// cover, a full-screen swap from arbitrary prior content). Without this, the
|
||||||
|
// X3 grayscale base takes its gentle differential happy path and the prior
|
||||||
|
// home/reader frame ghosts through the soft aa_pre_bw_mid waveform. Forcing a
|
||||||
|
// resync makes displayGrayscaleBase clear first, matching displayBuffer(HALF).
|
||||||
|
// The reader's FAST path is deliberately left on the differential path so
|
||||||
|
// per-page grayscale stays cheap.
|
||||||
|
if (gpio.deviceIsX3() && fallback == RefreshMode::HALF_REFRESH) {
|
||||||
|
einkDisplay.requestResync(1);
|
||||||
|
}
|
||||||
|
|
||||||
einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen);
|
einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ class HalDisplay {
|
|||||||
void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||||
|
|
||||||
// Display the framebuffer as the base frame for a grayscale overlay that
|
// Display the framebuffer as the base frame for a grayscale overlay that
|
||||||
// follows. X3 uses the OEM differential base waveform ("AA-pre-BW(mid)");
|
// follows. On X3, HALF fallback first requests a resync to match
|
||||||
// other panels display normally with `fallback` mode (previous behavior).
|
// displayBuffer(HALF); FAST fallback keeps the OEM differential base waveform
|
||||||
// Deliberately does NOT force the X3 resync that displayBuffer(HALF) does.
|
// ("AA-pre-BW(mid)"). Other panels display normally with `fallback` mode.
|
||||||
void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false);
|
void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false);
|
||||||
|
|
||||||
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
||||||
|
|||||||
+79
-17
@@ -1,5 +1,6 @@
|
|||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <PowerManager.h>
|
||||||
#include <Preferences.h>
|
#include <Preferences.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
@@ -192,14 +193,44 @@ HalGPIO::DeviceType detectDeviceTypeWithFingerprint() {
|
|||||||
|
|
||||||
void HalGPIO::begin() {
|
void HalGPIO::begin() {
|
||||||
inputMgr.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);
|
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if FREEINK_MCU_C3
|
||||||
_deviceType = detectDeviceTypeWithFingerprint();
|
_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()) {
|
if (deviceIsX4()) {
|
||||||
pinMode(BAT_GPIO0, INPUT);
|
pinMode(BAT_GPIO0, INPUT);
|
||||||
pinMode(UART0_RXD, 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() {
|
void HalGPIO::update() {
|
||||||
@@ -225,30 +256,55 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
|||||||
|
|
||||||
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
|
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() {
|
void HalGPIO::startDeepSleep() {
|
||||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||||
while (inputMgr.isPressed(BTN_POWER)) {
|
while (inputMgr.isPressed(BTN_POWER)) {
|
||||||
delay(50);
|
delay(50);
|
||||||
inputMgr.update();
|
inputMgr.update();
|
||||||
}
|
}
|
||||||
// Arm the wakeup trigger *after* the button is released
|
freeink::PowerManager::armPowerButtonWakeup();
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
freeink::PowerManager::deepSleep();
|
||||||
// Enter Deep Sleep
|
|
||||||
esp_deep_sleep_start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
|
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) {
|
if (shortPressAllowed) {
|
||||||
// Fast path - no duration check needed
|
// Fast path - no duration check needed
|
||||||
return;
|
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();
|
const auto start = millis();
|
||||||
inputMgr.update();
|
inputMgr.update();
|
||||||
// inputMgr.isPressed() may take up to ~500ms to return correct state
|
// 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();
|
inputMgr.update();
|
||||||
}
|
}
|
||||||
if (inputMgr.isPressed(BTN_POWER)) {
|
if (inputMgr.isPressed(BTN_POWER)) {
|
||||||
|
const auto holdStart = millis();
|
||||||
do {
|
do {
|
||||||
delay(10);
|
delay(10);
|
||||||
inputMgr.update();
|
inputMgr.update();
|
||||||
} while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration);
|
} while (inputMgr.isPressed(BTN_POWER) && millis() - holdStart < requiredDurationMs);
|
||||||
if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) {
|
if (millis() - holdStart < requiredDurationMs) {
|
||||||
startDeepSleep();
|
startDeepSleep();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -282,8 +339,10 @@ bool HalGPIO::isUsbConnected() const {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
if (BoardConfig::ACTIVE.usbDetect < 0) {
|
||||||
return digitalRead(UART0_RXD) == HIGH;
|
return false;
|
||||||
|
}
|
||||||
|
return digitalRead(BoardConfig::ACTIVE.usbDetect) == HIGH;
|
||||||
}
|
}
|
||||||
|
|
||||||
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
||||||
@@ -292,8 +351,11 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
|||||||
|
|
||||||
const bool usbConnected = isUsbConnected();
|
const bool usbConnected = isUsbConnected();
|
||||||
|
|
||||||
if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) ||
|
if (resetReason == ESP_RST_DEEPSLEEP &&
|
||||||
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) {
|
(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;
|
return WakeupReason::PowerButton;
|
||||||
}
|
}
|
||||||
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) {
|
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) {
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ class HalGPIO {
|
|||||||
inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; }
|
inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; }
|
||||||
inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; }
|
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
|
// Start button GPIO and setup SPI for screen and SD card
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
@@ -72,6 +77,34 @@ class HalGPIO {
|
|||||||
unsigned long getHeldTime() const;
|
unsigned long getHeldTime() const;
|
||||||
unsigned long getPowerButtonHeldTime() 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
|
// Setup wake up GPIO and enter deep sleep
|
||||||
void startDeepSleep();
|
void startDeepSleep();
|
||||||
|
|
||||||
|
|||||||
+45
-25
@@ -1,6 +1,7 @@
|
|||||||
#include "HalPowerManager.h"
|
#include "HalPowerManager.h"
|
||||||
|
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <PowerManager.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_sleep.h>
|
#include <esp_sleep.h>
|
||||||
|
|
||||||
@@ -10,15 +11,32 @@
|
|||||||
|
|
||||||
HalPowerManager powerManager; // Singleton instance
|
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() {
|
void HalPowerManager::begin() {
|
||||||
if (gpio.deviceIsX3()) {
|
const auto& gauge = BoardConfig::ACTIVE.batteryGauge;
|
||||||
// X3 uses an I2C fuel gauge for battery monitoring.
|
if (gauge.gaugeAddr != 0) {
|
||||||
// I2C init must come AFTER gpio.begin() so early hardware detection/probes are finished.
|
// Board has an I2C fuel gauge (X3, LilyGo, Sticky, ...). Pins/freq come from
|
||||||
Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ);
|
// the active board profile, not hardcoded X3 values. I2C init must come AFTER
|
||||||
Wire.setTimeOut(4);
|
// gpio.begin() so early hardware detection/probes are finished.
|
||||||
|
gaugeWire().begin(gauge.i2cSda, gauge.i2cScl, gauge.i2cHz);
|
||||||
|
gaugeWire().setTimeOut(4);
|
||||||
_batteryUseI2C = true;
|
_batteryUseI2C = true;
|
||||||
} else {
|
} else if (BoardConfig::ACTIVE.batteryAdc >= 0) {
|
||||||
pinMode(BAT_GPIO0, INPUT);
|
// 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();
|
normalFreq = getCpuFrequencyMhz();
|
||||||
modeMutex = xSemaphoreCreateMutex();
|
modeMutex = xSemaphoreCreateMutex();
|
||||||
@@ -76,22 +94,19 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Pre-sleep routines from the original firmware
|
// 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
|
// 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
|
// 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;
|
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
||||||
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
||||||
gpio_set_level(GPIO_SPIWP, 0);
|
gpio_set_level(GPIO_SPIWP, 0);
|
||||||
esp_sleep_config_gpio_isolate();
|
esp_sleep_config_gpio_isolate();
|
||||||
gpio_deep_sleep_hold_en();
|
gpio_deep_sleep_hold_en();
|
||||||
gpio_hold_en(GPIO_SPIWP);
|
gpio_hold_en(GPIO_SPIWP);
|
||||||
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
|
#endif
|
||||||
// Arm the wakeup trigger *after* the button is released
|
freeink::PowerManager::armPowerButtonWakeup();
|
||||||
// Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the
|
freeink::PowerManager::deepSleep();
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t HalPowerManager::getBatteryPercentage() const {
|
uint16_t HalPowerManager::getBatteryPercentage() const {
|
||||||
@@ -101,27 +116,32 @@ uint16_t HalPowerManager::getBatteryPercentage() const {
|
|||||||
return _batteryCachedPercent;
|
return _batteryCachedPercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read SOC directly from I2C fuel gauge (16-bit LE register).
|
// Read SOC directly from the I2C fuel gauge (16-bit LE register). Gauge
|
||||||
// On I2C error, keep last known value to avoid UI jitter/slowdowns.
|
// address comes from the active board profile (all current gauges are
|
||||||
Wire.beginTransmission(I2C_ADDR_BQ27220);
|
// BQ27220-class, SOC at 0x2C). On I2C error, keep last known value to avoid
|
||||||
Wire.write(BQ27220_SOC_REG);
|
// UI jitter/slowdowns.
|
||||||
if (Wire.endTransmission(false) != 0) {
|
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;
|
_batteryLastPollMs = now;
|
||||||
return _batteryCachedPercent;
|
return _batteryCachedPercent;
|
||||||
}
|
}
|
||||||
Wire.requestFrom(I2C_ADDR_BQ27220, (uint8_t)2);
|
w.requestFrom(gaugeAddr, (uint8_t)2);
|
||||||
if (Wire.available() < 2) {
|
if (w.available() < 2) {
|
||||||
_batteryLastPollMs = now;
|
_batteryLastPollMs = now;
|
||||||
return _batteryCachedPercent;
|
return _batteryCachedPercent;
|
||||||
}
|
}
|
||||||
const uint8_t lo = Wire.read();
|
const uint8_t lo = w.read();
|
||||||
const uint8_t hi = Wire.read();
|
const uint8_t hi = w.read();
|
||||||
const uint16_t soc = (hi << 8) | lo;
|
const uint16_t soc = (hi << 8) | lo;
|
||||||
_batteryCachedPercent = soc > 100 ? 100 : soc;
|
_batteryCachedPercent = soc > 100 ? 100 : soc;
|
||||||
_batteryLastPollMs = now;
|
_batteryLastPollMs = now;
|
||||||
return _batteryCachedPercent;
|
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 %.
|
// smooth the battery %.
|
||||||
if (_batteryCachedPercent == 0) {
|
if (_batteryCachedPercent == 0) {
|
||||||
|
|||||||
@@ -28,7 +28,16 @@ class HalPowerManager {
|
|||||||
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
||||||
|
|
||||||
public:
|
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 IDLE_POWER_SAVING_MS = 3000; // ms
|
||||||
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
|
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
|
|||||||
__real_panic_print_backtrace(frame, core);
|
__real_panic_print_backtrace(frame, core);
|
||||||
return;
|
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++) {
|
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
|
||||||
panicStack[i].sp = 0;
|
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);
|
__real_panic_print_backtrace(frame, core);
|
||||||
|
#endif // __riscv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ bool HalTiltSensor::readReg(uint8_t reg, uint8_t* val) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) 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.beginTransmission(_i2cAddr);
|
||||||
Wire.write(REG_GX_L); // Start reading at Gyro X Low
|
Wire.write(REG_GX_L); // Start reading at Gyro X Low
|
||||||
if (Wire.endTransmission(false) != 0) {
|
if (Wire.endTransmission(false) != 0) {
|
||||||
@@ -52,11 +61,22 @@ bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HalTiltSensor::begin() {
|
void HalTiltSensor::begin() {
|
||||||
|
_backend = Backend::None;
|
||||||
if (!gpio.deviceIsX3()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_backend = Backend::Qmi8658;
|
||||||
|
|
||||||
// Try primary address, then alternate
|
// Try primary address, then alternate
|
||||||
uint8_t whoami = 0;
|
uint8_t whoami = 0;
|
||||||
_i2cAddr = I2C_ADDR_QMI8658;
|
_i2cAddr = I2C_ADDR_QMI8658;
|
||||||
@@ -89,6 +109,14 @@ bool HalTiltSensor::wake() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_backend == Backend::SdkImu) {
|
||||||
|
_lastPollMs = millis();
|
||||||
|
_lastTiltMs = millis();
|
||||||
|
_wakeMs = millis();
|
||||||
|
_isAwake = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for init to complete before waking
|
// Wait for init to complete before waking
|
||||||
if ((millis() - _initMs) < SLEEP_STABILIZE_MS) {
|
if ((millis() - _initMs) < SLEEP_STABILIZE_MS) {
|
||||||
return false;
|
return false;
|
||||||
@@ -111,6 +139,13 @@ bool HalTiltSensor::deepSleep() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_backend == Backend::SdkImu) {
|
||||||
|
clearPendingEvents();
|
||||||
|
_inTilt = false;
|
||||||
|
_isAwake = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if ((millis() - _wakeMs) < SLEEP_STABILIZE_MS) {
|
if ((millis() - _wakeMs) < SLEEP_STABILIZE_MS) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <Imu.h>
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
|
||||||
#include "HalGPIO.h"
|
#include "HalGPIO.h"
|
||||||
@@ -18,8 +19,12 @@ class HalTiltSensor;
|
|||||||
extern HalTiltSensor halTiltSensor; // Singleton
|
extern HalTiltSensor halTiltSensor; // Singleton
|
||||||
|
|
||||||
class HalTiltSensor {
|
class HalTiltSensor {
|
||||||
|
enum class Backend : uint8_t { None, Qmi8658, SdkImu };
|
||||||
|
|
||||||
bool _available = false;
|
bool _available = false;
|
||||||
|
Backend _backend = Backend::None;
|
||||||
uint8_t _i2cAddr = 0;
|
uint8_t _i2cAddr = 0;
|
||||||
|
mutable Imu _sdkImu;
|
||||||
|
|
||||||
// Tilt gesture state machine
|
// Tilt gesture state machine
|
||||||
bool _tiltForwardEvent = false; // Consumed by wasTiltedForward()
|
bool _tiltForwardEvent = false; // Consumed by wasTiltedForward()
|
||||||
|
|||||||
-1
Submodule open-x4-sdk deleted from 198ad26721
+87
-13
@@ -4,7 +4,7 @@ build_cache_dir = .cache
|
|||||||
extra_configs = platformio.local.ini
|
extra_configs = platformio.local.ini
|
||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
version = 1.4.0
|
version = 1.4.1
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
|
||||||
@@ -55,47 +55,121 @@ extra_scripts =
|
|||||||
pre:scripts/patch_jpegdec.py
|
pre:scripts/patch_jpegdec.py
|
||||||
post:scripts/register_unit_tests_target.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 =
|
lib_deps =
|
||||||
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
BatteryMonitor=symlink://freeink-sdk/libs/hardware/BatteryMonitor
|
||||||
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
|
InputManager=symlink://freeink-sdk/libs/hardware/InputManager
|
||||||
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
|
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
|
||||||
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
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
|
bblanchon/ArduinoJson @ 7.4.2
|
||||||
ricmoo/QRCode @ 0.0.1
|
ricmoo/QRCode @ 0.0.1
|
||||||
bitbank2/PNGdec @ 1.1.6
|
bitbank2/PNGdec @ 1.1.6
|
||||||
https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b
|
https://github.com/bitbank2/JPEGDEC.git#86282979224c8a32fd51e091ed5a35b0c699a52b
|
||||||
links2004/WebSockets @ 2.7.3
|
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
|
extends = base
|
||||||
build_flags =
|
build_flags =
|
||||||
${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)
|
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
-DLOG_LEVEL=2 ; Set log level to debug for development builds
|
||||||
|
|
||||||
|
|
||||||
[env:gh_release]
|
[env:gh_release]
|
||||||
extends = base
|
extends = c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${c3.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=1 ; Set log level to info for release builds
|
-DLOG_LEVEL=1 ; Set log level to info for release builds
|
||||||
|
|
||||||
[env:gh_release_rc]
|
[env:gh_release_rc]
|
||||||
extends = base
|
extends = c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${c3.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
|
||||||
-DENABLE_SERIAL_LOG
|
-DENABLE_SERIAL_LOG
|
||||||
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
|
||||||
|
|
||||||
[env:slim]
|
[env:slim]
|
||||||
extends = base
|
extends = c3
|
||||||
build_flags =
|
build_flags =
|
||||||
${base.build_flags}
|
${c3.build_flags}
|
||||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-slim\"
|
-DCROSSPOINT_VERSION=\"${crosspoint.version}-slim\"
|
||||||
; serial output is disabled in slim builds to save space
|
; serial output is disabled in slim builds to save space
|
||||||
-UENABLE_SERIAL_LOG
|
-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
|
||||||
|
|||||||
Executable
+26
@@ -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"
|
||||||
@@ -65,6 +65,8 @@ class CrossPointSettings {
|
|||||||
XTC_STATUS_BAR_MODE_COUNT
|
XTC_STATUS_BAR_MODE_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum STATUS_BAR_CLOCK_MODE { STATUS_BAR_CLOCK_HIDE = 0, STATUS_BAR_CLOCK_RIGHT = 1, STATUS_BAR_CLOCK_LEFT = 2 };
|
||||||
|
|
||||||
enum ORIENTATION {
|
enum ORIENTATION {
|
||||||
PORTRAIT = 0, // 480x800 logical coordinates (current default)
|
PORTRAIT = 0, // 480x800 logical coordinates (current default)
|
||||||
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||||
@@ -188,7 +190,7 @@ class CrossPointSettings {
|
|||||||
uint8_t statusBarBattery = 1;
|
uint8_t statusBarBattery = 1;
|
||||||
uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE;
|
uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE;
|
||||||
// Clock display in status bar (X3 only, requires DS3231 RTC)
|
// Clock display in status bar (X3 only, requires DS3231 RTC)
|
||||||
uint8_t statusBarClock = 0;
|
uint8_t statusBarClock = STATUS_BAR_CLOCK_HIDE;
|
||||||
// Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t.
|
// Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t.
|
||||||
// Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00.
|
// Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00.
|
||||||
// Quarter-hour granularity supports oddball zones like Nepal (+5:45) and Chatham (+12:45).
|
// Quarter-hour granularity supports oddball zones like Nepal (+5:45) and Chatham (+12:45).
|
||||||
@@ -244,6 +246,9 @@ class CrossPointSettings {
|
|||||||
uint8_t uiTheme = LYRA;
|
uint8_t uiTheme = LYRA;
|
||||||
// Sunlight fading compensation
|
// Sunlight fading compensation
|
||||||
uint8_t fadingFix = 0;
|
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)
|
// Power button return from footnotes (1 = enabled, 0 = disabled)
|
||||||
uint8_t pwrBtnFootnoteBack = 1;
|
uint8_t pwrBtnFootnoteBack = 1;
|
||||||
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
|
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
|
||||||
|
|||||||
+187
-3
@@ -1,8 +1,12 @@
|
|||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "components/TouchRegistry.h"
|
||||||
|
|
||||||
bool MappedInputManager::isNavDirectionSwapped() const {
|
bool MappedInputManager::isNavDirectionSwapped() const {
|
||||||
// Key the swap on the orientation the screen is *actually* rendered at, not the persisted reader
|
// 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;
|
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); }
|
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(); }
|
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,
|
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
|
||||||
const char* next) const {
|
const char* next) const {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class GfxRenderer;
|
|||||||
class MappedInputManager {
|
class MappedInputManager {
|
||||||
public:
|
public:
|
||||||
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
|
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
|
||||||
|
enum class SwipeDir { None, Left, Right, Up, Down };
|
||||||
|
|
||||||
struct Labels {
|
struct Labels {
|
||||||
const char* btn1;
|
const char* btn1;
|
||||||
@@ -21,6 +22,33 @@ class MappedInputManager {
|
|||||||
bool wasPressed(Button button) const;
|
bool wasPressed(Button button) const;
|
||||||
bool wasReleased(Button button) const;
|
bool wasReleased(Button button) const;
|
||||||
bool isPressed(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 wasAnyPressed() const;
|
||||||
bool wasAnyReleased() const;
|
bool wasAnyReleased() const;
|
||||||
unsigned long getHeldTime() const;
|
unsigned long getHeldTime() const;
|
||||||
@@ -44,4 +72,13 @@ class MappedInputManager {
|
|||||||
const GfxRenderer& renderer;
|
const GfxRenderer& renderer;
|
||||||
|
|
||||||
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
+29
-5
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <BoardConfig.h>
|
||||||
#include <HalClock.h>
|
#include <HalClock.h>
|
||||||
|
#include <HalGPIO.h>
|
||||||
#include <HalTiltSensor.h>
|
#include <HalTiltSensor.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <SdCardFontRegistry.h>
|
#include <SdCardFontRegistry.h>
|
||||||
@@ -151,9 +153,10 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
|
|||||||
StrId::STR_CAT_READER),
|
StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
|
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
|
||||||
StrId::STR_CAT_READER),
|
StrId::STR_CAT_READER),
|
||||||
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
SettingInfo::Enum(
|
||||||
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
|
StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
||||||
"orientation", StrId::STR_CAT_READER),
|
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_ORIENTATION_INVERTED, StrId::STR_LANDSCAPE_CCW},
|
||||||
|
"orientation", StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing,
|
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing,
|
||||||
"extraParagraphSpacing", StrId::STR_CAT_READER),
|
"extraParagraphSpacing", StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
|
||||||
@@ -244,8 +247,9 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
|
|||||||
StrId::STR_CUSTOMISE_STATUS_BAR),
|
StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
// Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset).
|
// Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset).
|
||||||
// Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48.
|
// Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48.
|
||||||
SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock",
|
SettingInfo::Enum(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock,
|
||||||
StrId::STR_CUSTOMISE_STATUS_BAR),
|
{StrId::STR_HIDE, StrId::STR_DIR_LEFT, StrId::STR_DIR_RIGHT}, "statusBarClock",
|
||||||
|
StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1},
|
SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1},
|
||||||
"clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR),
|
"clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat,
|
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat,
|
||||||
@@ -256,6 +260,26 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
|
|||||||
SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced",
|
SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced",
|
||||||
StrId::STR_CUSTOMISE_STATUS_BAR),
|
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)
|
// Only show tilt page turn setting when the QMI8658 IMU is present (X3)
|
||||||
if (halTiltSensor.isAvailable()) {
|
if (halTiltSensor.isAvailable()) {
|
||||||
// Insert after the short power button setting (end of Controls section)
|
// Insert after the short power button setting (end of Controls section)
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ class Activity {
|
|||||||
// Finish this activity and return to the previous one on the stack (if any)
|
// Finish this activity and return to the previous one on the stack (if any)
|
||||||
void finish();
|
void finish();
|
||||||
|
|
||||||
// Convenience method to facilitate API transition to ActivityManager
|
// Convenience method to facilitate API transition to ActivityManager.
|
||||||
// TODO: remove this in near future
|
|
||||||
void onGoHome(HomeMenuItem item = HomeMenuItem::NONE);
|
void onGoHome(HomeMenuItem item = HomeMenuItem::NONE);
|
||||||
void onSelectBook(const std::string& path);
|
void onSelectBook(const std::string& path);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "boot_sleep/BootActivity.h"
|
#include "boot_sleep/BootActivity.h"
|
||||||
#include "boot_sleep/SleepActivity.h"
|
#include "boot_sleep/SleepActivity.h"
|
||||||
#include "browser/OpdsBookBrowserActivity.h"
|
#include "browser/OpdsBookBrowserActivity.h"
|
||||||
|
#include "components/TouchRegistry.h"
|
||||||
#include "home/CrashActivity.h"
|
#include "home/CrashActivity.h"
|
||||||
#include "home/FileBrowserActivity.h"
|
#include "home/FileBrowserActivity.h"
|
||||||
#include "home/HomeActivity.h"
|
#include "home/HomeActivity.h"
|
||||||
@@ -19,6 +20,13 @@
|
|||||||
#include "settings/SettingsActivity.h"
|
#include "settings/SettingsActivity.h"
|
||||||
#include "util/FullScreenMessageActivity.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() {
|
void ActivityManager::begin() {
|
||||||
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
|
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
|
||||||
8192, // Stack size
|
8192, // Stack size
|
||||||
@@ -42,14 +50,18 @@ void ActivityManager::renderTaskLoop() {
|
|||||||
RenderLock lock;
|
RenderLock lock;
|
||||||
if (currentActivity) {
|
if (currentActivity) {
|
||||||
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
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));
|
currentActivity->render(std::move(lock));
|
||||||
|
TouchRegistry::getInstance().publish();
|
||||||
}
|
}
|
||||||
// Notify any task blocked in requestUpdateAndWait() that the render is done.
|
// Notify any task blocked in requestUpdateAndWait() that the render is done.
|
||||||
TaskHandle_t waiter = nullptr;
|
TaskHandle_t waiter = nullptr;
|
||||||
taskENTER_CRITICAL(nullptr);
|
taskENTER_CRITICAL(&activityMux);
|
||||||
waiter = waitingTaskHandle;
|
waiter = waitingTaskHandle;
|
||||||
waitingTaskHandle = nullptr;
|
waitingTaskHandle = nullptr;
|
||||||
taskEXIT_CRITICAL(nullptr);
|
taskEXIT_CRITICAL(&activityMux);
|
||||||
if (waiter) {
|
if (waiter) {
|
||||||
xTaskNotify(waiter, 1, eIncrement);
|
xTaskNotify(waiter, 1, eIncrement);
|
||||||
}
|
}
|
||||||
@@ -280,7 +292,7 @@ void ActivityManager::requestUpdateAndWait() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Atomic section to perform checks
|
// Atomic section to perform checks
|
||||||
taskENTER_CRITICAL(nullptr);
|
taskENTER_CRITICAL(&activityMux);
|
||||||
auto currTaskHandler = xTaskGetCurrentTaskHandle();
|
auto currTaskHandler = xTaskGetCurrentTaskHandle();
|
||||||
auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex);
|
auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex);
|
||||||
bool isRenderTask = (currTaskHandler == renderTaskHandle);
|
bool isRenderTask = (currTaskHandler == renderTaskHandle);
|
||||||
@@ -289,7 +301,7 @@ void ActivityManager::requestUpdateAndWait() {
|
|||||||
if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) {
|
if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) {
|
||||||
waitingTaskHandle = currTaskHandler;
|
waitingTaskHandle = currTaskHandler;
|
||||||
}
|
}
|
||||||
taskEXIT_CRITICAL(nullptr);
|
taskEXIT_CRITICAL(&activityMux);
|
||||||
|
|
||||||
// Render task cannot call requestUpdateAndWait() or it will cause a deadlock
|
// Render task cannot call requestUpdateAndWait() or it will cause a deadlock
|
||||||
assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()");
|
assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "OpdsBookBrowserActivity.h"
|
#include "OpdsBookBrowserActivity.h"
|
||||||
|
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
#include "SilentRestart.h"
|
#include "SilentRestart.h"
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
#include "activities/util/KeyboardEntryActivity.h"
|
#include "activities/util/KeyboardEntryActivity.h"
|
||||||
|
#include "components/TouchRegistry.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
#include "network/HttpDownloader.h"
|
#include "network/HttpDownloader.h"
|
||||||
@@ -31,7 +33,6 @@ void OpdsBookBrowserActivity::onEnter() {
|
|||||||
currentPath = "";
|
currentPath = "";
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
consumeConfirm = false;
|
consumeConfirm = false;
|
||||||
consumeBack = false;
|
|
||||||
errorMessage.clear();
|
errorMessage.clear();
|
||||||
statusMessage = tr(STR_CHECKING_WIFI);
|
statusMessage = tr(STR_CHECKING_WIFI);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -60,10 +61,6 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
consumeConfirm = false;
|
consumeConfirm = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (consumeBack && mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
|
||||||
consumeBack = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == BrowserState::ERROR) {
|
if (state == BrowserState::ERROR) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
@@ -91,11 +88,28 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
if (state == BrowserState::DOWNLOADING) return;
|
if (state == BrowserState::DOWNLOADING) return;
|
||||||
|
|
||||||
if (state == BrowserState::BROWSING) {
|
if (state == BrowserState::BROWSING) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (!entries.empty()) {
|
||||||
if (!entries.empty()) {
|
if (mappedInput.wasListScroll(selectorIndex, static_cast<int>(entries.size()), PAGE_ITEMS)) {
|
||||||
const auto& entry = entries[selectorIndex];
|
requestUpdate();
|
||||||
entry.type == OpdsEntryType::BOOK ? downloadBook(entry) : navigateToEntry(entry);
|
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)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
} 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;
|
std::string displayText = (entry.type == OpdsEntryType::NAVIGATION) ? "> " + entry.title : entry.title;
|
||||||
if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author;
|
if (entry.type == OpdsEntryType::BOOK && !entry.author.empty()) displayText += " - " + entry.author;
|
||||||
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), pageWidth - 40);
|
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(),
|
const int itemY = 60 + (i % PAGE_ITEMS) * 30;
|
||||||
i != static_cast<size_t>(selectorIndex));
|
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();
|
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) {
|
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
||||||
if (server.url.empty()) {
|
if (server.url.empty()) {
|
||||||
state = BrowserState::ERROR;
|
state = BrowserState::ERROR;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
std::string currentPath;
|
std::string currentPath;
|
||||||
std::string searchTemplate;
|
std::string searchTemplate;
|
||||||
bool consumeConfirm = false;
|
bool consumeConfirm = false;
|
||||||
bool consumeBack = false; // Added missing member
|
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
std::string errorMessage;
|
std::string errorMessage;
|
||||||
std::string statusMessage;
|
std::string statusMessage;
|
||||||
@@ -51,5 +50,6 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
void downloadBook(const OpdsEntry& book);
|
void downloadBook(const OpdsEntry& book);
|
||||||
void launchSearch();
|
void launchSearch();
|
||||||
void performSearch(const std::string& query);
|
void performSearch(const std::string& query);
|
||||||
|
void activateSelectedEntry();
|
||||||
bool preventAutoSleep() override { return true; }
|
bool preventAutoSleep() override { return true; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ void CrashActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CrashActivity::loop() {
|
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();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,29 @@ void FileBrowserActivity::loop() {
|
|||||||
const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing;
|
const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing;
|
||||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved);
|
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) {
|
if (lockNextConfirmRelease) {
|
||||||
lockNextConfirmRelease = false;
|
lockNextConfirmRelease = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -179,7 +179,29 @@ void HomeActivity::loop() {
|
|||||||
requestUpdate();
|
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()) {
|
if (selectorIndex < recentBooks.size()) {
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].path);
|
||||||
} else {
|
} else {
|
||||||
@@ -233,7 +255,7 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
// Build menu items dynamically
|
// Build menu items dynamically
|
||||||
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
|
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
|
||||||
tr(STR_SETTINGS_TITLE)};
|
tr(STR_SETTINGS_TITLE)};
|
||||||
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Settings};
|
std::vector<UIIcon> menuIcons = {Folder, Book, Transfer, Settings};
|
||||||
|
|
||||||
if (hasOpdsServers) {
|
if (hasOpdsServers) {
|
||||||
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
|
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
|
||||||
@@ -246,12 +268,13 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
menuIcons.insert(menuIcons.begin(), Book);
|
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(
|
GUI.drawButtonMenu(
|
||||||
renderer,
|
renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve}, static_cast<int>(menuItems.size()),
|
||||||
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()),
|
|
||||||
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
|
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
|
||||||
[&menuItems](int index) { return std::string(menuItems[index]); },
|
[&menuItems](int index) { return std::string(menuItems[index]); },
|
||||||
[&menuIcons](int index) { return menuIcons[index]; });
|
[&menuIcons](int index) { return menuIcons[index]; });
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ void RecentBooksActivity::onExit() {
|
|||||||
void RecentBooksActivity::loop() {
|
void RecentBooksActivity::loop() {
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
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
|
// 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).
|
// (so the release doesn't also open the book; re-arm only once the button is up).
|
||||||
if (longPressFired) {
|
if (longPressFired) {
|
||||||
@@ -63,7 +71,16 @@ void RecentBooksActivity::loop() {
|
|||||||
return;
|
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())) {
|
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
onSelectBook(recentBooks[selectorIndex].path);
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_task_wdt.h>
|
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "SilentRestart.h"
|
#include "SilentRestart.h"
|
||||||
#include "WifiSelectionActivity.h"
|
#include "WifiSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "network/TaskWatchdog.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr const char* HOSTNAME = "crosspoint";
|
constexpr const char* HOSTNAME = "crosspoint";
|
||||||
@@ -110,12 +110,12 @@ void CalibreConnectActivity::loop() {
|
|||||||
LOG_DBG("CAL", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient);
|
LOG_DBG("CAL", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_task_wdt_reset();
|
feedTaskWatchdog();
|
||||||
constexpr int MAX_ITERATIONS = 80;
|
constexpr int MAX_ITERATIONS = 80;
|
||||||
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
|
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
|
||||||
webServer->handleClient();
|
webServer->handleClient();
|
||||||
if ((i & 0x07) == 0x07) {
|
if ((i & 0x07) == 0x07) {
|
||||||
esp_task_wdt_reset();
|
feedTaskWatchdog();
|
||||||
}
|
}
|
||||||
if ((i & 0x0F) == 0x0F) {
|
if ((i & 0x0F) == 0x0F) {
|
||||||
yield();
|
yield();
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_task_wdt.h>
|
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
@@ -16,6 +15,7 @@
|
|||||||
#include "activities/network/CalibreConnectActivity.h"
|
#include "activities/network/CalibreConnectActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "network/TaskWatchdog.h"
|
||||||
#include "util/QrUtils.h"
|
#include "util/QrUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -328,7 +328,7 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
|
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
|
||||||
esp_task_wdt_reset();
|
feedTaskWatchdog();
|
||||||
|
|
||||||
// Process HTTP requests in tight loop for maximum throughput
|
// Process HTTP requests in tight loop for maximum throughput
|
||||||
// More iterations = more data processed per main loop cycle
|
// More iterations = more data processed per main loop cycle
|
||||||
@@ -337,7 +337,7 @@ void CrossPointWebServerActivity::loop() {
|
|||||||
webServer->handleClient();
|
webServer->handleClient();
|
||||||
// Reset watchdog every 32 iterations
|
// Reset watchdog every 32 iterations
|
||||||
if ((i & 0x1F) == 0x1F) {
|
if ((i & 0x1F) == 0x1F) {
|
||||||
esp_task_wdt_reset();
|
feedTaskWatchdog();
|
||||||
}
|
}
|
||||||
// Yield and check for exit button every 64 iterations
|
// Yield and check for exit button every 64 iterations
|
||||||
if ((i & 0x3F) == 0x3F) {
|
if ((i & 0x3F) == 0x3F) {
|
||||||
|
|||||||
@@ -30,8 +30,17 @@ void NetworkModeSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle confirm button - select current option
|
int downId = -1;
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
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;
|
NetworkMode mode = NetworkMode::JOIN_NETWORK;
|
||||||
if (selectedIndex == 1) {
|
if (selectedIndex == 1) {
|
||||||
mode = NetworkMode::CONNECT_CALIBRE;
|
mode = NetworkMode::CONNECT_CALIBRE;
|
||||||
@@ -42,7 +51,6 @@ void NetworkModeSelectionActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
buttonNavigator.onNext([this] {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
|
||||||
requestUpdate();
|
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(menuItems[index])); },
|
||||||
[](int index) { return std::string(I18N.get(menuDescs[index])); }, [](int index) { return menuIcons[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));
|
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);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
|||||||
@@ -409,6 +409,27 @@ void WifiSelectionActivity::loop() {
|
|||||||
|
|
||||||
// Handle network list state
|
// Handle network list state
|
||||||
if (state == WifiSelectionState::NETWORK_LIST) {
|
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)
|
// Check for Back button to exit (cancel)
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onComplete(false);
|
onComplete(false);
|
||||||
@@ -442,7 +463,6 @@ void WifiSelectionActivity::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
buttonNavigator.onNext([this] {
|
buttonNavigator.onNext([this] {
|
||||||
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
|
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|||||||
@@ -83,9 +83,10 @@ ProgressRange getPageProgressRange(const std::shared_ptr<Epub>& epub, const int
|
|||||||
return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)};
|
return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const SavedProgressPosition& progress,
|
bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const int spineIndex, const int page, const int pageCount,
|
||||||
const ProgressRange& pageRange) {
|
const ProgressRange& pageRange) {
|
||||||
if (bookmark.xpath == progress.xpath) {
|
if (bookmark.computedSpineIndex == spineIndex && bookmark.computedChapterPageCount == pageCount &&
|
||||||
|
bookmark.computedChapterProgress == page) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,10 +298,14 @@ void EpubReaderActivity::loop() {
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter reader menu activity on short-press Confirm. A long-press that fired a bound
|
// Touch page nav (idempotent within a frame, so read here and again below). The
|
||||||
// function (bookmark or KOReader sync) sets ignoreNextConfirmRelease so the release
|
// top-left Back corner is consumed by wasReleased(Back) above, never reaching here.
|
||||||
// following the hold does not also open the menu.
|
const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
|
||||||
|
// 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) {
|
if (ignoreNextConfirmRelease) {
|
||||||
ignoreNextConfirmRelease = false;
|
ignoreNextConfirmRelease = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -399,7 +404,9 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
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) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -417,7 +424,8 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
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
|
// Don't skip chapter after screenshot
|
||||||
if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) {
|
if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) {
|
||||||
@@ -787,7 +795,6 @@ void EpubReaderActivity::pageTurn(bool isForwardTurn) {
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Failure handling
|
|
||||||
void EpubReaderActivity::render(RenderLock&& lock) {
|
void EpubReaderActivity::render(RenderLock&& lock) {
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
return;
|
return;
|
||||||
@@ -1311,10 +1318,12 @@ void EpubReaderActivity::addBookmark() {
|
|||||||
const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount);
|
const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount);
|
||||||
|
|
||||||
const size_t bookmarkCountBeforeToggle = cachedBookmarks.size();
|
const size_t bookmarkCountBeforeToggle = cachedBookmarks.size();
|
||||||
cachedBookmarks.erase(
|
cachedBookmarks.erase(std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(),
|
||||||
std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(),
|
[&](const BookmarkEntry& b) {
|
||||||
[&](const BookmarkEntry& b) { return bookmarkMatchesProgress(b, progress, pageRange); }),
|
return bookmarkMatchesProgress(b, currentSpineIndex, currentPage, pageCount,
|
||||||
cachedBookmarks.end());
|
pageRange);
|
||||||
|
}),
|
||||||
|
cachedBookmarks.end());
|
||||||
if (cachedBookmarks.size() != bookmarkCountBeforeToggle) {
|
if (cachedBookmarks.size() != bookmarkCountBeforeToggle) {
|
||||||
bookmarkRemoved = true;
|
bookmarkRemoved = true;
|
||||||
currentPageBookmarked = false;
|
currentPageBookmarked = false;
|
||||||
@@ -1350,11 +1359,10 @@ void EpubReaderActivity::updateBookmarkFlag() {
|
|||||||
currentPageBookmarked = false;
|
currentPageBookmarked = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
|
|
||||||
const ProgressRange pageRange =
|
const ProgressRange pageRange =
|
||||||
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
|
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
|
||||||
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
|
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
|
||||||
return bookmarkMatchesProgress(b, progress, pageRange);
|
return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, section->pageCount, pageRange);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
if (bookmarks.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,39 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.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() {
|
void EpubReaderChapterSelectionActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
@@ -30,23 +58,38 @@ void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
|
|||||||
void EpubReaderChapterSelectionActivity::loop() {
|
void EpubReaderChapterSelectionActivity::loop() {
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
|
if (!epub || totalItems <= 0) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
const auto tocItem = epub->getTocItem(selectorIndex);
|
cancelAndFinish();
|
||||||
if (tocItem.spineIndex == -1) {
|
|
||||||
ActivityResult result;
|
|
||||||
result.isCancelled = true;
|
|
||||||
setResult(std::move(result));
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor});
|
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
|
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)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
ActivityResult result;
|
cancelAndFinish();
|
||||||
result.isCancelled = true;
|
return;
|
||||||
setResult(std::move(result));
|
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonNavigator.onNextRelease([this, totalItems] {
|
buttonNavigator.onNextRelease([this, totalItems] {
|
||||||
@@ -83,10 +126,12 @@ void EpubReaderChapterSelectionActivity::render(RenderLock&&) {
|
|||||||
const int contentHeight = screen.height - contentTop - metrics.verticalSpacing;
|
const int contentHeight = screen.height - contentTop - metrics.verticalSpacing;
|
||||||
|
|
||||||
const int totalItems = getTotalItems();
|
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) {
|
[this](int index) {
|
||||||
auto item = epub->getTocItem(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;
|
return indent + item.title;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class EpubReaderChapterSelectionActivity final : public Activity {
|
|||||||
|
|
||||||
// Total TOC items count
|
// Total TOC items count
|
||||||
int getTotalItems() const;
|
int getTotalItems() const;
|
||||||
|
void cancelAndFinish();
|
||||||
|
void activateSelection(int index);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#include "EpubReaderFootnotesActivity.h"
|
#include "EpubReaderFootnotesActivity.h"
|
||||||
|
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/TouchRegistry.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@@ -18,6 +20,30 @@ void EpubReaderFootnotesActivity::onEnter() {
|
|||||||
void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); }
|
void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
void EpubReaderFootnotesActivity::loop() {
|
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)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
ActivityResult result;
|
ActivityResult result;
|
||||||
result.isCancelled = true;
|
result.isCancelled = true;
|
||||||
@@ -102,6 +128,7 @@ void EpubReaderFootnotesActivity::render(RenderLock&&) {
|
|||||||
label = tr(STR_LINK);
|
label = tr(STR_LINK);
|
||||||
}
|
}
|
||||||
renderer.drawText(UI_10_FONT_ID, marginLeft, y + 4, label.c_str(), !isSelected);
|
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), "", "");
|
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::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
void EpubReaderMenuActivity::loop() {
|
void EpubReaderMenuActivity::loop() {
|
||||||
// Handle navigation
|
|
||||||
buttonNavigator.onNext([this] {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -61,7 +60,27 @@ void EpubReaderMenuActivity::loop() {
|
|||||||
requestUpdate();
|
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;
|
const auto selectedAction = menuItems[selectedIndex].action;
|
||||||
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
if (selectedAction == MenuAction::ROTATE_SCREEN) {
|
||||||
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
// Cycle orientation preview locally; actual rotation happens on menu exit.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@@ -32,6 +34,22 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
|
|||||||
requestUpdate();
|
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() {
|
void EpubReaderPercentSelectionActivity::loop() {
|
||||||
// Back cancels, confirm selects, arrows adjust the percent.
|
// Back cancels, confirm selects, arrows adjust the percent.
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
@@ -48,6 +66,31 @@ void EpubReaderPercentSelectionActivity::loop() {
|
|||||||
return;
|
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::Left}, [this] { adjustPercent(-kSmallStep); });
|
||||||
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [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.
|
// Change the current percent by a delta and clamp within bounds.
|
||||||
void adjustPercent(int delta);
|
void adjustPercent(int delta);
|
||||||
|
bool setPercentFromTouch(int x, int y);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ void QrDisplayActivity::loop() {
|
|||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int tapX = 0;
|
||||||
|
int tapY = 0;
|
||||||
|
if (mappedInput.wasScreenTapped(tapX, tapY)) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QrDisplayActivity::render(RenderLock&&) {
|
void QrDisplayActivity::render(RenderLock&&) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <CrossPointSettings.h>
|
#include <CrossPointSettings.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalGPIO.h>
|
||||||
#include <HalTiltSensor.h>
|
#include <HalTiltSensor.h>
|
||||||
#include <Logging.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 SKIP_HOLD_MS = 700;
|
||||||
constexpr unsigned long BOOKMARK_HOLD_MS = 400;
|
constexpr unsigned long BOOKMARK_HOLD_MS = 400;
|
||||||
constexpr unsigned long BOOKMARK_MESSAGE_DURATION_MS = 2500;
|
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) {
|
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
@@ -59,6 +63,46 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
|||||||
return {prev, next, tiltPrev || tiltNext};
|
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) {
|
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ void TxtReaderActivity::loop() {
|
|||||||
return;
|
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) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,11 @@ void XtcReaderActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void XtcReaderActivity::loop() {
|
void XtcReaderActivity::loop() {
|
||||||
// Enter chapter selection activity
|
// Touch page nav; center hold opens chapter selection (the Confirm analogue).
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
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()) {
|
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||||
startActivityForResult(
|
startActivityForResult(
|
||||||
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
|
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
|
||||||
@@ -80,7 +83,9 @@ void XtcReaderActivity::loop() {
|
|||||||
return;
|
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) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -96,8 +101,9 @@ void XtcReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool skipPages = !fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP &&
|
const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime();
|
||||||
mappedInput.getHeldTime() > ReaderUtils::SKIP_HOLD_MS;
|
const bool skipPages =
|
||||||
|
!fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP && heldMs > ReaderUtils::SKIP_HOLD_MS;
|
||||||
const int skipAmount = skipPages ? 10 : 1;
|
const int skipAmount = skipPages ? 10 : 1;
|
||||||
|
|
||||||
if (prevTriggered) {
|
if (prevTriggered) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#include "XtcReaderChapterSelectionActivity.h"
|
#include "XtcReaderChapterSelectionActivity.h"
|
||||||
|
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/TouchRegistry.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@@ -56,6 +58,26 @@ void XtcReaderChapterSelectionActivity::loop() {
|
|||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = static_cast<int>(xtc->getChapters().size());
|
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)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) {
|
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++) {
|
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
|
||||||
const auto& chapter = chapters[i];
|
const auto& chapter = chapters[i];
|
||||||
const char* title = chapter.name.empty() ? tr(STR_UNNAMED) : chapter.name.c_str();
|
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)
|
// Skip button hints in landscape CW mode (they overlap content)
|
||||||
|
|||||||
@@ -122,6 +122,27 @@ void ClearCacheActivity::clearCache() {
|
|||||||
|
|
||||||
void ClearCacheActivity::loop() {
|
void ClearCacheActivity::loop() {
|
||||||
if (state == WARNING) {
|
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)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear");
|
LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear");
|
||||||
{
|
{
|
||||||
@@ -141,7 +162,9 @@ void ClearCacheActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state == SUCCESS || state == FAILED) {
|
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();
|
goBack();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/TouchRegistry.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.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() {
|
void ClockOffsetActivity::loop() {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
finish();
|
finish();
|
||||||
return;
|
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)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
activeField = static_cast<Field>((activeField + 1) % FIELD_COUNT);
|
selectRelativeField(+1);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,6 +227,8 @@ void ClockOffsetActivity::render(RenderLock&&) {
|
|||||||
}
|
}
|
||||||
const int textX = boxX + (boxWidth - widthOf(text)) / 2;
|
const int textX = boxX + (boxWidth - widthOf(text)) / 2;
|
||||||
renderer.drawText(UI_12_FONT_ID, textX, centreY, text, true, EpdFontFamily::BOLD);
|
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);
|
drawField(signStr, x, signBoxW, FIELD_SIGN);
|
||||||
|
|||||||
@@ -34,4 +34,5 @@ class ClockOffsetActivity final : public Activity {
|
|||||||
void saveToSettings() const;
|
void saveToSettings() const;
|
||||||
void adjustActiveField(int delta);
|
void adjustActiveField(int delta);
|
||||||
void clampForSign();
|
void clampForSign();
|
||||||
|
void selectRelativeField(int delta);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ void ClockSyncActivity::loop() {
|
|||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
finish();
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int tapX = 0;
|
||||||
|
int tapY = 0;
|
||||||
|
if (mappedInput.wasScreenTapped(tapX, tapY)) {
|
||||||
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "FontDownloadActivity.h"
|
#include "FontDownloadActivity.h"
|
||||||
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
@@ -420,6 +421,38 @@ bool FontDownloadActivity::isSelectedFamilyDeletable() const {
|
|||||||
return family.installed && !family.hasUpdate;
|
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 ---
|
// --- Input handling ---
|
||||||
|
|
||||||
void FontDownloadActivity::loop() {
|
void FontDownloadActivity::loop() {
|
||||||
@@ -431,6 +464,23 @@ void FontDownloadActivity::loop() {
|
|||||||
|
|
||||||
const int listSize = listItemCount();
|
const int listSize = listItemCount();
|
||||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false);
|
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] {
|
buttonNavigator_.onNextRelease([this, listSize] {
|
||||||
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
|
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
|
||||||
@@ -453,36 +503,8 @@ void FontDownloadActivity::loop() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
if (!families_.empty()) {
|
activateSelectedItem();
|
||||||
if (isDownloadAllRow(selectedIndex_)) {
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (state_ == COMPLETE) {
|
} else if (state_ == COMPLETE) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class FontDownloadActivity : public Activity {
|
|||||||
bool isDownloadAllRow(int index) const;
|
bool isDownloadAllRow(int index) const;
|
||||||
bool isUpdateAllRow(int index) const;
|
bool isUpdateAllRow(int index) const;
|
||||||
bool isSelectedFamilyDeletable() const;
|
bool isSelectedFamilyDeletable() const;
|
||||||
|
void activateSelectedItem();
|
||||||
void promptDeleteSelectedFamily();
|
void promptDeleteSelectedFamily();
|
||||||
void onDeleteConfirmationResult(const ActivityResult& result);
|
void onDeleteConfirmationResult(const ActivityResult& result);
|
||||||
int familyIndexFromList(int listIndex) const { return listIndex - specialRowCount(); }
|
int familyIndexFromList(int listIndex) const { return listIndex - specialRowCount(); }
|
||||||
|
|||||||
@@ -80,33 +80,58 @@ void FontSelectionActivity::loop() {
|
|||||||
return;
|
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 listSize = static_cast<int>(fonts_.size());
|
||||||
const int pageItems =
|
const int pageItems =
|
||||||
UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, previewHeight + metrics_.verticalSpacing);
|
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] {
|
buttonNavigator_.onNextRelease([this, listSize] {
|
||||||
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
|
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|||||||
@@ -101,8 +101,10 @@ void KOReaderAuthActivity::render(RenderLock&&) {
|
|||||||
|
|
||||||
void KOReaderAuthActivity::loop() {
|
void KOReaderAuthActivity::loop() {
|
||||||
if (state == SUCCESS || state == FAILED) {
|
if (state == SUCCESS || state == FAILED) {
|
||||||
|
int tapX = 0;
|
||||||
|
int tapY = 0;
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Confirm) || mappedInput.wasScreenTapped(tapX, tapY)) {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
|
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
@@ -33,12 +34,24 @@ void KOReaderSettingsActivity::loop() {
|
|||||||
return;
|
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)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
handleSelection();
|
handleSelection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
buttonNavigator.onNext([this] {
|
buttonNavigator.onNext([this] {
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -142,7 +155,6 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
|
|||||||
},
|
},
|
||||||
true);
|
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));
|
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);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,31 @@ void LanguageSelectActivity::loop() {
|
|||||||
return;
|
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)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
handleSelection();
|
handleSelection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false);
|
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
buttonNavigator.onNextRelease([this] {
|
buttonNavigator.onNextRelease([this] {
|
||||||
selectedIndex = ButtonNavigator::nextIndex(static_cast<int>(selectedIndex), totalItems);
|
selectedIndex = ButtonNavigator::nextIndex(static_cast<int>(selectedIndex), totalItems);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|||||||
@@ -41,6 +41,26 @@ void OpdsServerListActivity::loop() {
|
|||||||
return;
|
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)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
handleSelection();
|
handleSelection();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "OpdsSettingsActivity.h"
|
#include "OpdsSettingsActivity.h"
|
||||||
|
|
||||||
|
#include <FreeInkUI.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -53,12 +54,26 @@ void OpdsSettingsActivity::loop() {
|
|||||||
return;
|
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)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
handleSelection();
|
handleSelection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int menuItems = getMenuItemCount();
|
|
||||||
buttonNavigator.onNext([this, menuItems] {
|
buttonNavigator.onNext([this, menuItems] {
|
||||||
selectedIndex = (selectedIndex + 1) % menuItems;
|
selectedIndex = (selectedIndex + 1) % menuItems;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user