diff --git a/.skills/SKILL.md b/.skills/SKILL.md index b7b59dc7..848ea462 100644 --- a/.skills/SKILL.md +++ b/.skills/SKILL.md @@ -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 ``, 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 `` (rare), include the header explicitly so the typedef applies. + --- ## Coding Standards diff --git a/docs/file-formats.md b/docs/file-formats.md index 2fa0c60b..1b7ca3b8 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -61,29 +61,29 @@ struct TocEntry { struct BookBin { // Header u8 version [[comment("Format version"), color("FFD93D")]]; - + // Version validation if (version != EXPECTED_VERSION) { std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION)); } - + u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]]; u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]]; u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]]; - + // Metadata section Metadata metadata [[comment("Book metadata")]]; - + // Validate LUT offset alignment u32 currentOffset = $; if (currentOffset != lutOffset) { std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset)); } - + // Lookup Tables u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]]; u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]]; - + // Data Entries SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]]; TocEntry toc[tocCount] [[comment("Table of contents entries")]]; @@ -104,7 +104,7 @@ if (parsedSize != fileSize) { ## `section.bin` -### Version 8 +### Version 24 ImHex Pattern: @@ -114,7 +114,7 @@ import std.string; import std.core; // === Configuration === -#define EXPECTED_VERSION 8 +#define EXPECTED_VERSION 24 #define MAX_STRING_LENGTH 65535 // === String Structure === @@ -133,8 +133,10 @@ fn format_string(String s) { // === Page Structure === -enum StorageType : u8 { - PageLine = 1 +enum PageElementTag : u8 { + PageLine = 1, + PageImage = 2, + PageHorizontalRule = 3 }; enum WordStyle : u8 { @@ -161,10 +163,29 @@ struct PageLine { BlockStyle blockStyle; }; +struct PageImage { + s16 xPos; + s16 yPos; + String imagePath; + s16 width; + s16 height; +}; + +struct PageHorizontalRule { + s16 xPos; + s16 yPos; + u16 width; + u8 thickness; +}; + struct PageElement { u8 pageElementType; if (pageElementType == 1) { PageLine pageLine [[inline]]; + } else if (pageElementType == 2) { + PageImage pageImage [[inline]]; + } else if (pageElementType == 3) { + PageHorizontalRule horizontalRule [[inline]]; } else { std::error(std::format("Unknown page element type: {}", pageElementType)); } @@ -180,12 +201,12 @@ struct Page { struct SectionBin { // Header u8 version [[comment("Format version"), color("FFD93D")]]; - + // Version validation if (version != EXPECTED_VERSION) { std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION)); } - + // Cache busting parameters s32 fontId; float lineCompression; @@ -194,15 +215,15 @@ struct SectionBin { u16 vieportHeight; u16 pageCount; u32 lutOffset; - + Page page[pageCount]; - + // Validate LUT offset alignment u32 currentOffset = $; if (currentOffset != lutOffset) { std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset)); } - + // Lookup Tables u32 lut[pageCount]; }; diff --git a/docs/sd-card-fonts.md b/docs/sd-card-fonts.md index db9b1277..c6b5e98b 100644 --- a/docs/sd-card-fonts.md +++ b/docs/sd-card-fonts.md @@ -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` diff --git a/lib/EpdFont/scripts/fontconvert_sdcard.py b/lib/EpdFont/scripts/fontconvert_sdcard.py index f0f83c8e..1c5dad35 100755 --- a/lib/EpdFont/scripts/fontconvert_sdcard.py +++ b/lib/EpdFont/scripts/fontconvert_sdcard.py @@ -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), diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index ba12589e..5fcda220 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -1,8 +1,11 @@ #include "Page.h" +#include #include #include +#include + 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::deserialize(FsFile& file) { return std::unique_ptr(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::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(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::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; diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 46814b15..34076433 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -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 deserialize(FsFile& file); +}; + class Page { public: // the list of block index and line numbers on this page diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 7cbd37e1..30f0ef47 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -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) + diff --git a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp index b0863bb5..a21a59aa 100644 --- a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp @@ -5,8 +5,10 @@ #include #include #include +#include #include +#include #include #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 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 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 diff --git a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp index 0cc1616a..245f5c4f 100644 --- a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include #include +#include #include #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(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(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(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 diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 295f58a7..89184d5d 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -8,7 +8,9 @@ #include #include +#include #include +#include #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(renderer.getLineHeight(fontId) * lineCompression + 0.5f); + const int16_t defaultVerticalSpacing = static_cast(lineHeight / 2); + const int16_t topSpacing = + static_cast((blockStyle.marginTop > 0 ? blockStyle.marginTop : defaultVerticalSpacing) + + (blockStyle.paddingTop > 0 ? blockStyle.paddingTop : 0)); + const int16_t bottomSpacing = + static_cast((blockStyle.marginBottom > 0 ? blockStyle.marginBottom : defaultVerticalSpacing) + + (blockStyle.paddingBottom > 0 ? blockStyle.paddingBottom : 0)); + constexpr uint8_t ruleThickness = 2; + const int16_t availableWidth = + std::max(1, static_cast(viewportWidth - blockStyle.totalHorizontalInset())); + const int16_t width = std::max(1, static_cast(availableWidth / 4)); + const int16_t xPos = static_cast(blockStyle.leftInset() + ((availableWidth - width) / 2)); + const int16_t totalHeight = static_cast(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( + 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(currentPageNextY + ruleThickness + bottomSpacing); + + if (!pendingAnchorId.empty()) { + anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + pendingAnchorId.clear(); + } +} + void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) { auto* self = static_cast(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(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); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 623dc858..989184ea 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -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); diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index cecb40de..3a10e1e3 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -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: "Няма нядаўніх кніг" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 08b8f148..feb270ba 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -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" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 7363a98c..34cdedf5 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -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" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 0126aec8..86d2c334 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -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" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 917ac475..c75ed25b 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -24,7 +24,7 @@ STR_EMPTY_FILE: "Leeg bestand" STR_OUT_OF_BOUNDS: "Buiten bereik" STR_LOADING: "Laden..." STR_LOADING_POPUP: "Laden" -STR_WIFI_NETWORKS: "Wifi-netwerken" +STR_WIFI_NETWORKS: "Wi-Fi-netwerken" STR_NO_NETWORKS: "Geen netwerken gevonden" STR_NETWORKS_FOUND: "%zu netwerken gevonden" STR_SCANNING: "Scannen..." @@ -36,11 +36,11 @@ STR_SAVE_PASSWORD: "Wachtwoord opslaan voor volgende keer?" STR_PRESS_OK_SCAN: "Druk op OK om opnieuw te scannen" STR_JOIN_NETWORK: "Verbind met netwerk" STR_CREATE_HOTSPOT: "Hotspot maken" -STR_JOIN_DESC: "Verbind met een bestaand wifi-netwerk" -STR_HOTSPOT_DESC: "Maak een wifi-netwerk waar anderen mee kunnen verbinden" +STR_JOIN_DESC: "Verbind met een bestaand Wi-Fi-netwerk" +STR_HOTSPOT_DESC: "Maak een Wi-Fi-netwerk waar anderen mee kunnen verbinden" STR_STARTING_HOTSPOT: "Hotspot starten..." STR_HOTSPOT_MODE: "Hotspot-modus" -STR_CONNECT_WIFI_HINT: "Verbind je apparaat met dit wifi-netwerk" +STR_CONNECT_WIFI_HINT: "Verbind je apparaat met dit Wi-Fi-netwerk" STR_OPEN_URL_HINT: "Open deze URL in je browser" STR_OR_HTTP_PREFIX: "of http://" STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:" @@ -48,13 +48,13 @@ STR_CALIBRE_WIRELESS: "Calibre Wireless" STR_CALIBRE_WEB_URL: "OPDS URL" STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen" STR_MAC_ADDRESS: "MAC-adres:" -STR_CHECKING_WIFI: "Wifi controleren..." -STR_ENTER_WIFI_PASSWORD: "Voer wifi-wachtwoord in" +STR_CHECKING_WIFI: "Wi-Fi controleren..." +STR_ENTER_WIFI_PASSWORD: "Voer Wi-Fi-wachtwoord in" STR_TO_PREFIX: "met " STR_CALIBRE_RECEIVING: "Bezig met ontvangen: " STR_CALIBRE_RECEIVED: "Ontvangen: " STR_CALIBRE_INSTRUCTION_1: "1) Installeer CrossPoint Reader plugin" -STR_CALIBRE_INSTRUCTION_2: "2) Gebruik hetzelfde wifi-netwerk" +STR_CALIBRE_INSTRUCTION_2: "2) Gebruik hetzelfde Wi-Fi-netwerk" STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\"" STR_CALIBRE_INSTRUCTION_4: "\"Houd dit scherm open tijdens verzenden\"" STR_CAT_DISPLAY: "Scherm" @@ -103,7 +103,7 @@ STR_KOREADER_PASSWORD: "KOReader wachtwoord" STR_FILENAME: "Bestandsnaam" STR_BINARY: "Binair" STR_SET_CREDENTIALS_FIRST: "Stel eerst inloggegevens in" -STR_WIFI_CONN_FAILED: "Wifi-verbinding mislukt" +STR_WIFI_CONN_FAILED: "Wi-Fi-verbinding mislukt" STR_AUTHENTICATING: "Authenticeren..." STR_AUTH_SUCCESS: "Authenticatie geslaagd!" STR_KOREADER_AUTH: "KOReader-authenticatie" @@ -234,11 +234,12 @@ STR_THEME_CLASSIC: "Klassiek" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Uitgebreid" STR_SUNLIGHT_FADING_FIX: "Zonlicht vervaging fix" -STR_SEAMLESS_SLEEP: "Pagina als slaapscherm" +STR_QUICK_RESUME_TIMEOUT: "Snel hervatten bij timeout" STR_AFTER_TIMEOUT: "Na timeout" STR_REMAP_FRONT_BUTTONS: "Knoppen voorzijde wijzigen" STR_OPDS_BROWSER: "OPDS-browser" STR_COVER_CUSTOM: "Omslag + Aangepast" +STR_QUICK_RESUME: "Snel hervatten" STR_MENU_RECENT_BOOKS: "Recente boeken" STR_REMOVE_FROM_RECENTS: "Verwijderen uit Recente boeken?" STR_NO_RECENT_BOOKS: "Geen recente boeken" @@ -302,4 +303,4 @@ STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Screenshot maken" STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: " STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)" -STR_TILT_PAGE_TURN: "Kantel om te bladeren" \ No newline at end of file +STR_TILT_PAGE_TURN: "Kantel om te bladeren" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c10420fc..5da988ee 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -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" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index cd7b3b6d..03dcaab8 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -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" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 0cfc4b55..11270c84 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -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" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 9215635e..d0e8ee23 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -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" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 166e4b00..550c22da 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -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" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 4ccdacc7..07350e2c 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -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)" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 9c7417c2..bb3907f2 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -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: "Жуырда оқылған кітаптар жоқ" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index 154669e0..8b4158c6 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -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" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 3b85b70e..1d0823c2 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -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" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 1d13d16d..f1c86e58 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -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" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index e8ffe123..356b4a56 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -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ă" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index b74d8fa2..e95c62c6 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -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: "Нет недавних книг" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index eddfd5c9..2c5cd65e 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -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" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 65990dc2..99fe33b0 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -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" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 1c311be5..4c1c5dc9 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -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" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 5fa44cc1..7b426b5d 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -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" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index a23d33f8..f024e81f 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -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: "Немає останніх книг" diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index ef769f11..af619d1c 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -110,8 +110,16 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons if (self->inEntry) { if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr && strcmp(type, "application/epub+zip") == 0) { - self->currentEntry.type = OpdsEntryType::BOOK; - self->currentEntry.href = href; + // Prefer plain EPUB links over derived formats when multiple + // acquisition links are present for one entry. + const bool isPlainEpub = strstr(href, ".epub") != nullptr || strstr(href, "/epub/") != nullptr; + const bool alreadyHasPlainEpub = self->currentEntry.type == OpdsEntryType::BOOK && + (self->currentEntry.href.find(".epub") != std::string::npos || + self->currentEntry.href.find("/epub/") != std::string::npos); + if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) { + self->currentEntry.type = OpdsEntryType::BOOK; + self->currentEntry.href = href; + } } else if (type && strstr(type, "application/atom+xml") != nullptr) { if (self->currentEntry.type != OpdsEntryType::BOOK) { self->currentEntry.type = OpdsEntryType::NAVIGATION; diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index 0209923a..b5d22259 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -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; diff --git a/lib/Txt/Txt.h b/lib/Txt/Txt.h index b342ca88..859ae23b 100644 --- a/lib/Txt/Txt.h +++ b/lib/Txt/Txt.h @@ -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; diff --git a/lib/hal/HalStorage.cpp b/lib/hal/HalStorage.cpp index 154e2c34..095e389c 100644 --- a/lib/hal/HalStorage.cpp +++ b/lib/hal/HalStorage.cpp @@ -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(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) { diff --git a/platformio.ini b/platformio.ini index c34df816..bbf98aa5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -88,7 +88,7 @@ build_flags = ${base.build_flags} -DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\" -DENABLE_SERIAL_LOG - -DLOG_LEVEL=1 ; Set log level to info for release candidate builds + -DLOG_LEVEL=1 ; Set log level to info for release candidate builds [env:slim] extends = base diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 38d7c1c4..ba12a473 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -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; diff --git a/src/SettingsList.h b/src/SettingsList.h index 1f3e9f78..8ccb8614 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -106,15 +106,15 @@ inline std::vector 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", diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 5ea25653..797db488 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -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(); } diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 40eada07..6ff5b719 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -1,6 +1,5 @@ #include "OpdsBookBrowserActivity.h" -#include #include #include #include @@ -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; diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index ca8b5199..ff89cf27 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -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); diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 44ccfb5b..29737a5a 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -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); diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index c4fc4347..b0dcbfbb 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -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()); diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index 90d488a8..7686b24b 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -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; } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index afd707c6..8a5888d9 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -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(); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 1767272c..44a22ea9 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -156,12 +156,16 @@ class SettingsActivity final : public Activity { std::vector systemSettings; const std::vector* 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) diff --git a/src/main.cpp b/src/main.cpp index 2a93a1cd..37af16bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -142,7 +142,25 @@ constexpr uint32_t SILENT_REBOOT_MAGIC = 0xC1EAB007; constexpr uint32_t SILENT_REBOOT_TARGET_HOME = 0; constexpr uint32_t SILENT_REBOOT_TARGET_READER = 1; +// How the device is coming back to life, resolved once at boot. Both resume +// flows suppress the splash and leave the panel holding its pre-boot frame; a +// plain boot shows the splash. See setup() for the resolution. +enum class BootResume : uint8_t { + Splash, // cold boot, flash, panic, or plain reboot + Silent, // heap-defrag ESP.restart() (RTC flag; lost on power loss) + QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss) +}; + +// Latched true once enterDeepSleep() commits to sleeping, before it tears down +// the current activity. WiFi activities call silentRestart() in onExit() to +// clear heap fragmentation on the way out, but deep sleep is a full chip reset +// on wake and already clears the heap, so rebooting here would just power the +// device back up against the user's sleep gesture. Never cleared: +// startDeepSleep() does not return, so a set latch only ends at the wakeup reset. +static bool deepSleepInProgress = false; + void silentRestart() { + if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot silentRebootTarget = SILENT_REBOOT_TARGET_HOME; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=home)"); @@ -156,6 +174,7 @@ void silentRestart() { } void silentRestartToReader() { + if (deepSleepInProgress) return; // sleeping supersedes the heap-defrag reboot silentRebootTarget = SILENT_REBOOT_TARGET_READER; silentRebootMagic = SILENT_REBOOT_MAGIC; LOG_DBG("MAIN", "Silent restart (target=reader)"); @@ -243,16 +262,20 @@ void enterDeepSleep(bool fromTimeout = false) { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); - const bool isSeamless = SETTINGS.seamlessSleepScreen == CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_ALWAYS || - (fromTimeout && SETTINGS.seamlessSleepScreen == - CrossPointSettings::SEAMLESS_SLEEP_SCREEN::SEAMLESS_AFTER_TIMEOUT); - APP_STATE.showBootScreen = !isSeamless; + const bool isQuickResumeSleep = + SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME || + (fromTimeout && + SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT); + APP_STATE.showBootScreen = !isQuickResumeSleep; APP_STATE.saveToFile(); + // Commit to sleeping before goToSleep() runs the outgoing activity's onExit(): + // a WiFi activity would otherwise silentRestart() here and reboot instead. + deepSleepInProgress = true; activityManager.goToSleep(fromTimeout); - if (isSeamless) { + if (isQuickResumeSleep) { saveSleepFrameBuffer(); } @@ -399,27 +422,39 @@ void setup() { // First serial output only here to avoid timing inconsistencies for power button press duration verification LOG_DBG("MAIN", "Starting CrossPoint version " CROSSPOINT_VERSION); - setupDisplayAndFonts(isSilentReboot || /*seamless=*/!APP_STATE.showBootScreen); + // Resolve the single boot-presentation decision. Skipping the splash also + // skips the panel-clearing pass and the X3 initial-full-sync arming (see + // HalDisplay::begin), so the first paint is FAST_REFRESH (~500ms) over the + // retained frame and input dispatches against a visible UI. + const BootResume resume = isSilentReboot ? BootResume::Silent + : !APP_STATE.showBootScreen ? BootResume::QuickResume + : BootResume::Splash; - // Silent reboot suppresses the boot splash and the X3 initial-full-sync - // arming (see HalDisplay::begin), so the first Home paint is FAST_REFRESH - // (~500ms) and input dispatches against the visible menu. - if (!isSilentReboot) { - if (APP_STATE.showBootScreen) { - activityManager.goToBoot(); - } else if (loadSleepFrameBuffer()) { - // Seamless wake: buffer restored, replace moon icon with loading icon - const auto pageHeight = renderer.getScreenHeight(); - renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - APP_STATE.showBootScreen = true; - APP_STATE.saveToFile(); - } else { - // Frame buffer file missing — fall back to normal boot screen + setupDisplayAndFonts(resume != BootResume::Splash); + + switch (resume) { + case BootResume::Silent: + // Splash skipped: the routing block below picks the target activity; the + // panel keeps showing the pre-reboot popup until that first paint lands. + break; + case BootResume::QuickResume: + // One-shot flag: re-arm the splash for the next non-quick-resume boot. Save + // before any painting so a hang in the blocking paint path can't strand + // us in a quick-resume-with-no-frame loop on the next boot. APP_STATE.showBootScreen = true; APP_STATE.saveToFile(); + if (loadSleepFrameBuffer()) { + // Frame restored: swap the sleep moon for the loading icon. + const auto pageHeight = renderer.getScreenHeight(); + renderer.drawImage(LoadingIcon, 0, pageHeight - LOADINGICON_HEIGHT, LOADINGICON_WIDTH, LOADINGICON_HEIGHT); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + } else { + activityManager.goToBoot(); // frame file missing, fall back to the splash + } + break; + case BootResume::Splash: activityManager.goToBoot(); - } + break; } if (recoveryFirmwareMode) { @@ -429,9 +464,10 @@ void setup() { } else if (HalSystem::isRebootFromPanic()) { // If we rebooted from a panic, go to crash report screen to show the panic info activityManager.goToCrashReport(); - } else if (isSilentReboot && snapshotTarget == SILENT_REBOOT_TARGET_READER && !APP_STATE.openEpubPath.empty()) { + } else if (resume == BootResume::Silent && snapshotTarget == SILENT_REBOOT_TARGET_READER && + !APP_STATE.openEpubPath.empty()) { activityManager.goToReader(APP_STATE.openEpubPath); - } else if (isSilentReboot) { + } else if (resume == BootResume::Silent) { // target == home (or reader with no open book): land on home — don't fall // through to the sleep-wake "resume reader" logic, which fires on stale // openEpubPath + lastSleepFromReader from a prior session. @@ -450,7 +486,7 @@ void setup() { activityManager.goToReader(path); } - if (isSilentReboot) { + if (resume == BootResume::Silent) { // Block until the first paint physically completes. refreshDisplay() // waits on the panel BUSY pin so when this returns the user can see the // new activity. Without the wait, an edge captured by gpio.update() diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index aeaad11e..41606f8d 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1,7 +1,6 @@ #include "CrossPointWebServer.h" #include -#include #include #include #include @@ -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; diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index bd26a5ac..958b5845 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -1,204 +1,209 @@ #include "HttpDownloader.h" -#include +#include #include -#include -#include -#include +#include #include +#include +#include #include -#include -#include - -#include "util/UrlUtils.h" +#include +#include namespace { -class FileWriteStream final : public Stream { - public: - FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress, bool* cancelFlag) - : file_(file), total_(total), progress_(std::move(progress)), cancelFlag_(cancelFlag) {} +// RX holds the response headers. 4096 fits real OPDS servers; GitHub's release +// CDN sends more and logs HTTP_HEADER "Buffer length is small", but that's +// non-fatal: the headers we read (Location, Content-Length) come first and +// survive. Smaller keeps contiguous heap free while WiFi and TLS are up. TX +// only carries our GET; the body streams in READ_CHUNK pieces. +constexpr int HTTP_RX_BUF = 4096; +constexpr int HTTP_TX_BUF = 1024; +// Per-socket-op timeout. Some OPDS download endpoints are slow to send headers +// (>15s) and chunked catalogs stall mid-body, so 15s killed them. 60s gives +// slow servers room. esp_http_client's timeout_ms is uint32, so unlike Arduino +// HTTPClient's uint16 setTimeout it doesn't silently truncate. +constexpr int HTTP_TIMEOUT_MS = 60000; +constexpr size_t READ_CHUNK = 2048; - size_t write(uint8_t byte) override { return write(&byte, 1); } +struct Sink { + std::function write; // returns false to abort the transfer + HttpDownloader::ProgressCallback progress; + bool* cancelFlag = nullptr; + size_t total = 0; + size_t downloaded = 0; +}; - size_t write(const uint8_t* buffer, size_t size) override { - // Write-through stream for HTTPClient::writeToStream with progress tracking. - if (cancelFlag_ && *cancelFlag_) { - writeOk_ = false; - return 0; - } - const size_t written = file_.write(buffer, size); - if (written != size) { - writeOk_ = false; - } - downloaded_ += written; - if (progress_ && total_ > 0) { - progress_(downloaded_, total_); - } - return written; +bool isRedirect(int status) { + return status == 301 || status == 302 || status == 303 || status == 307 || status == 308; +} + +// Streams a GET body through sink.write in READ_CHUNK pieces. Uses the manual +// open/fetch_headers/read path rather than esp_http_client_perform(): perform() +// pushes the whole body through an event callback and reports a chunked body +// that ends early as ESP_ERR_HTTP_INCOMPLETE_DATA, whereas the read loop streams +// large/slow files and surfaces a short read directly. +HttpDownloader::DownloadError runGet(const std::string& url, const std::string& username, const std::string& password, + Sink& sink) { + esp_http_client_config_t config = {}; + config.url = url.c_str(); + config.buffer_size = HTTP_RX_BUF; + config.buffer_size_tx = HTTP_TX_BUF; + config.timeout_ms = HTTP_TIMEOUT_MS; + // Verify HTTPS against the bundled CA roots. This build has esp-tls + // CONFIG_ESP_TLS_INSECURE off, so an unverified TLS handshake can't be set + // up at all; the model is public servers over verified https and local + // servers over plain http (esp_http_client picks the transport from the URL + // scheme, so http:// needs no cert config). The prior setInsecure() worked + // only because Arduino's ssl_client drives mbedtls directly. + config.crt_bundle_attach = esp_crt_bundle_attach; + config.keep_alive_enable = true; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (!client) { + LOG_ERR("HTTP", "client init failed"); + return HttpDownloader::HTTP_ERROR; } - int available() override { return 0; } - int read() override { return -1; } - int peek() override { return -1; } - void flush() override { file_.flush(); } + esp_http_client_set_header(client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + if (!username.empty() && !password.empty()) { + // Preemptive Basic auth, like the prior addHeader; don't wait for a 401. + const std::string credentials = username + ":" + password; + const String header = "Basic " + base64::encode(credentials.c_str()); + esp_http_client_set_header(client, "Authorization", header.c_str()); + } - size_t downloaded() const { return downloaded_; } - bool ok() const { return writeOk_; } + // open()/read() does not auto-follow redirects (only perform() does), so step + // 30x responses manually. OPDS download endpoints and the GitHub release CDN + // both redirect. + esp_err_t err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + LOG_ERR("HTTP", "open failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return HttpDownloader::HTTP_ERROR; + } + int64_t contentLength = esp_http_client_fetch_headers(client); + int status = esp_http_client_get_status_code(client); + for (int hop = 0; isRedirect(status) && hop < 5; ++hop) { + if (esp_http_client_set_redirection(client) != ESP_OK) break; + err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + LOG_ERR("HTTP", "redirect open failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return HttpDownloader::HTTP_ERROR; + } + contentLength = esp_http_client_fetch_headers(client); + status = esp_http_client_get_status_code(client); + } - private: - FsFile& file_; - size_t total_; - size_t downloaded_ = 0; - bool writeOk_ = true; - HttpDownloader::ProgressCallback progress_; - bool* cancelFlag_; -}; + if (status != 200) { + LOG_ERR("HTTP", "unexpected status: %d", status); + esp_http_client_cleanup(client); + return HttpDownloader::HTTP_ERROR; + } + + // fetch_headers returns 0 for a chunked response (no Content-Length); leave + // total at 0 so progress stays silent and the size check is skipped. + sink.total = contentLength > 0 ? static_cast(contentLength) : 0; + + auto buf = makeUniqueNoThrow(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(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 client; - if (UrlUtils::isHttpsUrl(url)) { - auto* secureClient = new NetworkClientSecure(); - secureClient->setInsecure(); - client.reset(secureClient); - } else { - client.reset(new NetworkClient()); - } - HTTPClient http; - LOG_DBG("HTTP", "Fetching: %s", url.c_str()); - - http.begin(*client, url.c_str()); - http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); - - if (!username.empty() && !password.empty()) { - std::string credentials = username + ":" + password; - String encoded = base64::encode(credentials.c_str()); - http.addHeader("Authorization", "Basic " + encoded); - } - - const int httpCode = http.GET(); - if (httpCode != HTTP_CODE_OK) { - LOG_ERR("HTTP", "Fetch failed: %d", httpCode); - http.end(); - return false; - } - - http.writeToStream(&outContent); - - http.end(); - - LOG_DBG("HTTP", "Fetch success"); - return true; + Sink sink; + sink.write = [&outContent](const uint8_t* data, size_t len) { return outContent.write(data, len) == len; }; + return runGet(url, username, password, sink) == OK; } bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent, const std::string& username, const std::string& password) { - StreamString stream; - if (!fetchUrl(url, stream, username, password)) { - return false; - } - outContent = stream.c_str(); - return true; + LOG_DBG("HTTP", "Fetching: %s", url.c_str()); + outContent.clear(); // start clean; the sink appends, so don't carry prior content + Sink sink; + sink.write = [&outContent](const uint8_t* data, size_t len) { + outContent.append(reinterpret_cast(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 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(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; } diff --git a/src/network/HttpDownloader.h b/src/network/HttpDownloader.h index 5913c895..c52b790d 100644 --- a/src/network/HttpDownloader.h +++ b/src/network/HttpDownloader.h @@ -5,12 +5,16 @@ #include /** - * 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; + // 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; 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. */ diff --git a/src/network/OtaUpdater.cpp b/src/network/OtaUpdater.cpp index 658b145a..63a1903f 100644 --- a/src/network/OtaUpdater.cpp +++ b/src/network/OtaUpdater.cpp @@ -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 #include #include #include #include #include +// clang-format on + +#include 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(event->user_data); - parser->feed(static_cast(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(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"); diff --git a/src/network/WebDAVHandler.cpp b/src/network/WebDAVHandler.cpp index f20e8261..0fafb680 100644 --- a/src/network/WebDAVHandler.cpp +++ b/src/network/WebDAVHandler.cpp @@ -1,11 +1,12 @@ #include "WebDAVHandler.h" -#include #include #include #include #include +#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"; diff --git a/src/network/WebDAVHandler.h b/src/network/WebDAVHandler.h index 5911203f..e11e184b 100644 --- a/src/network/WebDAVHandler.h +++ b/src/network/WebDAVHandler.h @@ -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; }; diff --git a/src/network/html/SettingsPage.html b/src/network/html/SettingsPage.html index 0a73f9bd..4291a21b 100644 --- a/src/network/html/SettingsPage.html +++ b/src/network/html/SettingsPage.html @@ -312,6 +312,11 @@