Merge branch 'master' into jpegdec-fork

This commit is contained in:
Zach Nelson
2026-05-25 09:50:21 -05:00
61 changed files with 943 additions and 524 deletions
+6
View File
@@ -161,6 +161,12 @@ if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
- `HalStorage` serializes everything via `storageMutex`. Downstream code includes `<HalStorage.h>`, which transparently `using FsFile = HalFile;`; every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` directly. **Never** define `HAL_STORAGE_IMPL` outside `HalStorage.cpp`; that disables the `FsFile -> HalFile` typedef and you'll get a raw SdFat handle that bypasses the mutex.
- If you're storing a raw `FsFile` in a place that won't transitively include `<HalStorage.h>` (rare), include the header explicitly so the typedef applies.
---
## Coding Standards
+36 -15
View File
@@ -61,29 +61,29 @@ struct TocEntry {
struct BookBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
// Metadata section
Metadata metadata [[comment("Book metadata")]];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
// Data Entries
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
@@ -104,7 +104,7 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 8
### Version 24
ImHex Pattern:
@@ -114,7 +114,7 @@ import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 8
#define EXPECTED_VERSION 24
#define MAX_STRING_LENGTH 65535
// === String Structure ===
@@ -133,8 +133,10 @@ fn format_string(String s) {
// === Page Structure ===
enum StorageType : u8 {
PageLine = 1
enum PageElementTag : u8 {
PageLine = 1,
PageImage = 2,
PageHorizontalRule = 3
};
enum WordStyle : u8 {
@@ -161,10 +163,29 @@ struct PageLine {
BlockStyle blockStyle;
};
struct PageImage {
s16 xPos;
s16 yPos;
String imagePath;
s16 width;
s16 height;
};
struct PageHorizontalRule {
s16 xPos;
s16 yPos;
u16 width;
u8 thickness;
};
struct PageElement {
u8 pageElementType;
if (pageElementType == 1) {
PageLine pageLine [[inline]];
} else if (pageElementType == 2) {
PageImage pageImage [[inline]];
} else if (pageElementType == 3) {
PageHorizontalRule horizontalRule [[inline]];
} else {
std::error(std::format("Unknown page element type: {}", pageElementType));
}
@@ -180,12 +201,12 @@ struct Page {
struct SectionBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
// Cache busting parameters
s32 fontId;
float lineCompression;
@@ -194,15 +215,15 @@ struct SectionBin {
u16 vieportHeight;
u16 pageCount;
u32 lutOffset;
Page page[pageCount];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 lut[pageCount];
};
+1
View File
@@ -97,6 +97,7 @@ To convert your own TrueType/OpenType fonts:
| `cyrillic` | Cyrillic + Supplement |
| `cjk` | CJK Unified Ideographs + Hiragana + Katakana + Fullwidth |
| `hangul` | Korean Hangul syllables |
| `reading` | Literary fiction coverage: Latin, Greek, Cyrillic, math/symbol blocks, supplemental punctuation, and CJK quote marks |
| `builtin` | Matches built-in Bookerly coverage exactly |
Combine presets with commas: `--intervals latin-ext,greek,cyrillic`
+3 -1
View File
@@ -59,12 +59,14 @@ INTERVAL_PRESETS = {
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
(0x25A0, 0x25FF), (0x2600, 0x26FF), (0x2700, 0x27BF)],
# Composite preset for English-language literary fiction including scifi/popsci.
# Greek for physics terms, math operators, miscellaneous symbols (♪♫♬), dingbats.
# Greek for physics terms, math operators, geometric shapes, uncommon
# dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats.
"reading": [(0x0020, 0x024F), (0x0300, 0x036F), (0x0370, 0x03FF),
(0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
(0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F),
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
(0x25A0, 0x25FF), (0x2600, 0x26FF), (0x2700, 0x27BF),
(0x2900, 0x29FF), (0x2E00, 0x2E7F), (0x3000, 0x303F),
(0xFB00, 0xFB06)],
# Matches the built-in font intervals from fontconvert.py exactly
"builtin": [(0x0000, 0x007F), (0x0080, 0x00FF), (0x0100, 0x017F),
+50
View File
@@ -1,8 +1,11 @@
#include "Page.h"
#include <GfxRenderer.h>
#include <Logging.h>
#include <Serialization.h>
#include <new>
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
}
@@ -48,6 +51,47 @@ std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
}
void PageHorizontalRule::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
(void)fontId;
if (width == 0 || thickness == 0) {
return;
}
renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset, thickness, true);
}
bool PageHorizontalRule::serialize(FsFile& file) {
serialization::writePod(file, xPos);
serialization::writePod(file, yPos);
serialization::writePod(file, width);
serialization::writePod(file, thickness);
return true;
}
std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(FsFile& file) {
int16_t xPos = 0;
int16_t yPos = 0;
uint16_t width = 0;
uint8_t thickness = 0;
serialization::readPod(file, xPos);
serialization::readPod(file, yPos);
serialization::readPod(file, width);
serialization::readPod(file, thickness);
if (width == 0 || thickness == 0) {
LOG_ERR("PGE", "Deserialization failed: invalid horizontal rule metadata (width=%u thickness=%u)", width,
thickness);
return nullptr;
}
auto* rule = new (std::nothrow) PageHorizontalRule(width, thickness, xPos, yPos);
if (!rule) {
LOG_ERR("PGE", "Deserialization failed: could not allocate PageHorizontalRule");
return nullptr;
}
return std::unique_ptr<PageHorizontalRule>(rule);
}
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset);
@@ -98,6 +142,12 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
} else if (tag == TAG_PageImage) {
auto pi = PageImage::deserialize(file);
page->elements.push_back(std::move(pi));
} else if (tag == TAG_PageHorizontalRule) {
auto rule = PageHorizontalRule::deserialize(file);
if (!rule) {
return nullptr;
}
page->elements.push_back(std::move(rule));
} else {
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
return nullptr;
+16 -1
View File
@@ -12,7 +12,8 @@
enum PageElementTag : uint8_t {
TAG_PageLine = 1,
TAG_PageImage = 2, // New tag
TAG_PageImage = 2,
TAG_PageHorizontalRule = 3,
};
// represents something that has been added to a page
@@ -55,6 +56,20 @@ class PageImage final : public PageElement {
const ImageBlock& getImageBlock() const { return *imageBlock; }
};
class PageHorizontalRule final : public PageElement {
uint16_t width;
uint8_t thickness;
public:
PageHorizontalRule(uint16_t width, uint8_t thickness, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), width(width), thickness(thickness) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
PageElementTag getTag() const override { return TAG_PageHorizontalRule; }
static std::unique_ptr<PageHorizontalRule> deserialize(FsFile& file);
};
class Page {
public:
// the list of block index and line numbers on this page
+1 -1
View File
@@ -10,7 +10,7 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 23;
constexpr uint8_t SECTION_FILE_VERSION = 24;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
@@ -5,8 +5,10 @@
#include <HalStorage.h>
#include <JPEGDEC.h>
#include <Logging.h>
#include <Memory.h>
#include <cstdlib>
#include <memory>
#include <new>
#include "DirectPixelWriter.h"
@@ -347,16 +349,16 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat
return false;
}
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder for dimensions");
return false;
}
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, nullptr);
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG for dimensions (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
delete jpeg;
return false;
}
@@ -364,8 +366,6 @@ bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePat
out.height = jpeg->getHeight();
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
jpeg->close();
delete jpeg;
return true;
}
@@ -379,7 +379,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
return false;
}
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
return false;
@@ -392,9 +392,9 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
ctx.screenHeight = renderer.getScreenHeight();
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
delete jpeg;
return false;
}
@@ -403,14 +403,10 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (srcWidth <= 0 || srcHeight <= 0) {
LOG_ERR("JPG", "Invalid JPEG dimensions: %dx%d", srcWidth, srcHeight);
jpeg->close();
delete jpeg;
return false;
}
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
jpeg->close();
delete jpeg;
return false;
}
@@ -453,8 +449,6 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (destWidth <= 0 || destHeight <= 0) {
LOG_ERR("JPG", "Degenerate output dimensions %dx%d for %s, skipping render", destWidth, destHeight,
imagePath.c_str());
jpeg->close();
delete jpeg;
return false;
}
@@ -490,13 +484,9 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
if (rc != 1) {
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
jpeg->close();
delete jpeg;
return false;
}
jpeg->close();
delete jpeg;
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled
@@ -4,9 +4,11 @@
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
#include <Memory.h>
#include <PNGdec.h>
#include <cstdlib>
#include <memory>
#include <new>
#include "DirectPixelWriter.h"
@@ -244,7 +246,7 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
return false;
}
PNG* png = new (std::nothrow) PNG();
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
if (!png) {
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
return false;
@@ -252,18 +254,16 @@ bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
nullptr);
const ScopedCleanup cleanup{[&png]() { png->close(); }};
if (rc != 0) {
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
delete png;
return false;
}
out.width = png->getWidth();
out.height = png->getHeight();
png->close();
delete png;
return true;
}
@@ -278,7 +278,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
PNG* png = new (std::nothrow) PNG();
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
if (!png) {
LOG_ERR("PNG", "Failed to allocate PNG decoder");
return false;
@@ -292,15 +292,13 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
pngDrawCallback);
const ScopedCleanup cleanup{[&png]() { png->close(); }};
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
delete png;
return false;
}
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
png->close();
delete png;
return false;
}
@@ -335,8 +333,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
png->close();
delete png;
return false;
}
@@ -349,8 +345,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
if (!ctx.grayLineBuffer) {
LOG_ERR("PNG", "Failed to allocate gray line buffer");
png->close();
delete png;
return false;
}
@@ -380,13 +374,9 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
if (rc != PNG_SUCCESS) {
LOG_ERR("PNG", "Decode failed: %d", rc);
png->close();
delete png;
return false;
}
png->close();
delete png;
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled and buffer was allocated
@@ -8,7 +8,9 @@
#include <XmlParserUtils.h>
#include <expat.h>
#include <algorithm>
#include <iterator>
#include <new>
#include "Epub.h"
#include "Epub/Page.h"
@@ -145,6 +147,68 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
wordsExtractedInBlock = 0;
}
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
if (partWordBufferIndex > 0) {
flushPartWordBuffer();
}
if (currentTextBlock) {
const BlockStyle parentBlockStyle = currentTextBlock->getBlockStyle();
startNewTextBlock(parentBlockStyle);
}
if (!currentPage) {
currentPage.reset(new (std::nothrow) Page());
if (!currentPage) {
LOG_ERR("EHP", "Failed to create page for horizontal rule");
return;
}
currentPageNextY = 0;
}
const int16_t lineHeight = static_cast<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
const int16_t defaultVerticalSpacing = static_cast<int16_t>(lineHeight / 2);
const int16_t topSpacing =
static_cast<int16_t>((blockStyle.marginTop > 0 ? blockStyle.marginTop : defaultVerticalSpacing) +
(blockStyle.paddingTop > 0 ? blockStyle.paddingTop : 0));
const int16_t bottomSpacing =
static_cast<int16_t>((blockStyle.marginBottom > 0 ? blockStyle.marginBottom : defaultVerticalSpacing) +
(blockStyle.paddingBottom > 0 ? blockStyle.paddingBottom : 0));
constexpr uint8_t ruleThickness = 2;
const int16_t availableWidth =
std::max<int16_t>(1, static_cast<int16_t>(viewportWidth - blockStyle.totalHorizontalInset()));
const int16_t width = std::max<int16_t>(1, static_cast<int16_t>(availableWidth / 4));
const int16_t xPos = static_cast<int16_t>(blockStyle.leftInset() + ((availableWidth - width) / 2));
const int16_t totalHeight = static_cast<int16_t>(topSpacing + ruleThickness + bottomSpacing);
if (!currentPage->elements.empty() && currentPageNextY + totalHeight > viewportHeight) {
completePageFn(std::move(currentPage), xpathParagraphIndex, xpathListItemIndex);
completedPageCount++;
currentPage.reset(new (std::nothrow) Page());
if (!currentPage) {
LOG_ERR("EHP", "Failed to create page after horizontal-rule page break");
return;
}
currentPageNextY = 0;
}
currentPageNextY += topSpacing;
auto pageRule = std::shared_ptr<PageHorizontalRule>(
new (std::nothrow) PageHorizontalRule(width, ruleThickness, xPos, currentPageNextY));
if (!pageRule) {
LOG_ERR("EHP", "Failed to create PageHorizontalRule");
return;
}
currentPage->elements.push_back(pageRule);
currentPageNextY = static_cast<int16_t>(currentPageNextY + ruleThickness + bottomSpacing);
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
}
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
@@ -262,6 +326,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
if (self->tableDepth == 1 && strcmp(name, "hr") == 0) {
self->depth += 1;
return;
}
if (matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS))) {
std::string src;
std::string alt;
@@ -600,6 +669,25 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
if (strcmp(name, "hr") == 0) {
auto hrBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Left, self->viewportWidth);
if (!self->embeddedStyle) {
hrBlockStyle.marginLeft = 0;
hrBlockStyle.marginRight = 0;
hrBlockStyle.marginTop = 0;
hrBlockStyle.marginBottom = 0;
hrBlockStyle.paddingLeft = 0;
hrBlockStyle.paddingRight = 0;
hrBlockStyle.paddingTop = 0;
hrBlockStyle.paddingBottom = 0;
hrBlockStyle.textIndentDefined = false;
hrBlockStyle.textIndent = 0;
}
self->emitHorizontalRule(hrBlockStyle);
self->depth += 1;
return;
}
if (matches(name, HEADER_TAGS, std::size(HEADER_TAGS))) {
self->currentCssStyle = cssStyle;
auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth);
@@ -91,6 +91,7 @@ class ChapterHtmlSlimParser {
void startNewTextBlock(const BlockStyle& blockStyle);
void flushPartWordBuffer();
void makePages();
void emitHorizontalRule(const BlockStyle& blockStyle);
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
+2 -1
View File
@@ -206,11 +206,12 @@ STR_THEME_CLASSIC: "Класічная"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Кампенсацыя выцвітання"
STR_SEAMLESS_SLEEP: "Старонка як экран сну"
STR_QUICK_RESUME_TIMEOUT: "Хуткае узнаўленне пасля таймаўту"
STR_AFTER_TIMEOUT: "Пасля таймаўту"
STR_REMAP_FRONT_BUTTONS: "Пераназначыць пярэднія кнопкі"
STR_OPDS_BROWSER: "OPDS браўзер"
STR_COVER_CUSTOM: "Вокладка + Свой"
STR_QUICK_RESUME: "Хуткае узнаўленне"
STR_MENU_RECENT_BOOKS: "Нядаўнія кнігі"
STR_REMOVE_FROM_RECENTS: "Выдаліць з нядаўніх кніг?"
STR_NO_RECENT_BOOKS: "Няма нядаўніх кніг"
+2 -1
View File
@@ -234,11 +234,12 @@ STR_THEME_CLASSIC: "Clàssic"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
STR_SEAMLESS_SLEEP: "Pàgina com a repòs"
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
STR_AFTER_TIMEOUT: "Després del temps"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida"
STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Voleu suprimir-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Prázdný soubor"
STR_OUT_OF_BOUNDS: "Mimo hranice"
STR_LOADING: "Načítání..."
STR_LOADING_POPUP: "Načítání"
STR_WIFI_NETWORKS: "WiFi sítě"
STR_WIFI_NETWORKS: "Wi-Fi sítě"
STR_NO_NETWORKS: "Žádné sítě nenalezeny"
STR_NETWORKS_FOUND: "Nalezeno %zu sítí"
STR_SCANNING: "Skenování..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Uložit heslo pro příště?"
STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování"
STR_JOIN_NETWORK: "Připojit se k síti"
STR_CREATE_HOTSPOT: "Vytvořit hotspot"
STR_JOIN_DESC: "Připojit se k existující síti WiFi"
STR_HOTSPOT_DESC: "Vytvořit síť WiFi, ke které se mohou připojit ostatní"
STR_JOIN_DESC: "Připojit se k existující síti Wi-Fi"
STR_HOTSPOT_DESC: "Vytvořit síť Wi-Fi, ke které se mohou připojit ostatní"
STR_STARTING_HOTSPOT: "Spouštění hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Připojte své zařízení k této síti WiFi"
STR_CONNECT_WIFI_HINT: "Připojte své zařízení k této síti Wi-Fi"
STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
STR_OR_HTTP_PREFIX: "nebo http://"
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola WiFi..."
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi"
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
STR_TO_PREFIX: "pro"
STR_CALIBRE_RECEIVING: "Příjem:"
STR_CALIBRE_RECEIVED: "Přijato:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
STR_CAT_DISPLAY: "Displej"
@@ -98,7 +98,7 @@ STR_KOREADER_PASSWORD: "Heslo KOReaderu"
STR_FILENAME: "Název souboru"
STR_BINARY: "Binární"
STR_SET_CREDENTIALS_FIRST: "Nastavte přihlašovací údaje"
STR_WIFI_CONN_FAILED: "Připojení k WiFi selhalo"
STR_WIFI_CONN_FAILED: "Připojení k Wi-Fi selhalo"
STR_AUTHENTICATING: "Ověřování..."
STR_AUTH_SUCCESS: "Úspěšné ověření!"
STR_KOREADER_AUTH: "Ověření KOReaderu"
@@ -212,11 +212,12 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_SEAMLESS_SLEEP: "Stránka jako spánek"
STR_QUICK_RESUME_TIMEOUT: "Rychlé navázání po vypršení"
STR_AFTER_TIMEOUT: "Po vypršení"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
STR_OPDS_BROWSER: "Prohlížeč OPDS"
STR_COVER_CUSTOM: "Obálka + Vlastní"
STR_QUICK_RESUME: "Rychlé navázání"
STR_MENU_RECENT_BOOKS: "Nedávné knihy"
STR_REMOVE_FROM_RECENTS: "Odebrat z nedávných knih?"
STR_NO_RECENT_BOOKS: "Žádné nedávné knihy"
+9 -8
View File
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Gem adgangskode til næste gang?"
STR_PRESS_OK_SCAN: "Tryk OK for at scanne igen"
STR_JOIN_NETWORK: "Tilslut netværk"
STR_CREATE_HOTSPOT: "Opret Hotspot"
STR_JOIN_DESC: "Opret forbindelse til et eksisterende WiFi-netværk"
STR_HOTSPOT_DESC: "Opret et WiFi-netværk andre kan tilslutte sig"
STR_JOIN_DESC: "Opret forbindelse til et eksisterende Wi-Fi-netværk"
STR_HOTSPOT_DESC: "Opret et Wi-Fi-netværk andre kan tilslutte sig"
STR_STARTING_HOTSPOT: "Starter Hotspot..."
STR_HOTSPOT_MODE: "Hotspot-tilstand"
STR_CONNECT_WIFI_HINT: "Opret forbindelse fra din enhed til dette WiFi-netværk"
STR_CONNECT_WIFI_HINT: "Opret forbindelse fra din enhed til dette Wi-Fi-netværk"
STR_OPEN_URL_HINT: "Åbn denne URL i din browser"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt"
STR_MAC_ADDRESS: "MAC-adresse:"
STR_CHECKING_WIFI: "Tjekker WiFi..."
STR_ENTER_WIFI_PASSWORD: "Indtast WiFi-adgangskode"
STR_CHECKING_WIFI: "Tjekker Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Indtast Wi-Fi-adgangskode"
STR_TO_PREFIX: "til "
STR_CALIBRE_RECEIVING: "Modtager: "
STR_CALIBRE_RECEIVED: "Modtaget: "
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Vær på det samme WiFi-netværk"
STR_CALIBRE_INSTRUCTION_2: "2) Vær på det samme Wi-Fi-netværk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhed\""
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skærm åben under afsendelse\""
STR_CAT_DISPLAY: "Skærm"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "KOReader adgangskode"
STR_FILENAME: "Filnavn"
STR_BINARY: "Binær"
STR_SET_CREDENTIALS_FIRST: "Angiv legitimationsoplysninger først"
STR_WIFI_CONN_FAILED: "WiFi-forbindelsen mislykkedes"
STR_WIFI_CONN_FAILED: "Wi-Fi-forbindelsen mislykkedes"
STR_AUTHENTICATING: "Godkender..."
STR_AUTH_SUCCESS: "Godkendt!"
STR_KOREADER_AUTH: "KOReader-godkendelse"
@@ -234,11 +234,12 @@ STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sollysfading-rettelse"
STR_SEAMLESS_SLEEP: "Side som dvaleskærm"
STR_QUICK_RESUME_TIMEOUT: "Hurtig genoptagelse ved timeout"
STR_AFTER_TIMEOUT: "Efter timeout"
STR_REMAP_FRONT_BUTTONS: "Omtildel frontknapper"
STR_OPDS_BROWSER: "OPDS Browser"
STR_COVER_CUSTOM: "Omslag + Brugerdefineret"
STR_QUICK_RESUME: "Hurtig genoptagelse"
STR_MENU_RECENT_BOOKS: "Seneste bøger"
STR_REMOVE_FROM_RECENTS: "Fjern fra Seneste bøger?"
STR_NO_RECENT_BOOKS: "Ingen seneste bøger"
+11 -10
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Leeg bestand"
STR_OUT_OF_BOUNDS: "Buiten bereik"
STR_LOADING: "Laden..."
STR_LOADING_POPUP: "Laden"
STR_WIFI_NETWORKS: "Wifi-netwerken"
STR_WIFI_NETWORKS: "Wi-Fi-netwerken"
STR_NO_NETWORKS: "Geen netwerken gevonden"
STR_NETWORKS_FOUND: "%zu netwerken gevonden"
STR_SCANNING: "Scannen..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Wachtwoord opslaan voor volgende keer?"
STR_PRESS_OK_SCAN: "Druk op OK om opnieuw te scannen"
STR_JOIN_NETWORK: "Verbind met netwerk"
STR_CREATE_HOTSPOT: "Hotspot maken"
STR_JOIN_DESC: "Verbind met een bestaand wifi-netwerk"
STR_HOTSPOT_DESC: "Maak een wifi-netwerk waar anderen mee kunnen verbinden"
STR_JOIN_DESC: "Verbind met een bestaand Wi-Fi-netwerk"
STR_HOTSPOT_DESC: "Maak een Wi-Fi-netwerk waar anderen mee kunnen verbinden"
STR_STARTING_HOTSPOT: "Hotspot starten..."
STR_HOTSPOT_MODE: "Hotspot-modus"
STR_CONNECT_WIFI_HINT: "Verbind je apparaat met dit wifi-netwerk"
STR_CONNECT_WIFI_HINT: "Verbind je apparaat met dit Wi-Fi-netwerk"
STR_OPEN_URL_HINT: "Open deze URL in je browser"
STR_OR_HTTP_PREFIX: "of http://"
STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen"
STR_MAC_ADDRESS: "MAC-adres:"
STR_CHECKING_WIFI: "Wifi controleren..."
STR_ENTER_WIFI_PASSWORD: "Voer wifi-wachtwoord in"
STR_CHECKING_WIFI: "Wi-Fi controleren..."
STR_ENTER_WIFI_PASSWORD: "Voer Wi-Fi-wachtwoord in"
STR_TO_PREFIX: "met "
STR_CALIBRE_RECEIVING: "Bezig met ontvangen: "
STR_CALIBRE_RECEIVED: "Ontvangen: "
STR_CALIBRE_INSTRUCTION_1: "1) Installeer CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Gebruik hetzelfde wifi-netwerk"
STR_CALIBRE_INSTRUCTION_2: "2) Gebruik hetzelfde Wi-Fi-netwerk"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
STR_CALIBRE_INSTRUCTION_4: "\"Houd dit scherm open tijdens verzenden\""
STR_CAT_DISPLAY: "Scherm"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "KOReader wachtwoord"
STR_FILENAME: "Bestandsnaam"
STR_BINARY: "Binair"
STR_SET_CREDENTIALS_FIRST: "Stel eerst inloggegevens in"
STR_WIFI_CONN_FAILED: "Wifi-verbinding mislukt"
STR_WIFI_CONN_FAILED: "Wi-Fi-verbinding mislukt"
STR_AUTHENTICATING: "Authenticeren..."
STR_AUTH_SUCCESS: "Authenticatie geslaagd!"
STR_KOREADER_AUTH: "KOReader-authenticatie"
@@ -234,11 +234,12 @@ STR_THEME_CLASSIC: "Klassiek"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Uitgebreid"
STR_SUNLIGHT_FADING_FIX: "Zonlicht vervaging fix"
STR_SEAMLESS_SLEEP: "Pagina als slaapscherm"
STR_QUICK_RESUME_TIMEOUT: "Snel hervatten bij timeout"
STR_AFTER_TIMEOUT: "Na timeout"
STR_REMAP_FRONT_BUTTONS: "Knoppen voorzijde wijzigen"
STR_OPDS_BROWSER: "OPDS-browser"
STR_COVER_CUSTOM: "Omslag + Aangepast"
STR_QUICK_RESUME: "Snel hervatten"
STR_MENU_RECENT_BOOKS: "Recente boeken"
STR_REMOVE_FROM_RECENTS: "Verwijderen uit Recente boeken?"
STR_NO_RECENT_BOOKS: "Geen recente boeken"
@@ -302,4 +303,4 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
+12 -11
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Empty file"
STR_OUT_OF_BOUNDS: "Out of bounds"
STR_LOADING: "Loading..."
STR_LOADING_POPUP: "Loading"
STR_WIFI_NETWORKS: "WiFi Networks"
STR_WIFI_NETWORKS: "Wi-Fi Networks"
STR_NO_NETWORKS: "No networks found"
STR_NETWORKS_FOUND: "%zu networks found"
STR_SCANNING: "Scanning..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Save password for next time?"
STR_PRESS_OK_SCAN: "Press OK to scan again"
STR_JOIN_NETWORK: "Join a Network"
STR_CREATE_HOTSPOT: "Create Hotspot"
STR_JOIN_DESC: "Connect to an existing WiFi network"
STR_HOTSPOT_DESC: "Create a WiFi network others can join"
STR_JOIN_DESC: "Connect to an existing Wi-Fi network"
STR_HOTSPOT_DESC: "Create a Wi-Fi network others can join"
STR_STARTING_HOTSPOT: "Starting Hotspot..."
STR_HOTSPOT_MODE: "Hotspot Mode"
STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network"
STR_CONNECT_WIFI_HINT: "Connect your device to this Wi-Fi network"
STR_OPEN_URL_HINT: "Open this URL in your browser"
STR_OR_HTTP_PREFIX: "or http://"
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
STR_MAC_ADDRESS: "MAC address:"
STR_CHECKING_WIFI: "Checking WiFi..."
STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password"
STR_CHECKING_WIFI: "Checking Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
STR_TO_PREFIX: "to "
STR_CALIBRE_RECEIVING: "Receiving: "
STR_CALIBRE_RECEIVED: "Received: "
STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network"
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same Wi-Fi network"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\""
STR_CAT_DISPLAY: "Display"
@@ -62,7 +62,7 @@ STR_CAT_READER: "Reader"
STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Sleep Screen"
STR_SEAMLESS_SLEEP: "Page as Sleep Screen"
STR_QUICK_RESUME_TIMEOUT: "Quick Resume on Timeout"
STR_AFTER_TIMEOUT: "After Timeout"
STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
STR_HIDE_BATTERY: "Hide Battery %"
@@ -105,7 +105,7 @@ STR_KOREADER_PASSWORD: "KOReader Password"
STR_FILENAME: "Filename"
STR_BINARY: "Binary"
STR_SET_CREDENTIALS_FIRST: "Set credentials first"
STR_WIFI_CONN_FAILED: "WiFi connection failed"
STR_WIFI_CONN_FAILED: "Wi-Fi connection failed"
STR_AUTHENTICATING: "Authenticating..."
STR_AUTH_SUCCESS: "Successfully authenticated!"
STR_KOREADER_AUTH: "KOReader Auth"
@@ -250,8 +250,8 @@ STR_CLOCK_SYNC_NOW: "Sync clock now"
STR_CLOCK_SYNCING: "Syncing from NTP..."
STR_CLOCK_SYNC_OK: "Clock synced"
STR_CLOCK_SYNC_FAIL: "Sync failed"
STR_CLOCK_SYNC_NO_WIFI: "WiFi not connected"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Connect to WiFi first, then try again."
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi not connected"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Connect to Wi-Fi first, then try again."
STR_CLOCK_SYNCED: "Clock Synced"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
@@ -263,6 +263,7 @@ STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Search"
STR_COVER_CUSTOM: "Cover + Custom"
STR_QUICK_RESUME: "Quick Resume"
STR_MENU_RECENT_BOOKS: "Recent Books"
STR_REMOVE_FROM_RECENTS: "Remove from Recent Books?"
STR_NO_RECENT_BOOKS: "No recent books"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Tyhjä tiedosto"
STR_OUT_OF_BOUNDS: "Alueen ulkopuolella"
STR_LOADING: "Ladataan..."
STR_LOADING_POPUP: "Ladataan"
STR_WIFI_NETWORKS: "WiFi-verkot"
STR_WIFI_NETWORKS: "Wi-Fi-verkot"
STR_NO_NETWORKS: "Verkkoja ei löytynyt"
STR_NETWORKS_FOUND: "%zu verkkoa löydetty"
STR_SCANNING: "Etsitään..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Tallenna salasana seuraavaa kertaa varten?"
STR_PRESS_OK_SCAN: "Paina OK etsiäksesi uudelleen"
STR_JOIN_NETWORK: "Liity verkkoon"
STR_CREATE_HOTSPOT: "Luo yhteyspiste"
STR_JOIN_DESC: "Yhdistä olemassa olevaan WiFi-verkkoon"
STR_HOTSPOT_DESC: "Luo WiFi-verkko, johon muut voivat liittyä"
STR_JOIN_DESC: "Yhdistä olemassa olevaan Wi-Fi-verkkoon"
STR_HOTSPOT_DESC: "Luo Wi-Fi-verkko, johon muut voivat liittyä"
STR_STARTING_HOTSPOT: "Käynnistetään yhteyspiste..."
STR_HOTSPOT_MODE: "Yhteyspistetila"
STR_CONNECT_WIFI_HINT: "Yhdistä laitteesi tähän WiFi-verkkoon"
STR_CONNECT_WIFI_HINT: "Yhdistä laitteesi tähän Wi-Fi-verkkoon"
STR_OPEN_URL_HINT: "Avaa tämä osoite selaimessasi"
STR_OR_HTTP_PREFIX: "tai http://"
STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre langaton"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu"
STR_MAC_ADDRESS: "MAC-osoite:"
STR_CHECKING_WIFI: "Tarkistetaan WiFi..."
STR_ENTER_WIFI_PASSWORD: "Syötä WiFi-salasana"
STR_CHECKING_WIFI: "Tarkistetaan Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Syötä Wi-Fi-salasana"
STR_TO_PREFIX: "verkkoon "
STR_CALIBRE_RECEIVING: "Vastaanotetaan: "
STR_CALIBRE_RECEIVED: "Vastaanotettu: "
STR_CALIBRE_INSTRUCTION_1: "1) Asenna CrossPoint Reader -lisäosa"
STR_CALIBRE_INSTRUCTION_2: "2) Ole samassa WiFi-verkossa"
STR_CALIBRE_INSTRUCTION_2: "2) Ole samassa Wi-Fi-verkossa"
STR_CALIBRE_INSTRUCTION_3: "3) Calibressa: \"Lähetä laitteelle\""
STR_CALIBRE_INSTRUCTION_4: "\"Pidä tämä näyttö auki lähetyksen aikana\""
STR_CAT_DISPLAY: "Näyttö"
@@ -98,7 +98,7 @@ STR_KOREADER_PASSWORD: "KOReader-salasana"
STR_FILENAME: "Tiedostonimi"
STR_BINARY: "Binääri"
STR_SET_CREDENTIALS_FIRST: "Aseta tunnukset ensin"
STR_WIFI_CONN_FAILED: "WiFi-yhteys epäonnistui"
STR_WIFI_CONN_FAILED: "Wi-Fi-yhteys epäonnistui"
STR_AUTHENTICATING: "Tunnistaudutaan..."
STR_AUTH_SUCCESS: "Tunnistautuminen onnistui!"
STR_KOREADER_AUTH: "KOReader-tunnistautuminen"
@@ -211,11 +211,12 @@ STR_THEME_CLASSIC: "Klassinen"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Auringonvalon haalistumiskorjaus"
STR_SEAMLESS_SLEEP: "Sivu lepotilassa"
STR_QUICK_RESUME_TIMEOUT: "Pikajatko aikakatkaisulla"
STR_AFTER_TIMEOUT: "Aikakatkon jälkeen"
STR_REMAP_FRONT_BUTTONS: "Uudelleenmääritä etupainikkeet"
STR_OPDS_BROWSER: "OPDS-selain"
STR_COVER_CUSTOM: "Kansi + mukautettu"
STR_QUICK_RESUME: "Pikajatko"
STR_MENU_RECENT_BOOKS: "Viimeisimmät kirjat"
STR_REMOVE_FROM_RECENTS: "Poista viimeisimmistä kirjoista?"
STR_NO_RECENT_BOOKS: "Ei viimeisimpiä kirjoja"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Fichier vide"
STR_OUT_OF_BOUNDS: "Dépassement de mémoire"
STR_LOADING: "Chargement…"
STR_LOADING_POPUP: "Chargement"
STR_WIFI_NETWORKS: "Réseaux WiFi"
STR_WIFI_NETWORKS: "Réseaux Wi-Fi"
STR_NO_NETWORKS: "Aucun réseau"
STR_NETWORKS_FOUND: "%zu réseaux"
STR_SCANNING: "Recherche en cours…"
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Enregistrer le mot de passe ?"
STR_PRESS_OK_SCAN: "Appuyez sur OK pour scanner"
STR_JOIN_NETWORK: "Rejoindre un réseau"
STR_CREATE_HOTSPOT: "Créer un point daccès"
STR_JOIN_DESC: "Se connecter à un WiFi existant"
STR_HOTSPOT_DESC: "Créer un WiFi pour d'autres appareils"
STR_JOIN_DESC: "Se connecter à un Wi-Fi existant"
STR_HOTSPOT_DESC: "Créer un Wi-Fi pour d'autres appareils"
STR_STARTING_HOTSPOT: "Création du point daccès…"
STR_HOTSPOT_MODE: "Mode point daccès"
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce WiFi"
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce Wi-Fi"
STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou scannez le QR code :"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
STR_MAC_ADDRESS: "Adresse MAC :"
STR_CHECKING_WIFI: "Vérification du WiFi…"
STR_ENTER_WIFI_PASSWORD: "Entrez le mot de passe WiFi"
STR_CHECKING_WIFI: "Vérification du Wi-Fi…"
STR_ENTER_WIFI_PASSWORD: "Entrez le mot de passe Wi-Fi"
STR_TO_PREFIX: "vers "
STR_CALIBRE_RECEIVING: "Réception : "
STR_CALIBRE_RECEIVED: "Reçu : "
STR_CALIBRE_INSTRUCTION_1: "1) Installer plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Être sur le même réseau WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Être sur le même réseau Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) Calibre : Envoyer vers lappareil"
STR_CALIBRE_INSTRUCTION_4: "4) Gardez cet écran ouvert"
STR_CAT_DISPLAY: "Affichage"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "Mot de passe"
STR_FILENAME: "Nom de fichier"
STR_BINARY: "Binaire"
STR_SET_CREDENTIALS_FIRST: "Identifiants manquants"
STR_WIFI_CONN_FAILED: "Échec connexion WiFi"
STR_WIFI_CONN_FAILED: "Échec connexion Wi-Fi"
STR_AUTHENTICATING: "Authentification…"
STR_AUTH_SUCCESS: "Authentifié !"
STR_KOREADER_AUTH: "Auth KOReader"
@@ -235,11 +235,12 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Correction lisibilité au soleil"
STR_SEAMLESS_SLEEP: "Page comme veille"
STR_QUICK_RESUME_TIMEOUT: "Reprise rapide après délai"
STR_AFTER_TIMEOUT: "Après délai"
STR_REMAP_FRONT_BUTTONS: "Configurer boutons façade"
STR_OPDS_BROWSER: "Navigateur OPDS"
STR_COVER_CUSTOM: "Couverture + Perso"
STR_QUICK_RESUME: "Reprise rapide"
STR_MENU_RECENT_BOOKS: "Livres récents"
STR_REMOVE_FROM_RECENTS: "Retirer des Livres récents ?"
STR_NO_RECENT_BOOKS: "Aucun livre récent"
+2 -1
View File
@@ -233,11 +233,12 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
STR_SEAMLESS_SLEEP: "Seite als Ruhebild"
STR_QUICK_RESUME_TIMEOUT: "Schnelles Fortsetzen nach Timeout"
STR_AFTER_TIMEOUT: "Nach Timeout"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_OPDS_BROWSER: "OPDS-Browser"
STR_COVER_CUSTOM: "Umschlag + Eigenes"
STR_QUICK_RESUME: "Schnelles Fortsetzen"
STR_MENU_RECENT_BOOKS: "Zuletzt gelesen"
STR_REMOVE_FROM_RECENTS: "Aus Zuletzt gelesen entfernen?"
STR_NO_RECENT_BOOKS: "Keine Bücher"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Üres fájl"
STR_OUT_OF_BOUNDS: "Határon kívül"
STR_LOADING: "Betöltés..."
STR_LOADING_POPUP: "Betöltés"
STR_WIFI_NETWORKS: "WiFi hálózatok"
STR_WIFI_NETWORKS: "Wi-Fi hálózatok"
STR_NO_NETWORKS: "Nem található hálózat"
STR_NETWORKS_FOUND: "%zu hálózat található"
STR_SCANNING: "Keresés..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Jelszó mentése legközelebb?"
STR_PRESS_OK_SCAN: "Nyomj OK-t az újraellenőrzéshez"
STR_JOIN_NETWORK: "Csatlakozás hálózathoz"
STR_CREATE_HOTSPOT: "Hotspot létrehozása"
STR_JOIN_DESC: "Csatlakozás meglévő WiFi hálózathoz"
STR_HOTSPOT_DESC: "WiFi hálózat létrehozása másoknak"
STR_JOIN_DESC: "Csatlakozás meglévő Wi-Fi hálózathoz"
STR_HOTSPOT_DESC: "Wi-Fi hálózat létrehozása másoknak"
STR_STARTING_HOTSPOT: "Hotspot indítása..."
STR_HOTSPOT_MODE: "Hotspot mód"
STR_CONNECT_WIFI_HINT: "Csatlakoztasd az eszközöd ehhez a WiFi hálózathoz"
STR_CONNECT_WIFI_HINT: "Csatlakoztasd az eszközöd ehhez a Wi-Fi hálózathoz"
STR_OPEN_URL_HINT: "Nyisd meg ezt az URL-t a böngésződben"
STR_OR_HTTP_PREFIX: "vagy http://"
STR_SCAN_QR_HINT: "vagy olvasd be a QR-kódot a telefonoddal:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
STR_MAC_ADDRESS: "MAC-cím:"
STR_CHECKING_WIFI: "WiFi ellenőrzése..."
STR_ENTER_WIFI_PASSWORD: "Add meg a WiFi jelszót"
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
STR_TO_PREFIX: "- "
STR_CALIBRE_RECEIVING: "Fogadás: "
STR_CALIBRE_RECEIVED: "Fogadva: "
STR_CALIBRE_INSTRUCTION_1: "1) Telepítsd a CrossPoint Reader plugint"
STR_CALIBRE_INSTRUCTION_2: "2) Ugyanazon a WiFi hálózaton légy"
STR_CALIBRE_INSTRUCTION_2: "2) Ugyanazon a Wi-Fi hálózaton légy"
STR_CALIBRE_INSTRUCTION_3: "3) Calibre-ben: \"Küldés az eszközre\""
STR_CALIBRE_INSTRUCTION_4: "\"Tartsd nyitva a képernyőt küldés közben\""
STR_CAT_DISPLAY: "Megjelenítés"
@@ -100,7 +100,7 @@ STR_KOREADER_PASSWORD: "KOReader jelszó"
STR_FILENAME: "Fájlnév"
STR_BINARY: "Bináris"
STR_SET_CREDENTIALS_FIRST: "Először add meg az adatokat"
STR_WIFI_CONN_FAILED: "WiFi kapcsolat sikertelen"
STR_WIFI_CONN_FAILED: "Wi-Fi kapcsolat sikertelen"
STR_AUTHENTICATING: "Hitelesítés..."
STR_AUTH_SUCCESS: "Sikeres hitelesítés!"
STR_KOREADER_AUTH: "KOReader hitelesítés"
@@ -231,11 +231,12 @@ STR_THEME_CLASSIC: "Klasszikus"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Napfény halványulás javítás"
STR_SEAMLESS_SLEEP: "Oldal alvóképernyő"
STR_QUICK_RESUME_TIMEOUT: "Gyors folytatás időtúllépéskor"
STR_AFTER_TIMEOUT: "Időtúllépés után"
STR_REMAP_FRONT_BUTTONS: "Elülső gombok átállítása"
STR_OPDS_BROWSER: "OPDS böngésző"
STR_COVER_CUSTOM: "Borító + Egyéni"
STR_QUICK_RESUME: "Gyors folytatás"
STR_MENU_RECENT_BOOKS: "Legutóbbi könyvek"
STR_REMOVE_FROM_RECENTS: "Eltávolítás a legutóbbi könyvek közül?"
STR_NO_RECENT_BOOKS: "Nincsenek legutóbbi könyvek"
+18 -17
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "File vuoto"
STR_OUT_OF_BOUNDS: "Fuori dai limiti"
STR_LOADING: "Caricamento..."
STR_LOADING_POPUP: "Caricamento"
STR_WIFI_NETWORKS: "Reti WiFi"
STR_WIFI_NETWORKS: "Reti Wi-Fi"
STR_NO_NETWORKS: "Nessuna rete trovata"
STR_NETWORKS_FOUND: "%zu reti trovate"
STR_SCANNING: "Scansione..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Salvare la password?"
STR_PRESS_OK_SCAN: "Premere OK per ripetere scansione"
STR_JOIN_NETWORK: "Connetti a una rete"
STR_CREATE_HOTSPOT: "Crea Hotspot"
STR_JOIN_DESC: "Connessione ad una rete WiFi"
STR_HOTSPOT_DESC: "Creazione di una rete WiFi"
STR_JOIN_DESC: "Connessione ad una rete Wi-Fi"
STR_HOTSPOT_DESC: "Creazione di una rete Wi-Fi"
STR_STARTING_HOTSPOT: "Avvio Hotspot..."
STR_HOTSPOT_MODE: "Modalità Hotspot"
STR_CONNECT_WIFI_HINT: "Connettere il dispositivo a questa rete WiFi"
STR_CONNECT_WIFI_HINT: "Connettere il dispositivo a questa rete Wi-Fi"
STR_OPEN_URL_HINT: "Aprire questo URL nel browser"
STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "oppure scansionare il codice QR col telefono:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre wireless"
STR_CALIBRE_WEB_URL: "URL OPDS (Calibre)"
STR_NETWORK_LEGEND: "* = Protetta | + = Salvata"
STR_MAC_ADDRESS: "Indirizzo MAC:"
STR_CHECKING_WIFI: "Verifica WiFi..."
STR_ENTER_WIFI_PASSWORD: "Inserire password WiFi"
STR_CHECKING_WIFI: "Verifica Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Inserire password Wi-Fi"
STR_TO_PREFIX: "a "
STR_CALIBRE_RECEIVING: "Ricezione: "
STR_CALIBRE_RECEIVED: "Ricevuto: "
STR_CALIBRE_INSTRUCTION_1: "1) Installare il plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Collegarsi sulla stessa rete WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Collegarsi sulla stessa rete Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Invia al dispositivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Tenere questa schermata aperta durante l'invio\""
STR_CAT_DISPLAY: "Schermo"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "Password KOReader"
STR_FILENAME: "Nome file"
STR_BINARY: "Binario"
STR_SET_CREDENTIALS_FIRST: "Prima le credenziali"
STR_WIFI_CONN_FAILED: "Connessione WiFi non riuscita"
STR_WIFI_CONN_FAILED: "Connessione Wi-Fi non riuscita"
STR_AUTHENTICATING: "Autenticazione..."
STR_AUTH_SUCCESS: "Autenticazione riuscita!"
STR_KOREADER_AUTH: "Autenticazione KOReader"
@@ -122,7 +122,7 @@ STR_CLEAR_CACHE_FAILED: "Impossibile svuotare la cache"
STR_CHECK_SERIAL_OUTPUT: "Controllare l'output seriale per dettagli"
STR_DARK: "Scuro"
STR_LIGHT: "Chiaro"
STR_CUSTOM: "Wallpaper"
STR_CUSTOM: "Sfondo"
STR_COVER: "Copertina"
STR_NONE_OPT: "Nessuno"
STR_FIT: "Adatta"
@@ -147,7 +147,7 @@ STR_SMALL: "Piccolo"
STR_MEDIUM: "Medio"
STR_LARGE: "Grande"
STR_X_LARGE: "Molto grande"
STR_TIGHT: "Stretto"
STR_TIGHT: "Compatto"
STR_NORMAL: "Normale"
STR_WIDE: "Largo"
STR_JUSTIFY: "Giustificato"
@@ -175,13 +175,13 @@ STR_NO_UPDATE: "Nessun aggiornamento disponibile"
STR_UPDATE_FAILED: "Aggiornamento non riuscito"
STR_UPDATE_COMPLETE: "Aggiornamento completato"
STR_POWER_ON_HINT: "Tenere premuto il tasto di accensione per riavviare"
STR_RESTARTING_HINT: "Riavvio... Se il dispositivo non si accende, tenere premuto il tasto per qualche secondo."
STR_RESTARTING_HINT: "Riavvio... se il dispositivo non si accende, tenere premuto il tasto per qualche secondo."
STR_NO_ENTRIES: "Nessuna voce trovata"
STR_DOWNLOADING: "Download..."
STR_DOWNLOAD_FAILED: "Download non riuscito"
STR_ERROR_MSG: "Errore:"
STR_UNNAMED: "Senza nome"
STR_NO_SERVER_URL: "Nessun Server configurato"
STR_NO_SERVER_URL: "Nessun server configurato"
STR_FETCH_FEED_FAILED: "Impossibile recuperare il feed"
STR_PARSE_FEED_FAILED: "Impossibile analizzare il feed"
STR_NEXT_PAGE: "Pag. successiva »"
@@ -242,12 +242,13 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra esteso"
STR_SUNLIGHT_FADING_FIX: "Correzione luce solare"
STR_SEAMLESS_SLEEP: "Pagina come standby"
STR_QUICK_RESUME_TIMEOUT: "Ripresa rapida dopo timeout"
STR_AFTER_TIMEOUT: "Dopo timeout"
STR_REMAP_FRONT_BUTTONS: "Rimappa pulsanti frontali"
STR_OPDS_BROWSER: "Browser OPDS"
STR_SEARCH: "Cerca"
STR_COVER_CUSTOM: "Copertina + Wallpaper"
STR_QUICK_RESUME: "Ripresa rapida"
STR_MENU_RECENT_BOOKS: "Libri recenti"
STR_REMOVE_FROM_RECENTS: "Rimuovere da Libri recenti?"
STR_NO_RECENT_BOOKS: "Nessun libro recente"
@@ -349,10 +350,10 @@ STR_KB_HINT_EDIT_ENTRY: "Tieni premuto SU per modificare la voce"
STR_KB_TIPS: "Suggerimenti:"
STR_KB_HINT_RETURN_KEYBOARD: "GIÙ per tornare alla tastiera"
STR_KB_HINT_EXIT_URL_MODE: "ABC per uscire dalla modalità URL"
STR_KB_HINT_CLEAR_TEXT: "Tieni spinto CANC per cancellare tutto"
STR_KB_HINT_SECONDARY_CHAR: "Tieni spinto SELEZ. per carattere secondario"
STR_KB_HINT_UPPER_SECONDARY: "Tieni spinto SELEZ. per MAIUS. o car. secondario"
STR_KB_HINT_LOWER_SECONDARY: "Tieni spinto SELEZ. per minus. o car. secondario"
STR_KB_HINT_CLEAR_TEXT: "Tieni premuto CANC per cancellare tutto"
STR_KB_HINT_SECONDARY_CHAR: "Tieni premuto SELEZ. per carattere secondario"
STR_KB_HINT_UPPER_SECONDARY: "Tieni premuto SELEZ. per MAIUS. o car. secondario"
STR_KB_HINT_LOWER_SECONDARY: "Tieni premuto SELEZ. per minus. o car. secondario"
STR_KB_HINT_URL_SNIPPETS: "Premi l'URL per le anteprime"
STR_SD_FIRMWARE_UPDATE: "Aggiornamento firmware da SD"
STR_SELECT_FIRMWARE_FILE: "Seleziona il firmware (.bin)"
+10 -9
View File
@@ -23,7 +23,7 @@ STR_EMPTY_FILE: "Бос файл"
STR_OUT_OF_BOUNDS: "Шектен тыс"
STR_LOADING: "Жүктелуде..."
STR_LOADING_POPUP: "Жүктелуде"
STR_WIFI_NETWORKS: "WiFi желілері"
STR_WIFI_NETWORKS: "Wi-Fi желілері"
STR_NO_NETWORKS: "Желілер табылмады"
STR_NETWORKS_FOUND: "%zu желі табылды"
STR_SCANNING: "Іздеуде..."
@@ -35,11 +35,11 @@ STR_SAVE_PASSWORD: "Келесі жолға құпия сөзді сақтау
STR_PRESS_OK_SCAN: "Қайта іздеу үшін OK басыңыз"
STR_JOIN_NETWORK: "Желіге қосылу"
STR_CREATE_HOTSPOT: "Хотспот жасау"
STR_JOIN_DESC: "Бар WiFi желісіне қосылу"
STR_HOTSPOT_DESC: "Басқалар қоса алатын WiFi желісін жасау"
STR_JOIN_DESC: "Бар Wi-Fi желісіне қосылу"
STR_HOTSPOT_DESC: "Басқалар қоса алатын Wi-Fi желісін жасау"
STR_STARTING_HOTSPOT: "Хотспот іске қосылуда..."
STR_HOTSPOT_MODE: "Хотспот режимі"
STR_CONNECT_WIFI_HINT: "Құрылғыңызды осы WiFi желісіне қосыңыз"
STR_CONNECT_WIFI_HINT: "Құрылғыңызды осы Wi-Fi желісіне қосыңыз"
STR_OPEN_URL_HINT: "Браузерде осы URL мекенжайын ашыңыз"
STR_OR_HTTP_PREFIX: "немесе http://"
STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:"
@@ -47,13 +47,13 @@ STR_CALIBRE_WIRELESS: "Calibre сымсыз"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған"
STR_MAC_ADDRESS: "MAC мекенжайы:"
STR_CHECKING_WIFI: "WiFi тексерілуде..."
STR_ENTER_WIFI_PASSWORD: "WiFi құпия сөзін енгізіңіз"
STR_CHECKING_WIFI: "Wi-Fi тексерілуде..."
STR_ENTER_WIFI_PASSWORD: "Wi-Fi құпия сөзін енгізіңіз"
STR_TO_PREFIX: ""
STR_CALIBRE_RECEIVING: "Қабылдануда: "
STR_CALIBRE_RECEIVED: "Қабылданды: "
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader плагинін орнатыңыз"
STR_CALIBRE_INSTRUCTION_2: "2) Бір WiFi желісінде болыңыз"
STR_CALIBRE_INSTRUCTION_2: "2) Бір Wi-Fi желісінде болыңыз"
STR_CALIBRE_INSTRUCTION_3: "3) Calibre-де: \"Құрылғыға жіберу\""
STR_CALIBRE_INSTRUCTION_4: "\"Жіберу кезінде осы экранды ашық ұстаңыз\""
STR_CAT_DISPLAY: "Дисплей"
@@ -94,7 +94,7 @@ STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
STR_FILENAME: "Файл аты"
STR_BINARY: "Бинарлық"
STR_SET_CREDENTIALS_FIRST: "Алдымен тіркелгі деректерін орнатыңыз"
STR_WIFI_CONN_FAILED: "WiFi қосылуы сәтсіз"
STR_WIFI_CONN_FAILED: "Wi-Fi қосылуы сәтсіз"
STR_AUTHENTICATING: "Аутентификацияланып жатыр..."
STR_AUTH_SUCCESS: "Аутентификация сәтті!"
STR_KOREADER_AUTH: "KOReader аутентификациясы"
@@ -207,11 +207,12 @@ STR_THEME_CLASSIC: "Классикалық"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra кеңейтілген"
STR_SUNLIGHT_FADING_FIX: "Күн сәулесінен солу түзету"
STR_SEAMLESS_SLEEP: "Бет ұйқы экраны"
STR_QUICK_RESUME_TIMEOUT: "Таймауттан кейін жылдам жалғастыру"
STR_AFTER_TIMEOUT: "Таймауттан кейін"
STR_REMAP_FRONT_BUTTONS: "Алдыңғы түймелерді қайта баптау"
STR_OPDS_BROWSER: "OPDS шолғышы"
STR_COVER_CUSTOM: "Мұқаба + Өзгертілген"
STR_QUICK_RESUME: "Жылдам жалғастыру"
STR_MENU_RECENT_BOOKS: "Жуырда оқылған кітаптар"
STR_REMOVE_FROM_RECENTS: "Жуырда оқылған кітаптардан жою?"
STR_NO_RECENT_BOOKS: "Жуырда оқылған кітаптар жоқ"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Tuščias failas"
STR_OUT_OF_BOUNDS: "Už ribų"
STR_LOADING: "Kraunama..."
STR_LOADING_POPUP: "Kraunama"
STR_WIFI_NETWORKS: "WiFi tinklai"
STR_WIFI_NETWORKS: "Wi-Fi tinklai"
STR_NO_NETWORKS: "Tinklų nerasta"
STR_NETWORKS_FOUND: "Rasta: %zu"
STR_SCANNING: "Ieškoma..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Išsaugoti slaptažodį?"
STR_PRESS_OK_SCAN: "Ieškoti iš naujo (OK)"
STR_JOIN_NETWORK: "Prisijungti"
STR_CREATE_HOTSPOT: "Sukurti prieigą"
STR_JOIN_DESC: "Jungtis prie esamo WiFi"
STR_HOTSPOT_DESC: "Sukurti WiFi kitiems"
STR_JOIN_DESC: "Jungtis prie esamo Wi-Fi"
STR_HOTSPOT_DESC: "Sukurti Wi-Fi kitiems"
STR_STARTING_HOTSPOT: "Kuriamas ryšys..."
STR_HOTSPOT_MODE: "Prieigos režimas"
STR_CONNECT_WIFI_HINT: "Prijunkite įrenginį prie šio WiFi"
STR_CONNECT_WIFI_HINT: "Prijunkite įrenginį prie šio Wi-Fi"
STR_OPEN_URL_HINT: "Atidarykite šį adresą naršyklėje"
STR_OR_HTTP_PREFIX: "arba http://"
STR_SCAN_QR_HINT: "arba nuskaitykite QR kodą:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre belaidis"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Užšifruota | + = Išsaugota"
STR_MAC_ADDRESS: "MAC adresas:"
STR_CHECKING_WIFI: "Tikrinamas WiFi..."
STR_ENTER_WIFI_PASSWORD: "WiFi slaptažodis"
STR_CHECKING_WIFI: "Tikrinamas Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Wi-Fi slaptažodis"
STR_TO_PREFIX: "į "
STR_CALIBRE_RECEIVING: "Gaunama: "
STR_CALIBRE_RECEIVED: "Gauta: "
STR_CALIBRE_INSTRUCTION_1: "1) Įdiekite CrossPoint įskiepį"
STR_CALIBRE_INSTRUCTION_2: "2) Naudokite tą patį WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Naudokite tą patį Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) Calibre: „Send to device“"
STR_CALIBRE_INSTRUCTION_4: "Neišjunkite šio ekrano"
STR_CAT_DISPLAY: "Ekranas"
@@ -100,7 +100,7 @@ STR_KOREADER_PASSWORD: "KOReader slaptažodis"
STR_FILENAME: "Failas"
STR_BINARY: "Dvejetainis"
STR_SET_CREDENTIALS_FIRST: "Nustatykite duomenis"
STR_WIFI_CONN_FAILED: "WiFi ryšys nutrūko"
STR_WIFI_CONN_FAILED: "Wi-Fi ryšys nutrūko"
STR_AUTHENTICATING: "Jungiamasi..."
STR_AUTH_SUCCESS: "Prisijungta!"
STR_KOREADER_AUTH: "KOReader Login"
@@ -231,11 +231,12 @@ STR_THEME_CLASSIC: "Klasikinė"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Ext."
STR_SUNLIGHT_FADING_FIX: "Blyškumo pataisa"
STR_SEAMLESS_SLEEP: "Puslapis miego ekrane"
STR_QUICK_RESUME_TIMEOUT: "Greitas tęsimas po skirtojo laiko"
STR_AFTER_TIMEOUT: "Po skirtojo laiko"
STR_REMAP_FRONT_BUTTONS: "Keisti mygtukus"
STR_OPDS_BROWSER: "OPDS naršyklė"
STR_COVER_CUSTOM: "Viršelis + Kita"
STR_QUICK_RESUME: "Greitas tęsimas"
STR_MENU_RECENT_BOOKS: "Paskutinės"
STR_REMOVE_FROM_RECENTS: "Pašalinti iš paskutinių?"
STR_NO_RECENT_BOOKS: "Paskutinių nėra"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Pusty plik"
STR_OUT_OF_BOUNDS: "Poza granicami"
STR_LOADING: "Ładowanie..."
STR_LOADING_POPUP: "Ładowanie"
STR_WIFI_NETWORKS: "Sieci WiFi"
STR_WIFI_NETWORKS: "Sieci Wi-Fi"
STR_NO_NETWORKS: "Nie znaleziono sieci"
STR_NETWORKS_FOUND: "Znaleziono %zu sieci"
STR_SCANNING: "Skanowanie..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Zapisać hasło na przyszłość?"
STR_PRESS_OK_SCAN: "Naciśnij OK aby skanować ponownie"
STR_JOIN_NETWORK: "Dołącz do sieci"
STR_CREATE_HOTSPOT: "Stwórz Hotspot"
STR_JOIN_DESC: "Podłącz do istniejącej sieci WiFi"
STR_HOTSPOT_DESC: "Stwórz sieć WiFi do której podłączyć mogą się inni"
STR_JOIN_DESC: "Podłącz do istniejącej sieci Wi-Fi"
STR_HOTSPOT_DESC: "Stwórz sieć Wi-Fi do której podłączyć mogą się inni"
STR_STARTING_HOTSPOT: "Startowanie Hotspota..."
STR_HOTSPOT_MODE: "Tryb Hotspot"
STR_CONNECT_WIFI_HINT: "Podłącz swoje urządzenie do tej sieci WiFi"
STR_CONNECT_WIFI_HINT: "Podłącz swoje urządzenie do tej sieci Wi-Fi"
STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce"
STR_OR_HTTP_PREFIX: "albo http://"
STR_SCAN_QR_HINT: "albo zeskanuj kod QR telefonem:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Bezprzewodowe połączenie z Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Zaszyfrowane | + = Zapisane"
STR_MAC_ADDRESS: "Adres MAC:"
STR_CHECKING_WIFI: "Sprawdzanie WiFi..."
STR_ENTER_WIFI_PASSWORD: "Wprowadź hasło WiFi"
STR_CHECKING_WIFI: "Sprawdzanie Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Wprowadź hasło Wi-Fi"
STR_TO_PREFIX: "Z "
STR_CALIBRE_RECEIVING: "Odbieranie: "
STR_CALIBRE_RECEIVED: "Odebrano: "
STR_CALIBRE_INSTRUCTION_1: "1) Zainstaluj plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Bądź w tej samej sieci WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Bądź w tej samej sieci Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) W Calibre: \"Wyślij do urządzenia\""
STR_CALIBRE_INSTRUCTION_4: "\"Zostaw ten ekran podczas wysyłania\""
STR_CAT_DISPLAY: "Wyświetlacz"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "Hasło KOReader"
STR_FILENAME: "Nazwa pliku"
STR_BINARY: "Binary"
STR_SET_CREDENTIALS_FIRST: "Najpierw ustaw uwierzytelnianie"
STR_WIFI_CONN_FAILED: "Połączenie z WiFi nieudane"
STR_WIFI_CONN_FAILED: "Połączenie z Wi-Fi nieudane"
STR_AUTHENTICATING: "Uwierzytelnianie..."
STR_AUTH_SUCCESS: "Pomyślnie uwierzytelniono!"
STR_KOREADER_AUTH: "KOReader Auth"
@@ -242,12 +242,13 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Przeciwdziałanie blaknięciu od słońca"
STR_SEAMLESS_SLEEP: "Strona jako ekran snu"
STR_QUICK_RESUME_TIMEOUT: "Szybkie wznawianie po czasie"
STR_AFTER_TIMEOUT: "Po upływie czasu"
STR_REMAP_FRONT_BUTTONS: "Skonfiguruj przednie przyciski"
STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Szukaj"
STR_COVER_CUSTOM: "Okładka + Własne"
STR_QUICK_RESUME: "Szybkie wznawianie"
STR_MENU_RECENT_BOOKS: "Ostatnio czytane"
STR_REMOVE_FROM_RECENTS: "Usunąć z ostatnio czytanych?"
STR_NO_RECENT_BOOKS: "Brak ostatnio czytanych"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Arquivo vazio"
STR_OUT_OF_BOUNDS: "Fora dos limites"
STR_LOADING: "Carregando..."
STR_LOADING_POPUP: "Carregando"
STR_WIFI_NETWORKS: "Redes WiFi"
STR_WIFI_NETWORKS: "Redes Wi-Fi"
STR_NO_NETWORKS: "Sem redes"
STR_NETWORKS_FOUND: "%zu redes encontradas"
STR_SCANNING: "Procurando..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
STR_JOIN_NETWORK: "Entrar em uma rede"
STR_CREATE_HOTSPOT: "Criar hotspot"
STR_JOIN_DESC: "Conecte-se a uma rede WiFi existente"
STR_HOTSPOT_DESC: "Crie uma rede WiFi outras pessoas entrarem"
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas entrarem"
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
STR_HOTSPOT_MODE: "Modo hotspot"
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede WiFi"
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre sem fio"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Criptografada | + = Salva"
STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "Verificando WiFi..."
STR_ENTER_WIFI_PASSWORD: "Digite a senha WiFi"
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
STR_TO_PREFIX: "para"
STR_CALIBRE_RECEIVING: "Recebendo:"
STR_CALIBRE_RECEIVED: "Recebido:"
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja mesma rede WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
STR_CAT_DISPLAY: "Tela"
@@ -98,7 +98,7 @@ STR_KOREADER_PASSWORD: "Senha do KOReader"
STR_FILENAME: "Nome do arquivo"
STR_BINARY: "Binário"
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
STR_WIFI_CONN_FAILED: "Falha na conexão WiFi"
STR_WIFI_CONN_FAILED: "Falha na conexão Wi-Fi"
STR_AUTHENTICATING: "Autenticando..."
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
STR_KOREADER_AUTH: "Autenticação KOReader"
@@ -212,11 +212,12 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_SEAMLESS_SLEEP: "Página como repouso"
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
STR_AFTER_TIMEOUT: "Após tempo limite"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Capa + personalizado"
STR_QUICK_RESUME: "Retomada rápida"
STR_MENU_RECENT_BOOKS: "Livros recentes"
STR_REMOVE_FROM_RECENTS: "Remover dos Livros recentes?"
STR_NO_RECENT_BOOKS: "Sem livros recentes"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Fişier gol"
STR_OUT_OF_BOUNDS: "Eroare: În afara limitelor"
STR_LOADING: "Se încarcă..."
STR_LOADING_POPUP: "Se încarcă..."
STR_WIFI_NETWORKS: "Reţele WiFi"
STR_WIFI_NETWORKS: "Reţele Wi-Fi"
STR_NO_NETWORKS: "Nu s-au găsit reţele"
STR_NETWORKS_FOUND: "%zu reţele găsite"
STR_SCANNING: "Scanează..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Salvaţi parola?"
STR_PRESS_OK_SCAN: "Apăsaţi OK pentru a scana din nou"
STR_JOIN_NETWORK: "Conectaţi-vă la o reţea"
STR_CREATE_HOTSPOT: "Creaţi un hotspot"
STR_JOIN_DESC: "Conectaţi-vă la o reţea WiFi existentă"
STR_HOTSPOT_DESC: "Creaţi un hotspot WiFi"
STR_JOIN_DESC: "Conectaţi-vă la o reţea Wi-Fi existentă"
STR_HOTSPOT_DESC: "Creaţi un hotspot Wi-Fi"
STR_STARTING_HOTSPOT: "Hotspot porneşte..."
STR_HOTSPOT_MODE: "Mod Hotspot"
STR_CONNECT_WIFI_HINT: "Conectaţi-vă dispozitivul la această reţea WiFi"
STR_CONNECT_WIFI_HINT: "Conectaţi-vă dispozitivul la această reţea Wi-Fi"
STR_OPEN_URL_HINT: "Deschideţi acest URL în browserul dvs."
STR_OR_HTTP_PREFIX: "sau http://"
STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Criptat | + = Salvat"
STR_MAC_ADDRESS: "Adresă MAC:"
STR_CHECKING_WIFI: "Verificare WiFi..."
STR_ENTER_WIFI_PASSWORD: "Introduceţi parola WiFi"
STR_CHECKING_WIFI: "Verificare Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Introduceţi parola Wi-Fi"
STR_TO_PREFIX: "la "
STR_CALIBRE_RECEIVING: "Se primeşte: "
STR_CALIBRE_RECEIVED: "Primite: "
STR_CALIBRE_INSTRUCTION_1: "1) Instalaţi plugin-ul CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Fiţi în aceeaşi reţea WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Fiţi în aceeaşi reţea Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) În Calibre: \"Trimiteţi la dispozitiv\""
STR_CALIBRE_INSTRUCTION_4: "\"Păstraţi acest ecran deschis în timpul trimiterii\""
STR_CAT_DISPLAY: "Ecran"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "Parolă KOReader"
STR_FILENAME: "Nume fişier"
STR_BINARY: "Fişier binar"
STR_SET_CREDENTIALS_FIRST: "Vă rugăm să setaţi mai întâi acreditările"
STR_WIFI_CONN_FAILED: "Conexiune WiFi eşuată"
STR_WIFI_CONN_FAILED: "Conexiune Wi-Fi eşuată"
STR_AUTHENTICATING: "Se autentifică..."
STR_AUTH_SUCCESS: "Autentificare reuşită!"
STR_KOREADER_AUTH: "Autentificare KOReader"
@@ -234,11 +234,12 @@ STR_THEME_CLASSIC: "Clasic"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Corecţie estompare lumină"
STR_SEAMLESS_SLEEP: "Pagină ca repaus"
STR_QUICK_RESUME_TIMEOUT: "Reluare rapidă la timeout"
STR_AFTER_TIMEOUT: "După timeout"
STR_REMAP_FRONT_BUTTONS: "Remapare butoane frontale"
STR_OPDS_BROWSER: "Browser OPDS"
STR_COVER_CUSTOM: "Copertă + Personalizat"
STR_QUICK_RESUME: "Reluare rapidă"
STR_MENU_RECENT_BOOKS: "Cărţi recente"
STR_REMOVE_FROM_RECENTS: "Eliminați din Cărţi recente?"
STR_NO_RECENT_BOOKS: "Nicio carte recentă"
+2 -1
View File
@@ -242,12 +242,13 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_SEAMLESS_SLEEP: "Страница как экран сна"
STR_QUICK_RESUME_TIMEOUT: "Быстрое возобновление по таймауту"
STR_AFTER_TIMEOUT: "По таймауту"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_OPDS_BROWSER: "OPDS браузер"
STR_SEARCH: "Поиск"
STR_COVER_CUSTOM: "Обложка + Свой"
STR_QUICK_RESUME: "Быстрое возобновление"
STR_MENU_RECENT_BOOKS: "Недавние книги"
STR_REMOVE_FROM_RECENTS: "Удалить из недавних книг?"
STR_NO_RECENT_BOOKS: "Нет недавних книг"
+10 -9
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Prazna datoteka"
STR_OUT_OF_BOUNDS: "Izven meja"
STR_LOADING: "Nalaganje..."
STR_LOADING_POPUP: "Nalaganje"
STR_WIFI_NETWORKS: "WiFi omrežja"
STR_WIFI_NETWORKS: "Wi-Fi omrežja"
STR_NO_NETWORKS: "Ni najdenih omrežij"
STR_NETWORKS_FOUND: "Najdenih omrežij: %zu"
STR_SCANNING: "Iskanje..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Shranim geslo za naslednjič?"
STR_PRESS_OK_SCAN: "Pritisni OK za ponovno iskanje"
STR_JOIN_NETWORK: "Poveži se v omrežje"
STR_CREATE_HOTSPOT: "Ustvari dostopno točko"
STR_JOIN_DESC: "Poveži se v obstoječe WiFi omrežje"
STR_HOTSPOT_DESC: "Ustvari WiFi omrežje, v katerega se lahko povežejo drugi"
STR_JOIN_DESC: "Poveži se v obstoječe Wi-Fi omrežje"
STR_HOTSPOT_DESC: "Ustvari Wi-Fi omrežje, v katerega se lahko povežejo drugi"
STR_STARTING_HOTSPOT: "Zaganjanje dostopne točke..."
STR_HOTSPOT_MODE: "Način dostopne točke"
STR_CONNECT_WIFI_HINT: "Poveži svojo napravo v to WiFi omrežje"
STR_CONNECT_WIFI_HINT: "Poveži svojo napravo v to Wi-Fi omrežje"
STR_OPEN_URL_HINT: "Odpri ta URL v svojem brskalniku"
STR_OR_HTTP_PREFIX: "ali http://"
STR_SCAN_QR_HINT: "ali skeniraj QR kodo s telefonom:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Brezžični Calibre"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_NETWORK_LEGEND: "* = Šifrirano | + = Shranjeno"
STR_MAC_ADDRESS: "MAC naslov:"
STR_CHECKING_WIFI: "Preverjanje WiFi-ja..."
STR_ENTER_WIFI_PASSWORD: "Vnesi WiFi geslo"
STR_CHECKING_WIFI: "Preverjanje Wi-Fi-ja..."
STR_ENTER_WIFI_PASSWORD: "Vnesi Wi-Fi geslo"
STR_TO_PREFIX: "v "
STR_CALIBRE_RECEIVING: "Prejemanje: "
STR_CALIBRE_RECEIVED: "Prejeto: "
STR_CALIBRE_INSTRUCTION_1: "1) Namesti vtičnik CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Bodi v istem WiFi omrežju"
STR_CALIBRE_INSTRUCTION_2: "2) Bodi v istem Wi-Fi omrežju"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: \"Pošlji v napravo\""
STR_CALIBRE_INSTRUCTION_4: "\"Med pošiljanjem pusti ta zaslon odprt\""
STR_CAT_DISPLAY: "Zaslon"
@@ -100,7 +100,7 @@ STR_KOREADER_PASSWORD: "KOReader geslo"
STR_FILENAME: "Ime datoteke"
STR_BINARY: "Binarno"
STR_SET_CREDENTIALS_FIRST: "Najprej nastavi podatke za prijavo"
STR_WIFI_CONN_FAILED: "WiFi povezava ni uspela"
STR_WIFI_CONN_FAILED: "Wi-Fi povezava ni uspela"
STR_AUTHENTICATING: "Preverjanje..."
STR_AUTH_SUCCESS: "Uspešna prijava!"
STR_KOREADER_AUTH: "KOReader avtentikacija"
@@ -231,11 +231,12 @@ STR_THEME_CLASSIC: "Klasična"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra razširjena"
STR_SUNLIGHT_FADING_FIX: "Popravek bledenja na soncu"
STR_SEAMLESS_SLEEP: "Stran kot zaslon sna"
STR_QUICK_RESUME_TIMEOUT: "Hitro nadaljevanje po izteku"
STR_AFTER_TIMEOUT: "Po izteku"
STR_REMAP_FRONT_BUTTONS: "Prenastavi sprednje gumbe"
STR_OPDS_BROWSER: "OPDS brskalnik"
STR_COVER_CUSTOM: "Naslovnica + po meri"
STR_QUICK_RESUME: "Hitro nadaljevanje"
STR_MENU_RECENT_BOOKS: "Zadnje knjige"
STR_REMOVE_FROM_RECENTS: "Odstrani iz zadnjih knjig?"
STR_NO_RECENT_BOOKS: "Ni zadnjih knjig"
+2 -1
View File
@@ -242,12 +242,13 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extendido"
STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento"
STR_SEAMLESS_SLEEP: "Página como reposo"
STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
STR_AFTER_TIMEOUT: "Tras tiempo"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Buscar"
STR_COVER_CUSTOM: "Portada + Pers."
STR_QUICK_RESUME: "Reanudación rápida"
STR_MENU_RECENT_BOOKS: "Libros recientes"
STR_REMOVE_FROM_RECENTS: "¿Eliminar de Libros recientes?"
STR_NO_RECENT_BOOKS: "No hay libros recientes"
+3 -2
View File
@@ -49,7 +49,7 @@ STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
STR_MAC_ADDRESS: "MAC-adress:"
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
STR_ENTER_WIFI_PASSWORD: "Skriv in WiFi-lösenord"
STR_ENTER_WIFI_PASSWORD: "Skriv in Wi-Fi-lösenord"
STR_TO_PREFIX: "till "
STR_CALIBRE_RECEIVING: "Tar emot:"
STR_CALIBRE_RECEIVED: "Mottaget:"
@@ -242,12 +242,13 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_SEAMLESS_SLEEP: "Sida som viloskärm"
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
STR_AFTER_TIMEOUT: "Efter timeout"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_OPDS_BROWSER: "OPDS-webbläsare"
STR_SEARCH: "Sök"
STR_COVER_CUSTOM: "Omslag + Valfri"
STR_QUICK_RESUME: "Snabb återupptagning"
STR_MENU_RECENT_BOOKS: "Senaste böckerna"
STR_REMOVE_FROM_RECENTS: "Ta bort från Senaste böckerna?"
STR_NO_RECENT_BOOKS: "Inga senaste böcker"
+9 -8
View File
@@ -23,7 +23,7 @@ STR_EMPTY_FILE: "Boş dosya"
STR_OUT_OF_BOUNDS: "Sınırların dışında"
STR_LOADING: "Yükleniyor..."
STR_LOADING_POPUP: "Yükleniyor"
STR_WIFI_NETWORKS: "WiFi Ağları"
STR_WIFI_NETWORKS: "Wi-Fi Ağları"
STR_NO_NETWORKS: "Ağ bulunamadı"
STR_NETWORKS_FOUND: "%zu ağ bulundu"
STR_SCANNING: "Tarıyor..."
@@ -35,11 +35,11 @@ STR_SAVE_PASSWORD: "Şifre kaydedilsin mi?"
STR_PRESS_OK_SCAN: "Tekrar taramak için OK'e basın"
STR_JOIN_NETWORK: "Bir Ağa Katıl"
STR_CREATE_HOTSPOT: "Erişim Noktası Oluştur"
STR_JOIN_DESC: "Mevcut bir WiFi ağına bağlan"
STR_JOIN_DESC: "Mevcut bir Wi-Fi ağına bağlan"
STR_HOTSPOT_DESC: "Başkalarının katılabileceği ağ oluştur"
STR_STARTING_HOTSPOT: "Erişim Noktası Başlatılıyor..."
STR_HOTSPOT_MODE: "Erişim Noktası Modu"
STR_CONNECT_WIFI_HINT: "Cihazınızı bu WiFi ağına bağlayın"
STR_CONNECT_WIFI_HINT: "Cihazınızı bu Wi-Fi ağına bağlayın"
STR_OPEN_URL_HINT: "Tarayıcınızda bu adresi açın"
STR_OR_HTTP_PREFIX: "veya http://"
STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:"
@@ -47,13 +47,13 @@ STR_CALIBRE_WIRELESS: "Calibre Kablosuz"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Şifreli | + = Kayıtlı"
STR_MAC_ADDRESS: "MAC adresi:"
STR_CHECKING_WIFI: "WiFi kontrol ediliyor..."
STR_ENTER_WIFI_PASSWORD: "WiFi Şifresini Girin"
STR_CHECKING_WIFI: "Wi-Fi kontrol ediliyor..."
STR_ENTER_WIFI_PASSWORD: "Wi-Fi Şifresini Girin"
STR_TO_PREFIX: "Ağ: "
STR_CALIBRE_RECEIVING: "Alınıyor: "
STR_CALIBRE_RECEIVED: "Alındı: "
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader eklentisini kurun"
STR_CALIBRE_INSTRUCTION_2: "2) Aynı WiFi ağında olun"
STR_CALIBRE_INSTRUCTION_2: "2) Aynı Wi-Fi ağında olun"
STR_CALIBRE_INSTRUCTION_3: "3) Calibre'de: \"Cihaza gönder\""
STR_CALIBRE_INSTRUCTION_4: "\"Gönderim sırasında bu ekranı açık tutun\""
STR_CAT_DISPLAY: "Ekran"
@@ -98,7 +98,7 @@ STR_KOREADER_PASSWORD: "KOReader Şifresi"
STR_FILENAME: "Dosya Adı"
STR_BINARY: "İkili"
STR_SET_CREDENTIALS_FIRST: "Önce kimlik bilgilerini ayarlayın"
STR_WIFI_CONN_FAILED: "WiFi bağlantısı başarısız"
STR_WIFI_CONN_FAILED: "Wi-Fi bağlantısı başarısız"
STR_AUTHENTICATING: "Kimlik doğrulanıyor..."
STR_AUTH_SUCCESS: "Kimlik doğrulama başarılı!"
STR_KOREADER_AUTH: "KOReader Doğrulaması"
@@ -211,11 +211,12 @@ STR_THEME_CLASSIC: "Klasik"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Genişletilmiş"
STR_SUNLIGHT_FADING_FIX: "Güneş Işığı Solma Düzeltmesi"
STR_SEAMLESS_SLEEP: "Sayfa uyku ekranı"
STR_QUICK_RESUME_TIMEOUT: "Zaman aşımında Hızlı Devam"
STR_AFTER_TIMEOUT: "Zaman aşımında"
STR_REMAP_FRONT_BUTTONS: "Ön Tuşları Yeniden Ata"
STR_OPDS_BROWSER: "OPDS Tarayıcı"
STR_COVER_CUSTOM: "Kapak + Özel"
STR_QUICK_RESUME: "Hızlı Devam"
STR_MENU_RECENT_BOOKS: "Son Kitaplar"
STR_REMOVE_FROM_RECENTS: "Son Kitaplar listesinden kaldırılsın mı?"
STR_NO_RECENT_BOOKS: "Son okunan kitap yok"
+9 -8
View File
@@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Порожній файл"
STR_OUT_OF_BOUNDS: "Поза межами"
STR_LOADING: "Завантаження..."
STR_LOADING_POPUP: "Завантаження"
STR_WIFI_NETWORKS: "Мережі WiFi"
STR_WIFI_NETWORKS: "Мережі Wi-Fi"
STR_NO_NETWORKS: "Мереж не знайдено"
STR_NETWORKS_FOUND: "мереж %zu"
STR_SCANNING: "Сканування..."
@@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Зберегти пароль на наступний раз
STR_PRESS_OK_SCAN: "Натисніть OK для повторного сканування"
STR_JOIN_NETWORK: "Приєднатися до мережі"
STR_CREATE_HOTSPOT: "Створити точку доступу"
STR_JOIN_DESC: "Підключитися до існуючої мережі WiFi"
STR_JOIN_DESC: "Підключитися до існуючої мережі Wi-Fi"
STR_HOTSPOT_DESC: "Дозволити іншим підключення по Wi-Fi"
STR_STARTING_HOTSPOT: "Запуск точки доступу..."
STR_HOTSPOT_MODE: "Режим точки доступу"
STR_CONNECT_WIFI_HINT: "Підключіть пристрій до Цієї мережі WiFi"
STR_CONNECT_WIFI_HINT: "Підключіть пристрій до цієї мережі Wi-Fi"
STR_OPEN_URL_HINT: "Відкрийте посилання у своєму браузері"
STR_OR_HTTP_PREFIX: "або http://"
STR_SCAN_QR_HINT: "або відскануйте QR-код телефоном:"
@@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Отримати з Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Зашифровано | + = Збережено"
STR_MAC_ADDRESS: "MAC адреса:"
STR_CHECKING_WIFI: "Перевірка WiFi..."
STR_ENTER_WIFI_PASSWORD: "Введіть пароль WiFi"
STR_CHECKING_WIFI: "Перевірка Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Введіть пароль Wi-Fi"
STR_TO_PREFIX: "до "
STR_CALIBRE_RECEIVING: "Отримання: "
STR_CALIBRE_RECEIVED: "Отримано: "
STR_CALIBRE_INSTRUCTION_1: "1) Встановіть плагін CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Перебувайте в тій самій мережі WiFi"
STR_CALIBRE_INSTRUCTION_2: "2) Перебувайте в тій самій мережі Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) В Calibre: \"Надіслати на пристрій\""
STR_CALIBRE_INSTRUCTION_4: "\"Тримайте цей екран відкритим під час надсилання\""
STR_CAT_DISPLAY: "Екран"
@@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "Пароль KOReader"
STR_FILENAME: "Ім'я файлу"
STR_BINARY: "Побайтово"
STR_SET_CREDENTIALS_FIRST: "Спочатку вкажіть облікові дані"
STR_WIFI_CONN_FAILED: "Помилка підключення WiFi"
STR_WIFI_CONN_FAILED: "Помилка підключення Wi-Fi"
STR_AUTHENTICATING: "Автентифікація..."
STR_AUTH_SUCCESS: "Успішно автентифіковано!"
STR_KOREADER_AUTH: "Автентифікація KOReader"
@@ -242,12 +242,13 @@ STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на сонці"
STR_SEAMLESS_SLEEP: "Сторінка як екран сну"
STR_QUICK_RESUME_TIMEOUT: "Швидке продовження після таймауту"
STR_AFTER_TIMEOUT: "Після таймауту"
STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки"
STR_OPDS_BROWSER: "Браузер OPDS"
STR_SEARCH: "Пошук"
STR_COVER_CUSTOM: "Обкл. + власне"
STR_QUICK_RESUME: "Швидке продовження"
STR_MENU_RECENT_BOOKS: "Останні книги"
STR_REMOVE_FROM_RECENTS: "Видалити з останніх книг?"
STR_NO_RECENT_BOOKS: "Немає останніх книг"
+10 -2
View File
@@ -110,8 +110,16 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
if (self->inEntry) {
if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr &&
strcmp(type, "application/epub+zip") == 0) {
self->currentEntry.type = OpdsEntryType::BOOK;
self->currentEntry.href = href;
// Prefer plain EPUB links over derived formats when multiple
// acquisition links are present for one entry.
const bool isPlainEpub = strstr(href, ".epub") != nullptr || strstr(href, "/epub/") != nullptr;
const bool alreadyHasPlainEpub = self->currentEntry.type == OpdsEntryType::BOOK &&
(self->currentEntry.href.find(".epub") != std::string::npos ||
self->currentEntry.href.find("/epub/") != std::string::npos);
if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) {
self->currentEntry.type = OpdsEntryType::BOOK;
self->currentEntry.href = href;
}
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
if (self->currentEntry.type != OpdsEntryType::BOOK) {
self->currentEntry.type = OpdsEntryType::NAVIGATION;
+15
View File
@@ -155,6 +155,21 @@ bool Txt::generateCoverBmp() const {
return false;
}
bool Txt::clearCache() const {
if (!Storage.exists(cachePath.c_str())) {
LOG_DBG("TXT", "Cache does not exist, no action needed");
return true;
}
if (!Storage.removeDir(cachePath.c_str())) {
LOG_ERR("TXT", "Failed to clear cache");
return false;
}
LOG_DBG("TXT", "Cache cleared successfully");
return true;
}
bool Txt::readContent(uint8_t* buffer, size_t offset, size_t length) const {
if (!loaded) {
return false;
+1
View File
@@ -22,6 +22,7 @@ class Txt {
[[nodiscard]] size_t getFileSize() const { return fileSize; }
void setupCacheDir() const;
bool clearCache() const;
// Cover image support - looks for cover.bmp/jpg/jpeg/png in same folder as txt file
[[nodiscard]] std::string getCoverBmpPath() const;
+19 -7
View File
@@ -12,7 +12,13 @@
HalStorage HalStorage::instance;
HalStorage::HalStorage() {
storageMutex = xSemaphoreCreateMutex();
// Recursive so the same task can re-enter StorageLock without self-deadlock.
// openFileForRead/Write take the lock and then assign to a HalFile&
// out-param; if that out-param already held an Impl, its destructor takes
// the lock again to close the prior FsFile under serialization (see
// HalFile::Impl::~Impl below). Priority inheritance still applies to
// recursive mutexes.
storageMutex = xSemaphoreCreateRecursiveMutex();
assert(storageMutex != nullptr);
}
@@ -26,8 +32,8 @@ bool HalStorage::ready() const { return SDCard.ready(); }
class HalStorage::StorageLock {
public:
StorageLock() { xSemaphoreTake(HalStorage::getInstance().storageMutex, portMAX_DELAY); }
~StorageLock() { xSemaphoreGive(HalStorage::getInstance().storageMutex); }
StorageLock() { xSemaphoreTakeRecursive(HalStorage::getInstance().storageMutex, portMAX_DELAY); }
~StorageLock() { xSemaphoreGiveRecursive(HalStorage::getInstance().storageMutex); }
};
#define HAL_STORAGE_WRAPPED_CALL(method, ...) \
@@ -57,17 +63,23 @@ bool HalStorage::ensureDirectoryExists(const char* path) { HAL_STORAGE_WRAPPED_C
class HalFile::Impl {
public:
Impl(FsFile&& fsFile) : file(std::move(fsFile)) {}
// SdFat is not thread-safe; FsFile::close() touches SD/SPI and must run
// under StorageLock or it races SdSpiCard::m_spiActive across tasks and
// trips FreeRTOS's xTaskPriorityDisinherit assert. The FsFile member
// destructor (DESTRUCTOR_CLOSES_FILE=1) will close() again after the lock
// releases, but close() on an already-closed FsFile is a no-op. See SdFat
// issue #518 and the HAL note in CLAUDE.md.
~Impl() {
HalStorage::StorageLock lock;
file.close();
}
FsFile file;
};
HalFile::HalFile() = default;
HalFile::HalFile(std::unique_ptr<Impl> impl) : impl(std::move(impl)) {}
HalFile::~HalFile() = default;
HalFile::HalFile(HalFile&&) = default;
HalFile& HalFile::operator=(HalFile&&) = default;
HalFile HalStorage::open(const char* path, const oflag_t oflag) {
+1 -1
View File
@@ -88,7 +88,7 @@ build_flags =
${base.build_flags}
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds
[env:slim]
extends = base
+7 -7
View File
@@ -24,6 +24,7 @@ class CrossPointSettings {
COVER = 3,
BLANK = 4,
COVER_CUSTOM = 5,
QUICK_RESUME = 6,
SLEEP_SCREEN_MODE_COUNT
};
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
@@ -155,11 +156,10 @@ class CrossPointSettings {
enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT };
enum SEAMLESS_SLEEP_SCREEN {
SEAMLESS_NEVER = 0,
SEAMLESS_AFTER_TIMEOUT = 1,
SEAMLESS_ALWAYS = 2,
SEAMLESS_SLEEP_SCREEN_COUNT
enum QUICK_RESUME_SLEEP_SCREEN {
QUICK_RESUME_NEVER = 0,
QUICK_RESUME_AFTER_TIMEOUT = 1,
QUICK_RESUME_SLEEP_SCREEN_COUNT
};
// Sleep screen settings
@@ -249,8 +249,8 @@ class CrossPointSettings {
uint8_t tiltPageTurn = TILT_OFF;
// Language setting (Language enum index, default 0 = EN)
uint8_t language = 0;
// Seamless sleep: keep current content visible with moon icon instead of showing sleep screen
uint8_t seamlessSleepScreen = SEAMLESS_NEVER;
// Quick Resume: keep current content visible with moon icon instead of showing a static sleep screen.
uint8_t quickResumeSleepScreen = QUICK_RESUME_NEVER;
~CrossPointSettings() = default;
+3 -3
View File
@@ -106,15 +106,15 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
// --- Display ---
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT,
StrId::STR_COVER_CUSTOM},
StrId::STR_COVER_CUSTOM, StrId::STR_QUICK_RESUME},
"sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
{StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
"sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SEAMLESS_SLEEP, &CrossPointSettings::seamlessSleepScreen,
{StrId::STR_NEVER, StrId::STR_AFTER_TIMEOUT, StrId::STR_ALWAYS}, "seamlessSleepScreen",
SettingInfo::Enum(StrId::STR_QUICK_RESUME_TIMEOUT, &CrossPointSettings::quickResumeSleepScreen,
{StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, "quickResumeSleepScreen",
StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
{StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
+4 -4
View File
@@ -19,12 +19,12 @@
void SleepActivity::onEnter() {
Activity::onEnter();
const bool renderSeamless =
SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_ALWAYS ||
const bool renderQuickResume =
SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME ||
(fromTimeout &&
SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_AFTER_TIMEOUT);
SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT);
if (renderSeamless) {
if (renderQuickResume) {
return renderLastScreenSleepScreen();
}
@@ -1,6 +1,5 @@
#include "OpdsBookBrowserActivity.h"
#include <Epub.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
@@ -14,6 +13,7 @@
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h"
#include "util/StringUtils.h"
#include "util/UrlUtils.h"
@@ -283,7 +283,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
nullptr, server.username, server.password);
if (result == HttpDownloader::OK) {
Epub(filename, "/.crosspoint").clearCache();
clearBookCache(filename);
state = BrowserState::BROWSING;
} else {
state = BrowserState::ERROR;
@@ -52,6 +52,8 @@ void CalibreConnectActivity::onEnter() {
void CalibreConnectActivity::onExit() {
Activity::onExit();
MDNS.end();
if (WiFi.getMode() != WIFI_MODE_NULL) {
WiFi.disconnect(false);
delay(30);
@@ -72,6 +74,7 @@ void CalibreConnectActivity::startWebServer() {
state = CalibreConnectState::SERVER_STARTING;
requestUpdate();
MDNS.end();
if (MDNS.begin(HOSTNAME)) {
// mDNS is optional for the Calibre plugin but still helpful for users.
LOG_DBG("CAL", "mDNS started: http://%s.local/", HOSTNAME);
@@ -32,6 +32,23 @@ constexpr int QR_CODE_HEIGHT = 198;
DNSServer* dnsServer = nullptr;
constexpr uint16_t DNS_PORT = 53;
void stopDnsServer() {
if (!dnsServer) return;
dnsServer->stop();
delete dnsServer;
dnsServer = nullptr;
}
void restartMdns(const char* hostname, const char* tag) {
MDNS.end();
if (MDNS.begin(hostname)) {
LOG_DBG(tag, "mDNS started: http://%s.local/", hostname);
} else {
LOG_DBG(tag, "WARNING: mDNS failed to start");
}
}
// 0..4 bars from RSSI (dBm), with 3 dBm hysteresis on currentBars to suppress flicker.
int barsForRssi(int rssi, int currentBars) {
static constexpr int RISE_DBM[] = {-85, -75, -65, -55};
@@ -75,6 +92,8 @@ void CrossPointWebServerActivity::onExit() {
LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
state = WebServerActivityState::SHUTTING_DOWN;
stopDnsServer();
MDNS.end();
// Skip reboot if WiFi was never activated (e.g. user backed out of mode selection).
if (WiFi.getMode() != WIFI_MODE_NULL) {
@@ -151,9 +170,7 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
isApMode = false;
// Start mDNS for hostname resolution
if (MDNS.begin(AP_HOSTNAME)) {
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
}
restartMdns(AP_HOSTNAME, "WEBACT");
// Start the web server
startWebServer();
@@ -209,14 +226,11 @@ void CrossPointWebServerActivity::startAccessPoint() {
LOG_DBG("WEBACT", "IP: %s", connectedIP.c_str());
// Start mDNS for hostname resolution
if (MDNS.begin(AP_HOSTNAME)) {
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
} else {
LOG_DBG("WEBACT", "WARNING: mDNS failed to start");
}
restartMdns(AP_HOSTNAME, "WEBACT");
// Start DNS server for captive portal behavior
// This redirects all DNS queries to our IP, making any domain typed resolve to us
stopDnsServer();
dnsServer = new DNSServer();
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
dnsServer->start(DNS_PORT, "*", apIP);
@@ -8,6 +8,7 @@
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/BookCacheUtils.h"
void ClearCacheActivity::onEnter() {
Activity::onEnter();
@@ -94,8 +95,8 @@ void ClearCacheActivity::clearCache() {
file.getName(name, sizeof(name));
String itemName(name);
// Only delete directories starting with epub_ or xtc_
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) {
// Only delete directories matching known book cache names.
if (file.isDirectory() && isBookCacheDirectoryName(itemName.c_str())) {
String fullPath = "/.crosspoint/" + itemName;
LOG_DBG("CLEAR_CACHE", "Removing cache: %s", fullPath.c_str());
@@ -36,9 +36,9 @@ class FontDownloadActivity : public Activity {
void render(RenderLock&&) override;
bool preventAutoSleep() override {
return state_ == LOADING_MANIFEST || state_ == DOWNLOADING ||
// This is added because HTTPClient is a synchronous/blocking function,
// and blocks the main loop until the download is complete.
// So `activityManager.preventAutoSleep()` is never called during downloading
// The download is synchronous and blocks the main loop until it
// completes, so activityManager.preventAutoSleep() is never polled
// during downloading.
state_ == COMPLETE || state_ == ERROR;
}
bool skipLoopDelay() override { return true; }
@@ -86,6 +86,10 @@ void SettingsActivity::onEnter() {
// Reset selection to first category
selectedCategoryIndex = 0;
selectedSettingIndex = 0;
preserveQuickResumeTimeoutOn =
SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT;
quickResumeTimeoutAutoEnabled = false;
syncQuickResumeTimeoutForSleepScreen(/*sleepScreenChanged=*/true, /*quickResumeTimeoutChanged=*/false);
rebuildSettingsLists();
@@ -176,6 +180,8 @@ void SettingsActivity::toggleCurrentSetting() {
}
const auto& setting = (*currentSettings)[selectedSetting];
const bool sleepScreenChanged = setting.valuePtr == &CrossPointSettings::sleepScreen;
const bool quickResumeTimeoutChanged = setting.valuePtr == &CrossPointSettings::quickResumeSleepScreen;
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
// Toggle the boolean value using the member pointer
@@ -253,9 +259,33 @@ void SettingsActivity::toggleCurrentSetting() {
return;
}
syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged);
SETTINGS.saveToFile();
}
void SettingsActivity::syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChanged, bool quickResumeTimeoutChanged) {
if (quickResumeTimeoutChanged) {
preserveQuickResumeTimeoutOn =
SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT;
quickResumeTimeoutAutoEnabled = false;
}
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME) {
if (SETTINGS.quickResumeSleepScreen != CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT) {
SETTINGS.quickResumeSleepScreen = CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT;
quickResumeTimeoutAutoEnabled = !preserveQuickResumeTimeoutOn;
} else if (sleepScreenChanged && !preserveQuickResumeTimeoutOn) {
quickResumeTimeoutAutoEnabled = true;
}
return;
}
if (sleepScreenChanged && quickResumeTimeoutAutoEnabled && !preserveQuickResumeTimeoutOn) {
SETTINGS.quickResumeSleepScreen = CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_NEVER;
quickResumeTimeoutAutoEnabled = false;
}
}
void SettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
@@ -156,12 +156,16 @@ class SettingsActivity final : public Activity {
std::vector<SettingInfo> systemSettings;
const std::vector<SettingInfo>* currentSettings = nullptr;
bool preserveQuickResumeTimeoutOn = false;
bool quickResumeTimeoutAutoEnabled = false;
static constexpr int categoryCount = 4;
static const StrId categoryNames[categoryCount];
void enterCategory(int categoryIndex);
void toggleCurrentSetting();
void rebuildSettingsLists();
void syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChanged, bool quickResumeTimeoutChanged);
public:
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
+61 -25
View File
@@ -142,7 +142,25 @@ constexpr uint32_t SILENT_REBOOT_MAGIC = 0xC1EAB007;
constexpr uint32_t SILENT_REBOOT_TARGET_HOME = 0;
constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1;
// How the device is coming back to life, resolved once at boot. Both resume
// flows suppress the splash and leave the panel holding its pre-boot frame; a
// plain boot shows the splash. See setup() for the resolution.
enum class BootResume : uint8_t {
Splash, // cold boot, flash, panic, or plain reboot
Silent, // heap-defrag ESP.restart() (RTC flag; lost on power loss)
QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss)
};
// Latched true once enterDeepSleep() commits to sleeping, before it tears down
// the current activity. WiFi activities call silentRestart() in onExit() to
// clear heap fragmentation on the way out, but deep sleep is a full chip reset
// on wake and already clears the heap, so rebooting here would just power the
// device back up against the user's sleep gesture. Never cleared:
// startDeepSleep() does not return, so a set latch only ends at the wakeup reset.
static bool deepSleepInProgress = false;
void silentRestart() {
if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot
silentRebootTarget = SILENT_REBOOT_TARGET_HOME;
silentRebootMagic = SILENT_REBOOT_MAGIC;
LOG_DBG("MAIN", "Silent restart (target=home)");
@@ -156,6 +174,7 @@ void silentRestart() {
}
void silentRestartToReader() {
if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot
silentRebootTarget = SILENT_REBOOT_TARGET_READER;
silentRebootMagic = SILENT_REBOOT_MAGIC;
LOG_DBG("MAIN", "Silent restart (target=reader)");
@@ -243,16 +262,20 @@ void enterDeepSleep(bool fromTimeout = false) {
HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation
APP_STATE.lastSleepFromReader = activityManager.isReaderActivity();
const bool isSeamless = SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_ALWAYS ||
(fromTimeout && SETTINGS.seamlessSleepScreen ==
CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_AFTER_TIMEOUT);
APP_STATE.showBootScreen = !isSeamless;
const bool isQuickResumeSleep =
SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME ||
(fromTimeout &&
SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT);
APP_STATE.showBootScreen = !isQuickResumeSleep;
APP_STATE.saveToFile();
// Commit to sleeping before goToSleep() runs the outgoing activity's onExit():
// a WiFi activity would otherwise silentRestart() here and reboot instead.
deepSleepInProgress = true;
activityManager.goToSleep(fromTimeout);
if (isSeamless) {
if (isQuickResumeSleep) {
saveSleepFrameBuffer();
}
@@ -399,27 +422,39 @@ void setup() {
// First serial output only here to avoid timing inconsistencies for power button press duration verification
LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION);
setupDisplayAndFonts(isSilentReboot || /*seamless=*/!APP_STATE.showBootScreen);
// Resolve the single boot-presentation decision. Skipping the splash also
// skips the panel-clearing pass and the X3 initial-full-sync arming (see
// HalDisplay::begin), so the first paint is FAST_REFRESH (~500ms) over the
// retained frame and input dispatches against a visible UI.
const BootResume resume = isSilentReboot ? BootResume::Silent
: !APP_STATE.showBootScreen ? BootResume::QuickResume
: BootResume::Splash;
// Silent reboot suppresses the boot splash and the X3 initial-full-sync
// arming (see HalDisplay::begin), so the first Home paint is FAST_REFRESH
// (~500ms) and input dispatches against the visible menu.
if (!isSilentReboot) {
if (APP_STATE.showBootScreen) {
activityManager.goToBoot();
} else if (loadSleepFrameBuffer()) {
// Seamless wake: buffer restored, replace moon icon with loading icon
const auto pageHeight = renderer.getScreenHeight();
renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
APP_STATE.showBootScreen = true;
APP_STATE.saveToFile();
} else {
// Frame buffer file missing — fall back to normal boot screen
setupDisplayAndFonts(resume != BootResume::Splash);
switch (resume) {
case BootResume::Silent:
// Splash skipped: the routing block below picks the target activity; the
// panel keeps showing the pre-reboot popup until that first paint lands.
break;
case BootResume::QuickResume:
// One-shot flag: re-arm the splash for the next non-quick-resume boot. Save
// before any painting so a hang in the blocking paint path can't strand
// us in a quick-resume-with-no-frame loop on the next boot.
APP_STATE.showBootScreen = true;
APP_STATE.saveToFile();
if (loadSleepFrameBuffer()) {
// Frame restored: swap the sleep moon for the loading icon.
const auto pageHeight = renderer.getScreenHeight();
renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} else {
activityManager.goToBoot(); // frame file missing, fall back to the splash
}
break;
case BootResume::Splash:
activityManager.goToBoot();
}
break;
}
if (recoveryFirmwareMode) {
@@ -429,9 +464,10 @@ void setup() {
} else if (HalSystem::isRebootFromPanic()) {
// If we rebooted from a panic, go to crash report screen to show the panic info
activityManager.goToCrashReport();
} else if (isSilentReboot && snapshotTarget == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) {
} else if (resume == BootResume::Silent && snapshotTarget == SILENT_REBOOT_TARGET_READER &&
!APP_STATE.openEpubPath.empty()) {
activityManager.goToReader(APP_STATE.openEpubPath);
} else if (isSilentReboot) {
} else if (resume == BootResume::Silent) {
// target == home (or reader with no open book): land on home — don't fall
// through to the sleep-wake "resume reader" logic, which fires on stale
// openEpubPath + lastSleepFromReader from a prior session.
@@ -450,7 +486,7 @@ void setup() {
activityManager.goToReader(path);
}
if (isSilentReboot) {
if (resume == BootResume::Silent) {
// Block until the first paint physically completes. refreshDisplay()
// waits on the panel BUSY pin so when this returns the user can see the
// new activity. Without the wait, an edge captured by gpio.update()
+7 -16
View File
@@ -1,7 +1,6 @@
#include "CrossPointWebServer.h"
#include <ArduinoJson.h>
#include <Epub.h>
#include <FsHelpers.h>
#include <HalGPIO.h>
#include <HalStorage.h>
@@ -23,6 +22,7 @@
#include "html/HomePageHtml.generated.h"
#include "html/SettingsPageHtml.generated.h"
#include "html/js/jszip_minJs.generated.h"
#include "util/BookCacheUtils.h"
namespace {
// Folders/files to hide from the web interface file browser
@@ -48,15 +48,6 @@ String wsLastCompleteName;
size_t wsLastCompleteSize = 0;
unsigned long wsLastCompleteAt = 0;
// Helper function to clear epub cache after upload
void clearEpubCacheIfNeeded(const String& filePath) {
// Only clear cache for .epub files
if (FsHelpers::hasEpubExtension(filePath)) {
Epub(filePath.c_str(), "/.crosspoint").clearCache();
LOG_DBG("WEB", "Cleared epub cache for: %s", filePath.c_str());
}
}
String normalizeWebPath(const String& inputPath) {
if (inputPath.isEmpty() || inputPath == "/") {
return "/";
@@ -732,7 +723,7 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
String filePath = state.path;
if (!filePath.endsWith("/")) filePath += "/";
filePath += state.fileName;
clearEpubCacheIfNeeded(filePath);
clearBookCache(filePath.c_str());
}
}
} else if (upload.status == UPLOAD_FILE_ABORTED) {
@@ -878,7 +869,7 @@ void CrossPointWebServer::handleRename() const {
return;
}
clearEpubCacheIfNeeded(itemPath);
clearBookCache(itemPath.c_str());
const bool success = file.rename(newPath.c_str());
file.close();
@@ -971,7 +962,7 @@ void CrossPointWebServer::handleMove() const {
return;
}
clearEpubCacheIfNeeded(itemPath);
clearBookCache(itemPath.c_str());
const bool success = file.rename(newPath.c_str());
file.close();
@@ -1090,7 +1081,7 @@ void CrossPointWebServer::handleDelete() const {
// It's a file (or couldn't open as dir) — remove file
if (f) f.close();
success = Storage.remove(itemPath.c_str());
clearEpubCacheIfNeeded(itemPath);
clearBookCache(itemPath.c_str());
}
if (!success) {
@@ -1635,7 +1626,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
wsLastCompleteSize = 0;
wsLastCompleteAt = millis();
LOG_DBG("WS", "Zero-byte upload complete: %s", filePath.c_str());
clearEpubCacheIfNeeded(filePath);
clearBookCache(filePath.c_str());
wsServer->sendTXT(num, "DONE");
wsLastProgressSent = 0;
break;
@@ -1704,7 +1695,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
String filePath = wsUploadPath;
if (!filePath.endsWith("/")) filePath += "/";
filePath += wsUploadFileName;
clearEpubCacheIfNeeded(filePath);
clearBookCache(filePath.c_str());
wsServer->sendTXT(num, "DONE");
wsLastProgressSent = 0;
+161 -156
View File
@@ -1,204 +1,209 @@
#include "HttpDownloader.h"
#include <HTTPClient.h>
#include <Arduino.h>
#include <Logging.h>
#include <NetworkClient.h>
#include <NetworkClientSecure.h>
#include <StreamString.h>
#include <Memory.h>
#include <base64.h>
#include <esp_crt_bundle.h>
#include <esp_http_client.h>
#include <cstring>
#include <memory>
#include <utility>
#include "util/UrlUtils.h"
#include <functional>
#include <string>
namespace {
class FileWriteStream final : public Stream {
public:
FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress, bool* cancelFlag)
: file_(file), total_(total), progress_(std::move(progress)), cancelFlag_(cancelFlag) {}
// RX holds the response headers. 4096 fits real OPDS servers; GitHub's release
// CDN sends more and logs HTTP_HEADER "Buffer length is small", but that's
// non-fatal: the headers we read (Location, Content-Length) come first and
// survive. Smaller keeps contiguous heap free while WiFi and TLS are up. TX
// only carries our GET; the body streams in READ_CHUNK pieces.
constexpr int HTTP_RX_BUF = 4096;
constexpr int HTTP_TX_BUF = 1024;
// Per-socket-op timeout. Some OPDS download endpoints are slow to send headers
// (>15s) and chunked catalogs stall mid-body, so 15s killed them. 60s gives
// slow servers room. esp_http_client's timeout_ms is uint32, so unlike Arduino
// HTTPClient's uint16 setTimeout it doesn't silently truncate.
constexpr int HTTP_TIMEOUT_MS = 60000;
constexpr size_t READ_CHUNK = 2048;
size_t write(uint8_t byte) override { return write(&byte, 1); }
struct Sink {
std::function<bool(const uint8_t*, size_t)> write; // returns false to abort the transfer
HttpDownloader::ProgressCallback progress;
bool* cancelFlag = nullptr;
size_t total = 0;
size_t downloaded = 0;
};
size_t write(const uint8_t* buffer, size_t size) override {
// Write-through stream for HTTPClient::writeToStream with progress tracking.
if (cancelFlag_ && *cancelFlag_) {
writeOk_ = false;
return 0;
}
const size_t written = file_.write(buffer, size);
if (written != size) {
writeOk_ = false;
}
downloaded_ += written;
if (progress_ && total_ > 0) {
progress_(downloaded_, total_);
}
return written;
bool isRedirect(int status) {
return status == 301 || status == 302 || status == 303 || status == 307 || status == 308;
}
// Streams a GET body through sink.write in READ_CHUNK pieces. Uses the manual
// open/fetch_headers/read path rather than esp_http_client_perform(): perform()
// pushes the whole body through an event callback and reports a chunked body
// that ends early as ESP_ERR_HTTP_INCOMPLETE_DATA, whereas the read loop streams
// large/slow files and surfaces a short read directly.
HttpDownloader::DownloadError runGet(const std::string& url, const std::string& username, const std::string& password,
Sink& sink) {
esp_http_client_config_t config = {};
config.url = url.c_str();
config.buffer_size = HTTP_RX_BUF;
config.buffer_size_tx = HTTP_TX_BUF;
config.timeout_ms = HTTP_TIMEOUT_MS;
// Verify HTTPS against the bundled CA roots. This build has esp-tls
// CONFIG_ESP_TLS_INSECURE off, so an unverified TLS handshake can't be set
// up at all; the model is public servers over verified https and local
// servers over plain http (esp_http_client picks the transport from the URL
// scheme, so http:// needs no cert config). The prior setInsecure() worked
// only because Arduino's ssl_client drives mbedtls directly.
config.crt_bundle_attach = esp_crt_bundle_attach;
config.keep_alive_enable = true;
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) {
LOG_ERR("HTTP", "client init failed");
return HttpDownloader::HTTP_ERROR;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override { file_.flush(); }
esp_http_client_set_header(client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
if (!username.empty() && !password.empty()) {
// Preemptive Basic auth, like the prior addHeader; don't wait for a 401.
const std::string credentials = username + ":" + password;
const String header = "Basic " + base64::encode(credentials.c_str());
esp_http_client_set_header(client, "Authorization", header.c_str());
}
size_t downloaded() const { return downloaded_; }
bool ok() const { return writeOk_; }
// open()/read() does not auto-follow redirects (only perform() does), so step
// 30x responses manually. OPDS download endpoints and the GitHub release CDN
// both redirect.
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
LOG_ERR("HTTP", "open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return HttpDownloader::HTTP_ERROR;
}
int64_t contentLength = esp_http_client_fetch_headers(client);
int status = esp_http_client_get_status_code(client);
for (int hop = 0; isRedirect(status) && hop < 5; ++hop) {
if (esp_http_client_set_redirection(client) != ESP_OK) break;
err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
LOG_ERR("HTTP", "redirect open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return HttpDownloader::HTTP_ERROR;
}
contentLength = esp_http_client_fetch_headers(client);
status = esp_http_client_get_status_code(client);
}
private:
FsFile& file_;
size_t total_;
size_t downloaded_ = 0;
bool writeOk_ = true;
HttpDownloader::ProgressCallback progress_;
bool* cancelFlag_;
};
if (status != 200) {
LOG_ERR("HTTP", "unexpected status: %d", status);
esp_http_client_cleanup(client);
return HttpDownloader::HTTP_ERROR;
}
// fetch_headers returns 0 for a chunked response (no Content-Length); leave
// total at 0 so progress stays silent and the size check is skipped.
sink.total = contentLength > 0 ? static_cast<size_t>(contentLength) : 0;
auto buf = makeUniqueNoThrow<char[]>(READ_CHUNK);
if (!buf) {
LOG_ERR("HTTP", "OOM: %u byte read buffer", (unsigned)READ_CHUNK);
esp_http_client_cleanup(client);
return HttpDownloader::HTTP_ERROR;
}
while (true) {
if (sink.cancelFlag && *sink.cancelFlag) {
esp_http_client_cleanup(client);
return HttpDownloader::ABORTED;
}
const int read = esp_http_client_read(client, buf.get(), READ_CHUNK);
if (read < 0) {
LOG_ERR("HTTP", "read error after %zu bytes", sink.downloaded);
esp_http_client_cleanup(client);
return HttpDownloader::HTTP_ERROR;
}
if (read == 0) break; // all data received
if (!sink.write(reinterpret_cast<const uint8_t*>(buf.get()), read)) {
esp_http_client_cleanup(client);
return HttpDownloader::FILE_ERROR;
}
sink.downloaded += read;
if (sink.progress && sink.total > 0) sink.progress(sink.downloaded, sink.total);
}
const bool complete = esp_http_client_is_complete_data_received(client);
esp_http_client_cleanup(client);
if (!complete) {
LOG_ERR("HTTP", "incomplete: got %zu of %zu bytes", sink.downloaded, sink.total);
return HttpDownloader::HTTP_ERROR;
}
return HttpDownloader::OK;
}
} // namespace
bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent, const std::string& username,
const std::string& password) {
std::unique_ptr<NetworkClient> client;
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
secureClient->setInsecure();
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
}
HTTPClient http;
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
http.begin(*client, url.c_str());
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
if (!username.empty() && !password.empty()) {
std::string credentials = username + ":" + password;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
}
const int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Fetch failed: %d", httpCode);
http.end();
return false;
}
http.writeToStream(&outContent);
http.end();
LOG_DBG("HTTP", "Fetch success");
return true;
Sink sink;
sink.write = [&outContent](const uint8_t* data, size_t len) { return outContent.write(data, len) == len; };
return runGet(url, username, password, sink) == OK;
}
bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent, const std::string& username,
const std::string& password) {
StreamString stream;
if (!fetchUrl(url, stream, username, password)) {
return false;
}
outContent = stream.c_str();
return true;
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
outContent.clear(); // start clean; the sink appends, so don't carry prior content
Sink sink;
sink.write = [&outContent](const uint8_t* data, size_t len) {
outContent.append(reinterpret_cast<const char*>(data), len);
return true;
};
return runGet(url, username, password, sink) == OK;
}
bool HttpDownloader::fetchUrl(const std::string& url, const DataCallback& onData, const std::string& username,
const std::string& password) {
LOG_DBG("HTTP", "Fetching: %s", url.c_str());
Sink sink;
sink.write = onData;
return runGet(url, username, password, sink) == OK;
}
HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& url, const std::string& destPath,
ProgressCallback progress, bool* cancelFlag,
const std::string& username, const std::string& password) {
std::unique_ptr<NetworkClient> client;
if (UrlUtils::isHttpsUrl(url)) {
auto* secureClient = new NetworkClientSecure();
secureClient->setInsecure();
client.reset(secureClient);
} else {
client.reset(new NetworkClient());
}
HTTPClient http;
LOG_DBG("HTTP", "Downloading: %s -> %s", url.c_str(), destPath.c_str());
LOG_DBG("HTTP", "Downloading: %s", url.c_str());
LOG_DBG("HTTP", "Destination: %s", destPath.c_str());
http.begin(*client, url.c_str());
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
if (!username.empty() && !password.empty()) {
std::string credentials = username + ":" + password;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
}
const int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERR("HTTP", "Download failed: %d", httpCode);
http.end();
return HTTP_ERROR;
}
const int64_t reportedLength = http.getSize();
const size_t contentLength = reportedLength > 0 ? static_cast<size_t>(reportedLength) : 0;
if (contentLength > 0) {
LOG_DBG("HTTP", "Content-Length: %zu", contentLength);
} else {
LOG_DBG("HTTP", "Content-Length: unknown");
}
// Remove existing file if present
if (Storage.exists(destPath.c_str())) {
Storage.remove(destPath.c_str());
}
// Open file for writing
FsFile file;
if (!Storage.openFileForWrite("HTTP", destPath.c_str(), file)) {
LOG_ERR("HTTP", "Failed to open file for writing");
http.end();
return FILE_ERROR;
}
// Let HTTPClient handle chunked decoding and stream body bytes into the file.
FileWriteStream fileStream(file, contentLength, progress, cancelFlag);
const int writeResult = http.writeToStream(&fileStream);
Sink sink;
sink.progress = std::move(progress);
sink.cancelFlag = cancelFlag;
sink.write = [&file](const uint8_t* data, size_t len) { return file.write(data, len) == len; };
const DownloadError result = runGet(url, username, password, sink);
// Close before any remove() on the same path; DESTRUCTOR_CLOSES_FILE would
// otherwise close only after the remove.
file.close();
http.end();
if (cancelFlag && *cancelFlag) {
if (result != OK) {
Storage.remove(destPath.c_str());
return ABORTED;
return result;
}
if (writeResult < 0) {
LOG_ERR("HTTP", "writeToStream error: %d", writeResult);
if (sink.downloaded == 0) {
LOG_ERR("HTTP", "no data received");
Storage.remove(destPath.c_str());
return HTTP_ERROR;
}
const size_t downloaded = fileStream.downloaded();
LOG_DBG("HTTP", "Downloaded %zu bytes", downloaded);
// Guard against partial writes even if HTTPClient completes.
if (!fileStream.ok()) {
LOG_ERR("HTTP", "Write failed during download");
Storage.remove(destPath.c_str());
return FILE_ERROR;
}
if (contentLength == 0 && downloaded == 0) {
LOG_ERR("HTTP", "Download failed: no data received");
Storage.remove(destPath.c_str());
return HTTP_ERROR;
}
// Verify download size if known
if (contentLength > 0 && downloaded != contentLength) {
LOG_ERR("HTTP", "Size mismatch: got %zu, expected %zu", downloaded, contentLength);
Storage.remove(destPath.c_str());
return HTTP_ERROR;
}
LOG_DBG("HTTP", "Downloaded %zu bytes", sink.downloaded);
return OK;
}
+12 -2
View File
@@ -5,12 +5,16 @@
#include <string>
/**
* HTTP client utility for fetching content and downloading files.
* Wraps NetworkClientSecure and HTTPClient for HTTPS requests.
* HTTP client utility for fetching content and downloading files. Built on
* esp_http_client: https is verified against the CA bundle, plain http is
* used for local servers (transport is chosen from the URL scheme).
*/
class HttpDownloader {
public:
using ProgressCallback = std::function<void(size_t downloaded, size_t total)>;
// Called with each body chunk as it arrives; return false to abort. Lets a
// streaming parser consume the response without buffering the whole body.
using DataCallback = std::function<bool(const uint8_t* data, size_t len)>;
enum DownloadError {
OK = 0,
@@ -28,6 +32,12 @@ class HttpDownloader {
static bool fetchUrl(const std::string& url, Stream& stream, const std::string& username = "",
const std::string& password = "");
/**
* Stream the response body to onData as it arrives, without buffering it.
*/
static bool fetchUrl(const std::string& url, const DataCallback& onData, const std::string& username = "",
const std::string& password = "");
/**
* Download a file to the SD card with optional credentials.
*/
+21 -53
View File
@@ -1,11 +1,20 @@
#include "OtaUpdater.h"
// clang-format off
// HttpDownloader.h pulls Arduino/SdFat, whose macros collide with lwip's
// ip4_addr.h unless seen before esp_http_client (which includes lwip). Pin this
// order; clang-format would otherwise sort the local header last and break the
// build.
#include "HttpDownloader.h"
#include <Logging.h>
#include <ReleaseJsonParser.h>
#include <esp_crt_bundle.h>
#include <esp_http_client.h>
#include <esp_https_ota.h>
#include <esp_wifi.h>
// clang-format on
#include <string>
namespace {
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/latest";
@@ -13,67 +22,26 @@ constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-rea
esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) {
return esp_http_client_set_header(http_client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
}
size_t totalBytesReceived = 0;
esp_err_t event_handler(esp_http_client_event_t* event) {
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
totalBytesReceived += event->data_len;
LOG_DBG("OTA", "HTTP chunk: %d bytes (total: %zu)", event->data_len, totalBytesReceived);
auto* parser = static_cast<ReleaseJsonParser*>(event->user_data);
parser->feed(static_cast<const char*>(event->data), event->data_len);
return ESP_OK;
}
} // namespace
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
esp_err_t esp_err;
ReleaseJsonParser releaseParser;
esp_http_client_config_t client_config = {
.url = latestReleaseUrl,
.event_handler = event_handler,
// 4096 holds the API response headers; the 32KB body streams through the
// parser in chunks so RX needn't be larger. TX only carries our GET.
// Both free before installUpdate, so smaller leaves it less fragmentation.
.buffer_size = 4096,
.buffer_size_tx = 1024,
.user_data = &releaseParser,
.skip_cert_common_name_check = true,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
};
totalBytesReceived = 0;
LOG_DBG("OTA", "Checking for update (current: %s)", CROSSPOINT_VERSION);
esp_http_client_handle_t client_handle = esp_http_client_init(&client_config);
if (!client_handle) {
LOG_ERR("OTA", "HTTP Client Handle Failed");
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
esp_http_client_cleanup(client_handle);
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_http_client_perform(client_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_perform Failed : %s", esp_err_to_name(esp_err));
esp_http_client_cleanup(client_handle);
// Stream the ~32KB release JSON straight into the parser as it arrives.
// Buffering the whole body in a std::string would add a growing allocation
// on top of the TLS session's heap during the fetch; with -fno-exceptions an
// OOM there aborts. fetchUrl handles the verified-https GET, redirects, and
// User-Agent (see HttpDownloader).
ReleaseJsonParser releaseParser;
const bool ok = HttpDownloader::fetchUrl(latestReleaseUrl, [&releaseParser](const uint8_t* data, size_t len) {
releaseParser.feed(reinterpret_cast<const char*>(data), len);
return true;
});
if (!ok) {
LOG_ERR("OTA", "Release check fetch failed");
return HTTP_ERROR;
}
esp_err = esp_http_client_cleanup(client_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_cleanup Failed : %s", esp_err_to_name(esp_err));
return INTERNAL_UPDATE_ERROR;
}
LOG_DBG("OTA", "Response received: %zu bytes total", totalBytesReceived);
LOG_DBG("OTA", "Parser results: tag=%s firmware=%s", releaseParser.foundTag() ? "yes" : "no",
releaseParser.foundFirmware() ? "yes" : "no");
+5 -11
View File
@@ -1,11 +1,12 @@
#include "WebDAVHandler.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <HalStorage.h>
#include <Logging.h>
#include <esp_task_wdt.h>
#include "util/BookCacheUtils.h"
namespace {
constexpr const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
@@ -384,7 +385,7 @@ void WebDAVHandler::handlePut(WebServer& s) {
return;
}
clearEpubCacheIfNeeded(path);
clearBookCache(path.c_str());
s.send(_putExisted ? 204 : 201);
LOG_DBG("DAV", "PUT complete: %s", path.c_str());
}
@@ -433,7 +434,7 @@ void WebDAVHandler::handleDelete(WebServer& s) {
}
} else {
file.close();
clearEpubCacheIfNeeded(path);
clearBookCache(path.c_str());
if (Storage.remove(path.c_str())) {
s.send(204);
} else {
@@ -542,7 +543,7 @@ void WebDAVHandler::handleMove(WebServer& s) {
return;
}
clearEpubCacheIfNeeded(srcPath);
clearBookCache(srcPath.c_str());
bool success = file.rename(dstPath.c_str());
file.close();
@@ -797,13 +798,6 @@ bool WebDAVHandler::getOverwrite(WebServer& s) const {
return true; // Default is T
}
void WebDAVHandler::clearEpubCacheIfNeeded(const String& path) const {
if (FsHelpers::hasEpubExtension(path)) {
Epub(path.c_str(), "/.crosspoint").clearCache();
LOG_DBG("DAV", "Cleared epub cache for: %s", path.c_str());
}
}
String WebDAVHandler::getMimeType(const String& path) const {
if (FsHelpers::hasEpubExtension(path)) return "application/epub+zip";
if (FsHelpers::checkFileExtension(path, ".pdf")) return "application/pdf";
-1
View File
@@ -38,7 +38,6 @@ class WebDAVHandler : public RequestHandler {
bool isProtectedPath(const String& path) const;
int getDepth(WebServer& s) const;
bool getOverwrite(WebServer& s) const;
void clearEpubCacheIfNeeded(const String& path) const;
void sendPropEntry(WebServer& s, const String& href, bool isDir, size_t size, const String& lastModified) const;
String getMimeType(const String& path) const;
};
+104 -4
View File
@@ -312,6 +312,11 @@
<script>
let allSettings = [];
let originalValues = {};
let preserveQuickResumeTimeoutOn = false;
let quickResumeTimeoutAutoEnabled = false;
const SLEEP_SCREEN_MODE = {
QUICK_RESUME: 6
};
function escapeHtml(unsafe) {
return unsafe
@@ -336,12 +341,12 @@
if (setting.type === 'toggle') {
const checked = setting.value ? 'checked' : '';
return '<label class="toggle-switch">' +
'<input type="checkbox" id="' + id + '" ' + checked + ' onchange="markChanged()">' +
'<input type="checkbox" id="' + id + '" ' + checked + ' onchange="handleSettingChanged(\'' + setting.key + '\')">' +
'<span class="toggle-slider"></span></label>';
}
if (setting.type === 'enum') {
let html = '<select id="' + id + '" onchange="markChanged()">';
let html = '<select id="' + id + '" onchange="handleSettingChanged(\'' + setting.key + '\')">';
setting.options.forEach(function(opt, idx) {
const selected = idx === setting.value ? ' selected' : '';
html += '<option value="' + idx + '"' + selected + '>' + escapeHtml(opt) + '</option>';
@@ -353,14 +358,14 @@
if (setting.type === 'value') {
return '<input type="number" id="' + id + '" value="' + setting.value + '"' +
' min="' + setting.min + '" max="' + setting.max + '" step="' + setting.step + '"' +
' onchange="markChanged()">';
' onchange="handleSettingChanged(\'' + setting.key + '\')">';
}
if (setting.type === 'string') {
const inputType = setting.name.toLowerCase().includes('password') ? 'password' : 'text';
const val = setting.value || '';
return '<input type="' + inputType + '" id="' + id + '" value="' + escapeHtml(val) + '"' +
' oninput="markChanged()">';
' oninput="handleSettingChanged(\'' + setting.key + '\')">';
}
return '';
@@ -389,6 +394,96 @@
document.getElementById('saveBtn').disabled = false;
}
function findSetting(key) {
return allSettings.find(function(s) { return s.key === key; });
}
function getControl(key) {
return document.getElementById('setting-' + key);
}
function getControlValue(key) {
const setting = findSetting(key);
const el = getControl(key);
if (!setting || !el) return undefined;
if (setting.type === 'toggle') {
return el.checked ? 1 : 0;
}
if (setting.type === 'enum' || setting.type === 'value') {
return parseInt(el.value, 10);
}
return el.value;
}
function setControlValue(key, value) {
const setting = findSetting(key);
const el = getControl(key);
if (!setting || !el) return false;
if (setting.type === 'toggle') {
const checked = !!value;
if (el.checked === checked) return false;
el.checked = checked;
return true;
}
const nextValue = String(value);
if (el.value === nextValue) return false;
el.value = nextValue;
return true;
}
function isQuickResumeSleepScreenSelected() {
const sleepScreen = findSetting('sleepScreen');
const sleepScreenValue = getControlValue('sleepScreen');
if (!sleepScreen || sleepScreenValue === undefined || !Number.isFinite(sleepScreenValue)) return false;
let selectedValue = sleepScreenValue;
if (Array.isArray(sleepScreen.values)) {
selectedValue = Number(sleepScreen.values[sleepScreenValue]);
} else if (Array.isArray(sleepScreen.options)) {
const selectedOption = sleepScreen.options[sleepScreenValue];
if (selectedOption && typeof selectedOption === 'object' && 'value' in selectedOption) {
selectedValue = Number(selectedOption.value);
}
}
return selectedValue === SLEEP_SCREEN_MODE.QUICK_RESUME;
}
function syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged) {
const timeoutValue = getControlValue('quickResumeSleepScreen');
if (timeoutValue === undefined) return false;
let changed = false;
if (quickResumeTimeoutChanged) {
preserveQuickResumeTimeoutOn = timeoutValue === 1;
quickResumeTimeoutAutoEnabled = false;
}
if (isQuickResumeSleepScreenSelected()) {
if (timeoutValue !== 1) {
changed = setControlValue('quickResumeSleepScreen', 1);
quickResumeTimeoutAutoEnabled = !preserveQuickResumeTimeoutOn;
} else if (sleepScreenChanged && !preserveQuickResumeTimeoutOn) {
quickResumeTimeoutAutoEnabled = true;
}
return changed;
}
if (sleepScreenChanged && quickResumeTimeoutAutoEnabled && !preserveQuickResumeTimeoutOn) {
changed = setControlValue('quickResumeSleepScreen', 0);
quickResumeTimeoutAutoEnabled = false;
}
return changed;
}
function handleSettingChanged(key) {
syncQuickResumeTimeoutForSleepScreen(key === 'sleepScreen', key === 'quickResumeSleepScreen');
markChanged();
}
async function loadSettings() {
try {
const response = await fetch('/api/settings');
@@ -427,6 +522,11 @@
container.innerHTML = html;
document.getElementById('save-container').style.display = '';
document.getElementById('saveBtn').disabled = true;
preserveQuickResumeTimeoutOn = getControlValue('quickResumeSleepScreen') === 1;
quickResumeTimeoutAutoEnabled = false;
if (syncQuickResumeTimeoutForSleepScreen(true, false)) {
markChanged();
}
} catch (e) {
console.error(e);
document.getElementById('settings-container').innerHTML =
+34
View File
@@ -0,0 +1,34 @@
#include "BookCacheUtils.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <Logging.h>
#include <Txt.h>
#include <Xtc.h>
bool isBookCacheDirectoryName(const char* name) {
if (!name) {
return false;
}
constexpr char EPUB_PREFIX[] = "epub_";
constexpr char TXT_PREFIX[] = "txt_";
constexpr char XTC_PREFIX[] = "xtc_";
return strncmp(name, EPUB_PREFIX, std::size(EPUB_PREFIX) - 1) == 0 ||
strncmp(name, TXT_PREFIX, std::size(TXT_PREFIX) - 1) == 0 ||
strncmp(name, XTC_PREFIX, std::size(XTC_PREFIX) - 1) == 0;
}
void clearBookCache(const std::string& path) {
if (FsHelpers::hasEpubExtension(path)) {
Epub(path, "/.crosspoint").clearCache();
} else if (FsHelpers::hasXtcExtension(path)) {
Xtc(path, "/.crosspoint").clearCache();
} else if (FsHelpers::hasTxtExtension(path)) {
Txt(path, "/.crosspoint").clearCache();
} else {
return;
}
LOG_DBG("BookCache", "Done checking metadata cache for: %s", path.c_str());
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <string>
// Clears the reading cache for a book file if its extension is recognised
// (EPUB, XTC, or TXT). Does nothing for other file types.
void clearBookCache(const std::string& path);
// Returns true if the directory name matches a book cache entry.
bool isBookCacheDirectoryName(const char* name);
+6 -1
View File
@@ -88,7 +88,12 @@ void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) {
// Display a border around the screen to indicate a screenshot was taken
if (renderer.storeBwBuffer()) {
renderer.drawRect(6, 6, renderer.getDisplayHeight() - 12, renderer.getDisplayWidth() - 12, 2, true);
int marginTop, marginRight, marginBottom, marginLeft;
renderer.getOrientedViewableTRBL(&marginTop, &marginRight, &marginBottom, &marginLeft);
int width = renderer.getScreenWidth() - marginLeft - marginRight - 1;
int height = renderer.getScreenHeight() - marginTop - marginBottom - 1;
// Add extra margin to the border to make it more visible
renderer.drawRect(marginLeft + 1, marginTop + 1, width - 2, height - 2, 2, true);
renderer.displayBuffer();
delay(1000);
renderer.restoreBwBuffer();
-2
View File
@@ -2,8 +2,6 @@
namespace UrlUtils {
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
std::string ensureProtocol(const std::string& url) {
if (url.find("://") == std::string::npos) {
return "http://" + url;
-5
View File
@@ -3,11 +3,6 @@
namespace UrlUtils {
/**
* Check if URL uses HTTPS protocol
*/
bool isHttpsUrl(const std::string& url);
/**
* Prepend http:// if no protocol specified (server will redirect to https if needed)
*/