diff --git a/.skills/SKILL.md b/.skills/SKILL.md index e01a17c7..30cd9de3 100644 --- a/.skills/SKILL.md +++ b/.skills/SKILL.md @@ -110,7 +110,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior: - Only ONE framebuffer exists (not double-buffered) - Grayscale rendering requires temporary buffer allocation (`renderer.storeBwBuffer()`) - Must call `renderer.restoreBwBuffer()` to free temporary buffers -- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp) for malloc usage +- See [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) for malloc usage ### Directory Structure * lib/: Internal libraries (Epub engine, GfxRenderer, UITheme, I18n) @@ -130,7 +130,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior: | `HalGPIO` | `InputManager` | Button input handling | *(none)* | | `HalStorage` | `SDCardManager` | SD card file I/O | `Storage` | -**Location**: [lib/hal/](lib/hal/) +**Location**: [lib/hal/](../lib/hal/) **Why HAL?** - Provides consistent error logging per module @@ -247,7 +247,7 @@ When a template is necessary, limit instantiations: use explicit template instan ### Error Handling Philosophy -**Source**: [src/main.cpp:132-143](src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](lib/GfxRenderer/GfxRenderer.cpp) +**Source**: [src/main.cpp:132-143](../src/main.cpp), [lib/GfxRenderer/GfxRenderer.cpp:10](../lib/GfxRenderer/GfxRenderer.cpp) **Pattern Hierarchy**: 1. **LOG_ERR + return false** (90%): `LOG_ERR("MOD", "Failed: %s", reason); return false;` @@ -259,7 +259,7 @@ When a template is necessary, limit instantiations: use explicit template instan ### Acceptable malloc/free Patterns -**Source**: [src/activities/home/HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp) +**Source**: [src/activities/home/HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp), [lib/GfxRenderer/GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) Despite "prefer stack allocation," malloc is acceptable for: 1. **Large temporary buffers** (> 256 bytes, won't fit on stack) @@ -290,10 +290,10 @@ buffer = nullptr; - **Document size**: Comment why stack allocation was rejected **Examples in codebase**: -- Cover image buffers: [HomeActivity.cpp:166](src/activities/home/HomeActivity.cpp#L166) -- Text chunk buffers: [TxtReaderActivity.cpp:259](src/activities/reader/TxtReaderActivity.cpp#L259) -- Bitmap rendering: [GfxRenderer.cpp:439-440](lib/GfxRenderer/GfxRenderer.cpp#L439-L440) -- OTA update buffer: [OtaUpdater.cpp:40](src/network/OtaUpdater.cpp#L40) +- Cover image buffers: [HomeActivity.cpp:166](../src/activities/home/HomeActivity.cpp) +- Text chunk buffers: [TxtReaderActivity.cpp:259](../src/activities/reader/TxtReaderActivity.cpp) +- Bitmap rendering: [GfxRenderer.cpp:439-440](../lib/GfxRenderer/GfxRenderer.cpp) +- OTA update buffer: [OtaUpdater.cpp:40](../src/network/OtaUpdater.cpp) --- @@ -305,7 +305,7 @@ buffer = nullptr; ### Logical Button Mapping -**Source**: [src/MappedInputManager.cpp:20-55](src/MappedInputManager.cpp) +**Source**: [src/MappedInputManager.cpp:20-55](../src/MappedInputManager.cpp) Constraint: Physical button positions are fixed on hardware, but their logical functions change based on user settings and screen orientation. @@ -352,7 +352,7 @@ Constraint: Physical button positions are fixed on hardware, but their logical f ### Activity Lifecycle and Memory Management -**Source**: [src/main.cpp:132-143](src/main.cpp) +**Source**: [src/main.cpp:132-143](../src/main.cpp) **CRITICAL**: Activities are **heap-allocated** and **deleted on exit**. @@ -389,7 +389,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on ### FreeRTOS Task Guidelines -**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](src/activities/util/KeyboardEntryActivity.cpp) +**Source**: [src/activities/util/KeyboardEntryActivity.cpp:45-50](../src/activities/util/KeyboardEntryActivity.cpp) **Pattern**: See Activity Lifecycle above. `xTaskCreate(&taskTrampoline, "Name", stackSize, this, 1, &handle)` @@ -402,7 +402,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on ### Global Font Loading -**Source**: [src/main.cpp:40-115](src/main.cpp) +**Source**: [src/main.cpp:40-115](../src/main.cpp) **All fonts are loaded as global static objects** at firmware startup: - Bookerly: 12, 14, 16, 18pt (4 styles each: regular, bold, italic, bold-italic) @@ -423,7 +423,7 @@ void onExit() { /* free: vTaskDelete, free buffer, close files */ Activity::on - Fonts stored in **Flash** (marked as `static const` in `lib/EpdFont/builtinFonts/`) - Font rendering data cached in **DRAM** when first used - `OMIT_FONTS` can reduce binary size for minimal builds -- Font IDs defined in [src/fontIds.h](src/fontIds.h) +- Font IDs defined in [src/fontIds.h](../src/fontIds.h) **Usage**: ```cpp @@ -517,7 +517,7 @@ clang-format -i src/**/*.cpp src/**/*.h 4. **Corrupt Cache Files**: - Delete `.crosspoint/` directory on SD card - Forces clean re-parse of all EPUBs - - Check file format versions in [docs/file-formats.md](docs/file-formats.md) + - Check file format versions in [docs/file-formats.md](../docs/file-formats.md) 5. **Watchdog Timeout**: - Loop/task blocked for >5 seconds diff --git a/docs/i18n.md b/docs/i18n.md index 95e3d195..6a564d84 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -15,6 +15,7 @@ This guide explains the multi-language support system in CrossPoint Reader. - Ukrainian - Polish - Danish +- Turkish --- diff --git a/docs/translators.md b/docs/translators.md index eadf5560..ae7ab647 100644 --- a/docs/translators.md +++ b/docs/translators.md @@ -1,6 +1,6 @@ # Translators -Below is a list of users and languages CrossPoint may support in the future. +Below is a list of users and languages CrossPoint may support in the future. Note because a language is below does not mean there is official support for the language at this time. ## Contributing @@ -39,6 +39,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an ## Swedish - [dawiik](https://github.com/dawiik) +- [steka](https://github.com/steka) ## Romanian - [ariel-lindemann](https://github.com/ariel-lindemann) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 876bea75..cb0b1801 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -103,14 +103,11 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) { pos += strlen(pattern); const auto endPos = coverPageHtml.find('"', pos); if (endPos != std::string::npos) { - const auto ref = coverPageHtml.substr(pos, endPos - pos); + const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos); // Check if it's an image file - if (ref.length() >= 4) { - const auto ext = ref.substr(ref.length() - 4); - if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") { - imageRef = ref; - break; - } + if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) { + imageRef = ref; + break; } } pos = coverPageHtml.find(pattern, pos); @@ -541,8 +538,7 @@ bool Epub::generateCoverBmp(bool cropped) const { return false; } - if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || - coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { + if (FsHelpers::hasJpgExtension(coverImageHref)) { LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit"); const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; @@ -575,7 +571,7 @@ bool Epub::generateCoverBmp(bool cropped) const { return success; } - if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") { + if (FsHelpers::hasPngExtension(coverImageHref)) { LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit"); const auto coverPngTempPath = getCachePath() + "/.cover.png"; @@ -629,8 +625,7 @@ bool Epub::generateThumbBmp(int height) const { const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref; if (coverImageHref.empty()) { LOG_DBG("EBP", "No known cover image for thumbnail"); - } else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || - coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { + } else if (FsHelpers::hasJpgExtension(coverImageHref)) { LOG_DBG("EBP", "Generating thumb BMP from JPG cover image"); const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; @@ -666,7 +661,7 @@ bool Epub::generateThumbBmp(int height) const { } LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no"); return success; - } else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") { + } else if (FsHelpers::hasPngExtension(coverImageHref)) { LOG_DBG("EBP", "Generating thumb BMP from PNG cover image"); const auto coverPngTempPath = getCachePath() + "/.cover.png"; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index bc04d475..9365df20 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -10,10 +10,10 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 17; +constexpr uint8_t SECTION_FILE_VERSION = 18; 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(uint32_t); + sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t); } // namespace uint32_t Section::onPageComplete(std::unique_ptr page) { @@ -44,7 +44,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + - sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t), + sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); @@ -56,8 +56,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi serialization::writePod(file, hyphenationEnabled); serialization::writePod(file, embeddedStyle); serialization::writePod(file, imageRendering); - serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) - serialization::writePod(file, static_cast(0)); // Placeholder for LUT offset + serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later) + serialization::writePod(file, static_cast(0)); // Placeholder for LUT offset (patched later) + serialization::writePod(file, static_cast(0)); // Placeholder for anchor map offset (patched later) } bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, @@ -239,10 +240,20 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c return false; } - // Go back and write LUT offset - file.seek(HEADER_SIZE - sizeof(uint32_t) - sizeof(pageCount)); + // Write anchor-to-page map for fragment navigation (e.g. footnote targets) + const uint32_t anchorMapOffset = file.position(); + const auto& anchors = visitor.getAnchors(); + serialization::writePod(file, static_cast(anchors.size())); + for (const auto& [anchor, page] : anchors) { + serialization::writeString(file, anchor); + serialization::writePod(file, page); + } + + // Patch header with final pageCount, lutOffset, and anchorMapOffset + file.seek(HEADER_SIZE - sizeof(uint32_t) * 2 - sizeof(pageCount)); serialization::writePod(file, pageCount); serialization::writePod(file, lutOffset); + serialization::writePod(file, anchorMapOffset); file.close(); if (cssParser) { cssParser->clear(); @@ -255,7 +266,7 @@ std::unique_ptr Section::loadPageFromSectionFile() { return nullptr; } - file.seek(HEADER_SIZE - sizeof(uint32_t)); + file.seek(HEADER_SIZE - sizeof(uint32_t) * 2); uint32_t lutOffset; serialization::readPod(file, lutOffset); file.seek(lutOffset + sizeof(uint32_t) * currentPage); @@ -267,3 +278,36 @@ std::unique_ptr Section::loadPageFromSectionFile() { file.close(); return page; } + +std::optional Section::getPageForAnchor(const std::string& anchor) const { + FsFile f; + if (!Storage.openFileForRead("SCT", filePath, f)) { + return std::nullopt; + } + + const uint32_t fileSize = f.size(); + f.seek(HEADER_SIZE - sizeof(uint32_t)); + uint32_t anchorMapOffset; + serialization::readPod(f, anchorMapOffset); + if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) { + f.close(); + return std::nullopt; + } + + f.seek(anchorMapOffset); + uint16_t count; + serialization::readPod(f, count); + for (uint16_t i = 0; i < count; i++) { + std::string key; + uint16_t page; + serialization::readString(f, key); + serialization::readPod(f, page); + if (key == anchor) { + f.close(); + return page; + } + } + + f.close(); + return std::nullopt; +} diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 70fd0fe5..6f002c44 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include #include "Epub.h" @@ -37,4 +39,7 @@ class Section { uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, uint8_t imageRendering, const std::function& popupFn = nullptr); std::unique_ptr loadPageFromSectionFile(); + + // Look up the page number for an anchor id from the section cache file. + std::optional getPageForAnchor(const std::string& anchor) const; }; diff --git a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp index eb1f93b5..52aa6aec 100644 --- a/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp @@ -1,5 +1,6 @@ #include "JpegToFramebufferConverter.h" +#include #include #include #include @@ -486,9 +487,5 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat } bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) { - std::string ext = extension; - for (auto& c : ext) { - c = tolower(c); - } - return (ext == ".jpg" || ext == ".jpeg"); + return FsHelpers::hasJpgExtension(extension); } diff --git a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp index 3b15ebf0..6ba1bcdf 100644 --- a/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp +++ b/lib/Epub/Epub/converters/PngToFramebufferConverter.cpp @@ -1,5 +1,6 @@ #include "PngToFramebufferConverter.h" +#include #include #include #include @@ -391,9 +392,5 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath } bool PngToFramebufferConverter::supportsFormat(const std::string& extension) { - std::string ext = extension; - for (auto& c : ext) { - c = tolower(c); - } - return (ext == ".png"); + return FsHelpers::hasPngExtension(extension); } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 6a0636bb..1df3ceab 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -133,11 +133,21 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { // This handles cases like

