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