Merge branch 'master' into jpegdec-fork
This commit is contained in:
@@ -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
|
||||
|
||||
+25
-4
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: "Няма нядаўніх кніг"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 d’accè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 d’accès…"
|
||||
STR_HOTSPOT_MODE: "Mode point d’accè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 l’appareil’"
|
||||
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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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: "Жуырда оқылған кітаптар жоқ"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 Wi‑Fi"
|
||||
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 Wi‑Fi existente"
|
||||
STR_HOTSPOT_DESC: "Crie uma rede Wi‑Fi 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 Wi‑Fi"
|
||||
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 Wi‑Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi‑Fi"
|
||||
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 Wi‑Fi"
|
||||
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 Wi‑Fi"
|
||||
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"
|
||||
|
||||
@@ -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ă"
|
||||
|
||||
@@ -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: "Нет недавних книг"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: "Немає останніх книг"
|
||||
|
||||
@@ -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) {
|
||||
// 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+59
-23
@@ -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
|
||||
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);
|
||||
APP_STATE.showBootScreen = true;
|
||||
APP_STATE.saveToFile();
|
||||
} else {
|
||||
// Frame buffer file missing — fall back to normal boot screen
|
||||
APP_STATE.showBootScreen = true;
|
||||
APP_STATE.saveToFile();
|
||||
activityManager.goToBoot();
|
||||
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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
+164
-159
@@ -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); }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int available() override { return 0; }
|
||||
int read() override { return -1; }
|
||||
int peek() override { return -1; }
|
||||
void flush() override { file_.flush(); }
|
||||
|
||||
size_t downloaded() const { return downloaded_; }
|
||||
bool ok() const { return writeOk_; }
|
||||
|
||||
private:
|
||||
FsFile& file_;
|
||||
size_t total_;
|
||||
size_t downloaded_ = 0;
|
||||
bool writeOk_ = true;
|
||||
HttpDownloader::ProgressCallback progress_;
|
||||
bool* cancelFlag_;
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user