Compare commits

..
58 changed files with 877 additions and 532 deletions
-6
View File
@@ -25,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
/CMakeLists.txt
/dependencies.lock
/sdkconfig.default
/sdkconfig.defaults
+17 -44
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;
+1 -14
View File
@@ -168,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
@@ -198,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};
+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
-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;
+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
View File
@@ -347,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)"
+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"
+5
View File
@@ -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"
+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 -53
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 (~400 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
@@ -73,47 +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
; MEMFIX-PORT: custom_sdkconfig heap reclamation (~32-37 KB). Rebuilds the
; Arduino core libs on first build (slower once, cached after; needs the CMake
; pin in platformio.local.ini on macOS).
;
; If an interrupted rebuild fails with "multiple definition of 'app_main'"
; (stale generated scaffold), clean it up with:
; rm -rf .dummy CMakeLists.txt sdkconfig.default sdkconfig.defaults .pio/build/default
; Do NOT use `git clean -fdX` — it deletes platformio.local.ini.
custom_sdkconfig =
; 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; 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
+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
""" """
+8
View File
@@ -238,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
+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(); },
@@ -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;
+39 -181
View File
@@ -257,18 +257,6 @@ void EpubReaderActivity::openReaderMenu() {
}); });
} }
bool EpubReaderActivity::buildTickHeapGate() {
const size_t freeHeap = ESP.getFreeHeap();
const size_t maxBlock = ESP.getMaxAllocHeap();
// Below the floors: just wait. The tick is deferrable — page-turn transients
// free up between turns and the tick retries every loop pass. Track the
// paused state so skipLoopDelay() stops pinning the CPU at full speed while
// no build work is actually happening (the gate can stay closed for a long
// stretch if the retained build context itself holds the heap down).
buildHeapPaused = freeHeap < BACKGROUND_BUILD_MIN_FREE_HEAP || maxBlock < BACKGROUND_BUILD_MIN_MAX_ALLOC;
return !buildHeapPaused;
}
void EpubReaderActivity::loop() { void EpubReaderActivity::loop() {
if (!epub) { if (!epub) {
// Should never happen // Should never happen
@@ -276,40 +264,6 @@ void EpubReaderActivity::loop() {
return; return;
} }
// 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 && ESP.getMaxAllocHeap() > BACKGROUND_BUILD_MIN_MAX_ALLOC &&
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
RenderLock lock; // the page table must not change under the scan
// Re-check under the lock: peek() and acquisition are not atomic, so the render
// task may have reset/replaced the section or moved the page in between.
if (section && !section->isBuilding() &&
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
idlePrewarmSpine = currentSpineIndex;
idlePrewarmPage = section->currentPage;
const int nextPage = section->currentPage + 1;
if (nextPage < static_cast<int>(section->pageCount)) {
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
@@ -345,17 +299,14 @@ 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
// buildSomeMore() would fail and wrongly reset the section. The heap gate must be re-read // buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task
// too: a render that won the lock race can expand retained glyph buffers, invalidating the // mutation, so it flags this as always true.
// pre-lock heap reading. cppcheck can't see the cross-task mutation, so it flags this as
// always true.
// cppcheck-suppress knownConditionTrueFalse // cppcheck-suppress knownConditionTrueFalse
if (section->isBuilding() && buildTickHeapGate()) { if (section->isBuilding()) {
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();
@@ -1331,7 +1282,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
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();
} }
// 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.
@@ -1401,13 +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 (X3) it would just spend ~50 KB for the
// identical serial timing. Image pages take the blocking double-FAST path
// below (no async refresh is ever started), so they'd spend the buffers with
// nothing in flight to overlap.
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh() && !pageHasImages;
auto renderGrayscalePass = [&]() { auto renderGrayscalePass = [&]() {
if (needsTextGrayscale) { if (needsTextGrayscale) {
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
@@ -1453,79 +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 {
// Async form: start the waveform and return so the grayscale plane rendering ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
// below overlaps the panel's refresh time instead of following it.
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, 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. Each buffer is only attempted when it leaves ~60 KB free so const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
// the pass never starves concurrent allocations: the next page re-render renderer.beginStripTarget(scratch.get(), y, rows);
// allocates through throwing std::string paths that abort() on OOM under renderer.clearScreen(0x00);
// -fno-exceptions, so a plane buffer that "fits" but eats the render renderGrayscalePass();
// headroom is worse than the strip fallback. Blocking panels skip the renderer.endStripTarget();
// buffers entirely (nothing to overlap). renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows);
constexpr size_t PLANE_BUF_HEADROOM = 60000;
// Free-heap alone ignores fragmentation: taking the largest block for a
// plane can leave only slivers behind even when total headroom looks fine.
// Require the block to fit the plane with 16 KB contiguous to spare, which
// also keeps the advance-table batch scratch viable mid-render (same
// rationale as BACKGROUND_BUILD_MIN_MAX_ALLOC).
constexpr size_t PLANE_BUF_MAX_ALLOC_RESERVE = 16 * 1024;
const auto planeBufFits = [planeBytes] {
return ESP.getFreeHeap() >= planeBytes + PLANE_BUF_HEADROOM &&
ESP.getMaxAllocHeap() >= planeBytes + PLANE_BUF_MAX_ALLOC_RESERVE;
};
auto lsbPlaneBuf = (overlapRefresh && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
auto msbPlaneBuf = (lsbPlaneBuf && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : 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();
@@ -1534,70 +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 (X3) 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);
if (overlapRefresh) {
// The BW refresh ran the shadow-free async path, so controller RAM's
// differential baseline was never rebuilt. Even with AA skipped it must
// be re-synced from the intact BW framebuffer, or the next differential
// update diffs against stale contents.
renderer.cleanupGrayscaleWithFrameBuffer();
}
} 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
+2 -38
View File
@@ -41,13 +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;
// 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
@@ -93,33 +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;
// MEMFIX-PORT: background-build heap floor; portable
// 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 under heap pressure). 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.
static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 32 * 1024;
// Fragmentation floor for the same gate: a tick passed the free-heap floor at
// 34.7 KB free but the largest block was ~11 KB, and a parse allocation inside the
// tick aborted anyway. Free heap says how much memory exists; maxAlloc says whether
// any single allocation can actually have it. 16 KB also keeps the advance-table
// batch path (16 KB scratch) viable during builds.
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 16 * 1024;
// Gate for a background build tick: true when the heap can take parse allocations.
// Updates buildHeapPaused as a side effect.
bool buildTickHeapGate();
// True while the background build is gated on the heap floors. Lets skipLoopDelay()
// return the loop to normal delay/power-saving during the pause: isBuilding() stays
// true the whole time, and without this the loop would spin at full CPU speed doing
// no build work — indefinitely, if the build context itself keeps the heap low.
bool buildHeapPaused = false;
// Heap floor for optional render-adjacent work (idle prewarm). Page
// deserialization (TextBlock word vectors/strings) and glyph caching allocate
// through throwing paths that abort() on OOM; skip deferrable work below it.
static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024;
// 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
@@ -177,10 +143,8 @@ class EpubReaderActivity final : public Activity {
// Full CPU speed + fast loop ticks while a section build runs: at the low-power // Full CPU speed + fast loop ticks while a section build runs: at the low-power
// frequency a giant chapter's background rebuild stretches from ~40s to many // frequency a giant chapter's background rebuild stretches from ~40s to many
// minutes, so the reader exits before it can finalize and the next open restarts // minutes, so the reader exits before it can finalize and the next open restarts
// 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.
// and while the build is heap-paused (no work is happening, so spinning at full bool skipLoopDelay() override { return section && section->isBuilding(); }
// speed would only burn battery; the paused gate still retries every loop pass).
bool skipLoopDelay() override { return section && section->isBuilding() && !buildHeapPaused; }
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
CrossPointPosition getCurrentPosition() const; CrossPointPosition getCurrentPosition() const;
+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.
+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--;
} }
} }
@@ -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("");
}); });
} }
+4 -45
View File
@@ -159,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)) {
@@ -357,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
-17
View File
@@ -200,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);
@@ -237,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;
} }
@@ -281,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());
-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