text

where the // div's margin should be preserved, even though it has no direct text content. currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle)); + + if (!pendingAnchorId.empty()) { + anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + pendingAnchorId.clear(); + } return; } makePages(); } + // Record deferred anchor after previous block is flushed + if (!pendingAnchorId.empty()) { + anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + pendingAnchorId.clear(); + } currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle)); wordsExtractedInBlock = 0; } @@ -151,7 +161,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* return; } - // Extract class and style attributes for CSS processing + // Extract class, style, and id attributes std::string classAttr; std::string styleAttr; if (atts != nullptr) { @@ -160,6 +170,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* classAttr = atts[i + 1]; } else if (strcmp(atts[i], "style") == 0) { styleAttr = atts[i + 1]; + } else if (strcmp(atts[i], "id") == 0) { + // Defer recording until startNewTextBlock, after previous block is flushed to pages + self->pendingAnchorId = atts[i + 1]; } } } @@ -374,6 +387,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (self->currentPage && !self->currentPage->elements.empty() && (self->currentPageNextY + displayHeight > self->viewportHeight)) { self->completePageFn(std::move(self->currentPage)); + self->completedPageCount++; self->currentPage.reset(new Page()); if (!self->currentPage) { LOG_ERR("EHP", "Failed to create new page"); @@ -990,7 +1004,12 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { // Process last page if there is still text if (currentTextBlock) { makePages(); + if (!pendingAnchorId.empty()) { + anchorData.push_back({std::move(pendingAnchorId), static_cast(completedPageCount)}); + pendingAnchorId.clear(); + } completePageFn(std::move(currentPage)); + completedPageCount++; currentPage.reset(); currentTextBlock.reset(); } @@ -1003,6 +1022,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line) { if (currentPageNextY + lineHeight > viewportHeight) { completePageFn(std::move(currentPage)); + completedPageCount++; currentPage.reset(new Page()); currentPageNextY = 0; } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index ce530797..1cc0ea39 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "../FootnoteEntry.h" @@ -69,6 +70,11 @@ class ChapterHtmlSlimParser { int tableRowIndex = 0; int tableColIndex = 0; + // Anchor-to-page mapping: tracks which page each HTML id attribute lands on + int completedPageCount = 0; + std::vector> anchorData; + std::string pendingAnchorId; // deferred until after previous text block is flushed + // Footnote link tracking bool insideFootnoteLink = false; int footnoteLinkDepth = -1; @@ -119,4 +125,5 @@ class ChapterHtmlSlimParser { ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); void addLineToPage(std::shared_ptr line); + const std::vector>& getAnchors() const { return anchorData; } }; diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp index 4bd8fbfe..08ca4460 100644 --- a/lib/FsHelpers/FsHelpers.cpp +++ b/lib/FsHelpers/FsHelpers.cpp @@ -1,8 +1,12 @@ #include "FsHelpers.h" +#include +#include #include -std::string FsHelpers::normalisePath(const std::string& path) { +namespace FsHelpers { + +std::string normalisePath(const std::string& path) { std::vector components; std::string component; @@ -37,3 +41,41 @@ std::string FsHelpers::normalisePath(const std::string& path) { return result; } + +bool checkFileExtension(std::string_view fileName, const char* extension) { + const size_t extLen = strlen(extension); + if (fileName.length() < extLen) { + return false; + } + + const size_t offset = fileName.length() - extLen; + for (size_t i = 0; i < extLen; i++) { + if (tolower(static_cast(fileName[offset + i])) != + tolower(static_cast(extension[i]))) { + return false; + } + } + return true; +} + +bool hasJpgExtension(std::string_view fileName) { + return checkFileExtension(fileName, ".jpg") || checkFileExtension(fileName, ".jpeg"); +} + +bool hasPngExtension(std::string_view fileName) { return checkFileExtension(fileName, ".png"); } + +bool hasBmpExtension(std::string_view fileName) { return checkFileExtension(fileName, ".bmp"); } + +bool hasGifExtension(std::string_view fileName) { return checkFileExtension(fileName, ".gif"); } + +bool hasEpubExtension(std::string_view fileName) { return checkFileExtension(fileName, ".epub"); } + +bool hasXtcExtension(std::string_view fileName) { + return checkFileExtension(fileName, ".xtc") || checkFileExtension(fileName, ".xtch"); +} + +bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(fileName, ".txt"); } + +bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); } + +} // namespace FsHelpers diff --git a/lib/FsHelpers/FsHelpers.h b/lib/FsHelpers/FsHelpers.h index 5bb4218e..a2113512 100644 --- a/lib/FsHelpers/FsHelpers.h +++ b/lib/FsHelpers/FsHelpers.h @@ -1,7 +1,58 @@ #pragma once -#include +#include -class FsHelpers { - public: - static std::string normalisePath(const std::string& path); -}; +#include +#include + +namespace FsHelpers { + +std::string normalisePath(const std::string& path); + +/** + * Check if the given filename ends with the specified extension (case-insensitive). + */ +bool checkFileExtension(std::string_view fileName, const char* extension); +inline bool checkFileExtension(const String& fileName, const char* extension) { + return checkFileExtension(std::string_view{fileName.c_str(), fileName.length()}, extension); +} + +// Check for either .jpg or .jpeg extension (case-insensitive) +bool hasJpgExtension(std::string_view fileName); +inline bool hasJpgExtension(const String& fileName) { + return hasJpgExtension(std::string_view{fileName.c_str(), fileName.length()}); +} + +// Check for .png extension (case-insensitive) +bool hasPngExtension(std::string_view fileName); +inline bool hasPngExtension(const String& fileName) { + return hasPngExtension(std::string_view{fileName.c_str(), fileName.length()}); +} + +// Check for .bmp extension (case-insensitive) +bool hasBmpExtension(std::string_view fileName); + +// Check for .gif extension (case-insensitive) +bool hasGifExtension(std::string_view fileName); +inline bool hasGifExtension(const String& fileName) { + return hasGifExtension(std::string_view{fileName.c_str(), fileName.length()}); +} + +// Check for .epub extension (case-insensitive) +bool hasEpubExtension(std::string_view fileName); +inline bool hasEpubExtension(const String& fileName) { + return hasEpubExtension(std::string_view{fileName.c_str(), fileName.length()}); +} + +// Check for either .xtc or .xtch extension (case-insensitive) +bool hasXtcExtension(std::string_view fileName); + +// Check for .txt extension (case-insensitive) +bool hasTxtExtension(std::string_view fileName); +inline bool hasTxtExtension(const String& fileName) { + return hasTxtExtension(std::string_view{fileName.c_str(), fileName.length()}); +} + +// Check for .md extension (case-insensitive) +bool hasMarkdownExtension(std::string_view fileName); + +} // namespace FsHelpers diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 9d59ee28..9ae7d579 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -92,6 +92,10 @@ STR_STATUS_BAR: "Barra d'estat" STR_HIDE_BATTERY: "Oculta el % de bateria" STR_EXTRA_SPACING: "Espaiat de paràgraf extra" STR_TEXT_AA: "Antialiàsing del text" +STR_IMAGES: "Imatges" +STR_IMAGES_DISPLAY: "Mostrar" +STR_IMAGES_PLACEHOLDER: "Text de mostra" +STR_IMAGES_SUPPRESS: "Suprimir" STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada" STR_ORIENTATION: "Orientació de lectura" STR_FRONT_BTN_LAYOUT: "Disposició dels botons frontals" @@ -226,6 +230,7 @@ STR_EXIT: "« Surt" STR_HOME: "« Inici" STR_SAVE: "« Desa" STR_SELECT: "Selecciona" +STR_SELECTED: "Seleccionat" STR_TOGGLE: "Canvia" STR_CONFIRM: "Confirma" STR_CANCEL: "Cancel·la" @@ -235,6 +240,8 @@ STR_DOWNLOAD: "Descarrega" STR_RETRY: "Nou intent" STR_YES: "Sí" STR_NO: "No" +STR_SHOW: "Mostrar" +STR_HIDE: "Amagar" STR_STATE_ON: "ON" STR_STATE_OFF: "OFF" STR_NOT_SET: "No establert" @@ -247,6 +254,21 @@ STR_CAPS_OFF: "majs" STR_OK_BUTTON: "OK" STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs" STR_FILTER_CONTRAST: "Contrast" +STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat" +STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol" +STR_BOOK_PROGRESS_PERCENTAGE: "Percentatge de progrés del llibre" +STR_PROGRESS_BAR: "Barra de progrés" +STR_PROGRESS_BAR_THICKNESS: "Gruix de la barra de progrés" +STR_PROGRESS_BAR_THIN: "Fina" +STR_PROGRESS_BAR_MEDIUM: "Mitjana" +STR_PROGRESS_BAR_THICK: "Gruixuda" +STR_BOOK: "Llibre" +STR_CHAPTER: "Capítol" +STR_EXAMPLE_CHAPTER: "Capítol 21" +STR_EXAMPLE_BOOK: "Títol del llibre" +STR_PREVIEW: "Vista prèvia" +STR_TITLE: "Títol" +STR_BATTERY: "Bateria" STR_UI_THEME: "Tema de la interfície" STR_THEME_CLASSIC: "Clàssic" STR_THEME_LYRA: "Lyra" @@ -279,6 +301,8 @@ STR_GO_TO_PERCENT: "Ves al %" STR_GO_HOME_BUTTON: "Ves a l'inici" STR_SYNC_PROGRESS: "Sincronitza el progrés" STR_DELETE_CACHE: "Esborra la memòria cau del llibre" +STR_DELETE: "Esborra" +STR_DISPLAY_QR: "Mostra la pàgina com a QR" STR_CHAPTER_PREFIX: "Capítol: " STR_PAGES_SEPARATOR: " pàgines | " STR_BOOK_PREFIX: "Llibre: " @@ -311,3 +335,9 @@ STR_UPLOAD: "Puja" STR_BOOK_S_STYLE: "Estil del llibre" STR_EMBEDDED_STYLE: "Estil incrustat" STR_OPDS_SERVER_URL: "URL del servidor OPDS" +STR_FOOTNOTES: "Notes al peu" +STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina" +STR_LINK: "[enllaç]" +STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla" +STR_AUTO_TURN_ENABLED: "Passar automàtic activat: " +STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index c870b367..91228b08 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -92,6 +92,10 @@ STR_STATUS_BAR: "Status Bar" STR_HIDE_BATTERY: "Ukryj % baterii" STR_EXTRA_SPACING: "Dodatkowe odstępy paragrafów" STR_TEXT_AA: "Wygładzanie tekstu" +STR_IMAGES: "Obrazki" +STR_IMAGES_DISPLAY: "Pokazuj" +STR_IMAGES_PLACEHOLDER: "Ramki" +STR_IMAGES_SUPPRESS: "Pomijaj" STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania" STR_ORIENTATION: "Układ czytania" STR_FRONT_BTN_LAYOUT: "Układ przednich przycisków" @@ -297,6 +301,7 @@ STR_GO_TO_PERCENT: "Idź do %" STR_GO_HOME_BUTTON: "Wróć do głównego ekranu" STR_SYNC_PROGRESS: "Postęp synchronizacji" STR_DELETE_CACHE: "Usuń pamięć podręczną książek" +STR_DELETE: "Usuń" STR_DISPLAY_QR: "Pokaż stronę jako kod QR" STR_CHAPTER_PREFIX: "Rozdział: " STR_PAGES_SEPARATOR: " stron | " diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 65c39cf1..3d0bbb49 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -297,7 +297,9 @@ STR_GO_TO_PERCENT: "Săriţi la %" STR_GO_HOME_BUTTON: "Acasă" STR_SYNC_PROGRESS: "Progres sincronizare" STR_DELETE_CACHE: "Ştergere cache cărţi" +STR_DELETE: "Ştergeți" STR_DISPLAY_QR: "Afișați pagina ca cod QR" +STR_CHAPTER_PREFIX: "Capitol: " STR_PAGES_SEPARATOR: " pagini | " STR_BOOK_PREFIX: "Carte: " STR_KBD_SHIFT: "shift" @@ -329,4 +331,9 @@ STR_UPLOAD: "Încărcare" STR_BOOK_S_STYLE: "Stilul cărţii" STR_EMBEDDED_STYLE: "Stil încorporat" STR_OPDS_SERVER_URL: "URL server OPDS" +STR_FOOTNOTES: "Note de subsol" +STR_NO_FOOTNOTES: "Nicio notă de subsol" +STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Captură ecran" +STR_AUTO_TURN_ENABLED: "Răsfoire automată: " +STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 45a7d180..c31fd4a3 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -63,7 +63,7 @@ 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_TEXT: "Skriv text" -STR_TO_PREFIX: "till" +STR_TO_PREFIX: "till " STR_CALIBRE_DISCOVERING: "Söker Calibre…" STR_CALIBRE_CONNECTING_TO: "Ansluter till" STR_CALIBRE_CONNECTED_TO: "Ansluten till" @@ -92,6 +92,10 @@ STR_STATUS_BAR: "Statusrad" STR_HIDE_BATTERY: "Dölj batteriprocent" STR_EXTRA_SPACING: "Extra paragrafmellanrum" STR_TEXT_AA: "Textkantutjämning" +STR_IMAGES: "Bilder" +STR_IMAGES_DISPLAY: "Visa" +STR_IMAGES_PLACEHOLDER: "Platshållare" +STR_IMAGES_SUPPRESS: "Dölj" STR_SHORT_PWR_BTN: "Kort strömknappsklick" STR_ORIENTATION: "Läsrikting" STR_FRONT_BTN_LAYOUT: "Frontknappslayout" @@ -226,6 +230,7 @@ STR_EXIT: "« Avsluta" STR_HOME: "« Hem" STR_SAVE: "« Spara" STR_SELECT: "Välj " +STR_SELECTED: "Vald" STR_TOGGLE: "Växla" STR_CONFIRM: "Bekräfta" STR_CANCEL: "Avbryt" @@ -235,6 +240,8 @@ STR_DOWNLOAD: "Ladda ner" STR_RETRY: "Försök igen" STR_YES: "Ja" STR_NO: "Nej" +STR_SHOW: "Visa" +STR_HIDE: "Dölj" STR_STATE_ON: "PÅ" STR_STATE_OFF: "AV" STR_NOT_SET: "Inte inställd" @@ -247,6 +254,21 @@ STR_CAPS_OFF: "versaler" STR_OK_BUTTON: "Okej" STR_SLEEP_COVER_FILTER: "Viloskärmens omslagsfilter" STR_FILTER_CONTRAST: "Kontrast" +STR_CUSTOMISE_STATUS_BAR: "Anpassa statusfält" +STR_CHAPTER_PAGE_COUNT: "Antal sidor i kapitel" +STR_BOOK_PROGRESS_PERCENTAGE: "Procentuellt bokframsteg" +STR_PROGRESS_BAR: "Framstegsindikator" +STR_PROGRESS_BAR_THICKNESS: "Tjocklek på framstegsindikator" +STR_PROGRESS_BAR_THIN: "Tunn" +STR_PROGRESS_BAR_MEDIUM: "Mellan" +STR_PROGRESS_BAR_THICK: "Tjock" +STR_BOOK: "Bok" +STR_CHAPTER: "Kapitel" +STR_EXAMPLE_CHAPTER: "Kapitel 21" +STR_EXAMPLE_BOOK: "Boktitel" +STR_PREVIEW: "Förhandsgranskning" +STR_TITLE: "Titel" +STR_BATTERY: "Batteri" STR_UI_THEME: "Användargränssnittstema" STR_THEME_CLASSIC: "Klassisk" STR_THEME_LYRA: "Lyra" @@ -269,17 +291,18 @@ STR_DEFAULT_VALUE: "Standard" STR_REMAP_PROMPT: "Tryck en frontknapp för var funktion" STR_UNASSIGNED: "Otilldelad" STR_ALREADY_ASSIGNED: "Redan tilldelad" -STR_REMAP_RESET_HINT: "Översta sidoknapp: Återställ standardlayout" -STR_REMAP_CANCEL_HINT: "Nedre sidoknapp: Avbryt tilldelning" -STR_HW_BACK_LABEL: "Bak (Första knapp)" -STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knapp)" -STR_HW_LEFT_LABEL: "Vänster (Tredje knapp)" -STR_HW_RIGHT_LABEL: "Höger (Fjärde knapp)" +STR_REMAP_RESET_HINT: "Översta sidoknappen: Återställ standardlayout" +STR_REMAP_CANCEL_HINT: "Nedre sidoknappen: Avbryt tilldelning" +STR_HW_BACK_LABEL: "Bak (Första knappen)" +STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knappen)" +STR_HW_LEFT_LABEL: "Vänster (Tredje knappen)" +STR_HW_RIGHT_LABEL: "Höger (Fjärde knappen)" STR_GO_TO_PERCENT: "Gå till %" STR_GO_HOME_BUTTON: "Gå Hem" STR_SYNC_PROGRESS: "Synkroniseringsframsteg" STR_DELETE_CACHE: "Radera bokcache" STR_DELETE: "Radera" +STR_DISPLAY_QR: "Visa sida som QR-kod" STR_CHAPTER_PREFIX: "Kapitel:" STR_PAGES_SEPARATOR: " sidor | " STR_BOOK_PREFIX: "Bok:" @@ -312,4 +335,9 @@ STR_UPLOAD: "Uppladdning" STR_BOOK_S_STYLE: "Bokstil" STR_EMBEDDED_STYLE: "Inbäddad stil" STR_OPDS_SERVER_URL: "OPDS-serveradress" +STR_FOOTNOTES: "Fotnoter" +STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan" +STR_LINK: "[länk]" STR_SCREENSHOT_BUTTON: "Ta en skärmdump" +STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " +STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml new file mode 100644 index 00000000..8a9eec8a --- /dev/null +++ b/lib/I18n/translations/turkish.yaml @@ -0,0 +1,344 @@ +_language_name: "Türkçe" +_language_code: "TR" +_order: "17" + +STR_CROSSPOINT: "CrossPoint" +STR_BOOTING: "BAŞLATILIYOR" +STR_SLEEPING: "UYKU MODU" +STR_ENTERING_SLEEP: "Uyku moduna geçiliyor" +STR_BROWSE_FILES: "Dosyalara Göz At" +STR_FILE_TRANSFER: "Dosya Transferi" +STR_SETTINGS_TITLE: "Ayarlar" +STR_CALIBRE_LIBRARY: "Calibre Kütüphanesi" +STR_CONTINUE_READING: "Okumaya Devam Et" +STR_NO_OPEN_BOOK: "Açık kitap yok" +STR_START_READING: "Aşağıdan okumaya başlayın" +STR_BOOKS: "Kitaplar" +STR_SELECT_CHAPTER: "Bölüm Seç" +STR_NO_CHAPTERS: "Bölüm yok" +STR_END_OF_BOOK: "Kitabın sonu" +STR_EMPTY_CHAPTER: "Boş bölüm" +STR_INDEXING: "Endeksleniyor" +STR_MEMORY_ERROR: "Bellek hatası" +STR_PAGE_LOAD_ERROR: "Sayfa yükleme hatası" +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_LOAD_XTC_FAILED: "XTC yüklenemedi" +STR_LOAD_TXT_FAILED: "TXT yüklenemedi" +STR_LOAD_EPUB_FAILED: "EPUB yüklenemedi" +STR_SD_CARD_ERROR: "SD card hatası" +STR_WIFI_NETWORKS: "WiFi Ağları" +STR_NO_NETWORKS: "Ağ bulunamadı" +STR_NETWORKS_FOUND: "%zu ağ bulundu" +STR_SCANNING: "Tarıyor..." +STR_CONNECTING: "Bağlanıyor..." +STR_CONNECTED: "Bağlandı!" +STR_CONNECTION_FAILED: "Bağlantı Başarısız" +STR_CONNECTION_TIMEOUT: "Bağlantı zaman aşımı" +STR_FORGET_NETWORK: "Ağı Unut?" +STR_SAVE_PASSWORD: "Şifre kaydedilsin mi?" +STR_REMOVE_PASSWORD: "Kayıtlı şifre silinsin mi?" +STR_PRESS_OK_SCAN: "Tekrar taramak için OK'e basın" +STR_PRESS_ANY_CONTINUE: "Devam etmek için bir tuşa basın" +STR_SELECT_HINT: "SOL/SAĞ: Seç | OK: Onayla" +STR_HOW_CONNECT: "Nasıl bağlanmak istersiniz?" +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_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_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:" +STR_CALIBRE_WIRELESS: "Calibre Kablosuz" +STR_CALIBRE_WEB_URL: "Calibre Web Adresi" +STR_CONNECT_WIRELESS: "Kablosuz Cihaz Olarak Bağlan" +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_ENTER_TEXT: "Metin Girin" +STR_TO_PREFIX: "Ağ: " +STR_CALIBRE_DISCOVERING: "Calibre aranıyor..." +STR_CALIBRE_CONNECTING_TO: "Bağlanılıyor: " +STR_CALIBRE_CONNECTED_TO: "Bağlandı: " +STR_CALIBRE_WAITING_COMMANDS: "Komutlar bekleniyor..." +STR_CONNECTION_FAILED_RETRYING: "(Bağlantı başarısız, tekrar deneniyor)" +STR_CALIBRE_DISCONNECTED: "Calibre bağlantısı kesildi" +STR_CALIBRE_WAITING_TRANSFER: "Transfer bekleniyor..." +STR_CALIBRE_TRANSFER_HINT: "Transfer başarısız olursa, Calibre\nSmartDevice eklenti ayarlarından\n'Ignore free space'i etkinleştirin." +STR_CALIBRE_RECEIVING: "Alınıyor: " +STR_CALIBRE_RECEIVED: "Alındı: " +STR_CALIBRE_WAITING_MORE: "Devamı bekleniyor..." +STR_CALIBRE_FAILED_CREATE_FILE: "Dosya oluşturulamadı" +STR_CALIBRE_PASSWORD_REQUIRED: "Şifre gerekli" +STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer kesintiye uğradı" +STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader eklentisini kurun" +STR_CALIBRE_INSTRUCTION_2: "2) Aynı WiFi 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" +STR_CAT_READER: "Okuyucu" +STR_CAT_CONTROLS: "Kontroller" +STR_CAT_SYSTEM: "Sistem" +STR_SLEEP_SCREEN: "Uyku Ekranı" +STR_SLEEP_COVER_MODE: "Uyku Ekranı Kapak Modu" +STR_STATUS_BAR: "Durum Çubuğu" +STR_HIDE_BATTERY: "Pil Yüzdesini Gizle" +STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu" +STR_TEXT_AA: "Metin Yumuşatma (AA)" +STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması" +STR_ORIENTATION: "Okuma Yönü" +STR_FRONT_BTN_LAYOUT: "Ön Tuş Dizilimi" +STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" +STR_LONG_PRESS_SKIP: "Uzun Basışla Bölüm Atla" +STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" +STR_EXT_READER_FONT: "Harici Okuyucu Yazı Tipi" +STR_EXT_CHINESE_FONT: "Okuyucu Yazı Tipi" +STR_EXT_UI_FONT: "Arayüz Yazı Tipi" +STR_FONT_SIZE: "Arayüz Yazı Boyutu" +STR_LINE_SPACING: "Okuyucu Satır Aralığı" +STR_ASCII_LETTER_SPACING: "ASCII Harf Aralığı" +STR_ASCII_DIGIT_SPACING: "ASCII Rakam Aralığı" +STR_CJK_SPACING: "CJK Aralığı" +STR_COLOR_MODE: "Renk Modu" +STR_SCREEN_MARGIN: "Okuyucu Ekran Kenar Boşluğu" +STR_PARA_ALIGNMENT: "Okuyucu Paragraf Hizalaması" +STR_HYPHENATION: "Hecelerden Ayırma" +STR_TIME_TO_SLEEP: "Uykuya Geçme Süresi" +STR_REFRESH_FREQ: "Yenileme Sıklığı" +STR_CALIBRE_SETTINGS: "Calibre Ayarları" +STR_KOREADER_SYNC: "KOReader Senkronizasyonu" +STR_CHECK_UPDATES: "Güncellemeleri denetle" +STR_LANGUAGE: "Dil" +STR_SELECT_WALLPAPER: "Duvar Kağıdı Seç" +STR_CLEAR_READING_CACHE: "Okuma Önbelleğini Temizle" +STR_CALIBRE: "Calibre" +STR_USERNAME: "Kullanıcı Adı" +STR_PASSWORD: "Şifre" +STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi" +STR_DOCUMENT_MATCHING: "Belge Eşleştirme" +STR_AUTHENTICATE: "Kimlik Doğrula" +STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı" +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_AUTHENTICATING: "Kimlik doğrulanıyor..." +STR_AUTH_SUCCESS: "Kimlik doğrulama başarılı!" +STR_KOREADER_AUTH: "KOReader Doğrulaması" +STR_SYNC_READY: "KOReader senkronizasyonu hazır" +STR_AUTH_FAILED: "Kimlik Doğrulama Başarısız" +STR_DONE: "Tamamlandı" +STR_CLEAR_CACHE_WARNING_1: "Bu işlem tüm önbelleğe alınmış verileri siler." +STR_CLEAR_CACHE_WARNING_2: "Tüm okuma ilerlemesi kaybolacak!" +STR_CLEAR_CACHE_WARNING_3: "Kitapların tekrar açıldığında yeniden" +STR_CLEAR_CACHE_WARNING_4: "endekslenmesi gerekecek." +STR_CLEARING_CACHE: "Önbellek temizleniyor..." +STR_CACHE_CLEARED: "Önbellek Temizlendi" +STR_ITEMS_REMOVED: "öğe kaldırıldı" +STR_FAILED_LOWER: "başarısız" +STR_CLEAR_CACHE_FAILED: "Önbellek temizlenemedi" +STR_CHECK_SERIAL_OUTPUT: "Detaylar için seri çıktıya bakın" +STR_DARK: "Koyu" +STR_LIGHT: "Açık" +STR_CUSTOM: "Özel" +STR_COVER: "Kapak" +STR_NONE_OPT: "Yok" +STR_FIT: "Sığdır" +STR_CROP: "Kırp" +STR_NO_PROGRESS: "İlerleme Yok" +STR_FULL_OPT: "Tam" +STR_NEVER: "Asla" +STR_IN_READER: "Okuyucuda" +STR_ALWAYS: "Her Zaman" +STR_IGNORE: "Yoksay" +STR_SLEEP: "Uyku" +STR_PAGE_TURN: "Sayfa Çevirme" +STR_PORTRAIT: "Dikey" +STR_LANDSCAPE_CW: "Yatay (Saat Yönü)" +STR_INVERTED: "Ters" +STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)" +STR_FRONT_LAYOUT_BCLR: "Geri, Onayla, Sol, Sağ" +STR_FRONT_LAYOUT_LRBC: "Sol, Sağ, Geri, Onayla" +STR_FRONT_LAYOUT_LBCR: "Sol, Geri, Onayla, Sağ" +STR_PREV_NEXT: "Önceki/Sonraki" +STR_NEXT_PREV: "Sonraki/Önceki" +STR_BOOKERLY: "Bookerly" +STR_NOTO_SANS: "Noto Sans" +STR_OPEN_DYSLEXIC: "Open Dyslexic" +STR_SMALL: "Küçük" +STR_MEDIUM: "Orta" +STR_LARGE: "Büyük" +STR_X_LARGE: "Çok Büyük" +STR_TIGHT: "Dar" +STR_NORMAL: "Normal" +STR_WIDE: "Geniş" +STR_JUSTIFY: "İki Yana Yasla" +STR_ALIGN_LEFT: "Sola Yasla" +STR_CENTER: "Ortala" +STR_ALIGN_RIGHT: "Sağa Yasla" +STR_MIN_1: "1 dak" +STR_MIN_5: "5 dak" +STR_MIN_10: "10 dak" +STR_MIN_15: "15 dak" +STR_MIN_30: "30 dak" +STR_PAGES_1: "1 sayfa" +STR_PAGES_5: "5 sayfa" +STR_PAGES_10: "10 sayfa" +STR_PAGES_15: "15 sayfa" +STR_PAGES_30: "30 sayfa" +STR_UPDATE: "Güncelle" +STR_CHECKING_UPDATE: "Güncelleme denetleniyor..." +STR_NEW_UPDATE: "Yeni güncelleme mevcut!" +STR_CURRENT_VERSION: "Mevcut Sürüm: " +STR_NEW_VERSION: "Yeni Sürüm: " +STR_UPDATING: "Güncelleniyor..." +STR_NO_UPDATE: "Güncelleme yok" +STR_UPDATE_FAILED: "Güncelleme başarısız" +STR_UPDATE_COMPLETE: "Güncelleme tamamlandı" +STR_POWER_ON_HINT: "Açmak için güç tuşuna basılı tutun" +STR_EXTERNAL_FONT: "Harici Yazı Tipi" +STR_BUILTIN_DISABLED: "Yerleşik (Devre Dışı)" +STR_NO_ENTRIES: "Girdi bulunamadı" +STR_DOWNLOADING: "İndiriliyor..." +STR_DOWNLOAD_FAILED: "İndirme başarısız" +STR_ERROR_MSG: "Hata:" +STR_UNNAMED: "İsimsiz" +STR_NO_SERVER_URL: "Sunucu adresi ayarlanmamış" +STR_FETCH_FEED_FAILED: "Akış alınamadı" +STR_PARSE_FEED_FAILED: "Akış ayrıştırılamadı" +STR_NETWORK_PREFIX: "Ağ: " +STR_IP_ADDRESS_PREFIX: "IP Adresi: " +STR_SCAN_QR_WIFI_HINT: "veya WiFi'ye bağlanmak için QR kodu tarayın." +STR_ERROR_GENERAL_FAILURE: "Hata: Genel hata" +STR_ERROR_NETWORK_NOT_FOUND: "Hata: Ağ bulunamadı" +STR_ERROR_CONNECTION_TIMEOUT: "Hata: Bağlantı zaman aşımı" +STR_SD_CARD: "SD kart" +STR_BACK: "« Geri" +STR_EXIT: "« Çıkış" +STR_HOME: "« Ana Sayfa" +STR_SAVE: "« Kaydet" +STR_SELECT: "Seç" +STR_TOGGLE: "Değiştir" +STR_CONFIRM: "Onayla" +STR_CANCEL: "İptal" +STR_CONNECT: "Bağlan" +STR_OPEN: "Aç" +STR_DOWNLOAD: "İndir" +STR_RETRY: "Tekrar Dene" +STR_YES: "Evet" +STR_NO: "Hayır" +STR_STATE_ON: "AÇIK" +STR_STATE_OFF: "KAPALI" +STR_NOT_SET: "Ayarlanmadı" +STR_DIR_LEFT: "Sol" +STR_DIR_RIGHT: "Sağ" +STR_DIR_UP: "Yukarı" +STR_DIR_DOWN: "Aşağı" +STR_CAPS_ON: "BÜYÜK" +STR_CAPS_OFF: "küçük" +STR_OK_BUTTON: "Tamam" +STR_SLEEP_COVER_FILTER: "Uyku Ekranı Kapak Filtresi" +STR_FILTER_CONTRAST: "Kontrast" +STR_UI_THEME: "Arayüz Teması" +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_REMAP_FRONT_BUTTONS: "Ön Tuşları Yeniden Ata" +STR_OPDS_BROWSER: "OPDS Tarayıcı" +STR_COVER_CUSTOM: "Kapak + Özel" +STR_RECENTS: "Son Okunanlar" +STR_MENU_RECENT_BOOKS: "Son Kitaplar" +STR_NO_RECENT_BOOKS: "Son okunan kitap yok" +STR_CALIBRE_DESC: "Calibre kablosuz cihaz transferini kullan" +STR_FORGET_AND_REMOVE: "Ağı unut ve kayıtlı şifreyi sil?" +STR_FORGET_BUTTON: "Unut" +STR_CALIBRE_STARTING: "Calibre Başlatılıyor..." +STR_CALIBRE_SETUP: "Kurulum" +STR_CALIBRE_STATUS: "Durum" +STR_CLEAR_BUTTON: "Temizle" +STR_DEFAULT_VALUE: "Varsayılan" +STR_REMAP_PROMPT: "Her rol için bir ön tuşa basın" +STR_UNASSIGNED: "Atanmamış" +STR_ALREADY_ASSIGNED: "Zaten atanmış" +STR_REMAP_RESET_HINT: "Yan tuş Yukarı: Varsayılan dizilime dön" +STR_REMAP_CANCEL_HINT: "Yan tuş Aşağı: Atamayı iptal et" +STR_HW_BACK_LABEL: "Geri (1. tuş)" +STR_HW_CONFIRM_LABEL: "Onayla (2. tuş)" +STR_HW_LEFT_LABEL: "Sol (3. tuş)" +STR_HW_RIGHT_LABEL: "Sağ (4. tuş)" +STR_GO_TO_PERCENT: "%'ye git" +STR_GO_HOME_BUTTON: "Ana Sayfaya Git" +STR_SYNC_PROGRESS: "Okuma İlerlemesini Senkronize Et" +STR_DELETE_CACHE: "Kitap Önbelleğini Sil" +STR_CHAPTER_PREFIX: "Bölüm: " +STR_PAGES_SEPARATOR: " sayfa | " +STR_BOOK_PREFIX: "Kitap: " +STR_KBD_SHIFT: "shift" +STR_KBD_SHIFT_CAPS: "ÜST" +STR_KBD_LOCK: "KİLİT" +STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin" +STR_PERCENT_STEP_HINT: "Sol/Sağ: %1 Yukarı/Aşağı: %10" +STR_SYNCING_TIME: "Zaman senkronize ediliyor..." +STR_CALC_HASH: "Belge özeti hesaplanıyor..." +STR_HASH_FAILED: "Belge özeti hesaplanamadı" +STR_FETCH_PROGRESS: "Uzak ilerleme alınıyor..." +STR_UPLOAD_PROGRESS: "İlerleme yükleniyor..." +STR_NO_CREDENTIALS_MSG: "Kimlik bilgisi ayarlanmamış" +STR_KOREADER_SETUP_HINT: "Ayarlar'da KOReader hesabını kurun" +STR_PROGRESS_FOUND: "İlerleme bulundu!" +STR_REMOTE_LABEL: "Uzak:" +STR_LOCAL_LABEL: "Yerel:" +STR_PAGE_OVERALL_FORMAT: "Sayfa %d, genel %.2f%%" +STR_PAGE_TOTAL_OVERALL_FORMAT: "Sayfa %d/%d, genel %.2f%%" +STR_DEVICE_FROM_FORMAT: " Şuradan: %s" +STR_APPLY_REMOTE: "Uzak ilerlemeyi uygula" +STR_UPLOAD_LOCAL: "Yerel ilerlemeyi yükle" +STR_NO_REMOTE_MSG: "Uzak ilerleme bulunamadı" +STR_UPLOAD_PROMPT: "Mevcut konumu yükle?" +STR_UPLOAD_SUCCESS: "İlerleme yüklendi!" +STR_SYNC_FAILED_MSG: "Senkronizasyon başarısız" +STR_SECTION_PREFIX: "Bölüm " +STR_UPLOAD: "Yükle" +STR_BOOK_S_STYLE: "Kitabın Stili" +STR_EMBEDDED_STYLE: "Gömülü Stil" +STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi" + +STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: " +STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)" +STR_BATTERY: "Pil" +STR_BOOK: "Kitap" +STR_BOOK_PROGRESS_PERCENTAGE: "Kitap İlerleme Yüzdesi" +STR_CHAPTER: "Bölüm" +STR_CHAPTER_PAGE_COUNT: "Bölüm Sayfa Sayısı" +STR_CUSTOMISE_STATUS_BAR: "Durum Çubuğunu Özelleştir" +STR_DELETE: "Sil" +STR_DISPLAY_QR: "Sayfayı QR olarak göster" +STR_EXAMPLE_BOOK: "Kitap Başlığı" +STR_EXAMPLE_CHAPTER: "Bölüm 21" +STR_FOOTNOTES: "Dipnotlar" +STR_HIDE: "Gizle" +STR_IMAGES: "Görseller" +STR_IMAGES_DISPLAY: "Göster" +STR_IMAGES_PLACEHOLDER: "Yer Tutucu" +STR_IMAGES_SUPPRESS: "Bastır" +STR_LINK: "[bağlantı]" +STR_NO_FILES_FOUND: "Dosya bulunamadı" +STR_NO_FOOTNOTES: "Bu sayfada dipnot yok" +STR_PREVIEW: "Önizleme" +STR_PROGRESS_BAR: "İlerleme Çubuğu" +STR_PROGRESS_BAR_MEDIUM: "Orta" +STR_PROGRESS_BAR_THICK: "Kalın" +STR_PROGRESS_BAR_THICKNESS: "İlerleme Çubuğu Kalınlığı" +STR_PROGRESS_BAR_THIN: "İnce" +STR_SCREENSHOT_BUTTON: "Ekran görüntüsü al" +STR_SELECTED: "Seçili" +STR_SHOW: "Göster" +STR_TITLE: "Başlık" diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index 7d72c920..d7f83606 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -1,15 +1,28 @@ #include "Logging.h" +#include + +#define MAX_ENTRY_LEN 256 +#define MAX_LOG_LINES 16 + +// Simple ring buffer log, useful for error reporting when we encounter a crash +RTC_NOINIT_ATTR char logMessages[MAX_LOG_LINES][MAX_ENTRY_LEN]; +RTC_NOINIT_ATTR size_t logHead = 0; + +void addToLogRingBuffer(const char* message) { + // Add the message to the ring buffer, overwriting old messages if necessary + strncpy(logMessages[logHead], message, MAX_ENTRY_LEN - 1); + logMessages[logHead][MAX_ENTRY_LEN - 1] = '\0'; + logHead = (logHead + 1) % MAX_LOG_LINES; +} + // Since logging can take a large amount of flash, we want to make the format string as short as possible. // This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to // provide the format string for the message itself. void logPrintf(const char* level, const char* origin, const char* format, ...) { - if (!logSerial) { - return; // Serial not initialized, skip logging - } va_list args; va_start(args, format); - char buf[256]; + char buf[MAX_ENTRY_LEN]; char* c = buf; // add the timestamp { @@ -43,5 +56,26 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) { // add the user message vsnprintf(c, sizeof(buf) - (c - buf), format, args); va_end(args); - logSerial.print(buf); + if (logSerial) { + logSerial.print(buf); + } + addToLogRingBuffer(buf); +} + +std::string getLastLogs() { + std::string output; + for (size_t i = 0; i < MAX_LOG_LINES; i++) { + size_t idx = (logHead + i) % MAX_LOG_LINES; + if (logMessages[idx][0] != '\0') { + output += logMessages[idx]; + } + } + return output; +} + +void clearLastLogs() { + for (size_t i = 0; i < MAX_LOG_LINES; i++) { + logMessages[i][0] = '\0'; + } + logHead = 0; } diff --git a/lib/Logging/Logging.h b/lib/Logging/Logging.h index cbc7f86c..83cabdc1 100644 --- a/lib/Logging/Logging.h +++ b/lib/Logging/Logging.h @@ -2,6 +2,8 @@ #include +#include + /* Define ENABLE_SERIAL_LOG to enable logging Can be set in platformio.ini build_flags or as a compile definition @@ -53,6 +55,9 @@ void logPrintf(const char* level, const char* origin, const char* format, ...); #define LOG_INF(origin, format, ...) #endif +std::string getLastLogs(); +void clearLastLogs(); + class MySerialImpl : public Print { public: void begin(unsigned long baud) { logSerial.begin(baud); } diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index bb20a2bc..83ef123c 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -41,7 +41,7 @@ std::string Txt::getTitle() const { std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath; // Remove .txt extension - if (filename.length() >= 4 && filename.substr(filename.length() - 4) == ".txt") { + if (FsHelpers::hasTxtExtension(filename)) { filename = filename.substr(0, filename.length() - 4); } @@ -112,14 +112,7 @@ bool Txt::generateCoverBmp() const { // Setup cache directory setupCacheDir(); - // Get file extension - const size_t len = coverImagePath.length(); - const bool isJpg = - (len >= 4 && (coverImagePath.substr(len - 4) == ".jpg" || coverImagePath.substr(len - 4) == ".JPG")) || - (len >= 5 && (coverImagePath.substr(len - 5) == ".jpeg" || coverImagePath.substr(len - 5) == ".JPEG")); - const bool isBmp = len >= 4 && (coverImagePath.substr(len - 4) == ".bmp" || coverImagePath.substr(len - 4) == ".BMP"); - - if (isBmp) { + if (FsHelpers::hasBmpExtension(coverImagePath)) { // Copy BMP file to cache LOG_DBG("TXT", "Copying BMP cover image to cache"); FsFile src, dst; @@ -139,9 +132,7 @@ bool Txt::generateCoverBmp() const { dst.close(); LOG_DBG("TXT", "Copied BMP cover to cache"); return true; - } - - if (isJpg) { + } else if (FsHelpers::hasJpgExtension(coverImagePath)) { // Convert JPG/JPEG to BMP (same approach as Epub) LOG_DBG("TXT", "Generating BMP from JPG cover image"); FsFile coverJpg, coverBmp; diff --git a/lib/Xtc/Xtc/XtcTypes.h b/lib/Xtc/Xtc/XtcTypes.h index 773c7ad5..5cee691d 100644 --- a/lib/Xtc/Xtc/XtcTypes.h +++ b/lib/Xtc/Xtc/XtcTypes.h @@ -144,14 +144,4 @@ inline const char* errorToString(XtcError err) { } } -/** - * Check if filename has XTC/XTCH extension - */ -inline bool isXtcExtension(const char* filename) { - if (!filename) return false; - const char* ext = strrchr(filename, '.'); - if (!ext) return false; - return (strcasecmp(ext, ".xtc") == 0 || strcasecmp(ext, ".xtch") == 0); -} - } // namespace xtc diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 2a2af164..a740e14d 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -71,10 +71,15 @@ bool ZipFile::loadAllFileStatSlims() { file.read(&k, 2); file.seekCur(8); file.read(&fileStat.localHeaderOffset, 4); - file.read(itemName, nameLen); - itemName[nameLen] = '\0'; - fileStatSlimCache.emplace(itemName, fileStat); + if (nameLen < sizeof(itemName)) { + file.read(itemName, nameLen); + itemName[nameLen] = '\0'; + fileStatSlimCache.emplace(itemName, fileStat); + } else { + // Skip over oversized entry names to avoid writing past fixed buffer. + file.seekCur(nameLen); + } // Skip the rest of this entry (extra field + comment) file.seekCur(m + k); diff --git a/lib/hal/HalSystem.cpp b/lib/hal/HalSystem.cpp new file mode 100644 index 00000000..606bf80d --- /dev/null +++ b/lib/hal/HalSystem.cpp @@ -0,0 +1,140 @@ +#include "HalSystem.h" + +#include + +#include "Arduino.h" +#include "HalStorage.h" +#include "Logging.h" +#include "esp_debug_helpers.h" +#include "esp_private/esp_cpu_internal.h" +#include "esp_private/esp_system_attr.h" +#include "esp_private/panic_internal.h" + +#define MAX_PANIC_STACK_DEPTH 32 + +RTC_NOINIT_ATTR char panicMessage[256]; +RTC_NOINIT_ATTR HalSystem::StackFrame panicStack[MAX_PANIC_STACK_DEPTH]; + +extern "C" { + +void __real_panic_abort(const char* message); +void __real_panic_print_backtrace(const void* frame, int core); + +static DRAM_ATTR const char PANIC_REASON_UNKNOWN[] = "(unknown panic reason)"; +void IRAM_ATTR __wrap_panic_abort(const char* message) { + if (!message) message = PANIC_REASON_UNKNOWN; + // IRAM-safe bounded copy (strncpy is not IRAM-safe in panic context) + int i = 0; + for (; i < (int)sizeof(panicMessage) - 1 && message[i]; i++) { + panicMessage[i] = message[i]; + } + panicMessage[i] = '\0'; + + __real_panic_abort(message); +} + +void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) { + if (!frame) { + __real_panic_print_backtrace(frame, core); + return; + } + for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) { + panicStack[i].sp = 0; + } + + // Copied from components/esp_system/port/arch/riscv/panic_arch.c + uint32_t sp = (uint32_t)((RvExcFrame*)frame)->sp; + const int per_line = 8; + int depth = 0; + for (int x = 0; x < 1024; x += per_line * sizeof(uint32_t)) { + uint32_t* spp = (uint32_t*)(sp + x); + // panic_print_hex(sp + x); + // panic_print_str(": "); + panicStack[depth].sp = sp + x; + for (int y = 0; y < per_line; y++) { + // panic_print_str("0x"); + // panic_print_hex(spp[y]); + // panic_print_str(y == per_line - 1 ? "\r\n" : " "); + panicStack[depth].spp[y] = spp[y]; + } + + depth++; + if (depth >= MAX_PANIC_STACK_DEPTH) { + break; + } + } + + __real_panic_print_backtrace(frame, core); +} +} + +namespace HalSystem { + +void begin() { + // This is mostly for the first boot, we need to initialize the panic info and logs to empty state + // If we reboot from a panic state, we want to keep the panic info until we successfully dump it to the SD card, use + // `clearPanic()` to clear it after dumping + if (!isRebootFromPanic()) { + clearPanic(); + } +} + +void checkPanic() { + if (isRebootFromPanic()) { + auto panicInfo = getPanicInfo(true); + auto file = Storage.open("/crash_report.txt", O_WRITE | O_CREAT | O_TRUNC); + if (file) { + file.write(panicInfo.c_str(), panicInfo.size()); + file.close(); + LOG_INF("SYS", "Dumped panic info to SD card"); + } else { + LOG_ERR("SYS", "Failed to open crash_report.txt for writing"); + } + } +} + +void clearPanic() { + panicMessage[0] = '\0'; + for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) { + panicStack[i].sp = 0; + } + clearLastLogs(); +} + +std::string getPanicInfo(bool full) { + if (!full) { + return panicMessage; + } else { + std::string info; + + info += "CrossPoint version: " CROSSPOINT_VERSION; + info += "\n\nPanic reason: " + std::string(panicMessage); + info += "\n\nLast logs:\n" + getLastLogs(); + info += "\n\nStack memory:\n"; + + auto toHex = [](uint32_t value) { + char buffer[9]; + snprintf(buffer, sizeof(buffer), "%08X", value); + return std::string(buffer); + }; + for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) { + if (panicStack[i].sp == 0) { + break; + } + info += "0x" + toHex(panicStack[i].sp) + ": "; + for (size_t j = 0; j < 8; j++) { + info += "0x" + toHex(panicStack[i].spp[j]) + " "; + } + info += "\n"; + } + + return info; + } +} + +bool isRebootFromPanic() { + const auto resetReason = esp_reset_reason(); + return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP; +} + +} // namespace HalSystem diff --git a/lib/hal/HalSystem.h b/lib/hal/HalSystem.h new file mode 100644 index 00000000..e9df1b9d --- /dev/null +++ b/lib/hal/HalSystem.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace HalSystem { +struct StackFrame { + uint32_t sp; + uint32_t spp[8]; +}; + +void begin(); + +// Dump panic info to SD card if necessary +void checkPanic(); +void clearPanic(); + +std::string getPanicInfo(bool full = false); +bool isRebootFromPanic(); +} // namespace HalSystem diff --git a/platformio.ini b/platformio.ini index 691f1570..4ba1c750 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,6 +35,7 @@ build_flags = # Default is (320*4+1)*2=2562, we need more for larger images -DPNG_MAX_BUFFERED_PIXELS=16416 -Wno-bidi-chars + -Wl,--wrap=panic_print_backtrace,--wrap=panic_abort build_unflags = -std=gnu++11 @@ -77,7 +78,7 @@ build_flags = ${base.build_flags} -DCROSSPOINT_VERSION=\"${crosspoint.version}\" -DENABLE_SERIAL_LOG - -DLOG_LEVEL=0 ; Set log level to error for release builds + -DLOG_LEVEL=1 ; Set log level to info for release builds [env:gh_release_rc] extends = base diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index c9f42ab7..24ae40db 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -222,6 +222,7 @@ LANG_ABBREVIATIONS = { "עברית": "HE", "hebrew": "HE", "فارسی": "FA", "persian": "FA", "čeština": "CS", + "türkçe": "TR", "turkish": "TR", } @@ -492,7 +493,6 @@ def generate_strings_header( lines.append("") lines.append("} // namespace i18n_strings") - _write_file(output_path, lines) diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index e903aa8f..f5a2c048 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -1,6 +1,7 @@ #include "RecentBooksStore.h" #include +#include #include #include #include @@ -9,8 +10,6 @@ #include -#include "util/StringUtils.h" - namespace { constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 3; constexpr char RECENT_BOOKS_FILE_BIN[] = "/.crosspoint/recent.bin"; @@ -71,19 +70,17 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const { // If epub, try to load the metadata for title/author and cover. // Use buildIfMissing=false to avoid heavy epub loading on boot; getTitle()/getAuthor() may be // blank until the book is opened, and entries with missing title are omitted from recent list. - if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { + if (FsHelpers::hasEpubExtension(lastBookFileName)) { Epub epub(path, "/.crosspoint"); epub.load(false, true); return RecentBook{path, epub.getTitle(), epub.getAuthor(), epub.getThumbBmpPath()}; - } else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") || - StringUtils::checkFileExtension(lastBookFileName, ".xtc")) { + } else if (FsHelpers::hasXtcExtension(lastBookFileName)) { // Handle XTC file Xtc xtc(path, "/.crosspoint"); if (xtc.load()) { return RecentBook{path, xtc.getTitle(), xtc.getAuthor(), xtc.getThumbBmpPath()}; } - } else if (StringUtils::checkFileExtension(lastBookFileName, ".txt") || - StringUtils::checkFileExtension(lastBookFileName, ".md")) { + } else if (FsHelpers::hasTxtExtension(lastBookFileName) || FsHelpers::hasMarkdownExtension(lastBookFileName)) { return RecentBook{path, lastBookFileName, "", ""}; } return RecentBook{path, "", "", ""}; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index eea0211c..9c37b6e9 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -1,6 +1,7 @@ #include "SleepActivity.h" #include +#include #include #include #include @@ -18,7 +19,6 @@ #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" -#include "util/StringUtils.h" namespace { @@ -211,7 +211,7 @@ void SleepActivity::renderCustomSleepScreen() const { continue; } - if (filename.substr(filename.length() - 4) != ".bmp") { + if (!FsHelpers::hasBmpExtension(filename)) { LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name); file.close(); continue; @@ -378,8 +378,7 @@ void SleepActivity::renderCoverSleepScreen() const { bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP; // Check if the current book is XTC, TXT, or EPUB - if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") || - StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) { + if (FsHelpers::hasXtcExtension(APP_STATE.openEpubPath)) { // Handle XTC file Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastXtc.load()) { @@ -393,7 +392,7 @@ void SleepActivity::renderCoverSleepScreen() const { } coverBmpPath = lastXtc.getCoverBmpPath(); - } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) { + } else if (FsHelpers::hasTxtExtension(APP_STATE.openEpubPath)) { // Handle TXT file - looks for cover image in the same folder Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastTxt.load()) { @@ -407,7 +406,7 @@ void SleepActivity::renderCoverSleepScreen() const { } coverBmpPath = lastTxt.getCoverBmpPath(); - } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) { + } else if (FsHelpers::hasEpubExtension(APP_STATE.openEpubPath)) { // Handle EPUB file Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint"); // Skip loading css since we only need metadata here diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index 1e394d14..b8b910ba 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -1,6 +1,7 @@ #include "FileBrowserActivity.h" #include +#include #include #include #include @@ -11,7 +12,6 @@ #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" -#include "util/StringUtils.h" namespace { constexpr unsigned long GO_HOME_MS = 1000; @@ -91,10 +91,10 @@ void FileBrowserActivity::loadFiles() { if (file.isDirectory()) { files.emplace_back(std::string(name) + "/"); } else { - auto filename = std::string(name); - if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || - StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") || - StringUtils::checkFileExtension(filename, ".md") || StringUtils::checkFileExtension(filename, ".bmp")) { + std::string_view filename{name}; + if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) || + FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) || + FsHelpers::hasBmpExtension(filename)) { files.emplace_back(filename); } } @@ -120,7 +120,7 @@ void FileBrowserActivity::onExit() { void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) { // Only clear cache for .epub files - if (StringUtils::checkFileExtension(fullPath, ".epub")) { + if (FsHelpers::hasEpubExtension(fullPath)) { Epub(fullPath, "/.crosspoint").clearCache(); LOG_DBG("FileBrowser", "Cleared metadata cache for: %s", fullPath.c_str()); } diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index bfab4874..810cb50b 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -17,7 +18,6 @@ #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" -#include "util/StringUtils.h" int HomeActivity::getMenuItemCount() const { int count = 4; // File Browser, Recents, File transfer, Settings @@ -61,7 +61,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); if (!Storage.exists(coverPath.c_str())) { // If epub, try to load the metadata for title/author and cover - if (StringUtils::checkFileExtension(book.path, ".epub")) { + if (FsHelpers::hasEpubExtension(book.path)) { Epub epub(book.path, "/.crosspoint"); // Skip loading css since we only need metadata here epub.load(false, true); @@ -79,8 +79,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { } coverRendered = false; requestUpdate(); - } else if (StringUtils::checkFileExtension(book.path, ".xtch") || - StringUtils::checkFileExtension(book.path, ".xtc")) { + } else if (FsHelpers::hasXtcExtension(book.path)) { // Handle XTC file Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index e3e611fb..6ba8f8cf 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -10,7 +10,6 @@ #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" -#include "util/StringUtils.h" namespace { constexpr unsigned long GO_HOME_MS = 1000; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 020c6401..de5f7ce9 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -597,6 +597,16 @@ void EpubReaderActivity::render(RenderLock&& lock) { section->currentPage = nextPageNumber; } + if (!pendingAnchor.empty()) { + if (const auto page = section->getPageForAnchor(pendingAnchor)) { + section->currentPage = *page; + LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page); + } else { + LOG_DBG("ERS", "Anchor '%s' not found in section %d", pendingAnchor.c_str(), currentSpineIndex); + } + pendingAnchor.clear(); + } + // handles changes in reader settings and reset to approximate position based on cached progress if (cachedChapterTotalPageCount > 0) { // only goes to relative position if spine index matches cached value @@ -792,12 +802,18 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage); } + // Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1") + std::string anchor; + const auto hashPos = hrefStr.find('#'); + if (hashPos != std::string::npos && hashPos + 1 < hrefStr.size()) { + anchor = hrefStr.substr(hashPos + 1); + } + // Check for same-file anchor reference (#anchor only) bool sameFile = !hrefStr.empty() && hrefStr[0] == '#'; int targetSpineIndex; if (sameFile) { - // Same file — navigate to page 0 of current spine item targetSpineIndex = currentSpineIndex; } else { targetSpineIndex = epub->resolveHrefToSpineIndex(hrefStr); @@ -811,6 +827,7 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s { RenderLock lock(*this); + pendingAnchor = std::move(anchor); currentSpineIndex = targetSpineIndex; nextPageNumber = 0; section.reset(); diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 9acf3a60..ad150e9a 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -11,6 +11,9 @@ class EpubReaderActivity final : public Activity { std::unique_ptr
section = nullptr; int currentSpineIndex = 0; int nextPageNumber = 0; + // Set when navigating to a footnote href with a fragment (e.g. #note1). + // Cleared on the next render after the new section loads and resolves it to a page. + std::string pendingAnchor; int pagesUntilFullRefresh = 0; int cachedSpineIndex = 0; int cachedChapterTotalPageCount = 0; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index a7842a9e..2164a7f6 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -1,5 +1,6 @@ #include "ReaderActivity.h" +#include #include #include "CrossPointSettings.h" @@ -11,7 +12,6 @@ #include "XtcReaderActivity.h" #include "activities/util/BmpViewerActivity.h" #include "activities/util/FullScreenMessageActivity.h" -#include "util/StringUtils.h" std::string ReaderActivity::extractFolderPath(const std::string& filePath) { const auto lastSlash = filePath.find_last_of('/'); @@ -21,16 +21,14 @@ std::string ReaderActivity::extractFolderPath(const std::string& filePath) { return filePath.substr(0, lastSlash); } -bool ReaderActivity::isXtcFile(const std::string& path) { - return StringUtils::checkFileExtension(path, ".xtc") || StringUtils::checkFileExtension(path, ".xtch"); -} +bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasXtcExtension(path); } bool ReaderActivity::isTxtFile(const std::string& path) { - return StringUtils::checkFileExtension(path, ".txt") || - StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader) + return FsHelpers::hasTxtExtension(path) || + FsHelpers::hasMarkdownExtension(path); // Treat .md as txt files (until we have a markdown reader) } -bool ReaderActivity::isBmpFile(const std::string& path) { return StringUtils::checkFileExtension(path, ".bmp"); } +bool ReaderActivity::isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); } std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { if (!Storage.exists(path.c_str())) { diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index e29db872..57e49484 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -1,5 +1,6 @@ #include "UITheme.h" +#include #include #include @@ -10,7 +11,6 @@ #include "components/themes/BaseTheme.h" #include "components/themes/lyra/Lyra3CoversTheme.h" #include "components/themes/lyra/LyraTheme.h" -#include "util/StringUtils.h" namespace { constexpr int SKIP_PAGE_MS = 700; @@ -74,18 +74,17 @@ std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight return coverBmpPath; } -UIIcon UITheme::getFileIcon(std::string filename) { +UIIcon UITheme::getFileIcon(const std::string& filename) { if (filename.back() == '/') { return Folder; } - if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || - StringUtils::checkFileExtension(filename, ".xtc")) { + if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename)) { return Book; } - if (StringUtils::checkFileExtension(filename, ".txt") || StringUtils::checkFileExtension(filename, ".md")) { + if (FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename)) { return Text; } - if (StringUtils::checkFileExtension(filename, ".bmp")) { + if (FsHelpers::hasBmpExtension(filename)) { return Image; } return File; diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 5befe279..daa1ec45 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -21,7 +21,7 @@ class UITheme { static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, bool hasSubtitle); static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); - static UIIcon getFileIcon(std::string filename); + static UIIcon getFileIcon(const std::string& filename); static int getStatusBarHeight(); static int getProgressBarHeight(); diff --git a/src/main.cpp b/src/main.cpp index b27eb52c..d629346f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -227,6 +228,7 @@ void setupDisplayAndFonts() { void setup() { t1 = millis(); + HalSystem::begin(); gpio.begin(); powerManager.begin(); @@ -249,6 +251,9 @@ void setup() { return; } + HalSystem::checkPanic(); + HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info + SETTINGS.loadFromFile(); I18N.loadSettings(); KOREADER_STORE.loadFromFile(); diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 5578c59e..ede4aa39 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -16,7 +16,6 @@ #include "html/FilesPageHtml.generated.h" #include "html/HomePageHtml.generated.h" #include "html/SettingsPageHtml.generated.h" -#include "util/StringUtils.h" namespace { // Folders/files to hide from the web interface file browser @@ -44,7 +43,7 @@ unsigned long wsLastCompleteAt = 0; // Helper function to clear epub cache after upload void clearEpubCacheIfNeeded(const String& filePath) { // Only clear cache for .epub files - if (StringUtils::checkFileExtension(filePath, ".epub")) { + if (FsHelpers::hasEpubExtension(filePath)) { Epub(filePath.c_str(), "/.crosspoint").clearCache(); LOG_DBG("WEB", "Cleared epub cache for: %s", filePath.c_str()); } @@ -391,11 +390,7 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function #include -#include "util/StringUtils.h" - namespace { const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"}; constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); @@ -801,28 +799,26 @@ bool WebDAVHandler::getOverwrite(WebServer& s) const { } void WebDAVHandler::clearEpubCacheIfNeeded(const String& path) const { - if (StringUtils::checkFileExtension(path, ".epub")) { + 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 (StringUtils::checkFileExtension(path, ".epub")) return "application/epub+zip"; - if (StringUtils::checkFileExtension(path, ".pdf")) return "application/pdf"; - if (StringUtils::checkFileExtension(path, ".txt")) return "text/plain"; - if (StringUtils::checkFileExtension(path, ".html") || StringUtils::checkFileExtension(path, ".htm")) - return "text/html"; - if (StringUtils::checkFileExtension(path, ".css")) return "text/css"; - if (StringUtils::checkFileExtension(path, ".js")) return "application/javascript"; - if (StringUtils::checkFileExtension(path, ".json")) return "application/json"; - if (StringUtils::checkFileExtension(path, ".xml")) return "application/xml"; - if (StringUtils::checkFileExtension(path, ".jpg") || StringUtils::checkFileExtension(path, ".jpeg")) - return "image/jpeg"; - if (StringUtils::checkFileExtension(path, ".png")) return "image/png"; - if (StringUtils::checkFileExtension(path, ".gif")) return "image/gif"; - if (StringUtils::checkFileExtension(path, ".svg")) return "image/svg+xml"; - if (StringUtils::checkFileExtension(path, ".zip")) return "application/zip"; - if (StringUtils::checkFileExtension(path, ".gz")) return "application/gzip"; + if (FsHelpers::hasEpubExtension(path)) return "application/epub+zip"; + if (FsHelpers::checkFileExtension(path, ".pdf")) return "application/pdf"; + if (FsHelpers::hasTxtExtension(path)) return "text/plain"; + if (FsHelpers::checkFileExtension(path, ".html") || FsHelpers::checkFileExtension(path, ".htm")) return "text/html"; + if (FsHelpers::checkFileExtension(path, ".css")) return "text/css"; + if (FsHelpers::checkFileExtension(path, ".js")) return "application/javascript"; + if (FsHelpers::checkFileExtension(path, ".json")) return "application/json"; + if (FsHelpers::checkFileExtension(path, ".xml")) return "application/xml"; + if (FsHelpers::hasJpgExtension(path)) return "image/jpeg"; + if (FsHelpers::hasPngExtension(path)) return "image/png"; + if (FsHelpers::hasGifExtension(path)) return "image/gif"; + if (FsHelpers::checkFileExtension(path, ".svg")) return "image/svg+xml"; + if (FsHelpers::checkFileExtension(path, ".zip")) return "application/zip"; + if (FsHelpers::checkFileExtension(path, ".gz")) return "application/gzip"; return "application/octet-stream"; } diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp index ed989d55..81be07ee 100644 --- a/src/util/StringUtils.cpp +++ b/src/util/StringUtils.cpp @@ -2,8 +2,6 @@ #include -#include - namespace StringUtils { std::string sanitizeFilename(const std::string& name, size_t maxBytes) { @@ -45,30 +43,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxBytes) { return result.empty() ? "book" : result; } -bool checkFileExtension(const std::string& fileName, const char* extension) { - if (fileName.length() < strlen(extension)) { - return false; - } - - const std::string fileExt = fileName.substr(fileName.length() - strlen(extension)); - for (size_t i = 0; i < fileExt.length(); i++) { - if (tolower(fileExt[i]) != tolower(extension[i])) { - return false; - } - } - return true; -} - -bool checkFileExtension(const String& fileName, const char* extension) { - if (fileName.length() < strlen(extension)) { - return false; - } - - String localFile(fileName); - String localExtension(extension); - localFile.toLowerCase(); - localExtension.toLowerCase(); - return localFile.endsWith(localExtension); -} - } // namespace StringUtils diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index 7909fe44..1fa6cc01 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -1,7 +1,5 @@ #pragma once -#include - #include namespace StringUtils { @@ -13,10 +11,4 @@ namespace StringUtils { */ std::string sanitizeFilename(const std::string& name, size_t maxBytes = 100); -/** - * Check if the given filename ends with the specified extension (case-insensitive). - */ -bool checkFileExtension(const std::string& fileName, const char* extension); -bool checkFileExtension(const String& fileName, const char* extension); - } // namespace StringUtils