Compare commits

..
100 changed files with 1792 additions and 2297 deletions
+2 -6
View File
@@ -2,6 +2,8 @@
.idea .idea
.DS_Store .DS_Store
.vscode .vscode
open-x4-sdk
fs_
lib/EpdFont/fontsrc lib/EpdFont/fontsrc
lib/I18n/I18nKeys.h lib/I18n/I18nKeys.h
lib/I18n/I18nStrings.h lib/I18n/I18nStrings.h
@@ -23,9 +25,3 @@ lib/EpdFont/scripts/output/
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out. # (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
.claude/* .claude/*
!.claude/skills/ !.claude/skills/
/managed_components
/.dummy
dependencies.lock
sdkconfig.default
sdkconfig.defaults
CMakeLists.txt
-5
View File
@@ -369,11 +369,6 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
} }
stats.pageBufferBytes += totalBytes; stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry); stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
// Landmark for the heap block map: page slots are the largest flash-font
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
(unsigned)glyphCount);
slot.fontData = fontData; slot.fontData = fontData;
slot.glyphCount = glyphCount; slot.glyphCount = glyphCount;
+17 -71
View File
@@ -68,22 +68,6 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c
const char* asCStr(const std::string& s) { return s.c_str(); } const char* asCStr(const std::string& s) { return s.c_str(); }
const char* asCStr(const char* s) { return s; } const char* asCStr(const char* s) { return s; }
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
// current capacity. Freeing + reallocating slightly different sizes every page
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
// next page's need), eroding the largest contiguous block all session. With
// reuse, capacities converge on the book's max page after a few turns and page
// turns stop touching the allocator. Only three small instantiations exist
// (interval/glyph/byte arrays), so template bloat is negligible.
template <typename T, typename CapT>
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
if (buf && capacity >= needed) return true;
delete[] buf;
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
capacity = buf ? static_cast<CapT>(needed) : 0;
return buf != nullptr;
}
} // namespace } // namespace
SdCardFont::~SdCardFont() { freeAll(); } SdCardFont::~SdCardFont() { freeAll(); }
@@ -99,9 +83,6 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
s.miniBitmap = nullptr; s.miniBitmap = nullptr;
s.miniIntervalCount = 0; s.miniIntervalCount = 0;
s.miniGlyphCount = 0; s.miniGlyphCount = 0;
s.miniIntervalCapacity = 0;
s.miniGlyphCapacity = 0;
s.miniBitmapCapacity = 0;
freeStyleMiniKern(s); freeStyleMiniKern(s);
memset(&s.miniData, 0, sizeof(s.miniData)); memset(&s.miniData, 0, sizeof(s.miniData));
s.epdFont.data = &s.stubData; s.epdFont.data = &s.stubData;
@@ -128,9 +109,6 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) {
s.miniKernRightEntryCount = 0; s.miniKernRightEntryCount = 0;
s.miniKernLeftClassCount = 0; s.miniKernLeftClassCount = 0;
s.miniKernRightClassCount = 0; s.miniKernRightClassCount = 0;
s.miniKernLeftCapacity = 0;
s.miniKernRightCapacity = 0;
s.miniKernMatrixCapacity = 0;
} }
void SdCardFont::freeStyleAll(PerStyle& s) { void SdCardFont::freeStyleAll(PerStyle& s) {
@@ -333,13 +311,13 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++; if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
} }
// Step 4: size the three mini buffers (reused across pages when they fit; the // Step 4: allocate the three mini buffers. The matrix is <1KB in practice
// per-page sizes vary by a few entries, which as free+realloc churn was punching // (<30 × <30 × 1 byte) so fragmentation is a non-issue.
// non-coalescing holes in the heap every page turn).
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight; const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) || s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount];
!ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) || s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount];
!ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) { s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes];
if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) {
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u, LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
matrixBytes); matrixBytes);
freeStyleMiniKern(s); freeStyleMiniKern(s);
@@ -815,19 +793,12 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
return missed; return missed;
} }
// Build mini intervals from sorted codepoints. Reset counts and fall back to the // Build mini intervals from sorted codepoints
// stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits freeStyleMiniData(s);
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
s.miniKernLeftEntryCount = 0;
s.miniKernRightEntryCount = 0;
s.miniKernLeftClassCount = 0;
s.miniKernRightClassCount = 0;
memset(&s.miniData, 0, sizeof(s.miniData));
s.epdFont.data = &s.stubData;
if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) { uint32_t intervalCapacity = validCount;
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
if (!s.miniIntervals) {
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx); LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
delete[] mappings; delete[] mappings;
return static_cast<int>(cpCount); return static_cast<int>(cpCount);
@@ -845,14 +816,15 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
} }
} }
// Mini glyph array (reused across pages when it fits) // Allocate mini glyph array
if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) { s.miniGlyphCount = validCount;
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
if (!s.miniGlyphs) {
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx); LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
delete[] mappings; delete[] mappings;
freeStyleMiniData(s); freeStyleMiniData(s);
return static_cast<int>(cpCount); return static_cast<int>(cpCount);
} }
s.miniGlyphCount = validCount;
// Build sorted read order for sequential I/O // Build sorted read order for sequential I/O
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount]; uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
@@ -919,7 +891,8 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
totalBitmapSize += s.miniGlyphs[i].dataLength; totalBitmapSize += s.miniGlyphs[i].dataLength;
} }
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) { s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1];
if (!s.miniBitmap) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx); LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
delete[] readOrder; delete[] readOrder;
delete[] mappings; delete[] mappings;
@@ -1406,33 +1379,6 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
return &self->overflow_[slot].glyph; return &self->overflow_[slot].glyph;
} }
size_t SdCardFont::reportMemory() const {
size_t total = 0;
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
const auto& s = styles_[si];
if (!s.present) continue;
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
// kept-if-fits mini arenas: capacity (not count) is what stays resident
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
total += fixed + mini + adv;
}
size_t overflowBytes = 0;
for (uint32_t i = 0; i < overflowCount_; ++i) {
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
}
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
return total;
}
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const { bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
for (uint32_t i = 0; i < overflowCount_; i++) { for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) return true; if (&overflow_[i].glyph == glyph) return true;
+1 -19
View File
@@ -104,11 +104,6 @@ class SdCardFont {
uint32_t uniqueGlyphs = 0; uint32_t uniqueGlyphs = 0;
uint32_t bitmapBytes = 0; uint32_t bitmapBytes = 0;
}; };
// MEMFIX-PORT: SD font resident-bytes audit; portable
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
// advance tables + overflow bitmaps) and return the total in bytes. Pure
// accounting — no allocation, no state change.
size_t reportMemory() const;
void logStats(const char* label = "SDCF"); void logStats(const char* label = "SDCF");
void resetStats(); void resetStats();
const Stats& getStats() const { return stats_; } const Stats& getStats() const { return stats_; }
@@ -173,22 +168,13 @@ class SdCardFont {
// Stub EpdFontData returned when not prewarmed // Stub EpdFontData returned when not prewarmed
EpdFontData stubData{}; EpdFontData stubData{};
// Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages // Mini EpdFontData built during prewarm
// (capacities below track allocated sizes): freeing and reallocating slightly
// different sizes on every page turn was a primary heap fragmenter — each page's
// freed hole rarely fit the next page's need, so maxAlloc eroded all session.
// After a few pages the capacities converge on the book's max and page turns
// stop allocating entirely. freeStyleMiniData() still releases everything (and
// zeroes capacities) for style eviction / font unload.
EpdFontData miniData{}; EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr; EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr; EpdGlyph* miniGlyphs = nullptr;
uint8_t* miniBitmap = nullptr; uint8_t* miniBitmap = nullptr;
uint32_t miniIntervalCount = 0; uint32_t miniIntervalCount = 0;
uint32_t miniGlyphCount = 0; uint32_t miniGlyphCount = 0;
uint32_t miniIntervalCapacity = 0;
uint32_t miniGlyphCapacity = 0;
uint32_t miniBitmapCapacity = 0;
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full // Per-page mini kern matrix (built by buildMiniKernMatrix on each full
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints // prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
@@ -203,10 +189,6 @@ class SdCardFont {
uint8_t miniKernLeftClassCount = 0; uint8_t miniKernLeftClassCount = 0;
uint8_t miniKernRightClassCount = 0; uint8_t miniKernRightClassCount = 0;
int8_t* miniKernMatrix = nullptr; int8_t* miniKernMatrix = nullptr;
// Kept-if-fits capacities, same rationale as the mini glyph buffers above.
uint16_t miniKernLeftCapacity = 0;
uint16_t miniKernRightCapacity = 0;
uint32_t miniKernMatrixCapacity = 0;
// The EpdFont whose data pointer we manage // The EpdFont whose data pointer we manage
EpdFont epdFont{&stubData}; EpdFont epdFont{&stubData};
-8
View File
@@ -88,14 +88,6 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
loadedPointSize_ = 0; loadedPointSize_ = 0;
} }
size_t SdCardFontManager::reportMemory() const {
size_t total = 0;
for (const auto& lf : loaded_) {
if (lf.font) total += lf.font->reportMemory();
}
return total;
}
int SdCardFontManager::getFontId(const std::string& familyName) const { int SdCardFontManager::getFontId(const std::string& familyName) const {
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0; if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
return loaded_.front().fontId; return loaded_.front().fontId;
-4
View File
@@ -32,10 +32,6 @@ class SdCardFontManager {
// Get name of currently loaded family (empty if none). // Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; }; const std::string& currentFamilyName() const { return loadedFamilyName_; };
// MEMFIX-PORT: font manager audit passthrough; portable
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
size_t reportMemory() const;
// Point size that was actually loaded. // Point size that was actually loaded.
// 0 if nothing loaded. // 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; }; uint8_t currentPointSize() const { return loadedPointSize_; };
-12
View File
@@ -44,18 +44,6 @@ class Epub {
} }
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
// Approximate resident heap of the open book (audit): path strings, the CSS
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
// (counts + HalFile handles) and contributes little.
size_t residentBytes() const {
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
contentBasePath.capacity() + cachePath.capacity();
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
if (cssParser) total += cssParser->residentBytes();
return total;
}
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
bool load(bool buildIfMissing = true, bool skipLoadingCss = false); bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
-4
View File
@@ -173,10 +173,6 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
uint16_t count; uint16_t count;
serialization::readPod(file, count); serialization::readPod(file, count);
// Reserve up front: growth-by-doubling needs old + new capacity live at once and
// reallocates repeatedly — a field crash (bad_alloc -> abort under -fno-exceptions)
// hit exactly this append path on a heavily fragmented heap.
page->elements.reserve(count);
for (uint16_t i = 0; i < count; i++) { for (uint16_t i = 0; i < count; i++) {
uint8_t tag; uint8_t tag;
-15
View File
@@ -72,7 +72,6 @@ class Section {
// Builds write here and are swapped over filePath only on commit, so a prior // Builds write here and are swapped over filePath only on commit, so a prior
// partial/finalized file stays readable while a rebuild is in progress. // partial/finalized file stays readable while a rebuild is in progress.
std::string binTmpPath() const { return filePath + ".part"; } std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const; std::unique_ptr<Page> loadPageAt(int page) const;
// Read a page already laid out by the in-progress build (page < build LUT size), from // Read a page already laid out by the in-progress build (page < build LUT size), from
// the partially-written tmp .bin without disturbing the build's write cursor. // the partially-written tmp .bin without disturbing the build's write cursor.
@@ -132,20 +131,6 @@ class Section {
// (covers finalized sections and partials from a previous session). // (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const; std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// MEMFIX-PORT: section resident-bytes audit accessor; portable
// Approximate resident heap for the audit log. Steady state (no build) a
// Section holds little beyond itself; during a build the page LUT and path
// strings dominate (the parser's internal footprint is not walked here).
size_t residentBytes() const {
size_t total = sizeof(Section) + filePath.capacity();
if (build_) {
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
}
return total;
}
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a // True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild. // giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
bool hasHtmlCache() const; bool hasHtmlCache() const;
+5
View File
@@ -431,6 +431,11 @@ CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
// Rule processing // Rule processing
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) { void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
// Skip rules that don't define any supported properties to save RAM.
if (!style.defined.anySet()) {
return;
}
// Check if we've reached the rule limit before processing // Check if we've reached the rule limit before processing
if (rulesBySelector_.size() >= MAX_RULES) { if (rulesBySelector_.size() >= MAX_RULES) {
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES); LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
+1 -15
View File
@@ -33,7 +33,7 @@
class CssParser { class CssParser {
public: public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes // Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 7; static constexpr uint8_t CSS_CACHE_VERSION = 8;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default; ~CssParser() = default;
@@ -67,20 +67,6 @@ class CssParser {
*/ */
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue); [[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
// Approximate resident heap of the parsed stylesheet, for the audit log.
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
size_t residentBytes() const {
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
for (const auto& kv : rulesBySelector_) {
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
total += sizeof(kv);
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
}
return total;
}
/** /**
* Check if any rules have been loaded * Check if any rules have been loaded
*/ */
+19 -13
View File
@@ -21,6 +21,7 @@
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
constexpr size_t PARSE_BUFFER_SIZE = 1024; constexpr size_t PARSE_BUFFER_SIZE = 1024;
constexpr size_t MAX_BUFFERED_TEXT_WORDS = 300;
// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation // Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation
// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per // anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per
@@ -201,6 +202,8 @@ void ChapterHtmlSlimParser::flushPendingAnchor() {
// flush the contents of partWordBuffer to currentTextBlock // flush the contents of partWordBuffer to currentTextBlock
void ChapterHtmlSlimParser::flushPartWordBuffer() { void ChapterHtmlSlimParser::flushPartWordBuffer() {
flushLongTextBlockIfNeeded();
// Determine font style from depth-based tracking and CSS effective style // Determine font style from depth-based tracking and CSS effective style
const bool isBold = boldUntilDepth < depth || effectiveBold; const bool isBold = boldUntilDepth < depth || effectiveBold;
const bool isItalic = italicUntilDepth < depth || effectiveItalic; const bool isItalic = italicUntilDepth < depth || effectiveItalic;
@@ -228,6 +231,20 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
listItemBulletOnly = false; listItemBulletOnly = false;
} }
void ChapterHtmlSlimParser::flushLongTextBlockIfNeeded() {
if (!currentTextBlock || currentTextBlock->size() <= MAX_BUFFERED_TEXT_WORDS) {
return;
}
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth =
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
currentTextBlock->layoutAndExtractLines(
renderer, fontId, effectiveWidth,
[this](const std::shared_ptr<TextBlock>& textBlock) { this->addLineToPage(textBlock); }, false);
}
// start a new text block if needed // start a new text block if needed
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
nextWordContinues = false; // New block = new paragraph, no continuation nextWordContinues = false; // New block = new paragraph, no continuation
@@ -1155,20 +1172,9 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
self->partWordBuffer[self->partWordBufferIndex++] = s[i]; self->partWordBuffer[self->partWordBufferIndex++] = s[i];
} }
// If we have > 750 words buffered up, perform the layout and consume out all but the last line // If we have a large number of words buffered up, perform the layout and consume out all but the last line.
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
// memory.
// Spotted when reading Intermezzo, there are some really long text blocks in there. // Spotted when reading Intermezzo, there are some really long text blocks in there.
if (self->currentTextBlock->size() > 750) { self->flushLongTextBlockIfNeeded();
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
: self->viewportWidth;
self->currentTextBlock->layoutAndExtractLines(
self->renderer, self->fontId, effectiveWidth,
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
}
} }
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) { void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
@@ -4,6 +4,7 @@
#include <expat.h> #include <expat.h>
#include <climits> #include <climits>
#include <deque>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -84,7 +85,7 @@ class ChapterHtmlSlimParser {
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on // Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0; int completedPageCount = 0;
std::vector<std::pair<std::string, uint16_t>> anchorData; std::deque<std::pair<std::string, uint16_t>> anchorData;
std::string pendingAnchorId; // deferred until after previous text block is flushed std::string pendingAnchorId; // deferred until after previous text block is flushed
std::vector<std::string> tocAnchors; // the list of anchors that are TOC chapter boundaries std::vector<std::string> tocAnchors; // the list of anchors that are TOC chapter boundaries
uint16_t xpathParagraphIndex = 0; uint16_t xpathParagraphIndex = 0;
@@ -112,6 +113,7 @@ class ChapterHtmlSlimParser {
void startNewTextBlock(const BlockStyle& blockStyle); void startNewTextBlock(const BlockStyle& blockStyle);
void flushPendingAnchor(); void flushPendingAnchor();
void flushPartWordBuffer(); void flushPartWordBuffer();
void flushLongTextBlockIfNeeded();
void makePages(); void makePages();
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration); static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css); static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
@@ -173,7 +175,7 @@ class ChapterHtmlSlimParser {
void abortParse(); // tear down without flushing (error / abandon) void abortParse(); // tear down without flushing (error / abandon)
void addLineToPage(std::shared_ptr<TextBlock> line); void addLineToPage(std::shared_ptr<TextBlock> line);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; } const std::deque<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
// Byte progress of the in-flight parse, used to estimate a still-building section's total page // Byte progress of the in-flight parse, used to estimate a still-building section's total page
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid // count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
+8 -14
View File
@@ -62,14 +62,7 @@ void FontCacheManager::resetStats() {
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; } bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) { void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
if (!text) return; scanText_ += text;
const size_t remaining = (scanTextLen_ < SCAN_TEXT_CAPACITY - 1) ? (SCAN_TEXT_CAPACITY - 1 - scanTextLen_) : 0;
if (remaining > 0) {
const size_t textLen = strnlen(text, remaining);
memcpy(scanText_ + scanTextLen_, text, textLen);
scanTextLen_ += textLen;
scanText_[scanTextLen_] = '\0';
}
if (scanFontId_ < 0) scanFontId_ = fontId; if (scanFontId_ < 0) scanFontId_ = fontId;
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03; const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
const unsigned char* p = reinterpret_cast<const unsigned char*>(text); const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
@@ -87,15 +80,15 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
manager_->scanMode_ = ScanMode::Scanning; manager_->scanMode_ = ScanMode::Scanning;
manager_->clearCache(); manager_->clearCache();
manager_->resetStats(); manager_->resetStats();
manager_->scanTextLen_ = 0; manager_->scanText_.clear();
manager_->scanText_[0] = '\0'; manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_)); memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
manager_->scanFontId_ = -1; manager_->scanFontId_ = -1;
} }
void FontCacheManager::PrewarmScope::endScanAndPrewarm() { void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
manager_->scanMode_ = ScanMode::None; manager_->scanMode_ = ScanMode::None;
if (manager_->scanTextLen_ == 0) return; if (manager_->scanText_.empty()) return;
// Build style bitmask from all styles that appeared during the scan // Build style bitmask from all styles that appeared during the scan
uint8_t styleMask = 0; uint8_t styleMask = 0;
@@ -104,10 +97,11 @@ void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
} }
if (styleMask == 0) styleMask = 1; // default to regular if (styleMask == 0) styleMask = 1; // default to regular
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_, styleMask); manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
manager_->scanTextLen_ = 0; // Free scan string memory
manager_->scanText_[0] = '\0'; manager_->scanText_.clear();
manager_->scanText_.shrink_to_fit();
} }
FontCacheManager::PrewarmScope::~PrewarmScope() { FontCacheManager::PrewarmScope::~PrewarmScope() {
+2 -4
View File
@@ -2,9 +2,9 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <string>
class FontDecompressor; class FontDecompressor;
class SdCardFont; class SdCardFont;
@@ -51,9 +51,7 @@ class FontCacheManager {
enum class ScanMode : uint8_t { None, Scanning }; enum class ScanMode : uint8_t { None, Scanning };
ScanMode scanMode_ = ScanMode::None; ScanMode scanMode_ = ScanMode::None;
static constexpr size_t SCAN_TEXT_CAPACITY = 2048; std::string scanText_;
char scanText_[SCAN_TEXT_CAPACITY] = {};
size_t scanTextLen_ = 0;
uint32_t scanStyleCounts_[4] = {}; uint32_t scanStyleCounts_[4] = {};
int scanFontId_ = -1; int scanFontId_ = -1;
}; };
-14
View File
@@ -1451,20 +1451,6 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
display.displayBuffer(refreshMode, fadingFix); display.displayBuffer(refreshMode, fadingFix);
} }
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
// The async path has no turn-off-screen hook, which the sunlight fading fix
// relies on; keep those users on the blocking path.
if (fadingFix) {
display.displayBuffer(refreshMode, fadingFix);
return;
}
display.displayBufferAsync(refreshMode);
}
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const { const EpdFontFamily::Style style) const {
if (!text || maxWidth <= 0) return ""; if (!text || maxWidth <= 0) return "";
-11
View File
@@ -135,17 +135,6 @@ class GfxRenderer {
int getScreenWidth() const; int getScreenWidth() const;
int getScreenHeight() const; int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
// grayscale strip rendering) can overlap the panel's refresh time. The
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
// support. See HalDisplay::displayBufferAsync for the baseline contract.
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
void waitRefreshComplete() const;
// True when displayBufferAsync() genuinely overlaps: panel defers and
// fadingFix isn't forcing the blocking path. Callers can skip overlap
// scaffolding (e.g. whole-plane grayscale buffers) when false.
bool supportsAsyncRefresh() const;
// EXPERIMENTAL: Windowed update - display only a rectangular region // EXPERIMENTAL: Windowed update - display only a rectangular region
// void displayWindow(int x, int y, int width, int height) const; // void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;
+394
View File
@@ -0,0 +1,394 @@
_language_name: "العربية"
_language_code: "AR"
_order: "28"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "جاري بدء التشغيل"
STR_SLEEPING: "وضع السكون"
STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون"
STR_BROWSE_FILES: "تصفح الملفات"
STR_FILE_TRANSFER: "نقل الملفات"
STR_SETTINGS_TITLE: "الإعدادات"
STR_CONTINUE_READING: "متابعة القراءة"
STR_NO_OPEN_BOOK: "لا يوجد كتاب مفتوح"
STR_START_READING: "ابدأ القراءة أدناه"
STR_NO_FILES_FOUND: "لم يتم العثور على ملفات"
STR_SELECT_CHAPTER: "اختر الفصل"
STR_NO_CHAPTERS: "لا توجد فصول"
STR_END_OF_BOOK: "نهاية الكتاب"
STR_EMPTY_CHAPTER: "فصل فارغ"
STR_INDEXING: "جاري الفهرسة"
STR_INDEX_FAILED: "فشلت الفهرسة - كتاب غير صالح"
STR_MEMORY_ERROR: "خطأ في الذاكرة"
STR_PAGE_LOAD_ERROR: "خطأ في تحميل الصفحة"
STR_EMPTY_FILE: "ملف فارغ"
STR_OUT_OF_BOUNDS: "خارج النطاق"
STR_LOADING: "جاري التحميل..."
STR_LOADING_POPUP: "جاري التحميل"
STR_WIFI_NETWORKS: "شبكات Wi-Fi"
STR_NO_NETWORKS: "لم يتم العثور على شبكات"
STR_NETWORKS_FOUND: "تم العثور على %zu شبكة"
STR_SCANNING: "جاري البحث..."
STR_FINDING_SAVED_WIFI: "جاري البحث عن شبكة Wi-Fi محفوظة..."
STR_CONNECTING: "جاري الاتصال..."
STR_CONNECTING_SAVED_WIFI: "جاري الاتصال بشبكة Wi-Fi محفوظة..."
STR_SHOW_NETWORKS: "عرض"
STR_CONNECTED: "تم الاتصال!"
STR_CONNECTION_FAILED: "فشل الاتصال"
STR_FORGET_NETWORK: "نسيان هذه الشبكة؟"
STR_SAVE_PASSWORD: "حفظ كلمة المرور للمرة القادمة؟"
STR_PRESS_OK_SCAN: "اضغط موافق لإعادة البحث"
STR_JOIN_NETWORK: "الانضمام إلى شبكة"
STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال"
STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة"
STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها"
STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..."
STR_HOTSPOT_MODE: "وضع نقطة الاتصال"
STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه"
STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح"
STR_OR_HTTP_PREFIX: "أو http://"
STR_SCAN_QR_HINT: "أو امسح رمز QR بهاتفك:"
STR_CALIBRE_WIRELESS: "اتصال Calibre اللاسلكي"
STR_NETWORK_LEGEND: "* = مشفرة | + = محفوظة"
STR_MAC_ADDRESS: "عنوان MAC:"
STR_CHECKING_WIFI: "جاري فحص Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "أدخل كلمة مرور الشبكة"
STR_ADD_HIDDEN_NETWORK: "إضافة شبكة مخفية..."
STR_ENTER_WIFI_SSID: "أدخل اسم الشبكة (SSID)"
STR_TO_PREFIX: "إلى "
STR_CALIBRE_RECEIVING: "جاري الاستلام: "
STR_CALIBRE_RECEIVED: "تم الاستلام: "
STR_CALIBRE_INSTRUCTION_1: "1) قم بتثبيت إضافة CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) اتصل بنفس شبكة Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) في Calibre: اختر \"إرسال إلى الجهاز\""
STR_CALIBRE_INSTRUCTION_4: "اترك هذه الشاشة مفتوحة أثناء الإرسال"
STR_CAT_DISPLAY: "الشاشة"
STR_CAT_READER: "القراءة"
STR_CAT_CONTROLS: "الأزرار"
STR_CAT_SYSTEM: "النظام"
STR_SLEEP_SCREEN: "شاشة السكون"
STR_QUICK_RESUME_TIMEOUT: "استئناف سريع بعد المهلة"
STR_SLEEP_COVER_MODE: "عرض الغلاف في وضع السكون"
STR_HIDE_BATTERY: "إخفاء نسبة البطارية"
STR_EXTRA_SPACING: "تباعد إضافي بين الفقرات"
STR_TEXT_AA: "تنعيم حواف النص"
STR_IMAGES: "الصور"
STR_IMAGES_DISPLAY: "عرض"
STR_IMAGES_PLACEHOLDER: "عنصر نائب"
STR_IMAGES_SUPPRESS: "إخفاء"
STR_EOB_HOME: "الرئيسية"
STR_EOB_CONTINUE_WITH: "المتابعة إلى"
STR_SHORT_PWR_BTN: "ضغطة قصيرة على زر التشغيل"
STR_ORIENTATION: "اتجاه القراءة"
STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "تدوير الأزرار الأمامية مع الشاشة"
STR_LONG_PRESS_BEHAVIOR: "سلوك الضغطة الطويلة"
STR_LONG_PRESS_BEHAVIOR_OFF: "إيقاف"
STR_LONG_PRESS_BEHAVIOR_SKIP: "تخطي الفصل"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "تغيير الاتجاه"
STR_LONG_PRESS_MENU: "قائمة الضغطة الطويلة"
STR_FONT_PREVIEW_TEXT: "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"
STR_FONT_FAMILY: "خط القراءة"
STR_FONT_SIZE: "حجم الخط"
STR_LINE_SPACING: "تباعد الأسطر"
STR_SCREEN_MARGIN: "هوامش شاشة القراءة"
STR_PARA_ALIGNMENT: "محاذاة الفقرات"
STR_HYPHENATION: "تقسيم الكلمات"
STR_TIME_TO_SLEEP: "مهلة الدخول في السكون"
STR_SHOW_HIDDEN_FILES: "عرض الملفات المخفية"
STR_REMOVE_READ_FROM_RECENTS: "إزالة الكتب المقروءة من قائمة الكتب الأخيرة"
STR_MOVE_FINISHED_TO_READ: "نقل الكتب المنتهية إلى مجلد المقروءة"
STR_REFRESH_FREQ: "معدل تحديث الشاشة"
STR_KOREADER_SYNC: "مزامنة KOReader"
STR_CHECK_UPDATES: "التحقق من التحديثات"
STR_LANGUAGE: "اللغة"
STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة"
STR_USERNAME: "اسم المستخدم"
STR_PASSWORD: "كلمة المرور"
STR_SYNC_SERVER_URL: "عنوان خادم المزامنة"
STR_DOCUMENT_MATCHING: "مطابقة المستندات"
STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند"
STR_AUTHENTICATE: "تسجيل الدخول"
STR_KOREADER_USERNAME: "اسم مستخدم KOReader"
STR_KOREADER_PASSWORD: "كلمة مرور KOReader"
STR_FILENAME: "اسم الملف"
STR_BINARY: "ثنائي"
STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا"
STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة"
STR_AUTHENTICATING: "جاري التحقق..."
STR_AUTH_SUCCESS: "تم تسجيل الدخول بنجاح!"
STR_KOREADER_AUTH: "تسجيل الدخول إلى KOReader"
STR_SYNC_READY: "مزامنة KOReader جاهزة للاستخدام"
STR_AUTH_FAILED: "فشل تسجيل الدخول"
STR_DONE: "تم"
STR_CLEAR_CACHE_WARNING_1: "سيؤدي هذا إلى مسح جميع بيانات الكتب المخزنة."
STR_CLEAR_CACHE_WARNING_2: "سيتم فقدان كل تقدم القراءة!"
STR_CLEAR_CACHE_WARNING_3: "ستحتاج الكتب إلى إعادة فهرسة"
STR_CLEAR_CACHE_WARNING_4: "عند فتحها مرة أخرى."
STR_CLEARING_CACHE: "جاري مسح الذاكرة المؤقتة..."
STR_CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
STR_ITEMS_REMOVED: "عناصر تمت إزالتها"
STR_FAILED_LOWER: "فشل"
STR_CLEAR_CACHE_FAILED: "فشل مسح الذاكرة المؤقتة"
STR_CHECK_SERIAL_OUTPUT: "راجع مخرجات المنفذ التسلسلي للتفاصيل"
STR_DARK: "داكن"
STR_LIGHT: "فاتح"
STR_CUSTOM: "مخصص"
STR_COVER: "الغلاف"
STR_NONE_OPT: "بدون"
STR_FIT: "ملاءمة"
STR_CROP: "قص"
STR_NEVER: "أبدا"
STR_IN_READER: "أثناء القراءة"
STR_ALWAYS: "دائما"
STR_IGNORE: "تجاهل"
STR_SLEEP: "سكون"
STR_PAGE_TURN: "تقليب الصفحة"
STR_FORCE_REFRESH: "تحديث الشاشة"
STR_PORTRAIT: "عمودي"
STR_LANDSCAPE_CW: "أفقي (يمين)"
STR_INVERTED: "عكس الألوان"
STR_ORIENTATION_INVERTED: "عمودي 180°"
STR_LANDSCAPE_CCW: "أفقي (يسار)"
STR_PREV_NEXT: "السابق/التالي"
STR_NEXT_PREV: "التالي/السابق"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "إشارة مرجعية"
STR_DISABLED: "معطل"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "صغير"
STR_MEDIUM: "متوسط"
STR_LARGE: "كبير"
STR_X_LARGE: "كبير جدا"
STR_TIGHT: "ضيق"
STR_NORMAL: "عادي"
STR_WIDE: "واسع"
STR_JUSTIFY: "ضبط الطرفين"
STR_ALIGN_LEFT: "يسار"
STR_CENTER: "وسط"
STR_ALIGN_RIGHT: "يمين"
STR_PAGES_1: "صفحة واحدة"
STR_PAGES_5: "5 صفحات"
STR_PAGES_10: "10 صفحات"
STR_PAGES_15: "15 صفحة"
STR_PAGES_30: "30 صفحة"
STR_UPDATE: "تحديث"
STR_CHECKING_UPDATE: "جاري التحقق من التحديثات..."
STR_NEW_UPDATE: "يتوفر تحديث جديد!"
STR_CURRENT_VERSION: "الإصدار الحالي: "
STR_NEW_VERSION: "الإصدار الجديد: "
STR_UPDATING: "جاري التحديث..."
STR_NO_UPDATE: "لا يتوفر تحديث"
STR_UPDATE_FAILED: "فشل التحديث"
STR_UPDATE_COMPLETE: "اكتمل التحديث"
STR_POWER_ON_HINT: "اضغط مطولا على زر التشغيل لتشغيل الجهاز من جديد"
STR_RESTARTING_HINT: "جاري إعادة التشغيل... إذا لم يعمل الجهاز، اضغط مطولا على زر التشغيل لعدة ثوان."
STR_NO_ENTRIES: "لم يتم العثور على عناصر"
STR_DOWNLOADING: "جاري التنزيل..."
STR_DOWNLOAD_FAILED: "فشل التنزيل"
STR_ERROR_MSG: "خطأ:"
STR_UNNAMED: "بدون اسم"
STR_HOLD_OPEN_TO_DELETE: "اضغط مطولا على فتح للحذف"
STR_NO_SERVER_URL: "لم يتم تحديد عنوان الخادم"
STR_FETCH_FEED_FAILED: "فشل جلب القائمة"
STR_PARSE_FEED_FAILED: "فشل تحليل القائمة"
STR_NEXT_PAGE: "الصفحة التالية"
STR_PREV_PAGE: "الصفحة السابقة"
STR_NETWORK_PREFIX: "الشبكة: "
STR_IP_ADDRESS_PREFIX: "عنوان IP: "
STR_ERROR_GENERAL_FAILURE: "خطأ: فشل عام"
STR_ERROR_NETWORK_NOT_FOUND: "خطأ: الشبكة غير موجودة"
STR_ERROR_CONNECTION_TIMEOUT: "خطأ: انتهت مهلة الاتصال"
STR_SD_CARD: "بطاقة SD"
STR_BACK: "رجوع »"
STR_EXIT: "خروج »"
STR_HOME: "الرئيسية »"
STR_SELECT: "اختيار"
STR_SELECTED: "محدد"
STR_TOGGLE: "تبديل"
STR_TOGGLE_BOOKMARK: "إضافة/إزالة إشارة مرجعية"
STR_CONFIRM: "تأكيد"
STR_CANCEL: "إلغاء"
STR_CONNECT: "اتصال"
STR_OPEN: "فتح"
STR_DOWNLOAD: "تنزيل"
STR_RETRY: "إعادة المحاولة"
STR_YES: "نعم"
STR_NO: "لا"
STR_SHOW: "عرض"
STR_HIDE: "إخفاء"
STR_STATE_ON: "تشغيل"
STR_STATE_OFF: "إيقاف"
STR_NOT_SET: "غير محدد"
STR_DIR_LEFT: "يسار"
STR_DIR_RIGHT: "يمين"
STR_DIR_UP: "أعلى"
STR_DIR_DOWN: "أسفل"
STR_OK_BUTTON: "موافق"
STR_SLEEP_COVER_FILTER: "مرشح غلاف شاشة السكون"
STR_FILTER_CONTRAST: "التباين"
STR_CUSTOMISE_STATUS_BAR: "تخصيص شريط الحالة"
STR_CHAPTER_PAGE_COUNT: "عدد صفحات الفصل"
STR_BOOK_PROGRESS_PERCENTAGE: "نسبة التقدم في الكتاب"
STR_PROGRESS_BAR: "شريط التقدم"
STR_PROGRESS_BAR_THICKNESS: "سمك شريط التقدم"
STR_PROGRESS_BAR_THIN: "رفيع"
STR_PROGRESS_BAR_MEDIUM: "متوسط"
STR_PROGRESS_BAR_THICK: "سميك"
STR_BOOK: "الكتاب"
STR_CHAPTER: "الفصل"
STR_EXAMPLE_CHAPTER: "الفصل 21"
STR_EXAMPLE_BOOK: "عنوان الكتاب"
STR_PREVIEW: "معاينة"
STR_TITLE: "العنوان"
STR_BATTERY: "البطارية"
STR_XTC_STATUS_BAR: "شريط حالة XTC"
STR_BOTTOM: "أسفل"
STR_TOP: "أعلى"
STR_CLOCK: "الساعة"
STR_CLOCK_UTC_OFFSET: "فرق التوقيت عن UTC"
STR_CLOCK_FORMAT: "تنسيق الساعة"
STR_CLOCK_FORMAT_24H: "24 ساعة"
STR_CLOCK_FORMAT_12H: "12 ساعة"
STR_CURRENT_TIME: "الوقت الحالي:"
STR_NEXT_FIELD: "التالي"
STR_CLOCK_SYNC: "مزامنة الساعة"
STR_CLOCK_SYNC_NOW: "مزامنة الساعة الآن"
STR_CLOCK_SYNCING: "جاري المزامنة عبر NTP..."
STR_CLOCK_SYNC_OK: "تمت مزامنة الساعة"
STR_CLOCK_SYNC_FAIL: "فشلت المزامنة"
STR_CLOCK_SYNC_NO_WIFI: "لا يوجد اتصال Wi-Fi"
STR_CLOCK_SYNC_NO_WIFI_HINT: "اتصل بشبكة Wi-Fi أولا، ثم حاول مرة أخرى."
STR_CLOCK_SYNCED: "تمت مزامنة الساعة"
STR_UI_THEME: "مظهر الواجهة"
STR_THEME_CLASSIC: "كلاسيكي"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra موسع"
STR_SUNLIGHT_FADING_FIX: "إصلاح بهتان الشاشة في الشمس"
STR_REMAP_FRONT_BUTTONS: "تغيير وظائف الأزرار الأمامية"
STR_BOOKMARKS: "الإشارات المرجعية"
STR_BOOKMARK_ADDED: "تمت إضافة الإشارة المرجعية."
STR_BOOKMARK_REMOVED: "تمت إزالة الإشارة المرجعية."
STR_OPDS_BROWSER: "متصفح OPDS"
STR_SEARCH: "بحث"
STR_COVER_CUSTOM: "الغلاف + مخصص"
STR_QUICK_RESUME: "استئناف سريع"
STR_MENU_RECENT_BOOKS: "الكتب الأخيرة"
STR_REMOVE_FROM_RECENTS: "إزالة من الكتب الأخيرة؟"
STR_NO_RECENT_BOOKS: "لا توجد كتب أخيرة"
STR_CALIBRE_DESC: "استخدم النقل اللاسلكي من Calibre"
STR_FORGET_AND_REMOVE: "نسيان الشبكة وحذف كلمة المرور المحفوظة؟"
STR_FORGET_BUTTON: "نسيان"
STR_CALIBRE_STARTING: "جاري تشغيل Calibre..."
STR_CALIBRE_SETUP: "الإعداد"
STR_CALIBRE_STATUS: "الحالة"
STR_CLEAR_BUTTON: "مسح"
STR_DEFAULT_VALUE: "افتراضي"
STR_REMAP_PROMPT: "اضغط زرا أماميا لكل وظيفة"
STR_UNASSIGNED: "غير معين"
STR_ALREADY_ASSIGNED: "معين مسبقا"
STR_REMAP_RESET_HINT: "الزر الجانبي العلوي: استعادة التخطيط الافتراضي"
STR_REMAP_CANCEL_HINT: "الزر الجانبي السفلي: إلغاء التغيير"
STR_HW_BACK_LABEL: "رجوع (الزر 1)"
STR_HW_CONFIRM_LABEL: "تأكيد (الزر 2)"
STR_HW_LEFT_LABEL: "يسار (الزر 3)"
STR_HW_RIGHT_LABEL: "يمين (الزر 4)"
STR_GO_TO_PERCENT: "الانتقال إلى نسبة %"
STR_GO_HOME_BUTTON: "العودة إلى الرئيسية"
STR_SYNC_PROGRESS: "مزامنة التقدم"
STR_DELETE_CACHE: "حذف ذاكرة الكتاب المؤقتة"
STR_DELETE: "حذف"
STR_CONFIRM_DELETE_BOOKMARK: "حذف هذه الإشارة المرجعية؟"
STR_DISPLAY_QR: "عرض الصفحة كرمز QR"
STR_CHAPTER_PREFIX: "الفصل: "
STR_PAGES_SEPARATOR: " صفحات | "
STR_BOOK_PREFIX: "الكتاب: "
STR_CALIBRE_URL_HINT: "في Calibre، أضف /opds إلى العنوان"
STR_SYNCING_TIME: "جاري مزامنة الوقت..."
STR_CALC_HASH: "جاري حساب بصمة المستند..."
STR_HASH_FAILED: "فشل حساب بصمة المستند"
STR_FETCH_PROGRESS: "جاري جلب التقدم من الخادم..."
STR_UPLOAD_PROGRESS: "جاري رفع التقدم..."
STR_NO_CREDENTIALS_MSG: "لم يتم إعداد بيانات الاعتماد"
STR_KOREADER_SETUP_HINT: "قم بإعداد حساب KOReader في الإعدادات"
STR_PROGRESS_FOUND: "تم العثور على تقدم سابق!"
STR_REMOTE_LABEL: "الخادم:"
STR_LOCAL_LABEL: "الجهاز:"
STR_PAGE_OVERALL_FORMAT: "صفحة %d، %.2f%% إجمالا"
STR_PAGE_TOTAL_OVERALL_FORMAT: "صفحة %d من %d، %.2f%% إجمالا"
STR_DEVICE_FROM_FORMAT: " من: %s"
STR_APPLY_REMOTE: "استخدام تقدم الخادم"
STR_UPLOAD_LOCAL: "رفع التقدم المحلي"
STR_NO_REMOTE_MSG: "لا يوجد تقدم على الخادم"
STR_UPLOAD_PROMPT: "رفع الموضع الحالي؟"
STR_UPLOAD_SUCCESS: "تم رفع التقدم!"
STR_SYNC_FAILED_MSG: "فشلت المزامنة"
STR_SAVE_PROGRESS_FAILED: "تعذر حفظ التقدم"
STR_SECTION_PREFIX: "القسم "
STR_UPLOAD: "رفع"
STR_BOOK_S_STYLE: "تنسيق الكتاب الأصلي"
STR_EMBEDDED_STYLE: "التنسيق المضمن"
STR_FOCUS_READING: "قراءة مركزة"
STR_OPDS_SERVER_URL: "عنوان خادم OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي"
STR_SET_SLEEP_COVER: "تعيين الغلاف"
STR_FOOTNOTES: "الحواشي"
STR_NO_FOOTNOTES: "لا توجد حواش في هذه الصفحة"
STR_LINK: "[رابط]"
STR_SCREENSHOT_BUTTON: "التقاط لقطة شاشة"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u دقيقة"
STR_SLEEP_NEVER: "أبدا"
STR_STEP_HINT_FRONT: "الأزرار الأمامية:"
STR_STEP_HINT_SIDE: "الأزرار الجانبية:"
STR_ADD_SERVER: "إضافة خادم"
STR_SERVER_NAME: "اسم الخادم"
STR_NO_SERVERS: "لم يتم إعداد خوادم OPDS"
STR_DELETE_SERVER: "حذف الخادم"
STR_OPDS_SERVERS: "خوادم OPDS"
STR_AUTO_TURN_ENABLED: "التقليب التلقائي مفعل: "
STR_AUTO_TURN_PAGES_PER_MIN: "التقليب التلقائي (صفحات في الدقيقة)"
STR_MANAGE_FONTS: "إدارة الخطوط"
STR_FONT_BROWSER: "متصفح الخطوط"
STR_LOADING_FONT_LIST: "جاري تحميل قائمة الخطوط..."
STR_NO_FONTS_AVAILABLE: "لا تتوفر خطوط"
STR_FONT_INSTALLED: "تم تثبيت الخط!"
STR_FONT_INSTALL_FAILED: "فشل تثبيت الخط"
STR_INSTALLED: "مثبت"
STR_DOWNLOAD_ALL: "تنزيل الكل"
STR_UPDATE_ALL: "تحديث الكل"
STR_UPDATE_AVAILABLE: "تحديث"
STR_CRASH_TITLE: "تعطل النظام"
STR_CRASH_DESCRIPTION: "تم حفظ تقرير مفصل في الملف crash_report.txt. يرجى إرفاق هذا الملف عند الإبلاغ عن المشكلة."
STR_CRASH_REASON: "سبب التعطل:"
STR_CRASH_NO_REASON: "(لم يتم تسجيل سبب)"
STR_TILT_PAGE_TURN: "تقليب الصفحة بالإمالة"
STR_KB_HINT_MOVE_CURSOR: "اضغط زر اليمين أو اليسار لتحريك المؤشر"
STR_KB_HINT_RETURN_CURSOR: "اضغط زر اليسار للعودة إلى موضع المؤشر"
STR_KB_HINT_HIDE_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [***] لإخفاء كلمة المرور"
STR_KB_HINT_SHOW_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [abc] لإظهار كلمة المرور"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "اضغط [***] لإخفاء كلمة المرور"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "اضغط [abc] لإظهار كلمة المرور"
STR_KB_HINT_EDIT_ENTRY: "اضغط مطولا على زر الأعلى للتعديل"
STR_KB_TIPS: "نصائح:"
STR_KB_HINT_RETURN_KEYBOARD: "اضغط زر الأسفل للعودة إلى لوحة المفاتيح"
STR_KB_HINT_EXIT_URL_MODE: "اضغط ABC للخروج من وضع URL"
STR_KB_HINT_CLEAR_TEXT: "اضغط مطولا على DEL لمسح كل النص"
STR_KB_HINT_SECONDARY_CHAR: "اضغط مطولا على زر الاختيار للحرف الثانوي"
STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي"
STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي"
STR_KB_HINT_URL_SNIPPETS: "اضغط URL للاختصارات"
STR_SD_FIRMWARE_UPDATE: "تحديث البرنامج الثابت من بطاقة SD"
STR_SELECT_FIRMWARE_FILE: "اختر ملف البرنامج الثابت (.bin)"
STR_NO_BIN_FILES: "لم يتم العثور على ملفات .bin"
STR_VALIDATING_FIRMWARE: "جاري التحقق من البرنامج الثابت..."
STR_INVALID_FIRMWARE: "ملف برنامج ثابت غير صالح"
STR_FIRMWARE_TOO_LARGE: "البرنامج الثابت أكبر من القسم المخصص له"
STR_FIRMWARE_TOO_SMALL: "ملف البرنامج الثابت صغير جدا"
STR_FIRMWARE_UPDATE_PROMPT: "تحديث البرنامج الثابت؟"
STR_FIRMWARE_FILE_OPEN_FAILED: "تعذر فتح الملف"
STR_FIRMWARE_WRITE_FAILED: "فشلت كتابة البرنامج الثابت"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "لا تطفئ الجهاز!"
STR_RECOVERY_MODE: "وضع الاستعادة"
STR_RECOVERY_MODE_HINT: "ضع الملف firmware.bin في المجلد الرئيسي لبطاقة SD ثم اختره"
+5
View File
@@ -298,6 +298,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі" STR_SLEEP_NEVER: "Ніколі"
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:" STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:" STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
STR_OPDS_DOWNLOAD_FOLDER: "Папка для спампоўкі"
STR_OPDS_FILENAME_FORMAT: "Фармат назвы файла"
STR_FMT_AUTHOR_TITLE: "Аўтар - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Аўтар"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: " STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)" STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам" STR_TILT_PAGE_TURN: "Перагортванне нахілам"
+393
View File
@@ -0,0 +1,393 @@
_language_name: "Bosanski"
_language_code: "BS"
_order: "29"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "POKRETANJE"
STR_SLEEPING: "REŽIM MIROVANJA"
STR_ENTERING_SLEEP: "Prelazak u režim mirovanja"
STR_BROWSE_FILES: "Pregled datoteka"
STR_FILE_TRANSFER: "Prijenos datoteka"
STR_SETTINGS_TITLE: "Postavke"
STR_CONTINUE_READING: "Nastavi čitanje"
STR_NO_OPEN_BOOK: "Nema otvorene knjige"
STR_START_READING: "Počni čitati ispod"
STR_NO_FILES_FOUND: "Nema pronađenih datoteka"
STR_SELECT_CHAPTER: "Odaberi poglavlje"
STR_NO_CHAPTERS: "Nema poglavlja"
STR_END_OF_BOOK: "Kraj knjige"
STR_EMPTY_CHAPTER: "Prazno poglavlje"
STR_INDEXING: "Indeksiranje"
STR_INDEX_FAILED: "Indeksiranje nije uspjelo - nevažeća knjiga"
STR_MEMORY_ERROR: "Greška memorije"
STR_PAGE_LOAD_ERROR: "Greška pri učitavanju stranice"
STR_EMPTY_FILE: "Prazna datoteka"
STR_OUT_OF_BOUNDS: "Izvan granica"
STR_LOADING: "Učitavanje..."
STR_LOADING_POPUP: "Učitavanje"
STR_WIFI_NETWORKS: "Wi-Fi mreže"
STR_NO_NETWORKS: "Nema pronađenih mreža"
STR_NETWORKS_FOUND: "%zu pronađenih mreža"
STR_SCANNING: "Skeniranje..."
STR_FINDING_SAVED_WIFI: "Traženje sačuvanog Wi-Fi-a..."
STR_CONNECTING: "Povezivanje..."
STR_CONNECTING_SAVED_WIFI: "Povezivanje na sačuvani Wi-Fi..."
STR_SHOW_NETWORKS: "Prikaži"
STR_CONNECTED: "Povezano!"
STR_CONNECTION_FAILED: "Povezivanje nije uspjelo"
STR_FORGET_NETWORK: "Zaboraviti mrežu?"
STR_SAVE_PASSWORD: "Sačuvati lozinku za sljedeći put?"
STR_PRESS_OK_SCAN: "Pritisni OK za ponovno skeniranje"
STR_JOIN_NETWORK: "Pridruži se mreži"
STR_CREATE_HOTSPOT: "Kreiraj hotspot"
STR_JOIN_DESC: "Poveži se na postojeću Wi-Fi mrežu"
STR_HOTSPOT_DESC: "Kreiraj Wi-Fi mrežu kojoj se drugi mogu pridružiti"
STR_STARTING_HOTSPOT: "Pokretanje hotspota..."
STR_HOTSPOT_MODE: "Hotspot način rada"
STR_CONNECT_WIFI_HINT: "Poveži svoj uređaj na ovu Wi-Fi mrežu"
STR_OPEN_URL_HINT: "Otvori ovaj URL u svom pregledniku"
STR_OR_HTTP_PREFIX: "ili http://"
STR_SCAN_QR_HINT: "ili skeniraj QR kod svojim telefonom:"
STR_CALIBRE_WIRELESS: "Calibre bežično"
STR_NETWORK_LEGEND: "* = Šifrovano | + = Sačuvano"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Provjera Wi-Fi-a..."
STR_ENTER_WIFI_PASSWORD: "Unesi Wi-Fi lozinku"
STR_ADD_HIDDEN_NETWORK: "Dodaj skrivenu mrežu..."
STR_ENTER_WIFI_SSID: "Unesi naziv mreže (SSID)"
STR_TO_PREFIX: "na "
STR_CALIBRE_RECEIVING: "Primanje: "
STR_CALIBRE_RECEIVED: "Primljeno: "
STR_CALIBRE_INSTRUCTION_1: "1) Instaliraj CrossPoint Reader dodatak"
STR_CALIBRE_INSTRUCTION_2: "2) Budi na istoj Wi-Fi mreži"
STR_CALIBRE_INSTRUCTION_3: "3) U Calibreu: \"Pošalji na uređaj\""
STR_CALIBRE_INSTRUCTION_4: "\"Drži ovaj ekran otvorenim tokom slanja\""
STR_CAT_DISPLAY: "Ekran"
STR_CAT_READER: "Čitač"
STR_CAT_CONTROLS: "Kontrole"
STR_CAT_SYSTEM: "Sistem"
STR_SLEEP_SCREEN: "Ekran mirovanja"
STR_QUICK_RESUME_TIMEOUT: "Brzi nastavak nakon isteka vremena"
STR_SLEEP_COVER_MODE: "Način prikaza korica na ekranu mirovanja"
STR_HIDE_BATTERY: "Sakrij % baterije"
STR_EXTRA_SPACING: "Dodatni razmak između paragrafa"
STR_TEXT_AA: "Zaglađivanje teksta"
STR_IMAGES: "Slike"
STR_IMAGES_DISPLAY: "Prikaz"
STR_IMAGES_PLACEHOLDER: "Rezervisano mjesto"
STR_IMAGES_SUPPRESS: "Sakrij"
STR_EOB_HOME: "Početna"
STR_EOB_CONTINUE_WITH: "Nastavi sa"
STR_SHORT_PWR_BTN: "Kratak klik dugmeta napajanja"
STR_ORIENTATION: "Orijentacija čitanja"
STR_SIDE_BTN_LAYOUT: "Raspored bočnih dugmadi (čitač)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orijentiši prednja dugmad"
STR_LONG_PRESS_BEHAVIOR: "Ponašanje dugog pritiska dugmeta"
STR_LONG_PRESS_BEHAVIOR_OFF: "ISKLJUČENO"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskakanje poglavlja"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Promjena orijentacije"
STR_LONG_PRESS_MENU: "Meni dugog pritiska"
STR_FONT_PREVIEW_TEXT: "Brza smeđa lisica preskače lijenog psa"
STR_FONT_FAMILY: "Porodica fonta čitača"
STR_FONT_SIZE: "Veličina fonta čitača"
STR_LINE_SPACING: "Razmak između redova čitača"
STR_SCREEN_MARGIN: "Margina ekrana čitača"
STR_PARA_ALIGNMENT: "Poravnanje paragrafa čitača"
STR_HYPHENATION: "Rastavljanje riječi"
STR_TIME_TO_SLEEP: "Vrijeme do mirovanja"
STR_SHOW_HIDDEN_FILES: "Prikaži skrivene datoteke"
STR_REMOVE_READ_FROM_RECENTS: "Ukloni pročitane knjige sa liste nedavnih"
STR_MOVE_FINISHED_TO_READ: "Premjesti završene knjige u fasciklu Read"
STR_REFRESH_FREQ: "Učestalost osvježavanja"
STR_KOREADER_SYNC: "KOReader sinhronizacija"
STR_CHECK_UPDATES: "Provjeri ažuriranja"
STR_LANGUAGE: "Jezik"
STR_CLEAR_READING_CACHE: "Očisti keš čitanja"
STR_USERNAME: "Korisničko ime"
STR_PASSWORD: "Lozinka"
STR_SYNC_SERVER_URL: "URL servera za sinhronizaciju"
STR_DOCUMENT_MATCHING: "Podudaranje dokumenata"
STR_SEND_METADATA: "Pošalji metapodatke dokumenta"
STR_AUTHENTICATE: "Autentifikuj"
STR_KOREADER_USERNAME: "KOReader korisničko ime"
STR_KOREADER_PASSWORD: "KOReader lozinka"
STR_FILENAME: "Naziv datoteke"
STR_BINARY: "Binarno"
STR_SET_CREDENTIALS_FIRST: "Prvo postavi vjerodajnice"
STR_WIFI_CONN_FAILED: "Wi-Fi povezivanje nije uspjelo"
STR_AUTHENTICATING: "Autentifikacija..."
STR_AUTH_SUCCESS: "Uspješno autentifikovano!"
STR_KOREADER_AUTH: "KOReader autentifikacija"
STR_SYNC_READY: "KOReader sinhronizacija je spremna za upotrebu"
STR_AUTH_FAILED: "Autentifikacija nije uspjela"
STR_DONE: "Gotovo"
STR_CLEAR_CACHE_WARNING_1: "Ovo će očistiti sve keširane podatke knjiga."
STR_CLEAR_CACHE_WARNING_2: "Sav napredak čitanja će biti izgubljen!"
STR_CLEAR_CACHE_WARNING_3: "Knjige će morati biti ponovo indeksirane"
STR_CLEAR_CACHE_WARNING_4: "kada se ponovo otvore."
STR_CLEARING_CACHE: "Čišćenje keša..."
STR_CACHE_CLEARED: "Keš očišćen"
STR_ITEMS_REMOVED: "stavki uklonjeno"
STR_FAILED_LOWER: "nije uspjelo"
STR_CLEAR_CACHE_FAILED: "Čišćenje keša nije uspjelo"
STR_CHECK_SERIAL_OUTPUT: "Provjeri serijski izlaz za detalje"
STR_DARK: "Tamno"
STR_LIGHT: "Svijetlo"
STR_CUSTOM: "Prilagođeno"
STR_COVER: "Korice"
STR_NONE_OPT: "Ništa"
STR_FIT: "Prilagodi"
STR_CROP: "Isjeci"
STR_NEVER: "Nikad"
STR_IN_READER: "U čitaču"
STR_ALWAYS: "Uvijek"
STR_IGNORE: "Ignoriši"
STR_SLEEP: "Mirovanje"
STR_PAGE_TURN: "Okretanje stranice"
STR_FORCE_REFRESH: "Osvježi ekran"
STR_PORTRAIT: "Uspravno"
STR_LANDSCAPE_CW: "Položeno (u smjeru kazaljke na satu)"
STR_INVERTED: "Obrnuto"
STR_ORIENTATION_INVERTED: "Uspravno 180°"
STR_LANDSCAPE_CCW: "Položeno (suprotno smjeru kazaljke na satu)"
STR_PREV_NEXT: "Prethodno/Sljedeće"
STR_NEXT_PREV: "Sljedeće/Prethodno"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Obilježivač"
STR_DISABLED: "Onemogućeno"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malo"
STR_MEDIUM: "Srednje"
STR_LARGE: "Veliko"
STR_X_LARGE: "Vrlo veliko"
STR_TIGHT: "Usko"
STR_NORMAL: "Normalno"
STR_WIDE: "Široko"
STR_JUSTIFY: "Poravnaj obostrano"
STR_ALIGN_LEFT: "Lijevo"
STR_CENTER: "Centrirano"
STR_ALIGN_RIGHT: "Desno"
STR_PAGES_1: "1 stranica"
STR_PAGES_5: "5 stranica"
STR_PAGES_10: "10 stranica"
STR_PAGES_15: "15 stranica"
STR_PAGES_30: "30 stranica"
STR_UPDATE: "Ažuriraj"
STR_CHECKING_UPDATE: "Provjera ažuriranja..."
STR_NEW_UPDATE: "Dostupno je novo ažuriranje!"
STR_CURRENT_VERSION: "Trenutna verzija: "
STR_NEW_VERSION: "Nova verzija: "
STR_UPDATING: "Ažuriranje..."
STR_NO_UPDATE: "Nema dostupnih ažuriranja"
STR_UPDATE_FAILED: "Ažuriranje nije uspjelo"
STR_UPDATE_COMPLETE: "Ažuriranje završeno"
STR_POWER_ON_HINT: "Pritisni i drži dugme napajanja da ponovo uključiš"
STR_RESTARTING_HINT: "Ponovno pokretanje... Ako se uređaj ne pokrene ponovo, drži dugme napajanja nekoliko sekundi."
STR_NO_ENTRIES: "Nema pronađenih unosa"
STR_DOWNLOADING: "Preuzimanje..."
STR_DOWNLOAD_FAILED: "Preuzimanje nije uspjelo"
STR_ERROR_MSG: "Greška:"
STR_UNNAMED: "Neimenovano"
STR_HOLD_OPEN_TO_DELETE: "Drži Otvori za brisanje"
STR_NO_SERVER_URL: "Nije podešen URL servera"
STR_FETCH_FEED_FAILED: "Dohvatanje feeda nije uspjelo"
STR_PARSE_FEED_FAILED: "Obrada feeda nije uspjela"
STR_NEXT_PAGE: "Sljedeća stranica »"
STR_PREV_PAGE: "« Prethodna stranica"
STR_NETWORK_PREFIX: "Mreža: "
STR_IP_ADDRESS_PREFIX: "IP adresa: "
STR_ERROR_GENERAL_FAILURE: "Greška: Opšta greška"
STR_ERROR_NETWORK_NOT_FOUND: "Greška: Mreža nije pronađena"
STR_ERROR_CONNECTION_TIMEOUT: "Greška: Isteklo vrijeme veze"
STR_SD_CARD: "SD kartica"
STR_BACK: "« Nazad"
STR_EXIT: "« Izlaz"
STR_HOME: "« Početna"
STR_SELECT: "Odaberi"
STR_SELECTED: "Odabrano"
STR_TOGGLE: "Promijeni"
STR_TOGGLE_BOOKMARK: "Promijeni obilježivač"
STR_CONFIRM: "Potvrdi"
STR_CANCEL: "Otkaži"
STR_CONNECT: "Poveži"
STR_OPEN: "Otvori"
STR_DOWNLOAD: "Preuzmi"
STR_RETRY: "Pokušaj ponovo"
STR_YES: "Da"
STR_NO: "Ne"
STR_SHOW: "Prikaži"
STR_HIDE: "Sakrij"
STR_STATE_ON: "UKLJUČENO"
STR_STATE_OFF: "ISKLJUČENO"
STR_NOT_SET: "Nije postavljeno"
STR_DIR_LEFT: "Lijevo"
STR_DIR_RIGHT: "Desno"
STR_DIR_UP: "Gore"
STR_DIR_DOWN: "Dolje"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter korica ekrana mirovanja"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Prilagodi statusnu traku"
STR_CHAPTER_PAGE_COUNT: "Broj stranica poglavlja"
STR_BOOK_PROGRESS_PERCENTAGE: "Procenat napretka knjige"
STR_PROGRESS_BAR: "Traka napretka"
STR_PROGRESS_BAR_THICKNESS: "Debljina trake napretka"
STR_PROGRESS_BAR_THIN: "Tanka"
STR_PROGRESS_BAR_MEDIUM: "Srednja"
STR_PROGRESS_BAR_THICK: "Debela"
STR_BOOK: "Knjiga"
STR_CHAPTER: "Poglavlje"
STR_EXAMPLE_CHAPTER: "Poglavlje 21"
STR_EXAMPLE_BOOK: "Naslov knjige"
STR_PREVIEW: "Pregled"
STR_TITLE: "Naslov"
STR_BATTERY: "Baterija"
STR_XTC_STATUS_BAR: "XTC statusna traka"
STR_BOTTOM: "Dolje"
STR_TOP: "Gore"
STR_CLOCK: "Sat"
STR_CLOCK_UTC_OFFSET: "UTC odstupanje sata"
STR_CLOCK_FORMAT: "Format sata"
STR_CLOCK_FORMAT_24H: "24-časovni"
STR_CLOCK_FORMAT_12H: "12-časovni"
STR_CURRENT_TIME: "Trenutno vrijeme:"
STR_NEXT_FIELD: "Sljedeće"
STR_CLOCK_SYNC: "Sinhronizuj sat"
STR_CLOCK_SYNC_NOW: "Sinhronizuj sat sada"
STR_CLOCK_SYNCING: "Sinhronizacija sa NTP-a..."
STR_CLOCK_SYNC_OK: "Sat sinhronizovan"
STR_CLOCK_SYNC_FAIL: "Sinhronizacija nije uspjela"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nije povezan"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Prvo se poveži na Wi-Fi, pa pokušaj ponovo."
STR_CLOCK_SYNCED: "Sat sinhronizovan"
STR_UI_THEME: "Tema korisničkog interfejsa"
STR_THEME_CLASSIC: "Klasična"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra proširena"
STR_SUNLIGHT_FADING_FIX: "Ispravka blijeđenja na suncu"
STR_REMAP_FRONT_BUTTONS: "Premapiraj prednja dugmad"
STR_BOOKMARKS: "Obilježivači"
STR_BOOKMARK_ADDED: "Obilježivač dodan."
STR_BOOKMARK_REMOVED: "Obilježivač uklonjen."
STR_OPDS_BROWSER: "OPDS pregledač"
STR_SEARCH: "Pretraga"
STR_COVER_CUSTOM: "Korice + Prilagođeno"
STR_QUICK_RESUME: "Brzi nastavak"
STR_MENU_RECENT_BOOKS: "Nedavne knjige"
STR_REMOVE_FROM_RECENTS: "Ukloniti iz nedavnih knjiga?"
STR_NO_RECENT_BOOKS: "Nema nedavnih knjiga"
STR_CALIBRE_DESC: "Koristi Calibre bežični prijenos uređaja"
STR_FORGET_AND_REMOVE: "Zaboraviti mrežu i ukloniti sačuvanu lozinku?"
STR_FORGET_BUTTON: "Zaboravi"
STR_CALIBRE_STARTING: "Pokretanje Calibrea..."
STR_CALIBRE_SETUP: "Podešavanje"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Očisti"
STR_DEFAULT_VALUE: "Zadano"
STR_REMAP_PROMPT: "Pritisni prednje dugme za svaku ulogu"
STR_UNASSIGNED: "Nedodijeljeno"
STR_ALREADY_ASSIGNED: "Već dodijeljeno"
STR_REMAP_RESET_HINT: "Bočno dugme Gore: Vrati na zadani raspored"
STR_REMAP_CANCEL_HINT: "Bočno dugme Dolje: Otkaži premapiranje"
STR_HW_BACK_LABEL: "Nazad (1. dugme)"
STR_HW_CONFIRM_LABEL: "Potvrdi (2. dugme)"
STR_HW_LEFT_LABEL: "Lijevo (3. dugme)"
STR_HW_RIGHT_LABEL: "Desno (4. dugme)"
STR_GO_TO_PERCENT: "Idi na %"
STR_GO_HOME_BUTTON: "Idi na početnu"
STR_SYNC_PROGRESS: "Sinhronizuj napredak"
STR_DELETE_CACHE: "Obriši keš knjige"
STR_DELETE: "Obriši"
STR_CONFIRM_DELETE_BOOKMARK: "Obrisati ovaj obilježivač?"
STR_DISPLAY_QR: "Prikaži stranicu kao QR"
STR_CHAPTER_PREFIX: "Poglavlje: "
STR_PAGES_SEPARATOR: " stranica | "
STR_BOOK_PREFIX: "Knjiga: "
STR_CALIBRE_URL_HINT: "Za Calibre, dodaj /opds na svoj URL"
STR_SYNCING_TIME: "Sinhronizacija vremena..."
STR_CALC_HASH: "Izračunavanje heša dokumenta..."
STR_HASH_FAILED: "Izračunavanje heša dokumenta nije uspjelo"
STR_FETCH_PROGRESS: "Dohvatanje udaljenog napretka..."
STR_UPLOAD_PROGRESS: "Slanje napretka..."
STR_NO_CREDENTIALS_MSG: "Nisu podešene vjerodajnice"
STR_KOREADER_SETUP_HINT: "Podesi KOReader nalog u Postavkama"
STR_PROGRESS_FOUND: "Napredak pronađen!"
STR_REMOTE_LABEL: "Udaljeno:"
STR_LOCAL_LABEL: "Lokalno:"
STR_PAGE_OVERALL_FORMAT: "Stranica %d, %.2f%% ukupno"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stranica %d/%d, %.2f%% ukupno"
STR_DEVICE_FROM_FORMAT: " Sa: %s"
STR_APPLY_REMOTE: "Primijeni udaljeni napredak"
STR_UPLOAD_LOCAL: "Pošalji lokalni napredak"
STR_NO_REMOTE_MSG: "Nije pronađen udaljeni napredak"
STR_UPLOAD_PROMPT: "Poslati trenutnu poziciju?"
STR_UPLOAD_SUCCESS: "Napredak poslan!"
STR_SYNC_FAILED_MSG: "Sinhronizacija nije uspjela"
STR_SAVE_PROGRESS_FAILED: "Napredak nije mogao biti sačuvan"
STR_SECTION_PREFIX: "Sekcija "
STR_UPLOAD: "Pošalji"
STR_BOOK_S_STYLE: "Stil knjige"
STR_EMBEDDED_STYLE: "Ugrađeni stil"
STR_FOCUS_READING: "Fokusirano čitanje"
STR_OPDS_SERVER_URL: "URL OPDS servera"
STR_PWR_BTN_FOOTNOTE_BACK: "Brzi povratak iz fusnota"
STR_SET_SLEEP_COVER: "Postavi korice"
STR_FOOTNOTES: "Fusnote"
STR_NO_FOOTNOTES: "Nema fusnota na ovoj stranici"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Snimi ekran"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikad"
STR_STEP_HINT_FRONT: "Prednja dugmad:"
STR_STEP_HINT_SIDE: "Bočna dugmad:"
STR_ADD_SERVER: "Dodaj server"
STR_SERVER_NAME: "Naziv servera"
STR_NO_SERVERS: "Nema podešenih OPDS servera"
STR_DELETE_SERVER: "Obriši server"
STR_OPDS_SERVERS: "OPDS serveri"
STR_AUTO_TURN_ENABLED: "Automatsko okretanje omogućeno: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatsko okretanje (stranica po minuti)"
STR_MANAGE_FONTS: "Upravljaj fontovima"
STR_FONT_BROWSER: "Pregledač fontova"
STR_LOADING_FONT_LIST: "Učitavanje liste fontova..."
STR_NO_FONTS_AVAILABLE: "Nema dostupnih fontova"
STR_FONT_INSTALLED: "Font instaliran!"
STR_FONT_INSTALL_FAILED: "Instalacija fonta nije uspjela"
STR_INSTALLED: "Instalirano"
STR_DOWNLOAD_ALL: "Preuzmi sve"
STR_UPDATE_ALL: "Ažuriraj sve"
STR_UPDATE_AVAILABLE: "Ažuriranje"
STR_CRASH_TITLE: "Sistemski pad"
STR_CRASH_DESCRIPTION: "Detaljan izvještaj je sačuvan u crash_report.txt. Molimo priloži ovu datoteku u svoj izvještaj o grešci."
STR_CRASH_REASON: "Razlog pada:"
STR_CRASH_NO_REASON: "(Razlog nije zabilježen)"
STR_TILT_PAGE_TURN: "Okretanje stranice naginjanjem"
STR_KB_HINT_MOVE_CURSOR: "Pritisni LIJEVO ili DESNO za pomjeranje kursora"
STR_KB_HINT_RETURN_CURSOR: "Pritisni LIJEVO za povratak na poziciju kursora"
STR_KB_HINT_HIDE_PASSWORD: "Drži DESNO pa pritisni [***] za skrivanje lozinke"
STR_KB_HINT_SHOW_PASSWORD: "Drži DESNO pa pritisni [abc] za prikaz lozinke"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pritisni [***] za skrivanje lozinke"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pritisni [abc] za prikaz lozinke"
STR_KB_HINT_EDIT_ENTRY: "Drži GORE za uređivanje unosa"
STR_KB_TIPS: "Savjeti:"
STR_KB_HINT_RETURN_KEYBOARD: "Pritisni DOLJE za povratak na tastaturu"
STR_KB_HINT_EXIT_URL_MODE: "Pritisni ABC za izlazak iz URL načina"
STR_KB_HINT_CLEAR_TEXT: "Drži DEL za brisanje cijelog teksta"
STR_KB_HINT_SECONDARY_CHAR: "Drži SELECT za sekundarni znak"
STR_KB_HINT_UPPER_SECONDARY: "Drži SELECT za VELIKA SLOVA ili sekundarni znak"
STR_KB_HINT_LOWER_SECONDARY: "Drži SELECT za mala slova ili sekundarni znak"
STR_KB_HINT_URL_SNIPPETS: "Pritisni URL za isječke"
STR_SD_FIRMWARE_UPDATE: "Ažuriranje firmvera sa SD kartice"
STR_SELECT_FIRMWARE_FILE: "Odaberi datoteku firmvera (.bin)"
STR_NO_BIN_FILES: "Nema pronađenih .bin datoteka"
STR_VALIDATING_FIRMWARE: "Provjera firmvera..."
STR_INVALID_FIRMWARE: "Nevažeća datoteka firmvera"
STR_FIRMWARE_TOO_LARGE: "Firmver je prevelik za particiju"
STR_FIRMWARE_TOO_SMALL: "Datoteka firmvera je premala"
STR_FIRMWARE_UPDATE_PROMPT: "Ažurirati firmver?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Datoteka se ne može otvoriti"
STR_FIRMWARE_WRITE_FAILED: "Upisivanje firmvera nije uspjelo"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne isključuj napajanje!"
STR_RECOVERY_MODE: "Način oporavka"
STR_RECOVERY_MODE_HINT: "Stavi firmware.bin u korijenski direktorij SD kartice i odaberi ga"
+5
View File
@@ -347,6 +347,11 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats" STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Suprimeix el servidor" STR_DELETE_SERVER: "Suprimeix el servidor"
STR_OPDS_SERVERS: "Servidors OPDS" STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts" STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts" STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..." STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
+5
View File
@@ -270,6 +270,11 @@ STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl" STR_EMBEDDED_STYLE: "Vložený styl"
STR_FOCUS_READING: "Soustředěné čtení" STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS" STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Složka pro stahování"
STR_OPDS_FILENAME_FORMAT: "Formát názvu souboru"
STR_FMT_AUTHOR_TITLE: "Autor - Název"
STR_FMT_TITLE_AUTHOR: "Název - Autor"
STR_FMT_TITLE: "Název"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy" STR_SLEEP_NEVER: "Nikdy"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig" STR_SLEEP_NEVER: "Aldrig"
STR_STEP_HINT_FRONT: "Frontknapper:" STR_STEP_HINT_FRONT: "Frontknapper:"
STR_STEP_HINT_SIDE: "Sideknapper:" STR_STEP_HINT_SIDE: "Sideknapper:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmappe"
STR_OPDS_FILENAME_FORMAT: "Filnavnsformat"
STR_FMT_AUTHOR_TITLE: "Forfatter - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Forfatter"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede" STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: " STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nooit" STR_SLEEP_NEVER: "Nooit"
STR_STEP_HINT_FRONT: "Voorknoppen:" STR_STEP_HINT_FRONT: "Voorknoppen:"
STR_STEP_HINT_SIDE: "Zijknoppen:" STR_STEP_HINT_SIDE: "Zijknoppen:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmap"
STR_OPDS_FILENAME_FORMAT: "Bestandsnaamformaat"
STR_FMT_AUTHOR_TITLE: "Auteur - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Auteur"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Screenshot maken" STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: " STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)" STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
+6 -19
View File
@@ -295,25 +295,6 @@ STR_HW_BACK_LABEL: "Back (1st button)"
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)" STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
STR_HW_LEFT_LABEL: "Left (3rd button)" STR_HW_LEFT_LABEL: "Left (3rd button)"
STR_HW_RIGHT_LABEL: "Right (4th button)" STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_BLUETOOTH: "Bluetooth"
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
STR_BT_SCAN_PAIR: "Scan & Pair"
STR_BT_NO_DEVICES: "No devices found"
STR_BT_FREE_HINT1: "Set Free2/3 to Reader Mode and"
STR_BT_FREE_HINT2: "Volume Function to pair"
STR_BT_DISCONNECT: "Disconnect"
STR_BT_CONNECTED_TO: "Connected: %s"
STR_BT_NOT_CONNECTED: "Not connected"
STR_BT_PAIRED_DEVICES: "Paired Devices"
STR_BT_NO_PAIRED: "No paired devices"
STR_BT_MAP_BUTTONS: "Map Remote Buttons"
STR_BT_PRESS_REMOTE: "Press a button on your remote"
STR_BT_CONNECTING_POPUP: "BT Connecting..."
STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)"
STR_STATE_PAUSED: "PAUSED"
STR_BT_PAGE_FORWARD: "Page Forward"
STR_BT_PAGE_BACK: "Page Back"
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
STR_GO_TO_PERCENT: "Go to %" STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home" STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress" STR_SYNC_PROGRESS: "Sync Progress"
@@ -366,6 +347,12 @@ STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured" STR_NO_SERVERS: "No OPDS servers configured"
STR_DELETE_SERVER: "Delete Server" STR_DELETE_SERVER: "Delete Server"
STR_OPDS_SERVERS: "OPDS Servers" STR_OPDS_SERVERS: "OPDS Servers"
STR_OPDS_DOWNLOAD_FOLDER: "Download folder"
STR_OPDS_FILENAME_FORMAT: "Filename format"
STR_FMT_AUTHOR_TITLE: "Author - Title"
STR_FMT_TITLE_AUTHOR: "Title - Author"
STR_FMT_TITLE: "Title"
STR_OPDS_SD_ROOT: "SD root"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_MANAGE_FONTS: "Manage Fonts" STR_MANAGE_FONTS: "Manage Fonts"
+5
View File
@@ -268,6 +268,11 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli" STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen" STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite" STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_OPDS_DOWNLOAD_FOLDER: "Latauskansio"
STR_OPDS_FILENAME_FORMAT: "Tiedostonimen muoto"
STR_FMT_AUTHOR_TITLE: "Tekijä - Nimi"
STR_FMT_TITLE_AUTHOR: "Nimi - Tekijä"
STR_FMT_TITLE: "Nimi"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan" STR_SLEEP_NEVER: "Ei koskaan"
+5
View File
@@ -301,6 +301,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais" STR_SLEEP_NEVER: "Jamais"
STR_STEP_HINT_FRONT: "Boutons avant :" STR_STEP_HINT_FRONT: "Boutons avant :"
STR_STEP_HINT_SIDE: "Boutons latéraux :" STR_STEP_HINT_SIDE: "Boutons latéraux :"
STR_OPDS_DOWNLOAD_FOLDER: "Dossier de téléchargement"
STR_OPDS_FILENAME_FORMAT: "Format du nom de fichier"
STR_FMT_AUTHOR_TITLE: "Auteur - Titre"
STR_FMT_TITLE_AUTHOR: "Titre - Auteur"
STR_FMT_TITLE: "Titre"
STR_SCREENSHOT_BUTTON: "Capture d'écran" STR_SCREENSHOT_BUTTON: "Capture d'écran"
STR_AUTO_TURN_ENABLED: "Tourne-page auto : " STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
+5
View File
@@ -329,6 +329,11 @@ STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert" STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
STR_DELETE_SERVER: "Server entfernen" STR_DELETE_SERVER: "Server entfernen"
STR_OPDS_SERVERS: "OPDS-Server" STR_OPDS_SERVERS: "OPDS-Server"
STR_OPDS_DOWNLOAD_FOLDER: "Download-Ordner"
STR_OPDS_FILENAME_FORMAT: "Dateinamenformat"
STR_FMT_AUTHOR_TITLE: "Autor - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Autor"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: " STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)"
STR_IMAGES: "Bilder" STR_IMAGES: "Bilder"
+5
View File
@@ -306,6 +306,11 @@ STR_NO_SERVERS: "לא הוגדרו שרתי OPDS"
STR_DELETE_SERVER: "מחק שרת" STR_DELETE_SERVER: "מחק שרת"
STR_DELETE_CONFIRM: "למחוק שרת זה?" STR_DELETE_CONFIRM: "למחוק שרת זה?"
STR_OPDS_SERVERS: "שרתי OPDS" STR_OPDS_SERVERS: "שרתי OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "תיקיית הורדות"
STR_OPDS_FILENAME_FORMAT: "תבנית שם קובץ"
STR_FMT_AUTHOR_TITLE: "מחבר - כותרת"
STR_FMT_TITLE_AUTHOR: "כותרת - מחבר"
STR_FMT_TITLE: "כותרת"
STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: " STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: "
STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)" STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)"
STR_MANAGE_FONTS: "ניהול גופנים" STR_MANAGE_FONTS: "ניהול גופנים"
+5
View File
@@ -299,6 +299,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha" STR_SLEEP_NEVER: "Soha"
STR_STEP_HINT_FRONT: "Elülső gombok:" STR_STEP_HINT_FRONT: "Elülső gombok:"
STR_STEP_HINT_SIDE: "Oldalsó gombok:" STR_STEP_HINT_SIDE: "Oldalsó gombok:"
STR_OPDS_DOWNLOAD_FOLDER: "Letöltési mappa"
STR_OPDS_FILENAME_FORMAT: "Fájlnév formátuma"
STR_FMT_AUTHOR_TITLE: "Szerző - Cím"
STR_FMT_TITLE_AUTHOR: "Cím - Szerző"
STR_FMT_TITLE: "Cím"
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése" STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: " STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
+5
View File
@@ -315,6 +315,11 @@ STR_SERVER_NAME: "Nome server"
STR_NO_SERVERS: "Nessun server OPDS configurato" STR_NO_SERVERS: "Nessun server OPDS configurato"
STR_DELETE_SERVER: "Elimina server" STR_DELETE_SERVER: "Elimina server"
STR_OPDS_SERVERS: "Server OPDS" STR_OPDS_SERVERS: "Server OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Cartella download"
STR_OPDS_FILENAME_FORMAT: "Formato nome file"
STR_FMT_AUTHOR_TITLE: "Autore - Titolo"
STR_FMT_TITLE_AUTHOR: "Titolo - Autore"
STR_FMT_TITLE: "Titolo"
STR_AUTO_TURN_ENABLED: "Volta pagina automatico: " STR_AUTO_TURN_ENABLED: "Volta pagina automatico: "
STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)" STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)"
STR_MANAGE_FONTS: "Gestisci font" STR_MANAGE_FONTS: "Gestisci font"
+5
View File
@@ -296,6 +296,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан" STR_SLEEP_NEVER: "Ешқашан"
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:" STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
STR_STEP_HINT_SIDE: "Бүйір түймелері:" STR_STEP_HINT_SIDE: "Бүйір түймелері:"
STR_OPDS_DOWNLOAD_FOLDER: "Жүктеп алу қалтасы"
STR_OPDS_FILENAME_FORMAT: "Файл атауының пішімі"
STR_FMT_AUTHOR_TITLE: "Автор - Атауы"
STR_FMT_TITLE_AUTHOR: "Атауы - Автор"
STR_FMT_TITLE: "Атауы"
STR_SCREENSHOT_BUTTON: "Скриншот түсіру" STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: " STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
+5
View File
@@ -297,6 +297,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Niekada" STR_SLEEP_NEVER: "Niekada"
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:" STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:" STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
STR_OPDS_DOWNLOAD_FOLDER: "Atsisiuntimų aplankas"
STR_OPDS_FILENAME_FORMAT: "Failo pavadinimo formatas"
STR_FMT_AUTHOR_TITLE: "Autorius - Pavadinimas"
STR_FMT_TITLE_AUTHOR: "Pavadinimas - Autorius"
STR_FMT_TITLE: "Pavadinimas"
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka" STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
STR_AUTO_TURN_ENABLED: "Auto-vertimas: " STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
+385
View File
@@ -0,0 +1,385 @@
_language_name: "Norsk bokmål"
_language_code: "NB"
_order: "26"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "STARTER"
STR_SLEEPING: "HVILER"
STR_ENTERING_SLEEP: "Går i hvilemodus"
STR_BROWSE_FILES: "Bla i filer"
STR_FILE_TRANSFER: "Filoverføring"
STR_SETTINGS_TITLE: "Innstillinger"
STR_CONTINUE_READING: "Fortsett å lese"
STR_NO_OPEN_BOOK: "Ingen åpen bok"
STR_START_READING: "Start å lese nedenfor"
STR_NO_FILES_FOUND: "Ingen filer funnet"
STR_SELECT_CHAPTER: "Velg kapittel"
STR_NO_CHAPTERS: "Ingen kapitler"
STR_END_OF_BOOK: "Slutten av boken"
STR_EMPTY_CHAPTER: "Tomt kapittel"
STR_INDEXING: "Indekserer"
STR_MEMORY_ERROR: "Minnefeil"
STR_PAGE_LOAD_ERROR: "Feil ved sidelasting"
STR_EMPTY_FILE: "Tom fil"
STR_OUT_OF_BOUNDS: "Utenfor område"
STR_LOADING: "Laster..."
STR_LOADING_POPUP: "Laster"
STR_WIFI_NETWORKS: "WiFi-nettverk"
STR_NO_NETWORKS: "Ingen nettverk funnet"
STR_NETWORKS_FOUND: "%zu nettverk funnet"
STR_SCANNING: "Skanner..."
STR_CONNECTING: "Kobler til..."
STR_CONNECTED: "Tilkoblet!"
STR_CONNECTION_FAILED: "Tilkobling mislyktes"
STR_FORGET_NETWORK: "Glem nettverk?"
STR_SAVE_PASSWORD: "Lagre passord til neste gang?"
STR_PRESS_OK_SCAN: "Trykk OK for å skanne på nytt"
STR_JOIN_NETWORK: "Koble til et nettverk"
STR_CREATE_HOTSPOT: "Opprett hotspot"
STR_JOIN_DESC: "Koble til et eksisterende WiFi-nettverk"
STR_HOTSPOT_DESC: "Opprett et WiFi-nettverk andre kan koble seg til"
STR_STARTING_HOTSPOT: "Starter hotspot..."
STR_HOTSPOT_MODE: "Hotspot-modus"
STR_CONNECT_WIFI_HINT: "Koble enheten din til dette WiFi-nettverket"
STR_OPEN_URL_HINT: "Åpne denne URL-en i nettleseren din"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller skann QR-kode med telefonen din:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Kryptert | + = Lagret"
STR_MAC_ADDRESS: "MAC-adresse:"
STR_CHECKING_WIFI: "Sjekker WiFi..."
STR_ENTER_WIFI_PASSWORD: "Skriv inn WiFi-passord"
STR_TO_PREFIX: "til "
STR_CALIBRE_RECEIVING: "Mottar: "
STR_CALIBRE_RECEIVED: "Mottatt: "
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Vær på samme WiFi-nettverk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhet\""
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skjermen åpen mens du sender\""
STR_CAT_DISPLAY: "Skjerm"
STR_CAT_READER: "Leser"
STR_CAT_CONTROLS: "Betjening"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Hvileskjerm"
STR_QUICK_RESUME_TIMEOUT: "Hurtig gjenopptak ved tidsavbrudd"
STR_SLEEP_COVER_MODE: "Omslagsmodus for hvileskjerm"
STR_HIDE_BATTERY: "Skjul batteri %"
STR_EXTRA_SPACING: "Ekstra avsnittsavstand"
STR_TEXT_AA: "Tekstutjevning"
STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Vis"
STR_IMAGES_PLACEHOLDER: "Plassholder"
STR_IMAGES_SUPPRESS: "Skjul"
STR_SHORT_PWR_BTN: "Kort trykk på av/på-knapp"
STR_ORIENTATION: "Leseretning"
STR_SIDE_BTN_LAYOUT: "Sideknapp-oppsett (leser)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter frontknapper"
STR_LONG_PRESS_BEHAVIOR: "Atferd ved langt trykk"
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapittelhopp"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Endre orientering"
STR_LONG_PRESS_MENU: "Langt trykk på Meny"
STR_FONT_PREVIEW_TEXT: "Høvdingens kjære squaw får litt pizza i Mexico by"
STR_FONT_FAMILY: "Skrifttype i leser"
STR_FONT_SIZE: "Skriftstørrelse i leser"
STR_LINE_SPACING: "Linjeavstand i leser"
STR_SCREEN_MARGIN: "Skjermmarg i leser"
STR_PARA_ALIGNMENT: "Avsnittsjustering i leser"
STR_HYPHENATION: "Orddeling"
STR_TIME_TO_SLEEP: "Tid før hvile"
STR_SHOW_HIDDEN_FILES: "Vis skjulte filer"
STR_REMOVE_READ_FROM_RECENTS: "Fjern leste bøker fra Nylig-listen"
STR_MOVE_FINISHED_TO_READ: "Flytt fullførte bøker til Read-mappen"
STR_REFRESH_FREQ: "Oppdateringsfrekvens"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Se etter oppdateringer"
STR_LANGUAGE: "Språk"
STR_CLEAR_READING_CACHE: "Tøm lesebuffer"
STR_USERNAME: "Brukernavn"
STR_PASSWORD: "Passord"
STR_SYNC_SERVER_URL: "URL til synk-server"
STR_DOCUMENT_MATCHING: "Dokumentgjenkjenning"
STR_AUTHENTICATE: "Godkjenn"
STR_KOREADER_USERNAME: "KOReader-brukernavn"
STR_KOREADER_PASSWORD: "KOReader-passord"
STR_FILENAME: "Filnavn"
STR_BINARY: "Binær"
STR_SET_CREDENTIALS_FIRST: "Angi påloggingsinfo først"
STR_WIFI_CONN_FAILED: "WiFi-tilkobling mislyktes"
STR_AUTHENTICATING: "Godkjenner..."
STR_AUTH_SUCCESS: "Godkjenning vellykket!"
STR_KOREADER_AUTH: "KOReader-godkjenning"
STR_SYNC_READY: "KOReader-synk er klar til bruk"
STR_AUTH_FAILED: "Godkjenning mislyktes"
STR_DONE: "Ferdig"
STR_CLEAR_CACHE_WARNING_1: "Dette tømmer alle bufrede bokdata."
STR_CLEAR_CACHE_WARNING_2: "All lesefremdrift går tapt!"
STR_CLEAR_CACHE_WARNING_3: "Bøker må indekseres på nytt"
STR_CLEAR_CACHE_WARNING_4: "når de åpnes igjen."
STR_CLEARING_CACHE: "Tømmer buffer..."
STR_CACHE_CLEARED: "Buffer tømt"
STR_ITEMS_REMOVED: "elementer fjernet"
STR_FAILED_LOWER: "mislyktes"
STR_CLEAR_CACHE_FAILED: "Kunne ikke tømme buffer"
STR_CHECK_SERIAL_OUTPUT: "Sjekk seriell utdata for detaljer"
STR_DARK: "Mørk"
STR_LIGHT: "Lys"
STR_CUSTOM: "Egendefinert"
STR_COVER: "Omslag"
STR_NONE_OPT: "Ingen"
STR_FIT: "Tilpass"
STR_CROP: "Beskjær"
STR_NEVER: "Aldri"
STR_IN_READER: "I leseren"
STR_ALWAYS: "Alltid"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Hvile"
STR_PAGE_TURN: "Bla om"
STR_FORCE_REFRESH: "Oppdater skjerm"
STR_PORTRAIT: "Stående"
STR_LANDSCAPE_CW: "Liggende med klokken"
STR_INVERTED: "Opp ned"
STR_ORIENTATION_INVERTED: "Portrett 180°"
STR_LANDSCAPE_CCW: "Liggende mot klokken"
STR_PREV_NEXT: "Forrige/Neste"
STR_NEXT_PREV: "Neste/Forrige"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bokmerke"
STR_DISABLED: "Deaktivert"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Liten"
STR_MEDIUM: "Middels"
STR_LARGE: "Stor"
STR_X_LARGE: "Ekstra stor"
STR_TIGHT: "Tett"
STR_NORMAL: "Normal"
STR_WIDE: "Bred"
STR_JUSTIFY: "Blokkjustert"
STR_ALIGN_LEFT: "Venstre"
STR_CENTER: "Midtstilt"
STR_ALIGN_RIGHT: "Høyre"
STR_PAGES_1: "1 side"
STR_PAGES_5: "5 sider"
STR_PAGES_10: "10 sider"
STR_PAGES_15: "15 sider"
STR_PAGES_30: "30 sider"
STR_UPDATE: "Oppdater"
STR_CHECKING_UPDATE: "Ser etter oppdatering..."
STR_NEW_UPDATE: "Ny oppdatering tilgjengelig!"
STR_CURRENT_VERSION: "Nåværende versjon: "
STR_NEW_VERSION: "Ny versjon: "
STR_UPDATING: "Oppdaterer..."
STR_NO_UPDATE: "Ingen oppdatering tilgjengelig"
STR_UPDATE_FAILED: "Oppdatering mislyktes"
STR_UPDATE_COMPLETE: "Oppdatering fullført"
STR_POWER_ON_HINT: "Trykk og hold av/på-knappen for å slå på igjen"
STR_RESTARTING_HINT: "Starter på nytt... Hvis enheten ikke starter, hold av/på-knappen i noen sekunder."
STR_NO_ENTRIES: "Ingen oppføringer funnet"
STR_DOWNLOADING: "Laster ned..."
STR_DOWNLOAD_FAILED: "Nedlasting mislyktes"
STR_ERROR_MSG: "Feil:"
STR_UNNAMED: "Uten navn"
STR_HOLD_OPEN_TO_DELETE: "Hold Åpne for å slette"
STR_NO_SERVER_URL: "Ingen server-URL konfigurert"
STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
STR_PARSE_FEED_FAILED: "Kunne ikke tolke feed"
STR_NEXT_PAGE: "Neste side »"
STR_PREV_PAGE: "« Forrige side"
STR_NETWORK_PREFIX: "Nettverk: "
STR_IP_ADDRESS_PREFIX: "IP-adresse: "
STR_ERROR_GENERAL_FAILURE: "Feil: Generell feil"
STR_ERROR_NETWORK_NOT_FOUND: "Feil: Nettverk ikke funnet"
STR_ERROR_CONNECTION_TIMEOUT: "Feil: Tidsavbrudd for tilkobling"
STR_SD_CARD: "SD-kort"
STR_BACK: "« Tilbake"
STR_EXIT: "« Avslutt"
STR_HOME: "« Hjem"
STR_SELECT: "Velg"
STR_SELECTED: "Valgt"
STR_TOGGLE: "Veksle"
STR_TOGGLE_BOOKMARK: "Veksle bokmerke"
STR_CONFIRM: "Bekreft"
STR_CANCEL: "Avbryt"
STR_CONNECT: "Koble til"
STR_OPEN: "Åpne"
STR_DOWNLOAD: "Last ned"
STR_RETRY: "Prøv igjen"
STR_YES: "Ja"
STR_NO: "Nei"
STR_SHOW: "Vis"
STR_HIDE: "Skjul"
STR_STATE_ON: "PÅ"
STR_STATE_OFF: "AV"
STR_NOT_SET: "Ikke angitt"
STR_DIR_LEFT: "Venstre"
STR_DIR_RIGHT: "Høyre"
STR_DIR_UP: "Opp"
STR_DIR_DOWN: "Ned"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Omslagsfilter for hvileskjerm"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Tilpass statuslinje"
STR_CHAPTER_PAGE_COUNT: "Sideantall i kapittel"
STR_BOOK_PROGRESS_PERCENTAGE: "Bokfremdrift i prosent"
STR_PROGRESS_BAR: "Fremdriftslinje"
STR_PROGRESS_BAR_THICKNESS: "Tykkelse på fremdriftslinje"
STR_PROGRESS_BAR_THIN: "Tynn"
STR_PROGRESS_BAR_MEDIUM: "Middels"
STR_PROGRESS_BAR_THICK: "Tykk"
STR_BOOK: "Bok"
STR_CHAPTER: "Kapittel"
STR_EXAMPLE_CHAPTER: "Kapittel 21"
STR_EXAMPLE_BOOK: "Boktittel"
STR_PREVIEW: "Forhåndsvisning"
STR_TITLE: "Tittel"
STR_BATTERY: "Batteri"
STR_XTC_STATUS_BAR: "XTC-statuslinje"
STR_BOTTOM: "Nederst"
STR_TOP: "Øverst"
STR_CLOCK: "Klokke"
STR_CLOCK_UTC_OFFSET: "UTC-forskyvning for klokke"
STR_CLOCK_FORMAT: "Klokkeformat"
STR_CLOCK_FORMAT_24H: "24-timers"
STR_CLOCK_FORMAT_12H: "12-timers"
STR_CURRENT_TIME: "Nåværende tid:"
STR_NEXT_FIELD: "Neste"
STR_CLOCK_SYNC: "Synkroniser klokke"
STR_CLOCK_SYNC_NOW: "Synkroniser klokke nå"
STR_CLOCK_SYNCING: "Synkroniserer fra NTP..."
STR_CLOCK_SYNC_OK: "Klokke synkronisert"
STR_CLOCK_SYNC_FAIL: "Synkronisering mislyktes"
STR_CLOCK_SYNC_NO_WIFI: "WiFi ikke tilkoblet"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Koble til WiFi først, og prøv igjen."
STR_CLOCK_SYNCED: "Klokke synkronisert"
STR_UI_THEME: "Grensesnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Retting for soluttoning"
STR_REMAP_FRONT_BUTTONS: "Tilordne frontknapper på nytt"
STR_BOOKMARKS: "Bokmerker"
STR_BOOKMARK_ADDED: "Bokmerke lagt til."
STR_BOOKMARK_REMOVED: "Bokmerke fjernet."
STR_OPDS_BROWSER: "OPDS-leser"
STR_SEARCH: "Søk"
STR_COVER_CUSTOM: "Omslag + Egendefinert"
STR_QUICK_RESUME: "Hurtig gjenopptak"
STR_MENU_RECENT_BOOKS: "Nylige bøker"
STR_REMOVE_FROM_RECENTS: "Fjern fra Nylige bøker?"
STR_NO_RECENT_BOOKS: "Ingen nylige bøker"
STR_CALIBRE_DESC: "Bruk trådløs enhetsoverføring med Calibre"
STR_FORGET_AND_REMOVE: "Glem nettverk og fjern lagret passord?"
STR_FORGET_BUTTON: "Glem"
STR_CALIBRE_STARTING: "Starter Calibre..."
STR_CALIBRE_SETUP: "Oppsett"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Tøm"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Trykk en frontknapp for hver rolle"
STR_UNASSIGNED: "Ikke tilordnet"
STR_ALREADY_ASSIGNED: "Allerede tilordnet"
STR_REMAP_RESET_HINT: "Sideknapp opp: Nullstill til standardoppsett"
STR_REMAP_CANCEL_HINT: "Sideknapp ned: Avbryt omtilordning"
STR_HW_BACK_LABEL: "Tilbake (1. knapp)"
STR_HW_CONFIRM_LABEL: "Bekreft (2. knapp)"
STR_HW_LEFT_LABEL: "Venstre (3. knapp)"
STR_HW_RIGHT_LABEL: "Høyre (4. knapp)"
STR_GO_TO_PERCENT: "Gå til %"
STR_GO_HOME_BUTTON: "Hjem"
STR_SYNC_PROGRESS: "Synkroniser fremdrift"
STR_DELETE_CACHE: "Slett bokbuffer"
STR_DELETE: "Slett"
STR_CONFIRM_DELETE_BOOKMARK: "Slette dette bokmerket?"
STR_DISPLAY_QR: "Vis side som QR"
STR_CHAPTER_PREFIX: "Kapittel: "
STR_PAGES_SEPARATOR: " sider | "
STR_BOOK_PREFIX: "Bok: "
STR_CALIBRE_URL_HINT: "For Calibre, legg til /opds i URL-en din"
STR_PERCENT_STEP_HINT: "Venstre/Høyre: 1% Opp/Ned: 10%"
STR_SYNCING_TIME: "Synkroniserer tid..."
STR_CALC_HASH: "Beregner dokument-hash..."
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
STR_FETCH_PROGRESS: "Henter ekstern fremdrift..."
STR_UPLOAD_PROGRESS: "Laster opp fremdrift..."
STR_NO_CREDENTIALS_MSG: "Ingen påloggingsinfo angitt"
STR_KOREADER_SETUP_HINT: "Sett opp KOReader-konto i Innstillinger"
STR_PROGRESS_FOUND: "Fremdrift funnet!"
STR_REMOTE_LABEL: "Ekstern:"
STR_LOCAL_LABEL: "Lokal:"
STR_PAGE_OVERALL_FORMAT: "Side %d, %.2f%% totalt"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Side %d/%d, %.2f%% totalt"
STR_DEVICE_FROM_FORMAT: " Fra: %s"
STR_APPLY_REMOTE: "Bruk ekstern fremdrift"
STR_UPLOAD_LOCAL: "Last opp lokal fremdrift"
STR_NO_REMOTE_MSG: "Ingen ekstern fremdrift funnet"
STR_UPLOAD_PROMPT: "Last opp nåværende posisjon?"
STR_UPLOAD_SUCCESS: "Fremdrift lastet opp!"
STR_SYNC_FAILED_MSG: "Synkronisering mislyktes"
STR_SAVE_PROGRESS_FAILED: "Kunne ikke lagre fremdrift"
STR_SECTION_PREFIX: "Del "
STR_UPLOAD: "Last opp"
STR_BOOK_S_STYLE: "Bokens stil"
STR_EMBEDDED_STYLE: "Innebygd stil"
STR_FOCUS_READING: "Fokuslesing"
STR_OPDS_SERVER_URL: "OPDS-server-URL"
STR_PWR_BTN_FOOTNOTE_BACK: "Hurtig retur fra fotnoter"
STR_SET_SLEEP_COVER: "Velg omslag"
STR_FOOTNOTES: "Fotnoter"
STR_NO_FOOTNOTES: "Ingen fotnoter på denne siden"
STR_LINK: "[lenke]"
STR_SCREENSHOT_BUTTON: "Ta skjermbilde"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldri"
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Høyre: 1 min Opp/Ned: 5 min"
STR_ADD_SERVER: "Legg til server"
STR_SERVER_NAME: "Servernavn"
STR_NO_SERVERS: "Ingen OPDS-servere konfigurert"
STR_DELETE_SERVER: "Slett server"
STR_OPDS_SERVERS: "OPDS-servere"
STR_AUTO_TURN_ENABLED: "Automatisk bla aktivert: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk bla (sider per minutt)"
STR_MANAGE_FONTS: "Administrer skrifttyper"
STR_FONT_BROWSER: "Bla i skrifttyper"
STR_LOADING_FONT_LIST: "Laster skrifttypeliste..."
STR_NO_FONTS_AVAILABLE: "Ingen skrifttyper tilgjengelig"
STR_FONT_INSTALLED: "Skrifttype installert!"
STR_FONT_INSTALL_FAILED: "Installasjon av skrifttype mislyktes"
STR_INSTALLED: "Installert"
STR_DOWNLOAD_ALL: "Last ned alle"
STR_UPDATE_ALL: "Oppdater alle"
STR_UPDATE_AVAILABLE: "Oppdater"
STR_CRASH_TITLE: "Systemkrasj"
STR_CRASH_DESCRIPTION: "En detaljert rapport ble lagret i crash_report.txt. Legg ved denne filen i feilrapporten din."
STR_CRASH_REASON: "Krasjårsak:"
STR_CRASH_NO_REASON: "(Ingen årsak ble registrert)"
STR_TILT_PAGE_TURN: "Vipp for å bla"
STR_KB_HINT_MOVE_CURSOR: "Trykk VENSTRE eller HØYRE for å flytte markøren"
STR_KB_HINT_RETURN_CURSOR: "Trykk VENSTRE for å gå tilbake til markørposisjonen"
STR_KB_HINT_HIDE_PASSWORD: "Hold HØYRE og trykk [***] for å skjule passord"
STR_KB_HINT_SHOW_PASSWORD: "Hold HØYRE og trykk [abc] for å vise passord"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Trykk [***] for å skjule passord"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Trykk [abc] for å vise passord"
STR_KB_HINT_EDIT_ENTRY: "Hold OPP for å redigere oppføring"
STR_KB_TIPS: "Tips:"
STR_KB_HINT_RETURN_KEYBOARD: "Trykk NED for å gå tilbake til tastaturet"
STR_KB_HINT_EXIT_URL_MODE: "Trykk ABC for å avslutte URL-modus"
STR_KB_HINT_CLEAR_TEXT: "Hold DEL for å slette all tekst"
STR_KB_HINT_SECONDARY_CHAR: "Hold VELG for sekundærtegn"
STR_KB_HINT_UPPER_SECONDARY: "Hold VELG for STORE BOKSTAVER eller sekundærtegn"
STR_KB_HINT_LOWER_SECONDARY: "Hold VELG for små bokstaver eller sekundærtegn"
STR_KB_HINT_URL_SNIPPETS: "Trykk URL for tekstbiter"
STR_SD_FIRMWARE_UPDATE: "Fastvareoppdatering fra SD-kort"
STR_SELECT_FIRMWARE_FILE: "Velg fastvarefil (.bin)"
STR_NO_BIN_FILES: "Ingen .bin-filer funnet"
STR_VALIDATING_FIRMWARE: "Validerer fastvare..."
STR_INVALID_FIRMWARE: "Ugyldig fastvarefil"
STR_FIRMWARE_TOO_LARGE: "Fastvaren er for stor for partisjonen"
STR_FIRMWARE_TOO_SMALL: "Fastvarefilen er for liten"
STR_FIRMWARE_UPDATE_PROMPT: "Oppdatere fastvare?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Kan ikke åpne filen"
STR_FIRMWARE_WRITE_FAILED: "Skriving av fastvare mislyktes"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ikke slå av!"
STR_RECOVERY_MODE: "Gjenopprettingsmodus"
STR_RECOVERY_MODE_HINT: "Legg firmware.bin i roten av SD-kortet og velg den"
+5
View File
@@ -316,6 +316,11 @@ STR_SERVER_NAME: "Nazwa serwera"
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS" STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
STR_DELETE_SERVER: "Usuń serwer" STR_DELETE_SERVER: "Usuń serwer"
STR_OPDS_SERVERS: "Serwery OPDS" STR_OPDS_SERVERS: "Serwery OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Folder pobierania"
STR_OPDS_FILENAME_FORMAT: "Format nazwy pliku"
STR_FMT_AUTHOR_TITLE: "Autor - Tytuł"
STR_FMT_TITLE_AUTHOR: "Tytuł - Autor"
STR_FMT_TITLE: "Tytuł"
STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: " STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
STR_DOWNLOAD_FONTS: "Pobierz czcionki" STR_DOWNLOAD_FONTS: "Pobierz czcionki"
+5
View File
@@ -339,6 +339,11 @@ STR_SERVER_NAME: "Nome do Servidor"
STR_NO_SERVERS: "Nenhum servidor OPDS configurado" STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
STR_DELETE_SERVER: "Excluir Servidor" STR_DELETE_SERVER: "Excluir Servidor"
STR_OPDS_SERVERS: "Servidores OPDS" STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Pasta de downloads"
STR_OPDS_FILENAME_FORMAT: "Formato do nome do arquivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Virada automática ativada: " STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)" STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes" STR_MANAGE_FONTS: "Gerenciar fontes"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată" STR_SLEEP_NEVER: "Niciodată"
STR_STEP_HINT_FRONT: "Butoane frontale:" STR_STEP_HINT_FRONT: "Butoane frontale:"
STR_STEP_HINT_SIDE: "Butoane laterale:" STR_STEP_HINT_SIDE: "Butoane laterale:"
STR_OPDS_DOWNLOAD_FOLDER: "Dosar descărcări"
STR_OPDS_FILENAME_FORMAT: "Format nume fișier"
STR_FMT_AUTHOR_TITLE: "Autor - Titlu"
STR_FMT_TITLE_AUTHOR: "Titlu - Autor"
STR_FMT_TITLE: "Titlu"
STR_SCREENSHOT_BUTTON: "Captură ecran" STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: " STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut" STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
+5
View File
@@ -339,6 +339,11 @@ STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS" STR_NO_SERVERS: "Нет настроенных серверов OPDS"
STR_DELETE_SERVER: "Удалить сервер" STR_DELETE_SERVER: "Удалить сервер"
STR_OPDS_SERVERS: "Серверы OPDS" STR_OPDS_SERVERS: "Серверы OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка загрузок"
STR_OPDS_FILENAME_FORMAT: "Формат имени файла"
STR_FMT_AUTHOR_TITLE: "Автор - Название"
STR_FMT_TITLE_AUTHOR: "Название - Автор"
STR_FMT_TITLE: "Название"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: " STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)"
STR_MANAGE_FONTS: "Управление шрифтами" STR_MANAGE_FONTS: "Управление шрифтами"
+5
View File
@@ -335,6 +335,11 @@ STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery" STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server" STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery" STR_OPDS_SERVERS: "OPDS servery"
STR_OPDS_DOWNLOAD_FOLDER: "Priečinok sťahovania"
STR_OPDS_FILENAME_FORMAT: "Formát názvu súboru"
STR_FMT_AUTHOR_TITLE: "Autor - Názov"
STR_FMT_TITLE_AUTHOR: "Názov - Autor"
STR_FMT_TITLE: "Názov"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: " STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem" STR_MANAGE_FONTS: "Správa písiem"
+5
View File
@@ -297,6 +297,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikoli" STR_SLEEP_NEVER: "Nikoli"
STR_STEP_HINT_FRONT: "Sprednji gumbi:" STR_STEP_HINT_FRONT: "Sprednji gumbi:"
STR_STEP_HINT_SIDE: "Stranski gumbi:" STR_STEP_HINT_SIDE: "Stranski gumbi:"
STR_OPDS_DOWNLOAD_FOLDER: "Mapa za prenose"
STR_OPDS_FILENAME_FORMAT: "Oblika imena datoteke"
STR_FMT_AUTHOR_TITLE: "Avtor - Naslov"
STR_FMT_TITLE_AUTHOR: "Naslov - Avtor"
STR_FMT_TITLE: "Naslov"
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona" STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: " STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)" STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
+5
View File
@@ -345,6 +345,11 @@ STR_SERVER_NAME: "Nombre de servidor"
STR_NO_SERVERS: "No se configuraron servidores OPDS" STR_NO_SERVERS: "No se configuraron servidores OPDS"
STR_DELETE_SERVER: "Borrar servidor" STR_DELETE_SERVER: "Borrar servidor"
STR_OPDS_SERVERS: "Servidores OPDS" STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de descargas"
STR_OPDS_FILENAME_FORMAT: "Formato del nombre de archivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Avance activado: " STR_AUTO_TURN_ENABLED: "Avance activado: "
STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)" STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)"
STR_MANAGE_FONTS: "Gestionar tipografías" STR_MANAGE_FONTS: "Gestionar tipografías"
+5
View File
@@ -342,6 +342,11 @@ STR_SERVER_NAME: "Servernamn"
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade" STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
STR_DELETE_SERVER: "Ta bort server" STR_DELETE_SERVER: "Ta bort server"
STR_OPDS_SERVERS: "OPDS-servrar" STR_OPDS_SERVERS: "OPDS-servrar"
STR_OPDS_DOWNLOAD_FOLDER: "Hämtningsmapp"
STR_OPDS_FILENAME_FORMAT: "Filnamnsformat"
STR_FMT_AUTHOR_TITLE: "Författare - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Författare"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_MANAGE_FONTS: "Hantera teckensnitt" STR_MANAGE_FONTS: "Hantera teckensnitt"
+95 -4
View File
@@ -68,10 +68,10 @@ STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü" STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior" STR_LONG_PRESS_BEHAVIOR: "Uzun basma tuş davranışı"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "KAPALI"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Bölüm atlama"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Yön değiştirme"
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi" STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu" STR_FONT_SIZE: "Arayüz Yazı Boyutu"
@@ -291,6 +291,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla" STR_SLEEP_NEVER: "Asla"
STR_STEP_HINT_FRONT: "Ön tuşlar:" STR_STEP_HINT_FRONT: "Ön tuşlar:"
STR_STEP_HINT_SIDE: "Yan tuşlar:" STR_STEP_HINT_SIDE: "Yan tuşlar:"
STR_OPDS_DOWNLOAD_FOLDER: "İndirme klasörü"
STR_OPDS_FILENAME_FORMAT: "Dosya adı biçimi"
STR_FMT_AUTHOR_TITLE: "Yazar - Başlık"
STR_FMT_TITLE_AUTHOR: "Başlık - Yazar"
STR_FMT_TITLE: "Başlık"
STR_NO_FILES_FOUND: "Dosya bulunamadı" STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok" STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme" STR_PREVIEW: "Önizleme"
@@ -306,3 +311,89 @@ STR_TITLE: "Başlık"
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme" STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..." STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)" STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
STR_ADD_SERVER: "Sunucu Ekle"
STR_BOOKMARKS: "Yer İmleri"
STR_BOOKMARK_ADDED: "Yer imi eklendi."
STR_BOOKMARK_OPTION: "Yer İmi"
STR_BOTTOM: "Alt"
STR_CLOCK: "Saat"
STR_CLOCK_FORMAT: "Saat Biçimi"
STR_CLOCK_FORMAT_12H: "12 saat"
STR_CLOCK_FORMAT_24H: "24 saat"
STR_CLOCK_SYNC: "Saati Eşitle"
STR_CLOCK_SYNCED: "Saat Eşitlendi"
STR_CLOCK_SYNCING: "NTP'den eşitleniyor..."
STR_CLOCK_SYNC_FAIL: "Eşitleme başarısız"
STR_CLOCK_SYNC_NOW: "Saati şimdi eşitle"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi bağlı değil"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Önce Wi-Fi'ye bağlanın, sonra tekrar deneyin."
STR_CLOCK_SYNC_OK: "Saat eşitlendi"
STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı"
STR_CONFIRM_DELETE_BOOKMARK: "Bu yer imi silinsin mi?"
STR_CONNECTING_SAVED_WIFI: "Kayıtlı Wi-Fi'ye bağlanılıyor..."
STR_CRASH_DESCRIPTION: "Ayrıntılı rapor crash_report.txt dosyasına kaydedildi. Lütfen hata bildiriminize bu dosyayı ekleyin."
STR_CRASH_NO_REASON: "(Neden kaydedilmedi)"
STR_CRASH_REASON: "Çökme nedeni:"
STR_CRASH_TITLE: "Sistem Çökmesi"
STR_CURRENT_TIME: "Şu anki saat:"
STR_DELETE_SERVER: "Sunucuyu Sil"
STR_DISABLED: "Devre dışı"
STR_DOWNLOAD_ALL: "Tümünü İndir"
STR_EOB_CONTINUE_WITH: "Şununla devam et"
STR_EOB_HOME: "Ana Ekran"
STR_FINDING_SAVED_WIFI: "Kayıtlı Wi-Fi aranıyor..."
STR_FIRMWARE_FILE_OPEN_FAILED: "Dosya açılamıyor"
STR_FIRMWARE_TOO_LARGE: "Firmware, bölümlemeye sığmıyor"
STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
STR_FORCE_REFRESH: "Ekranı Yenile"
STR_INDEX_FAILED: "Endeksleme başarısız - geçersiz kitap"
STR_INSTALLED: "Yüklü"
STR_INVALID_FIRMWARE: "Geçersiz firmware dosyası"
STR_KB_HINT_CLEAR_TEXT: "Tüm metni silmek için DEL tuşunu basılı tutun"
STR_KB_HINT_EDIT_ENTRY: "Girdiyi düzenlemek için UP tuşunu basılı tutun"
STR_KB_HINT_EXIT_URL_MODE: "URL modundan çıkmak için ABC'ye basın"
STR_KB_HINT_HIDE_PASSWORD: "Şifreyi gizlemek için RIGHT'ı basılı tutup [***] tuşuna basın"
STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_MOVE_CURSOR: "İmleci taşımak için LEFT veya RIGHT'a basın"
STR_KB_HINT_RETURN_CURSOR: "İmleç konumuna dönmek için LEFT'e basın"
STR_KB_HINT_RETURN_KEYBOARD: "Klavyeye dönmek için DOWN'a basın"
STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_SHOW_PASSWORD: "Şifreyi göstermek için RIGHT'ı basılı tutup [abc] tuşuna basın"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Şifreyi gizlemek için [***] tuşuna basın"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Şifreyi göstermek için [abc] tuşuna basın"
STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK harf veya ikincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_URL_SNIPPETS: "Hazır kalıplar için URL'ye basın"
STR_KB_TIPS: "İpuçları:"
STR_KOSYNC: "KOSync"
STR_LOADING_FONT_LIST: "Yazı tipi listesi yükleniyor..."
STR_LONG_PRESS_MENU: "Uzun Basma Menüsü"
STR_MANAGE_FONTS: "Yazı Tiplerini Yönet"
STR_NEXT_FIELD: "Sonraki"
STR_NEXT_PAGE: "Sonraki Sayfa »"
STR_NO_BIN_FILES: ".bin dosyası bulunamadı"
STR_NO_FONTS_AVAILABLE: "Kullanılabilir yazı tipi yok"
STR_NO_SERVERS: "Yapılandırılmış OPDS sunucusu yok"
STR_OPDS_SERVERS: "OPDS Sunucuları"
STR_PREV_PAGE: "« Önceki Sayfa"
STR_PWR_BTN_FOOTNOTE_BACK: "Dipnottan hızlı dönüş"
STR_RECOVERY_MODE: "Kurtarma Modu"
STR_RECOVERY_MODE_HINT: "firmware.bin dosyasını SD kartın köküne koyup seçin"
STR_RESTARTING_HINT: "Yeniden başlatılıyor... Cihaz yeniden başlamazsa güç düğmesini birkaç saniye basılı tutun."
STR_SD_FIRMWARE_UPDATE: "SD Karttan Firmware Güncelleme"
STR_SEARCH: "Ara"
STR_SELECT_FIRMWARE_FILE: "Firmware dosyası seçin (.bin)"
STR_SERVER_NAME: "Sunucu Adı"
STR_SET_SLEEP_COVER: "Kapak Ayarla"
STR_SHOW_NETWORKS: "Göster"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_TOP: "Üst"
STR_UPDATE_ALL: "Tümünü Güncelle"
STR_UPDATE_AVAILABLE: "Güncelle"
STR_VALIDATING_FIRMWARE: "Firmware doğrulanıyor..."
STR_XTC_STATUS_BAR: "XTC Durum Çubuğu"
+5
View File
@@ -336,6 +336,11 @@ STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS" STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
STR_DELETE_SERVER: "Видалити сервер" STR_DELETE_SERVER: "Видалити сервер"
STR_OPDS_SERVERS: "Сервери OPDS" STR_OPDS_SERVERS: "Сервери OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка завантажень"
STR_OPDS_FILENAME_FORMAT: "Формат імені файлу"
STR_FMT_AUTHOR_TITLE: "Автор - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Автор"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: " STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
STR_MANAGE_FONTS: "Керування шрифтами" STR_MANAGE_FONTS: "Керування шрифтами"
+5
View File
@@ -348,6 +348,11 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats" STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Elimina el servidor" STR_DELETE_SERVER: "Elimina el servidor"
STR_OPDS_SERVERS: "Servidors OPDS" STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts" STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts" STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..." STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
+5
View File
@@ -335,6 +335,11 @@ STR_SERVER_NAME: "Tên máy chủ"
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS" STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
STR_DELETE_SERVER: "Xóa máy chủ" STR_DELETE_SERVER: "Xóa máy chủ"
STR_OPDS_SERVERS: "Máy chủ OPDS" STR_OPDS_SERVERS: "Máy chủ OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Thư mục tải xuống"
STR_OPDS_FILENAME_FORMAT: "Định dạng tên tệp"
STR_FMT_AUTHOR_TITLE: "Tác giả - Tựa đề"
STR_FMT_TITLE_AUTHOR: "Tựa đề - Tác giả"
STR_FMT_TITLE: "Tựa đề"
STR_AUTO_TURN_ENABLED: "Tự lật trang: " STR_AUTO_TURN_ENABLED: "Tự lật trang: "
STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)" STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)"
STR_MANAGE_FONTS: "Quản lý phông chữ" STR_MANAGE_FONTS: "Quản lý phông chữ"
+4 -18
View File
@@ -22,21 +22,7 @@ constexpr char DEVICE_ID[] = "crosspoint-reader";
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative // footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
// floor. Check both total free heap and largest contiguous block so fragmented // floor. Check both total free heap and largest contiguous block so fragmented
// heap does not fall through into a failed TLS allocation path. // heap does not fall through into a failed TLS allocation path.
// MEMFIX-PORT: TLS heap gate; portable constexpr uint32_t MIN_HEAP_FOR_TLS = 55000;
// Field data (July 2026): launching sync from a reader session lands at
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
// so an optimistic attempt degrades to the same clean "sync failed" as the
// gate — the gate only needs to keep out states where a doomed handshake
// would waste tens of seconds, not guarantee success.
//
// Free and largest-block have separate requirements: with SP ECC
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
// a run of fast-math bignums. A handshake was measured succeeding inside a
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native // Apply the shared KOSync auth headers after begin(). x-auth-* is the native
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility. // KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
@@ -53,9 +39,9 @@ void applyAuthHeaders(freeink::SecureHttpClient& http) {
bool insufficientHeap() { bool insufficientHeap() {
const uint32_t freeHeap = ESP.getFreeHeap(); const uint32_t freeHeap = ESP.getFreeHeap();
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap(); const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) { if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) {
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap, LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap,
MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS); maxAllocHeap, MIN_HEAP_FOR_TLS);
return true; return true;
} }
return false; return false;
-12
View File
@@ -65,18 +65,6 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen); einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
} }
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
}
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) { void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) { if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1); einkDisplay.requestResync(1);
-11
View File
@@ -39,17 +39,6 @@ class HalDisplay {
bool fromProgmem = false) const; bool fromProgmem = false) const;
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
// while the panel refreshes on its own. The framebuffer must stay untouched
// until waitRefreshComplete(), and the caller must rebuild the differential
// baseline before the next differential update (the tiled grayscale cleanup
// does). Panels without deferral fall back to a blocking refresh.
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
// Block until a pending deferred refresh completes (no-op when none is).
void waitRefreshComplete();
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
// false where it falls back to a blocking refresh.
bool supportsAsyncRefresh() const;
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Power management // Power management
+8 -19
View File
@@ -225,29 +225,17 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); } unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
void HalGPIO::startDeepSleep() { bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
// 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();
}
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
if (shortPressAllowed) { if (shortPressAllowed) {
// Fast path - no duration check needed // Fast path - no duration check needed
return; return true;
} }
// TODO: Intermittent edge case remains: a single tap followed by another single tap // 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. // can still power on the device. Tighten wake debounce/state handling here.
// Calibrate: subtract boot time already elapsed, assuming button held since boot // Calibrate: subtract boot time already elapsed, assuming button held since boot.
const uint16_t calibration = millis(); const unsigned long calibration = millis();
const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1; const unsigned long calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
const auto start = millis(); const auto start = millis();
inputMgr.update(); inputMgr.update();
@@ -262,11 +250,12 @@ void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
inputMgr.update(); inputMgr.update();
} while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration); } while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration);
if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) { if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) {
startDeepSleep(); return false;
} }
} else { } else {
startDeepSleep(); return false;
} }
return true;
} }
bool HalGPIO::isUsbConnected() const { bool HalGPIO::isUsbConnected() const {
+2 -5
View File
@@ -72,13 +72,10 @@ class HalGPIO {
unsigned long getHeldTime() const; unsigned long getHeldTime() const;
unsigned long getPowerButtonHeldTime() const; unsigned long getPowerButtonHeldTime() const;
// Setup wake up GPIO and enter deep sleep
void startDeepSleep();
// Verify power button was held long enough after wakeup. // Verify power button was held long enough after wakeup.
// If verification fails, enters deep sleep and does not return. // Returns true if verification succeeded, false if device should return to sleep.
// Should only be called when wakeup reason is PowerButton. // Should only be called when wakeup reason is PowerButton.
void verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed); bool verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
// Check if USB is connected // Check if USB is connected
bool isUsbConnected() const; bool isUsbConnected() const;
+2 -89
View File
@@ -13,10 +13,7 @@ framework = arduino
monitor_speed = 115200 monitor_speed = 115200
upload_speed = 921600 upload_speed = 921600
check_tool = cppcheck check_tool = cppcheck
; missingInclude (project headers) is suppressed alongside missingIncludeSystem: on a check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
; fresh CI checkout cppcheck has no resolved include paths, so it reports every
; project header as missing (~470 information-level lines) and fails the job.
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=missingInclude --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
check_skip_packages = yes check_skip_packages = yes
board_upload.flash_size = 16MB board_upload.flash_size = 16MB
@@ -45,14 +42,7 @@ build_flags =
-DWOLFSSL_OPTIONS_H -DWOLFSSL_OPTIONS_H
-DWOLFSSL_CLIENT_EXAMPLE -DWOLFSSL_CLIENT_EXAMPLE
-DWOLFSSL_TLS13 -DWOLFSSL_TLS13
# MEMFIX-PORT: single-precision ECC (sp_c32.c). Without it every P-256 operation -DWOLFSSL_SP_RISCV32
# (TLS 1.3 key_share keygen, ECDHE, ECDSA cert verify) runs on fast-math bignums
# that WOLFSSL_SMALL_STACK heap-allocates at FP_MAX_BITS size -- tens of KB of
# temporaries, which OOMs (MP_MEM) at the ~50KB free heap a reading session
# leaves. SP uses fixed 256-bit arrays: a few KB, and several times faster.
# SP_SMALL trades the large precomputed point tables for smaller flash.
-DWOLFSSL_HAVE_SP_ECC
-DWOLFSSL_SP_SMALL
-DHAVE_TLS_EXTENSIONS -DHAVE_TLS_EXTENSIONS
-DHAVE_SUPPORTED_CURVES -DHAVE_SUPPORTED_CURVES
-DHAVE_HKDF -DHAVE_HKDF
@@ -63,16 +53,6 @@ build_flags =
-Wno-bidi-chars -Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity -Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
-fno-exceptions -fno-exceptions
# FreeInk panel profiles: compile both X3 (792x528/UC8253) and X4 (800x480/SSD1677);
# the firmware picks the active one at runtime via HalDisplay setDisplayX3().
-DFREEINK_DEVICE_X3=1
-DFREEINK_DEVICE_X4=1
# BLE HID page-turner host (BleKeyboardHost). NimBLE role/bond config is baked into
# the prebuilt arduino-esp32 framework's sdkconfig.h, so we don't redefine it here
# (doing so only warns and has no effect). The host only compiles when
# FREEINK_CAP_BLE_HID_HOST is set on the env (below); with the capability off,
# BleKeyboardHost links stubs and pulls in zero NimBLE code.
-DFREEINK_BLE_HID_SHOW_UNNAMED_DEVICES=0
build_unflags = build_unflags =
-std=gnu++11 -std=gnu++11
@@ -83,63 +63,6 @@ board_build.flash_mode = dio
board_build.flash_size = 16MB board_build.flash_size = 16MB
board_build.partitions = partitions.csv board_build.partitions = partitions.csv
; Shrink the NimBLE footprint for a 1-connection HID host moving 3-6 byte reports.
; Field-measured: begin() costs ~52 KB with these trims vs ~68 KB with the prebuilt
; framework defaults — and that 15 KB is the difference between the stack landing
; above the reader's render shed floor (stable coexistence) and below it (a
; guaranteed shed/restart flap). Rebuilds the Arduino core libs on first build
; (slower once, cached after; needs the CMake pin in platformio.local.ini on macOS).
custom_sdkconfig =
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=n
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=n
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=2
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=23
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=6
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=6 ; was 24 x 320 B
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=6 ; was 24 x 255 B
CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=12 ; was 30 x 70 B; only scan bursts need many
; IDF 5.5 sizes the HCI transport pools under TRANSPORT_* names; pin both spellings.
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=6
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=12
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=4 ; was 64; a HID host never does prepared writes
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
; MEMFIX-PORT: task stack right-sizing (~7 KB); NOTE develop has no
; custom_sdkconfig block — port via sdkconfig.defaults or equivalent.
; Task stack right-sizing from measured high-water marks (heap block map +
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
; Move the WiFi stack's non-critical hot paths out of IRAM into flash.
; On the C3, IRAM and DRAM share one SRAM pool, so the ~25-30 KB this
; frees lands directly in the heap — paid for with lower WiFi throughput
; during transfers (occasional sync/OTA use, not streaming: acceptable).
; IRAM cost is static, so the heap gain applies even with WiFi off.
CONFIG_ESP_WIFI_IRAM_OPT=n
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
; Keep the Arduino wrappers for the removed cloud components (below) out of
; the core source list; all other bundled libraries default to enabled.
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
CONFIG_ARDUINO_SELECTIVE_Insights=n
; Drop unused cloud components from the core rebuild. esp_insights/rainmaker
; require embedded server certs the lib builder can't generate
; ("https_server.crt.S not found"); this firmware uses none of them.
custom_component_remove =
espressif/esp_insights
espressif/esp_rainmaker
espressif/esp_diagnostics
espressif/esp_diag_data_store
espressif/esp_schedule
espressif/esp_rcp_update
espressif/esp_secure_cert_mgr
espressif/cbor
extra_scripts = extra_scripts =
pre:scripts/patch_wolfssl.py pre:scripts/patch_wolfssl.py
pre:scripts/build_html.py pre:scripts/build_html.py
@@ -154,16 +77,11 @@ lib_deps =
InputManager=symlink://freeink-sdk/libs/hardware/InputManager InputManager=symlink://freeink-sdk/libs/hardware/InputManager
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
; FreeInk HAL support libs the above depend on (BoardConfig pin maps, etc.).
BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
Rtc=symlink://freeink-sdk/libs/hardware/Rtc
Imu=symlink://freeink-sdk/libs/hardware/Imu
SecureNet=symlink://freeink-sdk/libs/network/SecureNet SecureNet=symlink://freeink-sdk/libs/network/SecureNet
FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI
Icons=symlink://freeink-sdk/libs/assets/Icons Icons=symlink://freeink-sdk/libs/assets/Icons
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
h2zero/NimBLE-Arduino @ ^2.3.8
bblanchon/ArduinoJson @ 7.4.2 bblanchon/ArduinoJson @ 7.4.2
ricmoo/QRCode @ 0.0.1 ricmoo/QRCode @ 0.0.1
bitbank2/PNGdec @ 1.1.6 bitbank2/PNGdec @ 1.1.6
@@ -181,9 +99,6 @@ build_flags =
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA) ; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
-DENABLE_SERIAL_LOG -DENABLE_SERIAL_LOG
-DLOG_LEVEL=2 ; Set log level to debug for development builds -DLOG_LEVEL=2 ; Set log level to debug for development builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
-DFREEINK_BLE_HID_SCAN_DEBUG=1 ; verbose BLE scan lifecycle/advertisement logs for bring-up
-DFREEINK_BLE_HID_REPORT_DEBUG=1 ; raw HID report hex dumps + report-map hints (bring-up)
[env:gh_release] [env:gh_release]
@@ -193,7 +108,6 @@ build_flags =
-DCROSSPOINT_VERSION=\"${crosspoint.version}\" -DCROSSPOINT_VERSION=\"${crosspoint.version}\"
-DENABLE_SERIAL_LOG -DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release builds -DLOG_LEVEL=1 ; Set log level to info for release builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
[env:gh_release_rc] [env:gh_release_rc]
extends = base extends = base
@@ -202,7 +116,6 @@ build_flags =
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\" -DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
-DENABLE_SERIAL_LOG -DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds -DLOG_LEVEL=1 ; Set log level to info for release candidate builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
[env:slim] [env:slim]
extends = base extends = base
+1 -5
View File
@@ -12,12 +12,8 @@ OVERRIDES = f"""
#ifndef HAVE_FFDHE_2048 #ifndef HAVE_FFDHE_2048
#define HAVE_FFDHE_2048 #define HAVE_FFDHE_2048
#endif #endif
/* MEMFIX-PORT: 8192 handles up to RSA-4096 keys (the public-CA maximum,
ISRG Root X1 included) with half the per-bignum heap of 16384: with
WOLFSSL_SMALL_STACK each fast-math temp is FP_MAX_BITS/8 * 2 bytes on the
heap, and TLS cert verification allocates dozens at once. */
#undef FP_MAX_BITS #undef FP_MAX_BITS
#define FP_MAX_BITS 8192 #define FP_MAX_BITS 16384
""" """
-121
View File
@@ -1,121 +0,0 @@
#include "BleInput.h"
#include <GfxRenderer.h>
#include <HalPowerManager.h>
#include <I18n.h>
#include <cstdio>
#include <cstring>
#include "MappedInputManager.h"
#include "components/UITheme.h"
namespace bleinput {
namespace {
volatile bool g_startInProgress = false;
}
// NimBLE controller init/deinit hang (interrupt WDT) if run at the 10 MHz low-power
// frequency, so force normal CPU speed around both. Centralized here so every caller
// (boot restore, settings toggle, reader toggle, sleep) is covered automatically.
bool ensureStarted() {
g_startInProgress = true;
HalPowerManager::Lock powerLock;
const bool ok = BleHid.begin(kHostName);
g_startInProgress = false;
return ok;
}
bool startInProgress() { return g_startInProgress; }
// Full teardown (NimBLE deinit), not just a link drop, so the BLE stack's RAM is
// returned to the heap — otherwise memory-hungry work like EPUB inflate can't
// allocate even after the user turns Bluetooth off.
void stop() {
HalPowerManager::Lock powerLock;
BleHid.end();
}
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value) {
if (ev.special != freeink::SpecialKey::None) {
kind = 0;
value = static_cast<uint8_t>(ev.special);
return true;
}
if (ev.keycode != 0) {
kind = 1;
value = ev.keycode;
return true;
}
return false;
}
namespace {
const char* specialName(uint8_t value) {
switch (static_cast<freeink::SpecialKey>(value)) {
case freeink::SpecialKey::Enter:
return "Enter";
case freeink::SpecialKey::Backspace:
return "Backspace";
case freeink::SpecialKey::Tab:
return "Tab";
case freeink::SpecialKey::Escape:
return "Escape";
case freeink::SpecialKey::Delete:
return "Delete";
case freeink::SpecialKey::Left:
return "Left";
case freeink::SpecialKey::Right:
return "Right";
case freeink::SpecialKey::Up:
return "Up";
case freeink::SpecialKey::Down:
return "Down";
case freeink::SpecialKey::Home:
return "Home";
case freeink::SpecialKey::End:
return "End";
case freeink::SpecialKey::PageUp:
return "Page Up";
case freeink::SpecialKey::PageDown:
return "Page Down";
default:
return nullptr;
}
}
} // namespace
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input) {
if (!BleHid.isRunning() || BleHid.isConnected()) return;
// drawPopup refreshes the panel itself, so draw once and let e-ink hold it while we
// pump the host. Holds until the remote links, the user presses a button to bail, or
// a generous timeout (a remote that slept after a disconnect needs a button to wake).
GUI.drawPopup(renderer, tr(STR_BT_CONNECTING_POPUP));
const unsigned long deadline = millis() + 10000;
while (!BleHid.isConnected() && millis() < deadline) {
BleHid.poll();
input.update();
if (input.wasAnyPressed()) break;
delay(50);
}
// Note: the caller must redraw to clear the popup. For grayscale reader pages the
// caller should also request a ghost-cleanup (HALF) refresh first — a plain fast/
// partial refresh ghosts badly over the BW popup (see Activity::requestGhostCleanup).
}
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen) {
if (!out || outLen == 0) return;
if (kind == 0) {
const char* name = specialName(value);
if (name) {
strncpy(out, name, outLen - 1);
out[outLen - 1] = '\0';
return;
}
}
// Printable ASCII usage handled as a generic key code; show the raw value.
snprintf(out, outLen, "Key 0x%02X", static_cast<unsigned>(value));
}
} // namespace bleinput
-62
View File
@@ -1,62 +0,0 @@
#pragma once
// CrossPoint <-> FreeInk BLE HID host glue.
//
// Thin, capability-safe helpers around freeink::BleKeyboardHost (the `BleHid`
// singleton). When FREEINK_CAP_BLE_HID_HOST is compiled out the SDK links stubs,
// so every call here is still valid and simply no-ops / returns false — callers
// need no #ifdefs.
//
// The (kind, value) pair produced by encodeKey() is the stable identity stored in
// CrossPointSettings::bleKeyMap. Page-turner remotes emit "special" keys
// (PageUp/PageDown/arrows); plain keyboards emit usage codes. We deliberately
// ignore modifiers and the printable char for matching (page turners don't use
// modifiers), keeping the persisted entry a trivial two-byte comparison.
#include <BleKeyboardHost.h>
#include <cstdint>
class GfxRenderer;
class MappedInputManager;
namespace bleinput {
// Advertised central name shown to peripherals during pairing.
inline constexpr const char* kHostName = "CrossPoint";
// Heap floor for starting the NimBLE stack (measured begin() cost: ~52-57 KB).
// The reader now lends the framebuffer to section builds, so BLE startup no
// longer needs to reserve the old full build headroom. Keep a modest margin and
// let the render/build shed paths handle genuinely tight moments.
inline constexpr size_t kStartMinFreeHeap = 56 * 1024;
// Lower floor for the Bluetooth settings screen, where the user has explicitly asked
// for BLE right now (scanning/pairing is dead without the stack). No page renders or
// section builds run there, so the reader-sized reserve above doesn't apply — only
// NimBLE's own ~57 KB plus working margin.
inline constexpr size_t kStartMinFreeHeapExplicit = 70 * 1024;
// Start the BLE HID host (idempotent). Returns false if BLE is compiled out or
// NimBLE init failed. Safe to call repeatedly.
bool ensureStarted();
bool startInProgress();
// Drop the active link (e.g. before deep sleep or when the user disables BT).
void stop();
// Encode a decoded key event into the stable (kind, value) identity used by the
// settings map. kind: 0 = SpecialKey, 1 = HID usage. Returns false when the event
// carries no usable identity (no special key and no usage code).
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value);
// Human-readable name for a stored (kind, value) identity, for the mapping UI.
// Writes a null-terminated string into out (e.g. "Page Down", "Key 0x4B").
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen);
// Draw a "BT Connecting..." popup and pump the BLE host until the bonded remote
// links, the user presses a button to dismiss, or a timeout. No-op if BLE isn't
// running or is already connected. The caller must redraw afterward to clear it.
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input);
} // namespace bleinput
+8 -16
View File
@@ -225,22 +225,6 @@ class CrossPointSettings {
uint8_t frontButtonConfirm = FRONT_HW_CONFIRM; uint8_t frontButtonConfirm = FRONT_HW_CONFIRM;
uint8_t frontButtonLeft = FRONT_HW_LEFT; uint8_t frontButtonLeft = FRONT_HW_LEFT;
uint8_t frontButtonRight = FRONT_HW_RIGHT; uint8_t frontButtonRight = FRONT_HW_RIGHT;
// --- Bluetooth (BLE HID page-turner) ---
// Master on/off for the BLE HID host. Persisted; auto-restored on boot/wake.
// Managed by BluetoothSettingsActivity and the in-reader "Toggle Bluetooth" menu item.
uint8_t bluetoothEnabled = 0;
// Remote-button mapping table: each slot binds a decoded BLE key identity to a
// logical MappedInputManager::Button. Fixed-capacity POD (no heap), persisted
// manually in JsonSettingsIO (like the front-button remap). 0xFF = empty/unassigned.
// Headroom for several buttons plus optional presets and rolling-code remotes
// (some buttons emit more than one code). Each entry is 3 bytes.
static constexpr uint8_t BLE_MAP_CAPACITY = 10;
struct BleKeyMapEntry {
uint8_t keyKind = 0xFF; // 0 = SpecialKey, 1 = HID usage code; 0xFF = empty slot
uint8_t keyValue = 0; // (uint8_t)freeink::SpecialKey, or the raw HID usage id
uint8_t button = 0xFF; // (uint8_t)MappedInputManager::Button; 0xFF = unassigned
};
BleKeyMapEntry bleKeyMap[BLE_MAP_CAPACITY] = {};
// Reader font settings // Reader font settings
uint8_t fontFamily = NOTOSERIF; uint8_t fontFamily = NOTOSERIF;
uint8_t fontSize = MEDIUM; uint8_t fontSize = MEDIUM;
@@ -254,6 +238,14 @@ class CrossPointSettings {
// Reader screen margin settings // Reader screen margin settings
uint8_t screenMargin = 5; uint8_t screenMargin = 5;
// OPDS download destination folder ("" = SD root). Global; edited from the
// OPDS server list. Persisted via a category-less SettingInfo::String in
// SettingsList.h, so it stays out of the on-device Settings screen.
char opdsDownloadFolder[64] = "";
// On-disk filename format for OPDS downloads (0=Author-Title default, 1=Title-Author,
// 2=Title). See OpdsFilenameFormat. Persisted via a category-less SettingInfo::Enum,
// edited from the OPDS server list; hidden from the on-device Settings screen.
uint8_t opdsFilenameFormat = 0;
// Hide battery percentage // Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER; uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior // Long-press page turn button behavior
-85
View File
@@ -1,85 +0,0 @@
#include "HeapMap.h"
#include <Arduino.h>
#include <Logging.h>
#include <Memory.h>
#include <esp_heap_caps.h>
#include <cstdio>
#include "rom/ets_sys.h"
// heap_caps_dump walks each heap inside a critical section (interrupts
// masked), so its output can neither go through the UART-0 ROM path we can't
// see nor be flow-controlled toward the CDC (the CDC ring drains in an
// interrupt handler — waiting deadlocks into the interrupt WDT,
// field-verified). Instead the ROM putc parses each line into a compact
// record; the captured table is logged after the dump with interrupts live.
namespace heapmap {
namespace {
struct BlockRec {
uint32_t addr;
uint32_t size;
bool free;
};
constexpr uint16_t kMaxRecs = 1400;
BlockRec* g_recs = nullptr; // borrowed buffer, valid only during capture
uint16_t g_recCount = 0;
bool g_overflowed = false;
char g_line[96];
uint8_t g_lineLen = 0;
void captInterpolatePutc(char c) {
if (c != '\n') {
if (g_lineLen < sizeof(g_line) - 1) g_line[g_lineLen++] = c;
return;
}
g_line[g_lineLen] = '\0';
g_lineLen = 0;
// e.g. "Block 0x3fcc69bc data, size: 89424 bytes, Free: Yes"
unsigned addr = 0, size = 0;
char freeWord[4] = {0};
if (sscanf(g_line, "Block 0x%x data, size: %u bytes, Free: %3s", &addr, &size, freeWord) == 3) {
if (g_recs && g_recCount < kMaxRecs) {
g_recs[g_recCount++] = {addr, size, freeWord[0] == 'Y'};
} else {
g_overflowed = true;
}
}
}
} // namespace
void dump() {
auto recBuf = makeUniqueNoThrow<BlockRec[]>(kMaxRecs);
if (!recBuf) {
LOG_ERR("MEM", "heap map skipped: no room for capture buffer");
return;
}
g_recs = recBuf.get();
g_recCount = 0;
g_overflowed = false;
g_lineLen = 0;
// Capture (interrupts masked inside the dump): parse into records, never
// wait. Log the table afterward with the system live. NOTE: the capture
// buffer itself appears in the map as a used block of ~17.4 KB — it frees
// on return (observer effect, do not chase it as a leak/splitter).
ets_install_putc1(&captInterpolatePutc);
heap_caps_dump(MALLOC_CAP_8BIT);
ets_install_uart_printf();
g_recs = nullptr;
LOG_DBG("MEM", "---- heap block map: %u blocks%s ----", g_recCount, g_overflowed ? " (TRUNCATED)" : "");
uint32_t dustCount = 0, dustBytes = 0;
for (uint16_t i = 0; i < g_recCount; ++i) {
const auto& r = recBuf[i];
if (r.free || r.size >= 256) {
LOG_DBG("MEM", "%s 0x%08x %u", r.free ? "FREE" : "used", r.addr, r.size);
} else {
dustCount++;
dustBytes += r.size;
}
}
LOG_DBG("MEM", "dust: %u used blocks < 256B totaling %u bytes", dustCount, dustBytes);
LOG_DBG("MEM", "---- end heap block map ----");
}
} // namespace heapmap
-10
View File
@@ -1,10 +0,0 @@
#pragma once
// MEMFIX-PORT: heap block map (on-demand via CMD:MEMMAP + reader one-shot); portable, no BLE dependency
namespace heapmap {
// Capture-and-log the DRAM heap block map (address/size/free per block,
// sub-256B used blocks rolled up as "dust"). Safe to call from the main loop;
// ~60-100 LOG_DBG lines. See HeapMap.cpp for why capture-then-log is the only
// shape that works (heap_caps_dump runs with interrupts masked).
void dump();
} // namespace heapmap
-29
View File
@@ -7,13 +7,11 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <iterator>
#include <string> #include <string>
#include "BookmarkEntry.h" #include "BookmarkEntry.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h" #include "OpdsServerStore.h"
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "SettingsList.h" #include "SettingsList.h"
@@ -145,16 +143,6 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
doc["frontButtonConfirm"] = s.frontButtonConfirm; doc["frontButtonConfirm"] = s.frontButtonConfirm;
doc["frontButtonLeft"] = s.frontButtonLeft; doc["frontButtonLeft"] = s.frontButtonLeft;
doc["frontButtonRight"] = s.frontButtonRight; doc["frontButtonRight"] = s.frontButtonRight;
// Bluetooth — managed by BluetoothSettingsActivity, not in SettingsList.
doc["bluetoothEnabled"] = s.bluetoothEnabled;
JsonArray bleMap = doc["bleKeyMap"].to<JsonArray>();
for (const auto& e : s.bleKeyMap) {
if (e.keyKind == 0xFF || e.button == 0xFF) continue; // skip empty/unassigned slots
JsonObject o = bleMap.add<JsonObject>();
o["k"] = e.keyKind;
o["v"] = e.keyValue;
o["b"] = e.button;
}
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it. // Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
doc["fontFamily"] = s.fontFamily; doc["fontFamily"] = s.fontFamily;
// SD card font family name — not in SettingsList, save manually // SD card font family name — not in SettingsList, save manually
@@ -252,23 +240,6 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
CrossPointSettings::validateFrontButtonMapping(s); CrossPointSettings::validateFrontButtonMapping(s);
// Bluetooth — managed by BluetoothSettingsActivity, not in SettingsList.
s.bluetoothEnabled = clamp(doc["bluetoothEnabled"] | (uint8_t)0, 2, 0);
std::fill(std::begin(s.bleKeyMap), std::end(s.bleKeyMap), CrossPointSettings::BleKeyMapEntry{}); // reset to empty
JsonArrayConst bleMap = doc["bleKeyMap"];
if (!bleMap.isNull()) {
uint8_t slot = 0;
for (JsonObjectConst o : bleMap) {
if (slot >= CrossPointSettings::BLE_MAP_CAPACITY) break;
const uint8_t button = o["b"] | (uint8_t)0xFF;
if (button >= MappedInputManager::kButtonCount) continue; // drop invalid mappings
s.bleKeyMap[slot].keyKind = o["k"] | (uint8_t)0xFF;
s.bleKeyMap[slot].keyValue = o["v"] | (uint8_t)0;
s.bleKeyMap[slot].button = button;
slot++;
}
}
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it. // Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0; const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0;
s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0); s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0);
+4 -93
View File
@@ -2,7 +2,6 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
bool MappedInputManager::isNavDirectionSwapped() const { bool MappedInputManager::isNavDirectionSwapped() const {
@@ -75,105 +74,17 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
return false; return false;
} }
bool MappedInputManager::bleEdge(const bool* arr, const Button button) const { bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
// Mirror mapButton()'s composite navigation handling so a BLE key bound to a
// physical direction also satisfies the derived NavNext / NavPrevious logical
// buttons (used by list navigation), respecting the orientation axis flip.
switch (button) {
case Button::NavNext:
return isNavDirectionSwapped() ? (arr[(int)Button::Up] || arr[(int)Button::Left])
: (arr[(int)Button::Down] || arr[(int)Button::Right]);
case Button::NavPrevious:
return isNavDirectionSwapped() ? (arr[(int)Button::Down] || arr[(int)Button::Right])
: (arr[(int)Button::Up] || arr[(int)Button::Left]);
default:
return arr[(int)button];
}
}
bool MappedInputManager::wasPressed(const Button button) const { bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
return mapButton(button, &HalGPIO::wasPressed) || bleEdge(blePressEdge, button);
}
bool MappedInputManager::wasReleased(const Button button) const { bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
return mapButton(button, &HalGPIO::wasReleased) || bleEdge(bleReleaseEdge, button);
}
bool MappedInputManager::isPressed(const Button button) const {
// A BLE tap is momentary: report "pressed" only on the press-edge frame.
return mapButton(button, &HalGPIO::isPressed) || bleEdge(blePressEdge, button);
}
void MappedInputManager::setBleCaptureMode(const bool on) {
bleCaptureMode = on;
bleHasCaptured = false;
if (on) {
// Clear any stale overlay so a held remote key doesn't leak into the UI.
for (uint8_t i = 0; i < kButtonCount; i++) {
blePressEdge[i] = false;
bleReleaseEdge[i] = false;
}
}
}
bool MappedInputManager::takeCapturedBleKey(uint8_t& kind, uint8_t& value) {
if (!bleHasCaptured) return false;
kind = bleCapturedKind;
value = bleCapturedValue;
bleHasCaptured = false;
return true;
}
void MappedInputManager::pollBle() {
bleActivityThisFrame = false;
// Age last frame's press edges into this frame's release edges (the FreeInk host
// surfaces presses + synthetic repeats but never releases), then clear presses. A
// pending release also counts as BLE activity this frame so getHeldTime() reports
// zero on the release frame too (page-turn handlers often fire on release).
for (uint8_t i = 0; i < kButtonCount; i++) {
bleReleaseEdge[i] = blePressEdge[i];
blePressEdge[i] = false;
if (bleReleaseEdge[i]) bleActivityThisFrame = true;
}
freeink::KeyEvent ev;
while (BleHid.popKey(ev)) {
uint8_t kind = 0xFF;
uint8_t value = 0;
if (!bleinput::encodeKey(ev, kind, value)) continue;
if (bleCaptureMode) {
bleCapturedKind = kind;
bleCapturedValue = value;
bleHasCaptured = true;
continue;
}
// Resolve the key identity against the persisted mapping table.
for (const auto& e : SETTINGS.bleKeyMap) {
if (e.button == 0xFF || e.keyKind != kind || e.keyValue != value) continue;
if (e.button < kButtonCount) {
blePressEdge[e.button] = true;
bleActivityThisFrame = true;
}
break;
}
}
}
bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); } bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); } bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); }
unsigned long MappedInputManager::getHeldTime() const { unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); }
// A BLE-mapped key is a momentary tap with no physical hold (we don't model BLE
// press-and-hold). gpio.getHeldTime() returns the *last physical* button's hold
// duration, which is stale — if a BLE edge drove input this frame, reporting that
// stale value makes a tap look like a long-press (e.g. page tap -> chapter skip).
// Report zero in that case so BLE taps are always treated as short presses.
if (bleActivityThisFrame) return 0;
return gpio.getHeldTime();
}
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const { const char* next) const {
-33
View File
@@ -7,9 +7,6 @@ class GfxRenderer;
class MappedInputManager { class MappedInputManager {
public: public:
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious }; enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
// Number of values in Button (Back..NavPrevious). Used to size the BLE overlay and
// to clamp persisted BLE mappings. Keep in sync with the enum above.
static constexpr uint8_t kButtonCount = 11;
struct Labels { struct Labels {
const char* btn1; const char* btn1;
@@ -31,23 +28,6 @@ class MappedInputManager {
// Returns the raw front button index that was pressed this frame (or -1 if none). // Returns the raw front button index that was pressed this frame (or -1 if none).
int getPressedFrontButton() const; int getPressedFrontButton() const;
// --- BLE page-turner overlay -------------------------------------------------
// Drain decoded key events from the FreeInk BLE HID host and translate the ones
// bound in SETTINGS.bleKeyMap into per-frame logical-button edges that OR into
// wasPressed()/isPressed()/wasReleased(). Call once per main-loop iteration,
// right after gpio.update() and BleHid.poll(). No-ops when BLE is compiled out.
void pollBle();
// True when a mapped BLE key produced an edge this frame — keeps the inactivity
// / auto-sleep timer alive while a remote is the only input device in use.
bool bleHadActivityThisFrame() const { return bleActivityThisFrame; }
// Capture mode: while on, pollBle() stops mapping events and instead stashes the
// raw decoded key identity so the button-mapping UI can read it without racing the
// live mapping over the single popKey() queue.
void setBleCaptureMode(bool on);
// Pop a captured (kind, value) key identity grabbed while in capture mode.
// Returns false when nothing has been captured since the last call.
bool takeCapturedBleKey(uint8_t& kind, uint8_t& value);
// True when the control axis is flipped relative to the physical buttons: the user opted into // True when the control axis is flipped relative to the physical buttons: the user opted into
// orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED / // orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED /
// LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting, // LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting,
@@ -64,17 +44,4 @@ class MappedInputManager {
const GfxRenderer& renderer; const GfxRenderer& renderer;
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
// OR-in the BLE overlay for a logical button, mirroring mapButton()'s composite
// handling of NavNext/NavPrevious so a remote key bound to Up/Down/Left/Right also
// drives list navigation.
bool bleEdge(const bool* arr, Button button) const;
// Per-frame BLE overlay, indexed by (uint8_t)Button.
bool blePressEdge[kButtonCount] = {}; // press edge this frame -> wasPressed / isPressed
bool bleReleaseEdge[kButtonCount] = {}; // release edge this frame -> wasReleased
bool bleActivityThisFrame = false;
bool bleCaptureMode = false;
bool bleHasCaptured = false;
uint8_t bleCapturedKind = 0xFF;
uint8_t bleCapturedValue = 0;
}; };
-4
View File
@@ -32,10 +32,6 @@ class SdCardFontSystem {
/// Non-const access to the registry (for FontInstaller). /// Non-const access to the registry (for FontInstaller).
SdCardFontRegistry& registry() { return registry_; } SdCardFontRegistry& registry() { return registry_; }
// MEMFIX-PORT: font system audit passthrough; portable
/// Resident heap held by loaded SD fonts (audit; see SdCardFont::reportMemory).
size_t reportFontMemory() const { return manager_.reportMemory(); }
/// Mark the registry as needing re-discovery. /// Mark the registry as needing re-discovery.
/// Thread-safe: can be called from the web server task. /// Thread-safe: can be called from the web server task.
void markRegistryDirty() { registryDirty_.store(true, std::memory_order_release); } void markRegistryDirty() { registryDirty_.store(true, std::memory_order_release); }
+10
View File
@@ -194,6 +194,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder, SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder,
"moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM), "moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM),
// OPDS download folder: persisted + web-exposed, but category-less so it
// is hidden from the on-device Settings screen (edited via OPDS UI).
SettingInfo::String(StrId::STR_OPDS_DOWNLOAD_FOLDER, &SETTINGS.opdsDownloadFolder[0],
sizeof(SETTINGS.opdsDownloadFolder), "opdsDownloadFolder"),
// OPDS download filename format: persisted + web-exposed, category-less so it
// is hidden from the on-device Settings screen (cycled from the OPDS UI).
SettingInfo::Enum(StrId::STR_OPDS_FILENAME_FORMAT, &CrossPointSettings::opdsFilenameFormat,
{StrId::STR_FMT_AUTHOR_TITLE, StrId::STR_FMT_TITLE_AUTHOR, StrId::STR_FMT_TITLE},
"opdsFilenameFormat"),
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- // --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString( SettingInfo::DynamicString(
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
-4
View File
@@ -6,7 +6,3 @@
void silentRestart(); // home screen void silentRestart(); // home screen
void silentRestartToReader(); // currently-open EPUB (APP_STATE.openEpubPath) void silentRestartToReader(); // currently-open EPUB (APP_STATE.openEpubPath)
// True when this boot itself came from a silent restart. Callers that restart
// as a last-resort defrag must check this so a failure that survives the
// restart degrades to an error instead of a reboot loop.
bool bootWasSilentRestart();
-11
View File
@@ -44,17 +44,6 @@ class Activity {
virtual bool skipLoopDelay() { return false; } virtual bool skipLoopDelay() { return false; }
virtual bool preventAutoSleep() { return false; } virtual bool preventAutoSleep() { return false; }
virtual bool isReaderActivity() const { return false; } virtual bool isReaderActivity() const { return false; }
// True if this activity needs the BLE stack resident (beyond the readers, which are
// covered by isReaderActivity()). The Bluetooth settings screen overrides this so
// pairing/scanning works there. Everywhere else BLE is torn down to free heap.
virtual bool keepsBluetoothAlive() const { return false; }
// True while the current activity is doing heap-heavy work that must finish
// before the BLE stack (~52 KB) may start.
virtual bool deferBluetoothStart() const { return false; }
// Ask the activity to make its next render a full ghost-cleanup (HALF) refresh rather
// than a fast/partial one. Used after drawing a transient popup over grayscale content
// (e.g. the "BT Connecting..." popup over a reader page) so it clears without ghosting.
virtual void requestGhostCleanup() {}
virtual ScreenshotInfo getScreenshotInfo() const { return {}; } virtual ScreenshotInfo getScreenshotInfo() const { return {}; }
// Start a new activity without destroying the current one // Start a new activity without destroying the current one
-19
View File
@@ -256,25 +256,6 @@ bool ActivityManager::isReaderActivity() const {
(currentActivity && currentActivity->isReaderActivity()); (currentActivity && currentActivity->isReaderActivity());
} }
bool ActivityManager::currentKeepsBluetoothAlive() const {
return currentActivity && currentActivity->keepsBluetoothAlive();
}
void ActivityManager::requestGhostCleanup() {
if (currentActivity) currentActivity->requestGhostCleanup();
}
bool ActivityManager::bluetoothShouldBeActive() const {
const auto wants = [](const auto& activity) {
return activity && (activity->isReaderActivity() || activity->keepsBluetoothAlive());
};
return std::any_of(stackActivities.begin(), stackActivities.end(), wants) || wants(currentActivity);
}
bool ActivityManager::bluetoothStartDeferred() const {
return currentActivity && currentActivity->deferBluetoothStart();
}
bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); }
ScreenshotInfo ActivityManager::getScreenshotInfo() const { ScreenshotInfo ActivityManager::getScreenshotInfo() const {
-13
View File
@@ -102,16 +102,6 @@ class ActivityManager {
bool preventAutoSleep() const; bool preventAutoSleep() const;
bool isReaderActivity() const; bool isReaderActivity() const;
bool currentKeepsBluetoothAlive() const;
// True if BLE should be resident for the current context: any reader (page-turner
// input) or the Bluetooth settings screen (pairing) is on the stack.
bool bluetoothShouldBeActive() const;
// True while the CURRENT activity is mid heap-heavy work that must complete before
// NimBLE may start (see Activity::deferBluetoothStart). Current only, not the
// stack: a reader stacked under a menu has its loop() paused, so its build never
// advances — a stack-wide check would hold BLE off for as long as the menu stays
// open.
bool bluetoothStartDeferred() const;
bool skipLoopDelay() const; bool skipLoopDelay() const;
ScreenshotInfo getScreenshotInfo() const; ScreenshotInfo getScreenshotInfo() const;
@@ -119,9 +109,6 @@ class ActivityManager {
// Otherwise, it will be deferred until the end of the current loop iteration. // Otherwise, it will be deferred until the end of the current loop iteration.
void requestUpdate(bool immediate = false); void requestUpdate(bool immediate = false);
// Ask the current activity to make its next render a ghost-cleanup (HALF) refresh.
void requestGhostCleanup();
// Trigger a render and block until it completes. // Trigger a render and block until it completes.
// Must NOT be called from the render task or while holding a RenderLock. // Must NOT be called from the render task or while holding a RenderLock.
void requestUpdateAndWait(); void requestUpdateAndWait();
@@ -2,11 +2,13 @@
#include <Arduino.h> #include <Arduino.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h> #include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <OpdsStream.h> #include <OpdsStream.h>
#include <WiFi.h> #include <WiFi.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "SilentRestart.h" #include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h" #include "activities/network/WifiSelectionActivity.h"
@@ -15,6 +17,7 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h" #include "util/BookCacheUtils.h"
#include "util/OpdsFilename.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
#include "util/UrlUtils.h" #include "util/UrlUtils.h"
@@ -279,8 +282,26 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
// Build full download URL relative to the current feed, not the root server URL // Build full download URL relative to the current feed, not the root server URL
const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath); const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath);
std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href); std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href);
std::string filename = // opdsDownloadFolder is already a null-terminated char[64]; use it directly —
"/" + StringUtils::sanitizeFilename((book.author.empty() ? "" : book.author + " - ") + book.title) + ".epub"; // no std::string copy. exists()/mkdir() take const char*.
const char* folder = SETTINGS.opdsDownloadFolder; // "" => SD root
bool haveFolder = folder[0] != '\0';
if (haveFolder && !Storage.exists(folder) && !Storage.mkdir(folder)) {
// exists()-guard first: mkdir's return-on-existing is unconfirmed, and every
// existing caller checks exists() before mkdir. On real failure, fall back
// to SD root so the download is never lost.
LOG_ERR("OPDS", "mkdir failed for %s, using SD root", folder);
haveFolder = false;
}
// downloadToFile() needs a std::string, and titles are unbounded (a fixed
// char[] would truncate). Cold path (a multi-second download follows), so one
// reserve'd, in-place-appended owning string is the right call.
std::string filename;
filename.reserve(96);
if (haveFolder) filename += folder;
filename += '/';
filename += opdsBookFilename(book.author, book.title, static_cast<OpdsFilenameFormat>(SETTINGS.opdsFilenameFormat));
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
int lastRenderedPercent = -1; int lastRenderedPercent = -1;
@@ -18,8 +18,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "util/QrUtils.h" #include "util/QrUtils.h"
#include "BleInput.h"
namespace { namespace {
// AP Mode configuration // AP Mode configuration
constexpr const char* AP_SSID = "CrossPoint-Reader"; constexpr const char* AP_SSID = "CrossPoint-Reader";
@@ -143,12 +141,6 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
if (mode == NetworkMode::JOIN_NETWORK) { if (mode == NetworkMode::JOIN_NETWORK) {
// STA mode - launch WiFi selection // STA mode - launch WiFi selection
LOG_DBG("WEBACT", "Turning on WiFi (STA mode)..."); LOG_DBG("WEBACT", "Turning on WiFi (STA mode)...");
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
state = WebServerActivityState::WIFI_SELECTION; state = WebServerActivityState::WIFI_SELECTION;
@@ -201,13 +193,6 @@ void CrossPointWebServerActivity::startAccessPoint() {
LOG_DBG("WEBACT", "Starting Access Point mode..."); LOG_DBG("WEBACT", "Starting Access Point mode...");
LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap());
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
// Configure and start the AP // Configure and start the AP
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
delay(100); delay(100);
@@ -8,7 +8,6 @@
#include <algorithm> #include <algorithm>
#include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "WifiCredentialStore.h" #include "WifiCredentialStore.h"
@@ -98,11 +97,6 @@ void WifiSelectionActivity::startWifiScan(const bool autoScan) {
networks.clear(); networks.clear();
requestUpdate(); requestUpdate();
// Free the BLE stack before bringing WiFi up: the C3 has one radio and the two
// stacks can't both fit in heap. Reachable from the reader via KOReader sync, where
// BLE is still resident; a no-op when BLE is already off (launched from Settings).
bleinput::stop();
// Set WiFi mode to station // Set WiFi mode to station
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.disconnect(); WiFi.disconnect();
@@ -355,7 +349,6 @@ void WifiSelectionActivity::attemptConnection() {
connectionError.clear(); connectionError.clear();
requestUpdate(); requestUpdate();
bleinput::stop(); // free the BLE stack before WiFi (shared C3 radio, tight heap)
WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID
+48 -381
View File
@@ -17,7 +17,6 @@
#include <iterator> #include <iterator>
#include <limits> #include <limits>
#include "BleInput.h"
#include "BookmarkEntry.h" #include "BookmarkEntry.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
@@ -33,15 +32,10 @@
#include "QrDisplayActivity.h" #include "QrDisplayActivity.h"
#include "ReaderUtils.h" #include "ReaderUtils.h"
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "SilentRestart.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/BookmarkUtil.h" #include "util/BookmarkUtil.h"
#include "util/ScreenshotUtil.h" #include "util/ScreenshotUtil.h"
#include "SdCardFontSystem.h"
#include <esp_heap_caps.h>
#include "rom/ets_sys.h"
#include "HeapMap.h"
namespace { namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
@@ -71,55 +65,6 @@ bool isInReadFolder(const std::string& path) {
return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/'; return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/';
} }
class FrameBufferBuildLoan {
public:
explicit FrameBufferBuildLoan(GfxRenderer& renderer) : renderer_(renderer) {}
~FrameBufferBuildLoan() {
if (active_ && !restore()) {
ESP.restart();
}
}
void release() {
if (active_ || !renderer_.hasFrameBuffer()) return;
if (bleinput::startInProgress()) {
LOG_INF("ERS", "Framebuffer loan waiting for BLE start to settle");
const uint32_t deadline = millis() + 1000;
while (bleinput::startInProgress() && millis() < deadline) {
delay(5);
}
}
renderer_.releaseFrameBufferForBuild();
active_ = true;
LOG_DBG("ERS", "Framebuffer lent for section build (ble=%u heap=%u maxAlloc=%u)", BleHid.isRunning() ? 1 : 0,
(unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
}
bool restore() {
if (!active_) return true;
active_ = false;
if (renderer_.restoreFrameBufferAfterBuild()) {
LOG_DBG("ERS", "Framebuffer restored after section build");
return true;
}
if (BleHid.isRunning()) {
LOG_INF("ERS", "Framebuffer restore needs heap; freeing BLE and retrying (heap=%u maxAlloc=%u)",
(unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
bleinput::stop();
if (renderer_.restoreFrameBufferAfterBuild()) {
LOG_DBG("ERS", "Framebuffer restored after freeing BLE");
return true;
}
}
LOG_ERR("ERS", "Framebuffer restore failed after section build");
return false;
}
private:
GfxRenderer& renderer_;
bool active_ = false;
};
struct ProgressRange { struct ProgressRange {
float start; float start;
float end; float end;
@@ -212,8 +157,6 @@ void EpubReaderActivity::onEnter() {
ImageBlock::clearSessionRenderFailures(); ImageBlock::clearSessionRenderFailures();
statusBarBleConnected = BleHid.isConnected();
// Configure screen orientation based on settings // Configure screen orientation based on settings
// NOTE: This affects layout math and must be applied before any render calls. // NOTE: This affects layout math and must be applied before any render calls.
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
@@ -314,32 +257,6 @@ void EpubReaderActivity::openReaderMenu() {
}); });
} }
bool EpubReaderActivity::buildTickHeapGate() {
const size_t freeHeap = ESP.getFreeHeap();
const size_t maxBlock = ESP.getMaxAllocHeap();
if (freeHeap >= BACKGROUND_BUILD_MIN_FREE_HEAP && maxBlock >= BACKGROUND_BUILD_MIN_MAX_ALLOC) {
return true;
}
const size_t lendableFrameBuffer = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0;
if (lendableFrameBuffer > 0 && freeHeap + lendableFrameBuffer >= BACKGROUND_BUILD_MIN_FREE_HEAP &&
maxBlock + lendableFrameBuffer >= BACKGROUND_BUILD_MIN_MAX_ALLOC) {
return true;
}
// Below the floors. If the BLE stack is what's squeezing the heap, shed it — the
// established policy on this branch is that builds and resident BLE don't coexist,
// and this was the one build path without that protection (field crash: a tick's
// parse allocation aborted at maxAlloc ~11 KB with BLE resident). The lifecycle's
// build-pending deferral keeps BLE down until the window is caught up, then
// restarts it behind the start floor. Without BLE resident, just wait: page-turn
// transients free up between turns and the tick retries every loop pass.
if (BleHid.isRunning()) {
LOG_INF("ERS", "Background build needs heap (free=%u maxAlloc=%u); freeing BLE RAM", (unsigned)freeHeap,
(unsigned)maxBlock);
bleinput::stop();
}
return false;
}
void EpubReaderActivity::loop() { void EpubReaderActivity::loop() {
if (!epub) { if (!epub) {
// Should never happen // Should never happen
@@ -347,48 +264,6 @@ void EpubReaderActivity::loop() {
return; return;
} }
// Swap the "BT connecting" status bar placeholder back to the chapter/book
// title the moment the connection completes (and back on disconnect). Skips
// while a render is in flight or the framebuffer is lent to a build; the
// next page render shows the right state anyway.
if (SETTINGS.bluetoothEnabled && section && renderer.hasFrameBuffer() && !RenderLock::peek()) {
const bool connected = BleHid.isConnected();
if (connected != statusBarBleConnected) {
statusBarBleConnected = connected;
renderStatusBar();
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
}
// Idle glyph prewarm for the likely next page (currentPage + 1). The scan
// pass draws nothing (FCM scan mode suppresses pixels), so the displayed
// framebuffer is untouched; endScanAndPrewarm loads only glyphs not already
// cached. Debounced past rapid page-flipping, one attempt per position, and
// deferred while a render/build owns the CPU or the heap is at the render
// floor. Cross-chapter prewarm is deliberately out of scope (next spine's
// section isn't loaded).
constexpr unsigned long IDLE_PREWARM_DEBOUNCE_MS = 400;
if (section && !section->isBuilding() && !RenderLock::peek() && renderer.hasFrameBuffer() &&
lastRenderCompleteMs != 0 && millis() - lastRenderCompleteMs > IDLE_PREWARM_DEBOUNCE_MS &&
ESP.getFreeHeap() > RENDER_MIN_FREE_HEAP &&
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
idlePrewarmSpine = currentSpineIndex;
idlePrewarmPage = section->currentPage;
const int nextPage = section->currentPage + 1;
if (nextPage < static_cast<int>(section->pageCount)) {
RenderLock lock; // the page table must not change under the scan
if (const auto p = section->loadPage(nextPage)) {
if (auto* fcm = renderer.getFontCacheManager()) {
const auto t0 = millis();
auto scope = fcm->createPrewarmScope();
p->render(renderer, SETTINGS.getReaderFontId(), 0, 0); // scan only, no pixels
scope.endScanAndPrewarm();
LOG_DBG("ERS", "Idle prewarm: page %d in %lums", nextPage, millis() - t0);
}
}
}
}
// Lazily resume a partial's extension build once the reader nears its watermark. Far from // Lazily resume a partial's extension build once the reader nears its watermark. Far from
// it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this // it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this
// session, so reopening a partial deliberately does NOT start it (see the deferral in // session, so reopening a partial deliberately does NOT start it (see the deferral in
@@ -424,8 +299,7 @@ void EpubReaderActivity::loop() {
// "far enough ahead" and stall the build at 0 pages -- then the first turn past the // "far enough ahead" and stall the build at 0 pages -- then the first turn past the
// watermark re-parses the whole chapter synchronously. Keep ticking until it finalizes. // watermark re-parses the whole chapter synchronously. Keep ticking until it finalizes.
if (section && section->isBuilding() && !RenderLock::peek() && if (section && section->isBuilding() && !RenderLock::peek() &&
(section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) && (section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD)) {
buildTickHeapGate()) {
RenderLock lock; RenderLock lock;
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the // Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
// build between the outer isBuilding() check and acquiring the lock here, in which case // build between the outer isBuilding() check and acquiring the lock here, in which case
@@ -433,8 +307,6 @@ void EpubReaderActivity::loop() {
// mutation, so it flags this as always true. // mutation, so it flags this as always true.
// cppcheck-suppress knownConditionTrueFalse // cppcheck-suppress knownConditionTrueFalse
if (section->isBuilding()) { if (section->isBuilding()) {
FrameBufferBuildLoan buildLoan(renderer);
buildLoan.release();
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) { if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
LOG_ERR("ERS", "Background section build failed"); LOG_ERR("ERS", "Background section build failed");
section.reset(); section.reset();
@@ -444,9 +316,6 @@ void EpubReaderActivity::loop() {
// real page count, so re-render at the remapped page. No-op for an unchanged resume. // real page count, so re-render at the remapped page. No-op for an unchanged resume.
requestUpdate(); requestUpdate();
} }
if (!buildLoan.restore()) {
ESP.restart();
}
} }
} }
@@ -1048,17 +917,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
return; return;
} }
FrameBufferBuildLoan buildLoan(renderer);
// If BLE leaves the reader below the render floor, lend the framebuffer before
// deserializing/loading the page instead of tearing BLE down immediately. The
// restore path still frees BLE if the framebuffer cannot be reallocated.
if (BleHid.isRunning() && ESP.getFreeHeap() < RENDER_MIN_FREE_HEAP) {
LOG_INF("ERS", "Render heap %u below floor %u; lending framebuffer", (unsigned)ESP.getFreeHeap(),
(unsigned)RENDER_MIN_FREE_HEAP);
buildLoan.release();
}
const auto showPendingSyncSaveError = [this]() { const auto showPendingSyncSaveError = [this]() {
if (!pendingSyncSaveError) return; if (!pendingSyncSaveError) return;
pendingSyncSaveError = false; pendingSyncSaveError = false;
@@ -1068,11 +926,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// A section build failure (e.g. an invalid/corrupt EPUB that fails XML parsing) leaves the // A section build failure (e.g. an invalid/corrupt EPUB that fails XML parsing) leaves the
// "Indexing" popup on screen with no way forward. Surface an explicit error instead of hanging. // "Indexing" popup on screen with no way forward. Surface an explicit error instead of hanging.
// clearScreen first so the error popup doesn't overlay the stale "Indexing" popup. // clearScreen first so the error popup doesn't overlay the stale "Indexing" popup.
const auto showBuildError = [this, &buildLoan]() { const auto showBuildError = [this]() {
if (!buildLoan.restore()) {
ESP.restart();
return;
}
renderer.clearScreen(); renderer.clearScreen();
GUI.drawPopup(renderer, tr(STR_INDEX_FAILED)); GUI.drawPopup(renderer, tr(STR_INDEX_FAILED));
automaticPageTurnActive = false; automaticPageTurnActive = false;
@@ -1157,44 +1011,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
LOG_DBG("ERS", "Cache not found, building..."); LOG_DBG("ERS", "Cache not found, building...");
} }
// The layout code (line-break DP arrays, CSS lookups, glyph buffers) allocates freely
// and abort()s on OOM under -fno-exceptions, so a starved heap must be handled BEFORE
// the build: pre-flight the floor and take the recovery path up front instead of
// crashing mid-parse. Field data: builds succeed at ~46 KB free with BLE resident;
// abort() observed at ~11 KB free.
const size_t lendableFrameBuffer = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0;
const bool heapTooLow = ESP.getFreeHeap() + lendableFrameBuffer < BUILD_MIN_FREE_HEAP;
if (heapTooLow) {
LOG_ERR("ERS", "Pre-build heap %u (+fb %u) below floor %u; entering build recovery",
(unsigned)ESP.getFreeHeap(), (unsigned)lendableFrameBuffer, (unsigned)BUILD_MIN_FREE_HEAP);
}
// Building a section needs a large contiguous inflate (deflate) window that the
// resident NimBLE stack fragments out of existence (~16 KB max block with BT on).
// On build failure (or a pre-flight floor miss) with BT enabled: free the BLE stack
// and retry. The chapter is cached afterwards, so this recovery runs at most once
// per uncached chapter.
// Deliberately do NOT restart BLE inline: this render still has its own allocations
// to make. The main-loop lifecycle restarts BLE later, behind its activity,
// render-lock, framebuffer, and heap gates.
const auto retryWithBleFreed = [&](auto&& buildFn) {
LOG_INF("ERS", "Section build needs heap; freeing BLE RAM and retrying");
bleinput::stop();
return buildFn();
};
// Even with BLE freed, the build can fail when this session's parse churn has
// fragmented the heap beyond in-place recovery. A silent restart is the only
// real defrag on this heap (no compaction); it resumes into this book and
// rebuilds the section on a fresh heap. Guarded by bootWasSilentRestart() so a
// build that fails again after the restart degrades to the error popup below
// instead of reboot-looping.
const auto silentRestartDefrag = [&]() {
if (bootWasSilentRestart()) return;
LOG_ERR("ERS", "Section build failed after BLE recovery; silent restart to defrag heap");
silentRestartToReader();
};
// Jumps that need the final pagination or the anchor map -- explicit page jumps, // Jumps that need the final pagination or the anchor map -- explicit page jumps,
// fragment anchors, percent jumps, and cross-setting progress repositioning -- can't // fragment anchors, percent jumps, and cross-setting progress repositioning -- can't
// resolve their landing page until the whole chapter is laid out, so they take the full // resolve their landing page until the whole chapter is laid out, so they take the full
@@ -1221,18 +1037,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// Lend the framebuffer's 48 KB to the blocking full build; restored // Lend the framebuffer's 48 KB to the blocking full build; restored
// (white) at scope exit, and the page render below redraws everything. // (white) at scope exit, and the page render below redraws everything.
GfxRenderer::FrameBufferLoan loan(renderer); GfxRenderer::FrameBufferLoan loan(renderer);
const auto buildSection = [&]() { if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
return section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn);
};
bool built = !heapTooLow && buildSection();
if (!built && SETTINGS.bluetoothEnabled) {
built = retryWithBleFreed(buildSection);
}
if (!built) {
silentRestartDefrag();
LOG_ERR("ERS", "Failed to persist page data to SD"); LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset(); section.reset();
loan.end(); // restore before anything draws loan.end(); // restore before anything draws
@@ -1290,21 +1098,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// background buildSomeMore chunks in loop() do NOT get the loan: they // background buildSomeMore chunks in loop() do NOT get the loan: they
// deliberately interleave with page renders. Restored before render. // deliberately interleave with page renders. Restored before render.
GfxRenderer::FrameBufferLoan loan(renderer); GfxRenderer::FrameBufferLoan loan(renderer);
// startBuild does the zip inflate (the big contiguous allocation), so it gets if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
// the BLE free-and-retry fallback too; it cleans up fully on failure, making a SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
// retry safe. viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
const auto beginBuild = [&]() { SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
return section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled);
};
bool started = !heapTooLow && beginBuild();
if (!started && SETTINGS.bluetoothEnabled) {
started = retryWithBleFreed(beginBuild);
}
if (!started) {
silentRestartDefrag();
LOG_ERR("ERS", "Failed to start section build"); LOG_ERR("ERS", "Failed to start section build");
section.reset(); section.reset();
loan.end(); // restore before anything draws (showBuildError renders a popup) loan.end(); // restore before anything draws (showBuildError renders a popup)
@@ -1379,7 +1176,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) { while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) {
// Start a build to extend a partial toward the requested page. // Start a build to extend a partial toward the requested page.
buildLoan.release();
if (!section->isBuilding() && if (!section->isBuilding() &&
!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), !section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
@@ -1402,7 +1198,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
// For an in-progress incremental build, make sure the page we're about to show has been laid out. // For an in-progress incremental build, make sure the page we're about to show has been laid out.
if (section->isBuilding()) { if (section->isBuilding()) {
buildLoan.release();
while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) { while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) {
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) { if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
LOG_ERR("ERS", "Failed during incremental section build"); LOG_ERR("ERS", "Failed during incremental section build");
@@ -1413,14 +1208,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
} }
const auto restoreFramebufferForDraw = [&buildLoan]() {
if (!buildLoan.restore()) {
ESP.restart();
return false;
}
return true;
};
// The requested page is now as built as it will get. If it still lands past the end, // The requested page is now as built as it will get. If it still lands past the end,
// clamp to the last real page: the UINT16_MAX "last page" sentinel from backward chapter // clamp to the last real page: the UINT16_MAX "last page" sentinel from backward chapter
// navigation, an explicit jump beyond a finished chapter, or a stale saved position. // navigation, an explicit jump beyond a finished chapter, or a stale saved position.
@@ -1435,10 +1222,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion. // a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
applyDeferredReposition(); applyDeferredReposition();
renderer.clearScreen();
if (section->pageCount == 0) { if (section->pageCount == 0) {
LOG_DBG("ERS", "No pages to render"); LOG_DBG("ERS", "No pages to render");
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD);
renderStatusBar(); renderStatusBar();
renderer.displayBuffer(); renderer.displayBuffer();
@@ -1449,8 +1236,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) { if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount); LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD);
renderStatusBar(); renderStatusBar();
renderer.displayBuffer(); renderer.displayBuffer();
@@ -1479,7 +1264,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (giveUp) { if (giveUp) {
LOG_ERR("ERS", "Page load retry limit reached, aborting"); LOG_ERR("ERS", "Page load retry limit reached, aborting");
pageLoadRetryCount = 0; // Reset so a later user-initiated navigation can try afresh pageLoadRetryCount = 0; // Reset so a later user-initiated navigation can try afresh
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
@@ -1495,48 +1279,9 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// Collect footnotes from the loaded page // Collect footnotes from the loaded page
currentPageFootnotes = std::move(p->footnotes); currentPageFootnotes = std::move(p->footnotes);
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen();
const auto start = millis(); const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
LOG_DBG("ERS", "Rendered page in %dms", millis() - start); LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
lastRenderCompleteMs = millis();
// Fragmentation tracker: free vs largest block after every page. A falling
// maxAlloc/free ratio across pages points at whichever allocation pattern the
// preceding lines show (mini rebuilds, kern reloads, BLE churn).
LOG_DBG("MEM", "post-render: free=%u maxAlloc=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
{
// MEMFIX-PORT: per-render heap owner audit + one-shot block map; portable
// Heap audit: attribute resident heap to its owners so margin work is
// measurement-driven. `other` = IDF/Arduino baseline + SdFat + epub
// metadata + BLE (when on) + anything not yet instrumented.
const size_t fontBytes = sdFontSystem.reportFontMemory();
const size_t sectionBytes = section ? section->residentBytes() : 0;
const size_t epubBytes = epub ? epub->residentBytes() : 0;
const size_t fbBytes = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0;
const uint32_t heapTotal = ESP.getHeapSize();
const uint32_t heapFree = ESP.getFreeHeap();
LOG_DBG("MEM",
"audit: total=%u free=%u maxAlloc=%u fb=%u fonts=%u section=%u epub=%u cssRules=%u building=%d ble=%d "
"other=%d",
heapTotal, heapFree, (unsigned)ESP.getMaxAllocHeap(), (unsigned)fbBytes, (unsigned)fontBytes,
(unsigned)sectionBytes, (unsigned)epubBytes, (unsigned)(epub ? epub->cssRuleCount() : 0),
section && section->isBuilding() ? 1 : 0, BleHid.isRunning() ? 1 : 0,
(int)(heapTotal - heapFree - fbBytes - fontBytes - sectionBytes - epubBytes));
// One-shot heap block map for fragmentation analysis: dumps every block
// (address/size/free) once per boot, at the first settled (non-building)
// audit, so mid-heap long-lived allocations identify themselves. ~2-4 s
// of serial output; debug builds only.
static bool heapMapDumped = false;
if (!heapMapDumped && !(section && section->isBuilding())) {
heapMapDumped = true;
// Landmarks: known owners' addresses, so map blocks self-identify.
LOG_DBG("MEM", "landmark framebuffer=%p section=%p epub=%p activity=%p", renderer.getFrameBuffer(),
static_cast<void*>(section.get()), static_cast<void*>(epub.get()), static_cast<void*>(this));
heapmap::dump();
}
}
} }
// Only persist when the position actually changed. render() also runs on menu, // Only persist when the position actually changed. render() also runs on menu,
// bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes. // bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes.
@@ -1606,11 +1351,6 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode(); const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode();
const bool needsTextGrayscale = SETTINGS.textAntiAliasing; const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages; const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale();
// Whole-plane buffering only pays when the BW refresh genuinely runs async
// underneath it; on blocking panels it would just spend ~50 KB for the
// identical serial timing.
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh();
auto renderGrayscalePass = [&]() { auto renderGrayscalePass = [&]() {
if (needsTextGrayscale) { if (needsTextGrayscale) {
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
@@ -1656,67 +1396,50 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// regardless of residue. // regardless of residue.
pagesUntilFullRefresh = 1; pagesUntilFullRefresh = 1;
} else { } else {
// Deferred when a tiled grayscale pass follows: the plane rendering below ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
// then overlaps the panel's refresh time instead of following it.
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, /*async=*/overlapRefresh);
} }
const auto tDisplay = millis(); const auto tDisplay = millis();
// Tiled grayscale: render each plane band-by-band, leaving the BW // Tiled grayscale: render each plane band-by-band into a small scratch and
// framebuffer intact so no full-frame storeBwBuffer is needed; controller // stream straight to the controller, leaving the BW framebuffer intact so no
// RAM is re-synced from the live framebuffer afterward. The page is // full-frame storeBwBuffer is needed; controller RAM is re-synced from the
// re-rendered ceil(H/STRIP_ROWS) times per plane, but renderCharImpl culls // live framebuffer afterward. The page is re-rendered ceil(H/STRIP_ROWS) times
// out-of-band glyphs before decode so the cost stays close to one render. // per plane, but renderCharImpl culls out-of-band glyphs before decode so the
// Both text (drawPixel) and images (DirectPixelWriter) honor the active // cost stays close to one render. Both text (drawPixel) and images
// strip target. When the BW refresh above went out async, the plane // (DirectPixelWriter) honor the active strip target.
// rendering below overlaps the panel's refresh time; only the controller if (needsAnyGrayscale && renderer.supportsStripGrayscale()) {
// RAM writes wait for BUSY.
if (tiledGrayscale) {
constexpr int STRIP_ROWS = 80; constexpr int STRIP_ROWS = 80;
const int gh = renderer.getDisplayHeight(); const int gh = renderer.getDisplayHeight();
const int gwBytes = renderer.getDisplayWidthBytes(); const int gwBytes = renderer.getDisplayWidthBytes();
const size_t planeBytes = static_cast<size_t>(gwBytes) * gh;
// Render one plane band-by-band into a whole-plane buffer without touching auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
// the controller, so it can run while the refresh is still in flight. if (!scratch) {
auto renderPlaneToBuffer = [&](const bool lsbPlane, uint8_t* buf) { LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS);
renderer.setRenderMode(lsbPlane ? GfxRenderer::GRAYSCALE_LSB : GfxRenderer::GRAYSCALE_MSB); } else {
// Bands may be streamed in any order: X4 windows each via setRamArea, X3
// via PTL.
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
for (int y = 0; y < gh; y += STRIP_ROWS) { for (int y = 0; y < gh; y += STRIP_ROWS) {
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
renderer.beginStripTarget(buf + static_cast<size_t>(y) * gwBytes, y, rows); renderer.beginStripTarget(scratch.get(), y, rows);
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderGrayscalePass(); renderGrayscalePass();
renderer.endStripTarget(); renderer.endStripTarget();
renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows);
} }
}; const auto tGrayLsb = millis();
// Tiered on heap pressure: two plane buffers hide both plane renders // MSB plane.
// inside the refresh wait; one hides the LSB render (its buffer is reused renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
// for MSB after streaming); none falls back to the strip-scratch flow with for (int y = 0; y < gh; y += STRIP_ROWS) {
// no overlap. The MSB buffer is only attempted when it leaves ~60 KB free const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
// so the pass never starves concurrent allocations (BLE especially). renderer.beginStripTarget(scratch.get(), y, rows);
// Blocking panels skip the buffers entirely (nothing to overlap). renderer.clearScreen(0x00);
auto lsbPlaneBuf = overlapRefresh ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr; renderGrayscalePass();
auto msbPlaneBuf = (lsbPlaneBuf && ESP.getFreeHeap() >= planeBytes + 60000) renderer.endStripTarget();
? makeUniqueNoThrow<uint8_t[]>(planeBytes) renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows);
: nullptr;
if (lsbPlaneBuf) {
renderPlaneToBuffer(true, lsbPlaneBuf.get());
if (msbPlaneBuf) renderPlaneToBuffer(false, msbPlaneBuf.get());
const auto tGrayRender = millis();
renderer.waitRefreshComplete();
const auto tWait = millis();
renderer.writeGrayscalePlaneStrip(true, lsbPlaneBuf.get(), 0, gh);
if (msbPlaneBuf) {
renderer.writeGrayscalePlaneStrip(false, msbPlaneBuf.get(), 0, gh);
} else {
renderPlaneToBuffer(false, lsbPlaneBuf.get());
renderer.writeGrayscalePlaneStrip(false, lsbPlaneBuf.get(), 0, gh);
} }
const auto tGrayWrite = millis(); const auto tGrayMsb = millis();
renderer.setRenderMode(GfxRenderer::BW); renderer.setRenderMode(GfxRenderer::BW);
renderer.displayGrayBuffer(); renderer.displayGrayBuffer();
@@ -1725,63 +1448,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// BW framebuffer is intact; re-sync controller RAM for the next // BW framebuffer is intact; re-sync controller RAM for the next
// differential page turn directly from it. // differential page turn directly from it.
renderer.cleanupGrayscaleWithFrameBuffer(); renderer.cleanupGrayscaleWithFrameBuffer();
const auto tCleanup = millis();
const auto tEnd = millis(); const auto tEnd = millis();
LOG_DBG("ERS", LOG_DBG("ERS",
"Page render (tiled async): prewarm=%lums bw_render=%lums display=%lums gray_render=%lums " "Page render (tiled): prewarm=%lums bw_render=%lums display=%lums gray_lsb=%lums "
"wait=%lums gray_write=%lums gray_display=%lums cleanup=%lums total=%lums (planes buffered: %d)", "gray_msb=%lums gray_display=%lums cleanup=%lums total=%lums",
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayRender - tDisplay, tWait - tGrayRender, tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb,
tGrayWrite - tWait, tGrayDisplay - tGrayWrite, tEnd - tGrayDisplay, tEnd - t0, msbPlaneBuf ? 2 : 1); tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0);
} else {
// Per-strip scratch tier: blocking panels and the OOM fallback. The
// strip writes below need the panel idle, so wait out any pending async
// refresh first (no-op on blocking panels).
auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
renderer.waitRefreshComplete();
if (!scratch) {
LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS);
} else {
// Bands may be streamed in any order: X4 windows each via setRamArea,
// X3 via PTL.
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
for (int y = 0; y < gh; y += STRIP_ROWS) {
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
renderer.beginStripTarget(scratch.get(), y, rows);
renderer.clearScreen(0x00);
renderGrayscalePass();
renderer.endStripTarget();
renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows);
}
const auto tGrayLsb = millis();
// MSB plane.
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
for (int y = 0; y < gh; y += STRIP_ROWS) {
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
renderer.beginStripTarget(scratch.get(), y, rows);
renderer.clearScreen(0x00);
renderGrayscalePass();
renderer.endStripTarget();
renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows);
}
const auto tGrayMsb = millis();
renderer.setRenderMode(GfxRenderer::BW);
renderer.displayGrayBuffer();
const auto tGrayDisplay = millis();
// BW framebuffer is intact; re-sync controller RAM for the next
// differential page turn directly from it.
renderer.cleanupGrayscaleWithFrameBuffer();
const auto tCleanup = millis();
const auto tEnd = millis();
LOG_DBG("ERS",
"Page render (tiled): prewarm=%lums bw_render=%lums display=%lums gray_lsb=%lums "
"gray_msb=%lums gray_display=%lums cleanup=%lums total=%lums",
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb,
tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0);
}
} }
} else { } else {
// Fallback path for a controller without strip support. grayscale rendering // Fallback path for a controller without strip support. grayscale rendering
@@ -1869,15 +1543,8 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle(); title = epub->getTitle();
} }
if (SETTINGS.bluetoothEnabled && !BleHid.isConnected()) {
// Take over the title slot entirely while connecting; the watcher in
// loop() redraws the bar on the connect/disconnect flip, restoring the
// chapter/book title.
title = tr(STR_BT_CONNECTING_POPUP);
}
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked, GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
section->isBuilding(), BleHid.isConnected()); section->isBuilding());
} }
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
@@ -41,18 +41,6 @@ class EpubReaderActivity final : public Activity {
bool showBookmarkMessage = false; bool showBookmarkMessage = false;
bool ignoreNextConfirmRelease = false; bool ignoreNextConfirmRelease = false;
bool currentPageBookmarked = false; bool currentPageBookmarked = false;
// BLE connection state the status bar last rendered with; loop() watches for
// a flip and redraws the bar so the "BT connecting" placeholder swaps back
// to the chapter/book title the moment the connection completes (and
// returns on disconnect) instead of waiting for the next page turn.
bool statusBarBleConnected = false;
// Idle-time glyph prewarm: after a page settles, scan the LIKELY next page
// (scan mode draws nothing) and load its missing glyphs from SD during idle,
// so the next turn's in-render prewarm is a cache hit instead of ~100 ms of
// SD reads on the page-turn critical path. One attempt per position.
int idlePrewarmSpine = -1;
int idlePrewarmPage = -1;
unsigned long lastRenderCompleteMs = 0;
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text) bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
std::vector<BookmarkEntry> cachedBookmarks; std::vector<BookmarkEntry> cachedBookmarks;
// Tracks whether this book is currently removed from Recent Books by the // Tracks whether this book is currently removed from Recent Books by the
@@ -75,21 +63,6 @@ class EpubReaderActivity final : public Activity {
SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {}; SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {};
int footnoteDepth = 0; int footnoteDepth = 0;
// Heap floor for entering a section build. The layout code allocates freely (line-break DP
// arrays sized by word count, CSS rule lookups, glyph buffers) and under -fno-exceptions an
// OOM there abort()s the firmware instead of failing cleanly -- so a starved heap must be
// handled *before* the build, not after. Field data: builds succeed at ~46 KB free with BLE
// resident; abort() observed at ~11 KB free. CSS styling already degrades below 48 KB
// (MIN_FREE_HEAP_FOR_CSS), so 40 KB trades a few early BLE teardowns for not crashing.
static constexpr size_t BUILD_MIN_FREE_HEAP = 40 * 1024;
// Heap floor for rendering a page at all. Page deserialization (TextBlock word
// vectors/strings) and glyph caching allocate through throwing paths that abort()
// on OOM; below this floor render() sheds the BLE stack (~52 KB back, and it
// restores a large contiguous block) before touching the page. Field data: a
// session with no shed ground to <2.2 KB free and aborted on a page load.
static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024;
// Viewport of the last render(), captured so loop()'s lazy partial-extension start // Viewport of the last render(), captured so loop()'s lazy partial-extension start
// builds with IDENTICAL layout parameters to the pages already rendered (a mismatch // builds with IDENTICAL layout parameters to the pages already rendered (a mismatch
// would paginate differently than the partial being extended). 0 = no render yet. // would paginate differently than the partial being extended). 0 = no render yet.
@@ -113,28 +86,6 @@ class EpubReaderActivity final : public Activity {
// background build chunk never noticeably delays input or a pending render. // background build chunk never noticeably delays input or a pending render.
static constexpr int BUILD_PAGES_PER_CHUNK = 8; static constexpr int BUILD_PAGES_PER_CHUNK = 8;
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2; static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
// Skip background build ticks below this free-heap floor. The parse path grows
// word vectors of heap strings — throwing allocations that abort() on OOM under
// -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a
// background tick with the BLE stack resident). The tick is deferrable work:
// page-turn transients free up between turns and the build resumes; the render
// path still builds the page it actually needs regardless of this floor.
// Calibrated BETWEEN the measured states: steady reading with BLE resident runs at
// ~29.4 KB free / ~16.4 KB largest block (ticks are safe there — a 2-page parse
// transient is a few KB), while the field crash happened at 34.7 KB free with an
// ~11 KB largest block. A first cut at 32 KB/16 KB sat just ABOVE the healthy
// steady state, guaranteeing a pointless BLE shed the moment any build work was
// pending (the maxAlloc floor fired on a 12-byte shortfall).
static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 26 * 1024;
// Fragmentation floor for the same gate: free heap says how much memory exists;
// maxAlloc says whether any single allocation can actually have it.
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 13 * 1024;
// Gate for a background build tick: true when the heap can take parse allocations.
// When BLE is what's squeezing the heap, sheds it (build-pending deferral in the
// lifecycle then holds restarts off until the window is caught up) instead of
// stalling the build forever below the floors.
bool buildTickHeapGate();
// How many pages to keep laid out ahead of the reader for a still-building section. A page // How many pages to keep laid out ahead of the reader for a still-building section. A page
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder // turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
// -- a tiny buffer is enough. The background build stops once the watermark is this far // -- a tiny buffer is enough. The background build stops once the watermark is this far
@@ -195,7 +146,6 @@ class EpubReaderActivity final : public Activity {
// it from page 0. Reverts to normal power behavior the moment the build finishes. // it from page 0. Reverts to normal power behavior the moment the build finishes.
bool skipLoopDelay() override { return section && section->isBuilding(); } bool skipLoopDelay() override { return section && section->isBuilding(); }
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
void requestGhostCleanup() override { pagesUntilFullRefresh = 1; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
CrossPointPosition getCurrentPosition() const; CrossPointPosition getCurrentPosition() const;
}; };
@@ -2,12 +2,8 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h> #include <I18n.h>
#include <Logging.h>
#include "BleInput.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "SilentRestart.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
@@ -26,7 +22,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
bool hasBookmarks) { bool hasBookmarks) {
std::vector<MenuItem> items; std::vector<MenuItem> items;
items.reserve(13); items.reserve(12);
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
if (hasFootnotes) { if (hasFootnotes) {
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
@@ -40,7 +36,6 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
items.push_back({MenuAction::TOGGLE_BLUETOOTH, StrId::STR_TOGGLE_BLUETOOTH});
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
@@ -90,24 +85,6 @@ void EpubReaderMenuActivity::loop() {
return; return;
} }
if (selectedAction == MenuAction::TOGGLE_BLUETOOTH) {
// Just flip the preference and stay in the menu. The main-loop lifecycle check
// brings the BLE stack up/down to match, so start/stop has a single owner.
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
SETTINGS.saveToFile();
// Turning BT on below the lifecycle's heap floor would otherwise wait
// until the heap happens to recover -- which a long session's fragmentation never
// gives back. The user asked for BT *now*: silent-restart into this book to
// defrag (fresh boot is ~118 KB free, comfortably above the floor), and BT
// auto-starts on the way back in.
if (SETTINGS.bluetoothEnabled && !BleHid.isRunning() && ESP.getFreeHeap() < bleinput::kStartMinFreeHeap) {
LOG_INF("ERM", "BT enabled below heap floor (%u); silent restart to defrag", ESP.getFreeHeap());
silentRestartToReader();
}
requestUpdate();
return;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption}); setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
finish(); finish();
return; return;
@@ -159,12 +136,6 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
} else if (value == MenuAction::AUTO_PAGE_TURN) { } else if (value == MenuAction::AUTO_PAGE_TURN) {
// Render current page turn value on the right edge of the content area. // Render current page turn value on the right edge of the content area.
return pageTurnLabels[selectedPageTurnOption]; return pageTurnLabels[selectedPageTurnOption];
} else if (value == MenuAction::TOGGLE_BLUETOOTH) {
if (SETTINGS.bluetoothEnabled) {
if (!BleHid.isRunning()) return tr(STR_CONNECTING);
return BleHid.isConnected() ? tr(STR_STATE_ON) : tr(STR_CONNECTING);
}
return tr(STR_STATE_OFF);
} else { } else {
return ""; return "";
} }
@@ -24,8 +24,7 @@ class EpubReaderMenuActivity final : public Activity {
DISPLAY_QR, DISPLAY_QR,
GO_HOME, GO_HOME,
SYNC, SYNC,
DELETE_CACHE, DELETE_CACHE
TOGGLE_BLUETOOTH
}; };
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
+15 -3
View File
@@ -208,15 +208,27 @@ void KOReaderSyncActivity::performUpload() {
// Optionally include document metadata (KOReader PR #15306) // Optionally include document metadata (KOReader PR #15306)
if (KOREADER_STORE.getSendMetadata()) { if (KOREADER_STORE.getSendMetadata()) {
// The Epub is released before the sync network calls and is only reloaded on the
// remote-progress path (performSync). When uploading from NO_REMOTE_PROGRESS the
// Epub is still null, so reload it here and guard the title/author reads to avoid
// dereferencing a null Epub. Filename is derived from the path and is always safe.
ensureEpubLoaded();
KOReaderMetadata meta; KOReaderMetadata meta;
// Extract filename from path
const auto lastSlash = epubPath.rfind('/'); const auto lastSlash = epubPath.rfind('/');
meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath; meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath;
meta.title = epub->getTitle(); if (epub) {
meta.authors = epub->getAuthor(); meta.title = epub->getTitle();
meta.authors = epub->getAuthor();
} else {
LOG_ERR("KOSync", "Epub unavailable for metadata; sending filename only");
}
progress.metadata = std::move(meta); progress.metadata = std::move(meta);
} }
// Release the Epub before the network call so the TLS handshake has enough free heap
// (consistent with the release-before-sync pattern in performSync); nothing below needs it.
epub.reset();
const auto result = KOReaderSyncClient::updateProgress(progress); const auto result = KOReaderSyncClient::updateProgress(progress);
// Drop the radio while user reads the result; full teardown happens at silent reboot. // Drop the radio while user reads the result; full teardown happens at silent reboot.
+1 -2
View File
@@ -31,6 +31,5 @@ class ReaderActivity final : public Activity {
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath) explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath)
: Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)) {} : Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)) {}
void onEnter() override; void onEnter() override;
bool isReaderActivity() const override { return false; } bool isReaderActivity() const override { return true; }
bool deferBluetoothStart() const override { return true; }
}; };
+3 -12
View File
@@ -59,21 +59,12 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
return {prev, next, tiltPrev || tiltNext}; return {prev, next, tiltPrev || tiltNext};
} }
// One helper, blocking or deferred: the async form starts the refresh and inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
// returns so the caller can overlap CPU work with the panel's refresh time.
// Async callers must not touch the framebuffer until
// renderer.waitRefreshComplete() and must rebuild the differential baseline
// before the next page turn (the tiled grayscale cleanup does).
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh, bool async = false) {
const auto mode = (pagesUntilFullRefresh <= 1) ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH;
if (async) {
renderer.displayBufferAsync(mode);
} else {
renderer.displayBuffer(mode);
}
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else { } else {
renderer.displayBuffer();
pagesUntilFullRefresh--; pagesUntilFullRefresh--;
} }
} }
@@ -49,6 +49,5 @@ class TxtReaderActivity final : public Activity {
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
void requestGhostCleanup() override { pagesUntilFullRefresh = 1; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
}; };
@@ -46,6 +46,5 @@ class XtcReaderActivity final : public Activity {
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
void requestGhostCleanup() override { pagesUntilFullRefresh = 1; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
}; };
@@ -1,167 +0,0 @@
#include "BleButtonMapActivity.h"
#include <GfxRenderer.h>
#include <algorithm>
#include <cstdio>
#include <iterator>
#include "BleInput.h"
#include "CrossPointSettings.h"
#include "components/UITheme.h"
#include "fontIds.h"
// Logical functions offered for binding. Page navigation + confirm cover Free2 /
// Free3; the directions are included so a remote can also drive menu navigation.
const BleButtonMapActivity::Fn BleButtonMapActivity::kFunctions[] = {
{MappedInputManager::Button::PageForward, StrId::STR_BT_PAGE_FORWARD},
{MappedInputManager::Button::PageBack, StrId::STR_BT_PAGE_BACK},
{MappedInputManager::Button::Confirm, StrId::STR_CONFIRM},
{MappedInputManager::Button::Back, StrId::STR_BACK},
{MappedInputManager::Button::Up, StrId::STR_DIR_UP},
{MappedInputManager::Button::Down, StrId::STR_DIR_DOWN},
{MappedInputManager::Button::Left, StrId::STR_DIR_LEFT},
{MappedInputManager::Button::Right, StrId::STR_DIR_RIGHT},
};
const uint8_t BleButtonMapActivity::kFunctionCount = static_cast<uint8_t>(sizeof(kFunctions) / sizeof(kFunctions[0]));
void BleButtonMapActivity::onEnter() {
Activity::onEnter();
step = Step::WaitForKey;
capturedKind = 0xFF;
functionIndex = 0;
// Start every mapping session from a clean slate: the user re-maps each remote
// button once, so a button can't be left bound to a stale action and there's no
// separate "clear mappings" step to remember.
std::fill(std::begin(SETTINGS.bleKeyMap), std::end(SETTINGS.bleKeyMap), CrossPointSettings::BleKeyMapEntry{});
SETTINGS.saveToFile();
mappedInput.setBleCaptureMode(true);
requestUpdate();
}
void BleButtonMapActivity::onExit() {
mappedInput.setBleCaptureMode(false);
Activity::onExit();
}
bool BleButtonMapActivity::assignCapturedKey(MappedInputManager::Button button) {
const uint8_t btn = static_cast<uint8_t>(button);
// Mutated via std::replace_if below and through `slot`; cppcheck's CI parse
// (no include paths) can't see the writes and suggests const.
// cppcheck-suppress constVariableReference
auto& map = SETTINGS.bleKeyMap;
using Entry = CrossPointSettings::BleKeyMapEntry;
const uint8_t kind = capturedKind;
const uint8_t value = capturedValue;
// One key per action: drop any other key currently bound to this action so the same
// action can't be triggered by two different remote buttons.
std::replace_if(
std::begin(map), std::end(map),
[&](const Entry& e) { return e.button == btn && !(e.keyKind == kind && e.keyValue == value); }, Entry{});
// Reuse the slot already bound to this key, else the first free slot.
auto* slot = std::find_if(std::begin(map), std::end(map), [&](const Entry& e) {
return e.button != 0xFF && e.keyKind == kind && e.keyValue == value;
});
if (slot == std::end(map)) {
slot = std::find_if(std::begin(map), std::end(map),
[](const Entry& e) { return e.button == 0xFF || e.keyKind == 0xFF; });
}
if (slot == std::end(map)) return false; // table full
slot->keyKind = kind;
slot->keyValue = value;
slot->button = btn;
SETTINGS.saveToFile();
return true;
}
void BleButtonMapActivity::loop() {
// Front Back button exits the mapping screen at any step.
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
if (step == Step::WaitForKey) {
uint8_t kind = 0xFF;
uint8_t value = 0;
if (mappedInput.takeCapturedBleKey(kind, value)) {
capturedKind = kind;
capturedValue = value;
functionIndex = 0;
step = Step::SelectFunction;
requestUpdate();
}
return;
}
// Step::SelectFunction — pick a logical function for the captured key.
buttonNavigator.onNext([this] {
functionIndex = ButtonNavigator::nextIndex(functionIndex, kFunctionCount);
requestUpdate();
});
buttonNavigator.onPrevious([this] {
functionIndex = ButtonNavigator::previousIndex(functionIndex, kFunctionCount);
requestUpdate();
});
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
assignCapturedKey(kFunctions[functionIndex].button);
// Back to capturing so the user can map (or re-map) the next remote button.
step = Step::WaitForKey;
capturedKind = 0xFF;
requestUpdate();
}
}
void BleButtonMapActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_BT_MAP_BUTTONS));
const int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
if (step == Step::WaitForKey) {
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
tr(STR_BT_PRESS_REMOTE));
// Show the current mappings so the user sees progress.
int row = 0;
for (const auto& e : SETTINGS.bleKeyMap) {
if (e.button == 0xFF) continue;
char keyName[24];
bleinput::describeKey(e.keyKind, e.keyValue, keyName, sizeof(keyName));
const char* fnName = "";
for (uint8_t i = 0; i < kFunctionCount; i++) {
if (static_cast<uint8_t>(kFunctions[i].button) == e.button) {
fnName = I18N.get(kFunctions[i].label);
break;
}
}
char line[64];
snprintf(line, sizeof(line), "%s -> %s", keyName, fnName);
GUI.drawHelpText(renderer, Rect{0, topOffset + row * 22, pageWidth, 20}, line);
row++;
}
} else {
char captured[24];
bleinput::describeKey(capturedKind, capturedValue, captured, sizeof(captured));
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
captured);
GUI.drawList(
renderer, Rect{0, topOffset, pageWidth, contentHeight}, kFunctionCount, functionIndex,
[this](int i) { return std::string(I18N.get(kFunctions[i].label)); }, nullptr, nullptr, nullptr, false);
}
const char* confirm = step == Step::WaitForKey ? "" : tr(STR_SELECT);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirm, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
@@ -1,47 +0,0 @@
#pragma once
#include <I18n.h>
#include <cstdint>
#include "MappedInputManager.h"
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
// Capture-then-assign mapping for BLE page-turner buttons. The user presses a
// button on the remote; we capture its decoded key identity (via the
// MappedInputManager BLE capture mode) and let them bind it to a logical button.
// Repeat to map each remote button; Back exits. Mirrors ButtonRemapActivity's
// flow, but the input source is the BLE host instead of the front buttons.
class BleButtonMapActivity final : public Activity {
public:
explicit BleButtonMapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("BleButtonMap", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
private:
// Logical functions a remote button can be bound to.
struct Fn {
MappedInputManager::Button button;
StrId label;
};
static const Fn kFunctions[];
static const uint8_t kFunctionCount;
enum class Step { WaitForKey, SelectFunction };
Step step = Step::WaitForKey;
uint8_t capturedKind = 0xFF;
uint8_t capturedValue = 0;
int functionIndex = 0;
ButtonNavigator buttonNavigator;
// Bind the captured key to the chosen logical button in SETTINGS.bleKeyMap and
// persist. Returns false when the table is full and the key is new.
bool assignCapturedKey(MappedInputManager::Button button);
};
@@ -1,314 +0,0 @@
#include "BluetoothSettingsActivity.h"
#include <BleKeyboardHost.h>
#include <GfxRenderer.h>
#include <Logging.h>
#include <cstdio>
#include "BleButtonMapActivity.h"
#include "BleInput.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
constexpr unsigned long kBannerMs = 2000;
constexpr uint32_t kScanMs = 8000;
constexpr unsigned long kForgetHoldMs = 1200; // hold Confirm this long in the Paired view to forget
} // namespace
void BluetoothSettingsActivity::onEnter() {
Activity::onEnter();
view = View::Menu;
menuIndex = 0;
rebuildMenuRows();
requestUpdate();
}
void BluetoothSettingsActivity::onExit() {
if (BleHid.isScanning()) BleHid.stopScan();
Activity::onExit();
}
void BluetoothSettingsActivity::setBanner(const char* text) {
banner = text ? text : "";
bannerUntil = millis() + kBannerMs;
}
void BluetoothSettingsActivity::rebuildMenuRows() {
menuRows.clear();
menuRows.reserve(8);
menuRows.push_back({Action::ToggleBt, StrId::STR_BLUETOOTH});
if (SETTINGS.bluetoothEnabled) {
menuRows.push_back({Action::Scan, StrId::STR_BT_SCAN_PAIR});
if (BleHid.isConnected()) menuRows.push_back({Action::Disconnect, StrId::STR_BT_DISCONNECT});
menuRows.push_back({Action::PairedDevices, StrId::STR_BT_PAIRED_DEVICES});
menuRows.push_back({Action::MapButtons, StrId::STR_BT_MAP_BUTTONS});
}
if (menuIndex >= static_cast<int>(menuRows.size())) menuIndex = 0;
}
void BluetoothSettingsActivity::startScanView() {
LOG_INF("BLEUI", "scan view: begin running=%d scanning=%d devices=%u paired=%u", BleHid.isRunning(),
BleHid.isScanning(), BleHid.deviceCount(), BleHid.pairedCount());
view = View::Scan;
scanIndex = 0;
awaitingConnect = false;
lastLoggedScanState = false;
lastLoggedDeviceCount = 0xFF;
// The main-loop lifecycle owns steady-state start/stop, but a scan needs the stack
// this instant — entering this screen can precede the lifecycle's next tick, or its
// heap gate may have deferred the start. ensureStarted() is idempotent, and with
// this screen on top the lifecycle keeps the stack up (keepsBluetoothAlive).
if (!BleHid.isRunning() && !bleinput::ensureStarted()) {
LOG_ERR("BLEUI", "scan: BLE start failed (heap=%u)", ESP.getFreeHeap());
}
BleHid.startScan(kScanMs);
LOG_INF("BLEUI", "scan view: startScan requested scanning=%d devices=%u", BleHid.isScanning(), BleHid.deviceCount());
requestUpdate();
}
void BluetoothSettingsActivity::handleMenuConfirm() {
if (menuRows.empty()) return;
const Action action = menuRows[menuIndex].action;
switch (action) {
case Action::ToggleBt:
// Flip the preference only; the main-loop lifecycle check starts/stops the BLE
// stack to match (and shows the "BT Connecting..." popup). Single owner.
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
SETTINGS.saveToFile();
rebuildMenuRows();
requestUpdate();
break;
case Action::Scan:
startScanView();
break;
case Action::Disconnect:
BleHid.disconnect();
setBanner(tr(STR_BT_NOT_CONNECTED));
rebuildMenuRows();
requestUpdate();
break;
case Action::PairedDevices:
view = View::Paired;
pairedIndex = 0;
requestUpdate();
break;
case Action::MapButtons:
startActivityForResult(std::make_unique<BleButtonMapActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
rebuildMenuRows();
requestUpdate();
});
break;
}
}
void BluetoothSettingsActivity::loop() {
// Clear an expired status banner.
if (bannerUntil > 0 && millis() > bannerUntil) {
banner.clear();
bannerUntil = 0;
requestUpdate();
}
// Watch for an async connect result (from either the scan list or the paired list).
if (awaitingConnect) {
char reason[48];
if (BleHid.isConnected()) {
awaitingConnect = false;
BleHid.releaseScanResults();
view = View::Menu;
rebuildMenuRows();
char buf[64];
snprintf(buf, sizeof(buf), tr(STR_BT_CONNECTED_TO), BleHid.connectedName());
setBanner(buf);
requestUpdate();
} else if (BleHid.takeConnectFailure(reason, sizeof(reason))) {
awaitingConnect = false;
setBanner(reason);
requestUpdate();
}
}
// Back returns to the menu from a sub-view, or leaves the screen from the menu.
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
if (view == View::Menu) {
finish();
} else {
if (BleHid.isScanning()) BleHid.stopScan();
view = View::Menu;
rebuildMenuRows();
requestUpdate();
}
return;
}
// Navigation within the active list.
const int count = view == View::Menu ? static_cast<int>(menuRows.size())
: view == View::Scan ? BleHid.deviceCount()
: BleHid.pairedCount();
int* idx = view == View::Menu ? &menuIndex : view == View::Scan ? &scanIndex : &pairedIndex;
buttonNavigator.onNext([this, count, idx] {
if (count > 0) *idx = ButtonNavigator::nextIndex(*idx, count);
requestUpdate();
});
buttonNavigator.onPrevious([this, count, idx] {
if (count > 0) *idx = ButtonNavigator::previousIndex(*idx, count);
requestUpdate();
});
// Paired view: tap Confirm to connect, hold Confirm to forget. Uses release for
// connect so a hold can fire forget without also connecting on the same press.
if (view == View::Paired) {
if (mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
if (!pairedActionTaken && mappedInput.getHeldTime() >= kForgetHoldMs && pairedIndex < BleHid.pairedCount()) {
const auto& p = BleHid.paired(static_cast<uint8_t>(pairedIndex));
BleHid.forget(p.addr);
if (pairedIndex > 0) pairedIndex--;
setBanner(tr(STR_FORGET_BUTTON));
pairedActionTaken = true;
rebuildMenuRows();
requestUpdate();
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (!pairedActionTaken && !awaitingConnect && pairedIndex < BleHid.pairedCount()) {
const auto& p = BleHid.paired(static_cast<uint8_t>(pairedIndex));
awaitingConnect = true;
setBanner(tr(STR_CONNECTING));
BleHid.connect(p.addr);
requestUpdate();
}
pairedActionTaken = false;
}
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (view == View::Menu) {
handleMenuConfirm();
} else if (view == View::Scan) {
const int scanCount = BleHid.deviceCount();
if (!awaitingConnect && scanCount == 0 && !BleHid.isScanning()) {
LOG_INF("BLEUI", "scan view: restart scan requested");
if (!BleHid.isRunning() && !bleinput::ensureStarted()) {
LOG_ERR("BLEUI", "scan restart: BLE start failed (heap=%u)", ESP.getFreeHeap());
}
BleHid.startScan(kScanMs);
LOG_INF("BLEUI", "scan view: restart scan state scanning=%d devices=%u", BleHid.isScanning(),
BleHid.deviceCount());
requestUpdate();
} else if (!awaitingConnect && scanIndex < scanCount) {
if (BleHid.isScanning()) BleHid.stopScan();
const auto& d = BleHid.device(static_cast<uint8_t>(scanIndex));
LOG_INF("BLEUI", "scan view: connect addr=%s name='%s' rssi=%d type=%u hid=%d conn=%d", d.addr, d.name, d.rssi,
d.addrType, d.hid, d.connectable);
awaitingConnect = true;
setBanner(tr(STR_CONNECTING));
BleHid.connect(d.addr);
requestUpdate();
}
}
return;
}
// The scan list changes as devices are discovered — keep repainting while active.
if (view == View::Scan) {
const bool scanning = BleHid.isScanning();
const uint8_t deviceCount = BleHid.deviceCount();
if (scanning != lastLoggedScanState || deviceCount != lastLoggedDeviceCount) {
LOG_INF("BLEUI", "scan view: state scanning=%d devices=%u", scanning, deviceCount);
lastLoggedScanState = scanning;
lastLoggedDeviceCount = deviceCount;
}
if (scanning) requestUpdate();
}
}
std::string BluetoothSettingsActivity::deviceLabel(int index) const {
if (index >= BleHid.deviceCount()) return "";
const auto& d = BleHid.device(static_cast<uint8_t>(index));
return std::string(d.name);
}
std::string BluetoothSettingsActivity::pairedLabel(int index) const {
if (index >= BleHid.pairedCount()) return "";
const auto& p = BleHid.paired(static_cast<uint8_t>(index));
return std::string(p.name);
}
void BluetoothSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
const char* title = tr(STR_BLUETOOTH);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title);
// Sub-header: connection status.
const char* status = BleHid.isConnected() ? BleHid.connectedName() : tr(STR_BT_NOT_CONNECTED);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
status);
const int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
const Rect listRect{0, topOffset, pageWidth, contentHeight};
if (view == View::Menu) {
GUI.drawList(
renderer, listRect, static_cast<int>(menuRows.size()), menuIndex,
[this](int i) { return std::string(I18N.get(menuRows[i].label)); }, nullptr, nullptr,
[this](int i) -> std::string {
if (menuRows[i].action == Action::ToggleBt)
return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF);
return "";
},
true);
} else if (view == View::Scan) {
// Free2/3 remotes only advertise in the right slider mode — tell the user how.
GUI.drawHelpText(renderer, Rect{0, topOffset, pageWidth, 16}, tr(STR_BT_FREE_HINT1));
GUI.drawHelpText(renderer, Rect{0, topOffset + 16, pageWidth, 16}, tr(STR_BT_FREE_HINT2));
const int scanTop = topOffset + 38;
const int count = BleHid.deviceCount();
if (count == 0) {
GUI.drawHelpText(renderer, Rect{0, scanTop, pageWidth, 24},
BleHid.isScanning() ? tr(STR_SCANNING) : tr(STR_BT_NO_DEVICES));
} else {
GUI.drawList(
renderer, Rect{0, scanTop, pageWidth, contentHeight - 38}, count, scanIndex,
[this](int i) { return deviceLabel(i); }, nullptr, nullptr, nullptr, false);
}
} else { // Paired
const int count = BleHid.pairedCount();
if (count == 0) {
GUI.drawHelpText(renderer, Rect{0, topOffset + metrics.verticalSpacing, pageWidth, 24}, tr(STR_BT_NO_PAIRED));
} else {
GUI.drawList(
renderer, listRect, count, pairedIndex, [this](int i) { return pairedLabel(i); }, nullptr, nullptr, nullptr,
false);
}
}
// Transient banner above the hints.
if (!banner.empty()) {
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20}, banner.c_str());
}
// In the paired list, Confirm connects and a hold forgets — surface the hold hint.
if (view == View::Paired && BleHid.pairedCount() > 0 && banner.empty()) {
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20},
tr(STR_BT_FORGET_PROMPT));
}
// Button hints differ by view (Menu selects; Scan and Paired both connect).
const bool scanCanRestart = view == View::Scan && BleHid.deviceCount() == 0 && !BleHid.isScanning();
const char* confirm = view == View::Menu ? tr(STR_SELECT) : scanCanRestart ? "Scan" : tr(STR_CONNECT);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirm, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
@@ -1,65 +0,0 @@
#pragma once
#include <I18n.h>
#include <string>
#include <vector>
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
// Bluetooth page-turner settings. One screen with three views:
// Menu — enable/disable BT, scan & pair, disconnect, map buttons, presets.
// Scan — live list of discovered BLE HID devices; Confirm connects.
// Paired — bonded devices; Confirm forgets the selected one.
// All BLE access goes through the FreeInk BleHid singleton; everything no-ops
// gracefully when BLE is compiled out (BleHid.begin() returns false).
class BluetoothSettingsActivity final : public Activity {
public:
explicit BluetoothSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("BluetoothSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
bool keepsBluetoothAlive() const override { return true; }
private:
enum class View { Menu, Scan, Paired };
// Menu row actions.
enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices };
struct MenuRow {
Action action;
StrId label;
};
View view = View::Menu;
std::vector<MenuRow> menuRows;
int menuIndex = 0;
int scanIndex = 0;
int pairedIndex = 0;
ButtonNavigator buttonNavigator;
// Transient status banner (connect result, forget confirmation, etc.).
std::string banner;
unsigned long bannerUntil = 0;
// Set when a connect() has been issued and we're waiting for the async result.
bool awaitingConnect = false;
// Guards the Paired view's hold-to-forget so it fires once per hold and suppresses
// the tap-to-connect on the same press.
bool pairedActionTaken = false;
bool lastLoggedScanState = false;
uint8_t lastLoggedDeviceCount = 0xFF;
void rebuildMenuRows();
void handleMenuConfirm();
void startScanView();
void setBanner(const char* text);
std::string deviceLabel(int index) const; // scan list row text
std::string pairedLabel(int index) const; // paired list row text
};
@@ -17,8 +17,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
#include "BleInput.h"
FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {} : Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {}
@@ -26,12 +24,6 @@ FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputMan
void FontDownloadActivity::onEnter() { void FontDownloadActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
@@ -3,19 +3,51 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h> #include <I18n.h>
#include <cstring>
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OpdsServerStore.h" #include "OpdsServerStore.h"
#include "OpdsSettingsActivity.h" #include "OpdsSettingsActivity.h"
#include "activities/ActivityManager.h" #include "activities/ActivityManager.h"
#include "activities/browser/OpdsBookBrowserActivity.h" #include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/OpdsFilename.h"
namespace {
// Normalizes a user-typed folder: trims spaces, "" => SD root, otherwise a
// single leading '/' and no trailing '/'. Cold path (runs once per edit).
std::string normalizeFolder(std::string v) {
while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.erase(v.begin());
while (!v.empty() && (v.back() == ' ' || v.back() == '\t')) v.pop_back();
if (v.empty()) return "";
if (v.front() != '/') v.insert(v.begin(), '/');
while (v.size() > 1 && v.back() == '/') v.pop_back();
if (v == "/") return ""; // a bare slash is SD root, same as empty
return v;
}
// Label shown for the current OPDS filename format in the list subtitle.
StrId opdsFormatLabel(uint8_t format) {
switch (format) {
case static_cast<uint8_t>(OpdsFilenameFormat::TitleAuthor):
return StrId::STR_FMT_TITLE_AUTHOR;
case static_cast<uint8_t>(OpdsFilenameFormat::TitleOnly):
return StrId::STR_FMT_TITLE;
default:
return StrId::STR_FMT_AUTHOR_TITLE;
}
}
} // namespace
int OpdsServerListActivity::getItemCount() const { int OpdsServerListActivity::getItemCount() const {
int count = static_cast<int>(OPDS_STORE.getCount()); int count = static_cast<int>(OPDS_STORE.getCount());
// In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers // Settings mode appends three virtual items: "Add Server", "Download folder"
// and "Filename format".
if (!pickerMode) { if (!pickerMode) {
count++; count += 3;
} }
return count; return count;
} }
@@ -74,6 +106,34 @@ void OpdsServerListActivity::handleSelection() {
return; return;
} }
// Index layout: [servers 0..serverCount-1], [Add Server], [Download folder], [Filename format].
if (selectedIndex == serverCount + 1) {
auto folderHandler = [this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
const std::string norm = normalizeFolder(kb.text);
strncpy(SETTINGS.opdsDownloadFolder, norm.c_str(), sizeof(SETTINGS.opdsDownloadFolder) - 1);
SETTINGS.opdsDownloadFolder[sizeof(SETTINGS.opdsDownloadFolder) - 1] = '\0';
SETTINGS.saveToFile();
requestUpdate();
}
};
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_DOWNLOAD_FOLDER),
std::string(SETTINGS.opdsDownloadFolder), 63, InputType::Text),
folderHandler);
return;
}
// "Filename format": tap cycles through the available formats.
if (selectedIndex == serverCount + 2) {
SETTINGS.opdsFilenameFormat =
static_cast<uint8_t>((SETTINGS.opdsFilenameFormat + 1) % static_cast<uint8_t>(OpdsFilenameFormat::Count));
SETTINGS.saveToFile();
requestUpdate();
return;
}
// Settings mode: open editor for selected server, or create a new one // Settings mode: open editor for selected server, or create a new one
auto resultHandler = [this](const ActivityResult&) { auto resultHandler = [this](const ActivityResult&) {
// Reload server list when returning from editor // Reload server list when returning from editor
@@ -111,17 +171,30 @@ void OpdsServerListActivity::render(RenderLock&&) {
// Secondary label: server URL (shown as subtitle when name is set). // Secondary label: server URL (shown as subtitle when name is set).
GUI.drawList( GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex, renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex,
[&servers, serverCount](int index) { [&servers, serverCount](int index) -> std::string {
if (index < serverCount) { if (index < serverCount) {
const auto& server = servers[index]; const auto& server = servers[index];
return server.name.empty() ? server.url : server.name; return server.name.empty() ? server.url : server.name;
} }
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER)); if (index == serverCount) {
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER));
}
if (index == serverCount + 1) {
return std::string(I18n::getInstance().get(StrId::STR_OPDS_DOWNLOAD_FOLDER));
}
return std::string(I18n::getInstance().get(StrId::STR_OPDS_FILENAME_FORMAT));
}, },
[&servers, serverCount](int index) { [&servers, serverCount](int index) -> std::string {
if (index < serverCount && !servers[index].name.empty()) { if (index < serverCount && !servers[index].name.empty()) {
return servers[index].url; return servers[index].url;
} }
if (index == serverCount + 1) {
const char* f = SETTINGS.opdsDownloadFolder;
return f[0] ? std::string(f) : std::string(I18n::getInstance().get(StrId::STR_OPDS_SD_ROOT));
}
if (index == serverCount + 2) {
return std::string(I18n::getInstance().get(opdsFormatLabel(SETTINGS.opdsFilenameFormat)));
}
return std::string(""); return std::string("");
}); });
} }
@@ -11,8 +11,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/OtaUpdater.h" #include "network/OtaUpdater.h"
#include "BleInput.h"
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
if (!success) { if (!success) {
LOG_ERR("OTA", "WiFi connection failed, exiting"); LOG_ERR("OTA", "WiFi connection failed, exiting");
@@ -58,12 +56,6 @@ void OtaUpdateActivity::onEnter() {
// Turn on WiFi immediately // Turn on WiFi immediately
LOG_DBG("OTA", "Turning on WiFi..."); LOG_DBG("OTA", "Turning on WiFi...");
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
// Launch WiFi selection subactivity // Launch WiFi selection subactivity
@@ -7,7 +7,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include "BluetoothSettingsActivity.h"
#include "ButtonRemapActivity.h" #include "ButtonRemapActivity.h"
#include "ClearCacheActivity.h" #include "ClearCacheActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@@ -60,7 +59,6 @@ void SettingsActivity::rebuildSettingsLists() {
// Append device-only ACTION items // Append device-only ACTION items
controlsSettings.insert(controlsSettings.begin(), controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
controlsSettings.push_back(SettingInfo::Action(StrId::STR_BLUETOOTH, SettingAction::Bluetooth));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser)); systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
@@ -297,9 +295,6 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language: case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler); startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break; break;
case SettingAction::Bluetooth:
startActivityForResult(std::make_unique<BluetoothSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::None: case SettingAction::None:
// Do nothing // Do nothing
break; break;
@@ -24,7 +24,6 @@ enum class SettingAction {
SdFirmwareUpdate, SdFirmwareUpdate,
Language, Language,
DownloadFonts, DownloadFonts,
Bluetooth,
}; };
struct SettingInfo { struct SettingInfo {
-7
View File
@@ -1,7 +0,0 @@
#pragma once
#include <cstdint>
// size: 16x16, generated from Lucide bluetooth.svg with FreeInk's icon generator.
static const uint8_t BluetoothStatusIcon[] = {0xFF, 0xFF, 0xFE, 0x7F, 0xFE, 0x3F, 0xFE, 0x1F, 0xF6, 0x4F, 0xFA,
0x5F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFA, 0x5F,
0xF6, 0x4F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFE, 0x7F, 0xFF, 0xFF};
+4 -27
View File
@@ -13,7 +13,6 @@
#include "I18n.h" #include "I18n.h"
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "components/icons/bluetooth.h"
#include "components/icons/bookmark.h" #include "components/icons/bookmark.h"
#include "fontIds.h" #include "fontIds.h"
@@ -24,10 +23,8 @@ constexpr int homeMarginTop = 30;
constexpr int subtitleY = 738; constexpr int subtitleY = 738;
constexpr int bookmarkStatusIconWidth = 16; constexpr int bookmarkStatusIconWidth = 16;
constexpr int bookmarkStatusIconHeight = 14; constexpr int bookmarkStatusIconHeight = 14;
constexpr int bookmarkStatusIconGap = 4;
constexpr int bookmarkStatusIconTopCrop = 2; constexpr int bookmarkStatusIconTopCrop = 2;
constexpr int bluetoothStatusIconWidth = 16;
constexpr int bluetoothStatusIconHeight = 16;
constexpr int statusIconGap = 4;
bool statusBarTextLaneVisible() { bool statusBarTextLaneVisible() {
return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
@@ -46,17 +43,6 @@ void drawBookmarkStatusIcon(const GfxRenderer& renderer, const int x, const int
} }
} }
void drawBluetoothStatusIcon(const GfxRenderer& renderer, const int x, const int y) {
constexpr int bytesPerRow = bluetoothStatusIconWidth / 8;
for (int row = 0; row < bluetoothStatusIconHeight; ++row) {
for (int col = 0; col < bluetoothStatusIconWidth; ++col) {
const uint8_t byte = BluetoothStatusIcon[row * bytesPerRow + col / 8];
const uint8_t mask = 1U << (7 - (col % 8));
renderer.drawPixel(x + col, y + row, (byte & mask) == 0);
}
}
}
} // namespace } // namespace
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) { void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
@@ -763,8 +749,7 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou
void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
const int pageCount, std::string title, const int paddingBottom, const int textYOffset, const int pageCount, std::string title, const int paddingBottom, const int textYOffset,
const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated, const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated) const {
const bool bluetoothConnected) const {
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
@@ -860,17 +845,9 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
} }
} }
// Draw status icons // Draw Bookmark
if (showStatusBarTextLane && bluetoothConnected) {
const int bluetoothGap = leftClusterWidth > 0 ? statusIconGap : 0;
const int bluetoothX = leftClusterX + leftClusterWidth + bluetoothGap;
const int bluetoothY = textY + 3;
drawBluetoothStatusIcon(renderer, bluetoothX, bluetoothY);
leftClusterWidth += bluetoothStatusIconWidth + bluetoothGap;
}
if (showStatusBarTextLane && isPageBookmarked) { if (showStatusBarTextLane && isPageBookmarked) {
const int bookmarkGap = leftClusterWidth > 0 ? statusIconGap : 0; const int bookmarkGap = leftClusterWidth > 0 ? bookmarkStatusIconGap : 0;
const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap; const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap;
const int bookmarkY = textY + 5; const int bookmarkY = textY + 5;
drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY); drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY);
+1 -1
View File
@@ -238,7 +238,7 @@ class BaseTheme {
void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount,
std::string title, const int paddingBottom = 0, const int textYOffset = 0, std::string title, const int paddingBottom = 0, const int textYOffset = 0,
const bool fillMargin = true, const bool isPageBookmarked = false, const bool fillMargin = true, const bool isPageBookmarked = false,
const bool pageCountEstimated = false, const bool bluetoothConnected = false) const; const bool pageCountEstimated = false) const;
void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false,
int contentStartX = 0, int contentWidth = 0) const; int contentStartX = 0, int contentWidth = 0) const;
+6 -178
View File
@@ -18,7 +18,6 @@
#include <cstring> #include <cstring>
#include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
@@ -34,8 +33,6 @@
#include "images/LoadingIcon.h" #include "images/LoadingIcon.h"
#include "util/ButtonNavigator.h" #include "util/ButtonNavigator.h"
#include "util/ScreenshotUtil.h" #include "util/ScreenshotUtil.h"
#include <Memory.h>
#include "HeapMap.h"
GfxRenderer renderer(display); GfxRenderer renderer(display);
MappedInputManager mappedInputManager(gpio, renderer); MappedInputManager mappedInputManager(gpio, renderer);
@@ -130,12 +127,6 @@ enum class BootResume : uint8_t {
QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss) QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss)
}; };
// Latched in setup() from the read-and-clear of the RTC flag, so the reboot-loop
// guard in bootWasSilentRestart() has the answer for the whole session.
static bool bootWasSilentRestartFlag = false;
bool bootWasSilentRestart() { return bootWasSilentRestartFlag; }
// Latched true once enterDeepSleep() commits to sleeping, before it tears down // Latched true once enterDeepSleep() commits to sleeping, before it tears down
// the current activity. WiFi activities call silentRestart() in onExit() to // the current activity. WiFi activities call silentRestart() in onExit() to
// clear heap fragmentation on the way out, but deep sleep is a full chip reset // clear heap fragmentation on the way out, but deep sleep is a full chip reset
@@ -168,49 +159,6 @@ void silentRestartToReader() {
ESP.restart(); ESP.restart();
} }
// Verify power button press duration on wake-up from deep sleep
// Pre-condition: isWakeupByPowerButton() == true
void verifyPowerButtonDuration() {
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) {
// Fast path for short press
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
return;
}
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
const auto start = millis();
bool abort = false;
// Subtract the current time, because inputManager only starts counting the HeldTime from the first update()
// This way, we remove the time we already took to reach here from the duration,
// assuming the button was held until now from millis()==0 (i.e. device start time).
const uint16_t calibration = start;
const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
gpio.update();
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) {
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
gpio.update();
}
t2 = millis();
if (gpio.isPressed(HalGPIO::BTN_POWER)) {
do {
delay(10);
gpio.update();
} while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() < calibratedPressDuration);
abort = gpio.getPowerButtonHeldTime() < calibratedPressDuration;
} else {
abort = true;
}
if (abort) {
// Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
powerManager.startDeepSleep(gpio);
}
}
void waitForPowerRelease() { void waitForPowerRelease() {
gpio.update(); gpio.update();
while (gpio.isPressed(HalGPIO::BTN_POWER)) { while (gpio.isPressed(HalGPIO::BTN_POWER)) {
@@ -271,10 +219,6 @@ void enterDeepSleep(bool fromTimeout = false) {
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
} }
// Drop any BLE HID link cleanly so the page-turner sees the disconnect promptly.
// bluetoothEnabled persists, so the setting is restored on the next wake.
bleinput::stop();
halTiltSensor.deepSleep(); halTiltSensor.deepSleep();
display.deepSleep(); display.deepSleep();
LOG_DBG("MAIN", "Entering deep sleep"); LOG_DBG("MAIN", "Entering deep sleep");
@@ -338,7 +282,6 @@ void setup() {
(isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0;
silentRebootMagic = 0; silentRebootMagic = 0;
silentRebootTarget = 0; silentRebootTarget = 0;
bootWasSilentRestartFlag = isSilentReboot;
gpio.begin(); gpio.begin();
powerManager.begin(); powerManager.begin();
@@ -371,8 +314,10 @@ void setup() {
switch (wakeupReason) { switch (wakeupReason) {
case HalGPIO::WakeupReason::PowerButton: case HalGPIO::WakeupReason::PowerButton:
LOG_DBG("MAIN", "Verifying power button press duration"); LOG_DBG("MAIN", "Verifying power button press duration");
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(), if (!gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP); SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP)) {
powerManager.startDeepSleep(gpio);
}
break; break;
case HalGPIO::WakeupReason::AfterUSBPower: case HalGPIO::WakeupReason::AfterUSBPower:
// If USB power caused a cold boot, go back to sleep // If USB power caused a cold boot, go back to sleep
@@ -492,78 +437,6 @@ void setup() {
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForPowerRelease(); waitForPowerRelease();
allowSleepAt = millis() + 2000; allowSleepAt = millis() + 2000;
// Bluetooth is started lazily by the lifecycle check in loop() once a reader or the
// Bluetooth settings screen is on the stack — not here at boot — so home/browser and
// WiFi activities keep the ~50 KB the BLE stack would otherwise hold.
}
// Bring the BLE stack up or down to match the current context. BLE is only resident
// while a reader (page-turner input) or the Bluetooth settings screen (pairing) is on
// the stack AND WiFi is off. Heap-heavy reader phases may stop BLE directly; this
// lifecycle then restarts it only after the normal activity/render/heap gates pass.
void updateBluetoothLifecycle() {
const bool wanted =
SETTINGS.bluetoothEnabled && activityManager.bluetoothShouldBeActive() && WiFi.getMode() == WIFI_MODE_NULL;
if (wanted && !BleHid.isRunning() && activityManager.bluetoothStartDeferred()) {
static uint32_t lastActivityDeferLogMs = 0;
if (millis() - lastActivityDeferLogMs > 10000) {
lastActivityDeferLogMs = millis();
LOG_INF("BLELC", "start deferred: activity busy heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
}
return;
}
// Heap gate: NimBLE needs ~57 KB. If the reader cannot spare that yet, retry
// on the next loop without entering any separate BLE hold state.
// The BT settings screen is explicit user intent to run BLE right now (scan/pair is
// dead without the stack). Its floor only needs to cover NimBLE itself — the 100 KB
// reader floor reserves build/render headroom that never gets used there.
const bool explicitBtContext = activityManager.currentKeepsBluetoothAlive();
const size_t startFloor = explicitBtContext ? bleinput::kStartMinFreeHeapExplicit : bleinput::kStartMinFreeHeap;
if (wanted && !BleHid.isRunning() && activityManager.isReaderActivity() && !renderer.hasFrameBuffer()) {
static uint32_t lastFramebufferLoanDeferLogMs = 0;
if (millis() - lastFramebufferLoanDeferLogMs > 10000) {
lastFramebufferLoanDeferLogMs = millis();
LOG_INF("BLELC", "start deferred: framebuffer lent heap=%u maxAlloc=%u", ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
}
return;
}
if (wanted && !BleHid.isRunning() && activityManager.isReaderActivity() && RenderLock::peek()) {
static uint32_t lastReaderRenderDeferLogMs = 0;
if (millis() - lastReaderRenderDeferLogMs > 10000) {
lastReaderRenderDeferLogMs = millis();
LOG_INF("BLELC", "start deferred: reader render in progress heap=%u maxAlloc=%u", ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
}
return;
}
if (wanted && !BleHid.isRunning() && ESP.getFreeHeap() < startFloor) {
static uint32_t lastGateLogMs = 0;
if (millis() - lastGateLogMs > 10000) {
lastGateLogMs = millis();
LOG_INF("BLELC", "start deferred: heap %u floor %u", ESP.getFreeHeap(), (unsigned)startFloor);
}
return;
}
if (wanted && !BleHid.isRunning()) {
LOG_INF("BLELC", "start requested enabled=%u reader=%d settings=%d wifi=%d paired=%u heap=%u maxAlloc=%u",
SETTINGS.bluetoothEnabled, activityManager.isReaderActivity(), activityManager.currentKeepsBluetoothAlive(),
WiFi.getMode(), BleHid.pairedCount(), ESP.getFreeHeap(), ESP.getMaxAllocHeap());
// Start immediately once the lifecycle gates pass. Do not draw a reconnect
// popup here: that schedules a reader redraw while BLE has just consumed ~53 KB,
// which can immediately trip the render heap shed path and create a start/stop loop.
if (!bleinput::ensureStarted()) {
LOG_ERR("BLELC", "start failed heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
return;
}
LOG_INF("BLELC", "started paired=%u heap=%u maxAlloc=%u", BleHid.pairedCount(), ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
} else if (!wanted && BleHid.isRunning()) {
LOG_INF("BLELC", "stop requested enabled=%u active=%d wifi=%d heap=%u maxAlloc=%u", SETTINGS.bluetoothEnabled,
activityManager.bluetoothShouldBeActive(), WiFi.getMode(), ESP.getFreeHeap(), ESP.getMaxAllocHeap());
bleinput::stop();
LOG_INF("BLELC", "stopped heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
}
} }
void loop() { void loop() {
@@ -572,24 +445,6 @@ void loop() {
static unsigned long lastMemPrint = 0; static unsigned long lastMemPrint = 0;
gpio.update(); gpio.update();
updateBluetoothLifecycle(); // bring BLE up/down for the current activity context
static bool lastBleConnected = false;
BleHid.poll(); // drive BLE auto-reconnect + key auto-repeat (no-op when BT off)
const bool bleConnected = BleHid.isConnected();
if (bleConnected && !lastBleConnected) {
LOG_INF("BLELC", "connected name=%s heap=%u maxAlloc=%u", BleHid.connectedName(), ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
if (activityManager.isReaderActivity() && !activityManager.currentKeepsBluetoothAlive()) {
activityManager.requestUpdate();
}
} else if (!bleConnected && lastBleConnected) {
LOG_INF("BLELC", "disconnected heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
if (activityManager.isReaderActivity() && !activityManager.currentKeepsBluetoothAlive()) {
activityManager.requestUpdate();
}
}
lastBleConnected = bleConnected;
mappedInputManager.pollBle(); // drain BLE keys -> logical-button overlay for this frame
halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity()); halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
renderer.setFadingFix(SETTINGS.fadingFix); renderer.setFadingFix(SETTINGS.fadingFix);
@@ -598,23 +453,6 @@ void loop() {
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(), LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
lastMemPrint = millis(); lastMemPrint = millis();
// MEMFIX-PORT: per-task stack high-water audit; portable
// Task stack audit (~once/min): name, stack base (matches blocks in the
// heap map), and high-water free bytes — the margin available for
// right-sizing each stack. TRACE_FACILITY is on in this sdkconfig.
static uint8_t memPrintCount = 0;
if (++memPrintCount >= 6) {
memPrintCount = 0;
const UBaseType_t taskCount = uxTaskGetNumberOfTasks();
auto taskStatus = makeUniqueNoThrow<TaskStatus_t[]>(taskCount + 2);
if (taskStatus) {
const UBaseType_t got = uxTaskGetSystemState(taskStatus.get(), taskCount + 2, nullptr);
for (UBaseType_t i = 0; i < got; ++i) {
LOG_DBG("MEM", "task %-20s stackBase=%p highWaterFree=%u", taskStatus[i].pcTaskName,
taskStatus[i].pxStackBase, (unsigned)taskStatus[i].usStackHighWaterMark);
}
}
}
} }
// Handle incoming serial commands, // Handle incoming serial commands,
@@ -630,11 +468,6 @@ void loop() {
uint8_t* buf = display.getFrameBuffer(); uint8_t* buf = display.getFrameBuffer();
logSerial.write(buf, bufferSize); logSerial.write(buf, bufferSize);
logSerial.printf("SCREENSHOT_END\n"); logSerial.printf("SCREENSHOT_END\n");
// MEMFIX-PORT: on-demand heap map serial command; portable
} else if (cmd == "MEMMAP") {
// On-demand heap block map: capture the heap exactly when it looks
// interesting (e.g. maxAlloc degraded mid-session) without a reboot.
heapmap::dump();
} }
} }
} }
@@ -642,7 +475,7 @@ void loop() {
// Check for any user activity (button press or release) or active background work // Check for any user activity (button press or release) or active background work
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() || if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
mappedInputManager.bleHadActivityThisFrame() || activityManager.preventAutoSleep()) { activityManager.preventAutoSleep()) {
lastActivityTime = millis(); // Reset inactivity timer lastActivityTime = millis(); // Reset inactivity timer
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
} }
@@ -723,16 +556,11 @@ void loop() {
powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested
yield(); // Give FreeRTOS a chance to run tasks, but return immediately yield(); // Give FreeRTOS a chance to run tasks, but return immediately
} else { } else {
// The BLE controller cannot run at the 10 MHz low-power frequency — NimBLE's if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
// controller reset/maintenance hangs the radio and trips the interrupt WDT (the
// same reason WiFi force-disables power saving in HalPowerManager). Keep full CPU
// speed whenever the BLE stack is actually resident, regardless of input idleness.
if (!BleHid.isRunning() && millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
// If we've been inactive for a while, increase the delay to save power // If we've been inactive for a while, increase the delay to save power
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
delay(50); delay(50);
} else { } else {
if (BleHid.isRunning()) powerManager.setPowerSaving(false); // keep the BLE radio stable
// Short delay to prevent tight loop while still being responsive // Short delay to prevent tight loop while still being responsive
delay(10); delay(10);
} }
+7 -57
View File
@@ -26,7 +26,6 @@
#include "html/SettingsPageHtml.generated.h" #include "html/SettingsPageHtml.generated.h"
#include "html/js/jszip_minJs.generated.h" #include "html/js/jszip_minJs.generated.h"
#include "util/BookCacheUtils.h" #include "util/BookCacheUtils.h"
#include <Memory.h>
namespace { namespace {
// Folders/files to hide from the web interface file browser // Folders/files to hide from the web interface file browser
@@ -201,14 +200,6 @@ void CrossPointWebServer::begin() {
udpActive = udp.begin(LOCAL_UDP_PORT); udpActive = udp.begin(LOCAL_UDP_PORT);
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT); LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
// All request handlers run on the task that calls handleClient(). Register
// that task before any handler can call esp_task_wdt_reset().
const esp_err_t watchdogResult = esp_task_wdt_add(nullptr);
watchdogTaskRegistered = watchdogResult == ESP_OK;
if (!watchdogTaskRegistered) {
LOG_ERR("WEB", "Failed to register web server task with watchdog: %s", esp_err_to_name(watchdogResult));
}
running = true; running = true;
LOG_DBG("WEB", "Web server started on port %d", port); LOG_DBG("WEB", "Web server started on port %d", port);
@@ -238,10 +229,6 @@ void CrossPointWebServer::abortWsUpload(const char* tag) {
void CrossPointWebServer::stop() { void CrossPointWebServer::stop() {
if (!running || !server) { if (!running || !server) {
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get()); LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
if (watchdogTaskRegistered) {
esp_task_wdt_delete(nullptr);
watchdogTaskRegistered = false;
}
return; return;
} }
@@ -282,11 +269,6 @@ void CrossPointWebServer::stop() {
LOG_DBG("WEB", "Web server stopped and deleted"); LOG_DBG("WEB", "Web server stopped and deleted");
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
if (watchdogTaskRegistered) {
esp_task_wdt_delete(nullptr);
watchdogTaskRegistered = false;
}
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared // Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
// later in the file and will be cleared when they go out of scope or on next upload // later in the file and will be cleared when they go out of scope or on next upload
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
@@ -502,33 +484,13 @@ void CrossPointWebServer::handleFileListData() const {
server->setContentLength(CONTENT_LENGTH_UNKNOWN); server->setContentLength(CONTENT_LENGTH_UNKNOWN);
server->send(200, "application/json", ""); server->send(200, "application/json", "");
server->sendContent("[");
// Batch entries into ~one-TCP-segment flushes. One sendContent per entry
// means one chunked frame in its own TCP segment per file; the client's
// delayed ACK (~40-200 ms each) turns a large directory into tens of
// seconds. ~1.4 KB batches collapse that to a handful of segments.
constexpr size_t BATCH_CAPACITY = 1400;
auto batch = makeUniqueNoThrow<char[]>(BATCH_CAPACITY);
size_t batchLen = 0;
const auto flushBatch = [&]() {
if (batchLen == 0) return;
server->sendContent(batch.get(), batchLen);
batchLen = 0;
};
char output[512]; char output[512];
constexpr size_t outputSize = sizeof(output); constexpr size_t outputSize = sizeof(output);
bool seenFirst = false; bool seenFirst = false;
JsonDocument doc; JsonDocument doc;
if (batch) { scanFiles(currentPath.c_str(), [this, &output, &doc, seenFirst](const FileInfo& info) mutable {
batch[batchLen++] = '[';
} else {
LOG_ERR("WEB", "OOM: file list batch buffer; falling back to per-entry sends");
server->sendContent("[");
}
scanFiles(currentPath.c_str(), [&](const FileInfo& info) {
doc.clear(); doc.clear();
doc["name"] = info.name; doc["name"] = info.name;
doc["size"] = info.size; doc["size"] = info.size;
@@ -542,26 +504,14 @@ void CrossPointWebServer::handleFileListData() const {
return; return;
} }
const size_t need = written + (seenFirst ? 1 : 0); if (seenFirst) {
if (batch) { server->sendContent(",");
if (batchLen + need > BATCH_CAPACITY) flushBatch();
if (seenFirst) batch[batchLen++] = ',';
memcpy(batch.get() + batchLen, output, written);
batchLen += written;
} else { } else {
if (seenFirst) server->sendContent(","); seenFirst = true;
server->sendContent(output);
} }
seenFirst = true; server->sendContent(output);
}); });
server->sendContent("]");
if (batch) {
if (batchLen + 1 > BATCH_CAPACITY) flushBatch();
batch[batchLen++] = ']';
flushBatch();
} else {
server->sendContent("]");
}
// End of streamed response, empty chunk to signal client // End of streamed response, empty chunk to signal client
server->sendContent(""); server->sendContent("");
LOG_DBG("WEB", "Served file listing page for path: %s", currentPath.c_str()); LOG_DBG("WEB", "Served file listing page for path: %s", currentPath.c_str());
-1
View File
@@ -72,7 +72,6 @@ class CrossPointWebServer {
std::unique_ptr<WebServer> server = nullptr; std::unique_ptr<WebServer> server = nullptr;
std::unique_ptr<WebSocketsServer> wsServer = nullptr; std::unique_ptr<WebSocketsServer> wsServer = nullptr;
bool running = false; bool running = false;
bool watchdogTaskRegistered = false;
bool apMode = false; // true when running in AP mode, false for STA mode bool apMode = false; // true when running in AP mode, false for STA mode
uint16_t port = 80; uint16_t port = 80;
uint16_t wsPort = 81; // WebSocket port uint16_t wsPort = 81; // WebSocket port
+23
View File
@@ -0,0 +1,23 @@
#include "OpdsFilename.h"
#include "StringUtils.h"
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format) {
std::string base;
switch (format) {
case OpdsFilenameFormat::TitleAuthor:
base = author.empty() ? title : title + " - " + author;
break;
case OpdsFilenameFormat::TitleOnly:
base = title;
break;
case OpdsFilenameFormat::AuthorTitle:
default:
base = author.empty() ? title : author + " - " + title;
break;
}
// sanitizeFilename caps at 100 bytes and never returns empty (falls back to
// "book"); ".epub" is appended after so the extension is never truncated —
// identical treatment to the previous inline construction.
return StringUtils::sanitizeFilename(base) + ".epub";
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
#include <string>
// On-disk filename format for books downloaded from an OPDS server. Stored as a
// uint8_t in CrossPointSettings; cast to this enum at the call sites. `Count` is
// the number of selectable formats (used to cycle the setting in the UI).
enum class OpdsFilenameFormat : uint8_t {
AuthorTitle = 0, // "Author - Title.epub" (default; matches legacy behaviour)
TitleAuthor = 1, // "Title - Author.epub"
TitleOnly = 2, // "Title.epub"
Count = 3,
};
// Composes and sanitizes the on-disk filename (including the ".epub" extension)
// for a downloaded OPDS book, according to `format`. When the author is empty,
// every format collapses to just the sanitized title. Pure: no I/O, no globals.
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format);
+1
View File
@@ -43,5 +43,6 @@ add_subdirectory(release_json_parser)
add_subdirectory(differential_rounding) add_subdirectory(differential_rounding)
add_subdirectory(hyphenation_eval) add_subdirectory(hyphenation_eval)
add_subdirectory(utf8_compose) add_subdirectory(utf8_compose)
add_subdirectory(opds_filename)
add_subdirectory(minibidi_arabic) add_subdirectory(minibidi_arabic)
add_subdirectory(combining_marks) add_subdirectory(combining_marks)
+18
View File
@@ -0,0 +1,18 @@
add_executable(OpdsFilenameTest
OpdsFilenameTest.cpp
${REPO_ROOT}/src/util/OpdsFilename.cpp
${REPO_ROOT}/src/util/StringUtils.cpp
${REPO_ROOT}/lib/Utf8/Utf8.cpp
)
target_include_directories(OpdsFilenameTest PRIVATE
${REPO_ROOT}/src/util
${REPO_ROOT}/lib/Utf8
)
target_link_libraries(OpdsFilenameTest PRIVATE
crosspoint_test_common
GTest::gtest_main
)
gtest_discover_tests(OpdsFilenameTest)
+52
View File
@@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include <string>
#include "OpdsFilename.h"
namespace {
TEST(OpdsFilename, AuthorTitleIsDefaultOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::AuthorTitle), "J. Doe - My Book.epub");
}
TEST(OpdsFilename, TitleAuthorSwapsOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book - J. Doe.epub");
}
TEST(OpdsFilename, TitleOnlyIgnoresAuthor) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, EmptyAuthorCollapsesToTitleForEveryFormat) {
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::AuthorTitle), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, IllegalCharactersAreSanitized) {
// '/' ':' '*' '?' etc. are replaced with '_' by sanitizeFilename.
EXPECT_EQ(opdsBookFilename("A/B", "C:D*E?", OpdsFilenameFormat::AuthorTitle), "A_B - C_D_E_.epub");
}
TEST(OpdsFilename, EmptyAuthorAndTitleFallsBackToBook) {
// sanitizeFilename returns "book" when nothing usable remains.
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::AuthorTitle), "book.epub");
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::TitleOnly), "book.epub");
}
TEST(OpdsFilename, LongNameIsTruncatedToByteBudgetBeforeExtension) {
// sanitizeFilename caps the base at 100 bytes; ".epub" is appended after.
const std::string longTitle(200, 'a');
const std::string result = opdsBookFilename("", longTitle, OpdsFilenameFormat::TitleOnly);
EXPECT_EQ(result, std::string(100, 'a') + ".epub");
EXPECT_EQ(result.size(), 105u);
}
TEST(OpdsFilename, UnknownFormatValueFallsBackToAuthorTitle) {
// Defensive: a persisted value outside the enum still yields a valid name.
const auto bogus = static_cast<OpdsFilenameFormat>(99);
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", bogus), "J. Doe - My Book.epub");
}
} // namespace