From bb417e68f01801daad60af247cac67299db8ee55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=80ngel?= <153315454+angeldenom@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:28:25 +0100 Subject: [PATCH 01/12] chore: add missing Catalan strings (#1302) ## Summary * **What is the goal of this PR?** Add missing Catalan strings. * **What changes are included?** Changes on catalan.yaml file only. ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? PARTIALLY --- lib/I18n/translations/catalan.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) 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)" From 6b931629eeffbc3516b2a2110e7e8fd4392df87d Mon Sep 17 00:00:00 2001 From: pablohc Date: Thu, 5 Mar 2026 15:36:53 +0100 Subject: [PATCH 02/12] fix: Correct relative file paths in SKILL.md documentation (#1304) ## Summary * **What is the goal of this PR?** Update relative paths to correctly navigate from .skills/ directory to project root by adding ../ prefix to file references. * **What changes are included?** .skills/SKILL.md --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< YES >**_ --- .skills/SKILL.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) 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 From 1dc2bb4dbd270400e0a8249ed1c40912603f0658 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Thu, 5 Mar 2026 10:08:36 -0600 Subject: [PATCH 03/12] chore: Image settings Polish localization (#1299) ## Summary **What is the goal of this PR?** Quick follow up to #1291, adding Polish translations suggested by @th0m4sek --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- lib/I18n/translations/polish.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index c870b367..a69dc1b2 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" From 5b8787b2bc6b25f145cc7e75aa118d01de3300c8 Mon Sep 17 00:00:00 2001 From: Zach Nelson Date: Thu, 5 Mar 2026 10:12:22 -0600 Subject: [PATCH 04/12] perf: Avoid creating strings for file extension checks (#1303) ## Summary **What is the goal of this PR?** This change avoids the pattern of creating a `std::string` using `.substr` in order to compare against a file extension literal. ```c++ std::string path; if (path.length() >= 4 && path.substr(path.length() - 4) == ".ext") ``` The `checkFileExtension` utility has moved from StringUtils to FsHelpers, to be available to code in lib/. The signature now accepts a `std::string_view` instead of `std::string`, which makes the single implementation reusable for Arduino `String`. Added utility functions for commonly repeated extensions. These changes **save about 2 KB of flash (5,999,427 to 5,997,343)**. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- lib/Epub/Epub.cpp | 21 +++---- .../converters/JpegToFramebufferConverter.cpp | 7 +-- .../converters/PngToFramebufferConverter.cpp | 7 +-- lib/FsHelpers/FsHelpers.cpp | 44 ++++++++++++- lib/FsHelpers/FsHelpers.h | 61 +++++++++++++++++-- lib/Txt/Txt.cpp | 15 +---- lib/Xtc/Xtc/XtcTypes.h | 10 --- src/RecentBooksStore.cpp | 11 ++-- src/activities/boot_sleep/SleepActivity.cpp | 11 ++-- src/activities/home/FileBrowserActivity.cpp | 12 ++-- src/activities/home/HomeActivity.cpp | 7 +-- src/activities/home/RecentBooksActivity.cpp | 1 - src/activities/reader/ReaderActivity.cpp | 12 ++-- src/components/UITheme.cpp | 11 ++-- src/components/UITheme.h | 2 +- src/network/CrossPointWebServer.cpp | 9 +-- src/network/WebDAVHandler.cpp | 34 +++++------ src/util/StringUtils.cpp | 28 --------- src/util/StringUtils.h | 8 --- 19 files changed, 160 insertions(+), 151 deletions(-) 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/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/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/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/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 35a0ee50..0b2c1825 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 @@ -12,7 +13,6 @@ #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" -#include "util/StringUtils.h" void SleepActivity::onEnter() { Activity::onEnter(); @@ -61,7 +61,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; @@ -228,8 +228,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()) { @@ -243,7 +242,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()) { @@ -257,7 +256,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/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/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 From 9305ab9337ee4743ec72a2dd3eac63ac7461a5a1 Mon Sep 17 00:00:00 2001 From: ariel-lindemann <41641978+ariel-lindemann@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:19:17 +0100 Subject: [PATCH 05/12] chore: add missing translations for Romanian (#1265) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) added Romanian ranslations from recent commits. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< NO >**_ --- lib/I18n/translations/romanian.yaml | 7 +++++++ 1 file changed, 7 insertions(+) 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" From ab617e491515980e5ae9cc13409d9031c33e5456 Mon Sep 17 00:00:00 2001 From: Stefan Blixten Karlsson Date: Thu, 5 Mar 2026 21:25:29 +0100 Subject: [PATCH 06/12] fix: improve and add Swedish translations (#1317) ## Summary * **What is the goal of this PR?** * Improve and add the latest missing Swedish translations. * **What changes are included?** * Added missing Swedish translations in `lib\I18n\translations\swedish.yaml` ## Additional Context * (none) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- docs/translators.md | 3 ++- lib/I18n/translations/swedish.yaml | 42 +++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 8 deletions(-) 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/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)" From 595efc277f405c8ab659dc6e14ac23f1d58f0f1a Mon Sep 17 00:00:00 2001 From: Baris Albayrak <80099286+barbarhan@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:24:44 +1100 Subject: [PATCH 07/12] feat: add turkish translation (#1192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description This Pull Request introduces Turkish language support to CrossPoint Reader firmware. Key Changes: - Translation File: Added lib/I18n/translations/turkish.yaml with 315 translated string keys, covering all system UI elements. - I18N Script Update: Modified scripts/gen_i18n.py to include the "TR" abbreviation mapping for Turkish. - System Integration: Regenerated I18N C++ files to include the new Language::TR enum and STRINGS_TR array. - UI Availability: The language is now selectable in the Settings menu and correctly handles Turkish-specific characters (ç, ğ, ı, ö, ş, ü). - Documentation: Updated docs/i18n.md to include Turkish in the list of supported languages. Testing: - Verified the build locally with PlatformIO. - Flashed the firmware to an Xteink X4 device and confirmed the Turkish UI renders correctly. --- ### AI Usage Did you use AI tools to help write this code? Yes Gemini --------- Co-authored-by: Baris Albayrak Co-authored-by: Barış Albayrak --- docs/i18n.md | 1 + lib/I18n/translations/turkish.yaml | 344 +++++++++++++++++++++++++++++ scripts/gen_i18n.py | 2 +- 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 lib/I18n/translations/turkish.yaml 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/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/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) From 49b54e1e8e894c74fb1120d55f8dd13786256db6 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 6 Mar 2026 04:25:17 +0100 Subject: [PATCH 08/12] fix: avoid zip filename overflow (#1321) ## Summary * **What is the goal of this PR?** Potential stack buffer overflow from untrusted ZIP entry name length * **What changes are included?** If nameLen >= 256 , this writes past the stack buffer. Risk: memory corruption/crash on malformed EPUB/ZIP. ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** PARTIALLY **_ Issue identified by AI --- lib/ZipFile/ZipFile.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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); From 1ae9529639fe1a595923248ab5411cfc53a3d31d Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Fri, 6 Mar 2026 17:46:13 +0100 Subject: [PATCH 09/12] feat: dump crash report to sdcard (#1145) ## Summary This allow dumping crash message (i.e. assertion fail) and stack trace to `crash_report.txt` file on sdcard. The stack trace can then be decoded using https://esphome.github.io/esp-stacktrace-decoder/ Could be useful to debug things like https://github.com/crosspoint-reader/crosspoint-reader/issues/1137 where error doesn't always happen. May also be useful to show a screen to tell what happen (show on next boot after crash), similar to [flipper zero crash message](https://www.reddit.com/r/flipperzero/comments/10f8m3f/anyone_who_can_tell_me_why_this_message_pops_up/) , but this is better to be a dedicated PR (I'm missing the `drawTextWrapped` function, too lazy to code it ; update: exactly what I need in https://github.com/crosspoint-reader/crosspoint-reader/pull/1141) To test this: - Option 1: add an `assert(false)` somewhere in the code - Option 2: try dereferencing a nullptr - Option 3: try `throw` an exception Example of a crash report: ``` CrossPoint version: 1.1.0-dev Panic reason: abort() was called at PC 0x4214585b on core 0 Recent logs: [196] [DBG] [GFX] Time = 2 ms from clearScreen to displayBuffer [1831] [DBG] [RBS] Recent books loaded from file (7 entries) [1832] [DBG] [ACT] Exiting activity: Boot [1832] [DBG] [ACT] Entering activity: Home [1891] [DBG] [GFX] Time = 54 ms from clearScreen to displayBuffer [2521] [DBG] [GFX] Time = 46 ms from clearScreen to displayBuffer [4839] [DBG] [PWR] Going to low-power mode [10048] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes [20060] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes [30072] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes [34453] [DBG] [PWR] Restoring normal CPU frequency [34485] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer [35182] [DBG] [GFX] Time = 31 ms from clearScreen to displayBuffer [36675] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer [38800] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer [40079] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes Stack memory: 0x3FCB0650: 0x00000000 0x00000000 0x3FCB0668 0x4038DBB6 0x00000000 0x00000000 0x3FCA0030 0x3FC936D0 0x3FCB0670: 0x3FCB067C 0x3FC936EC 0x3FCB0668 0x34313234 0x62353835 0x00000000 0x726F6261 0x20292874 0x3FCB0690: 0x20736177 0x6C6C6163 0x61206465 0x43502074 0x34783020 0x35343132 0x20623538 0x63206E6F 0x3FCB06B0: 0x2065726F 0x00000030 0x3FCA0000 0xB37A603F 0x00000001 0x3FCA7000 0x3FCABCDC 0x4214585E 0x3FCB06D0: 0x3FCA7000 0x3FCA7000 0x3FCABCDC 0x421458AA 0x3FCABCDC 0x3FCA7000 0x3FCABCDC 0x421459CC 0x3FCB06F0: 0x3FCA7000 0x3FCA7000 0x42145D5A 0x3C205624 0x40388560 0x3FCA7000 0x3FCABCFC 0x42079866 0x3FCB0710: 0x3FCA7000 0x3FCA7000 0x00009C9A 0x4207B7F6 0x3FCA7000 0x42090000 0x001B7740 0x00000001 0x3FCB0730: 0x3FCA7000 0x3FCA7000 0x00000001 0x600C0028 0x00000001 0x3FCA1000 0x00000000 0x00000000 0x3FCB0750: 0x00000000 0x00000000 0x00000000 0xB37A603F 0x00000000 0x00000000 0x00000000 0x00000000 0x3FCB0770: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x42090000 0x3FCA7000 0x4208F9C4 0x3FCB0790: 0x00000000 0x00000000 0x00000000 0x40388368 0x00000000 0x00000000 0x00000000 0x00000000 0x3FCB07B0: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0x3FCB07D0: 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xBAAD5678 0xDA6D3601 0x5EB5B9C5 0x2602E480 0x3FCB07F0: 0x2BCDD33F 0x15556D4A 0x1F2140A0 0x5D59BEE3 0x8E76449F 0x6FB2D0CE 0xF5F46FAC 0x0112946A 0x3FCB0810: 0x3B0B32E0 0x7A52B537 0x46801DB4 0xDA85DF9F 0x37E83D20 0x12861028 0x47A702BB 0x287A3C8A 0x3FCB0830: 0x03632209 0xD44C5489 0x5E258453 0xFDA77529 0xE6748E23 0xADCF1394 0x67AD6778 0x2C208663 0x3FCB0850: 0xC7985786 0xD4AA3AB2 0x312E1760 0xEC7AEAAE 0x1857020E 0x48003E7E 0xD6CB8763 0x9B4A3F66 0x3FCB0870: 0x4B79E9F6 0xCBF739F0 0x3794C641 0xD0DBA3CB 0x95B9BE15 0x581C9983 0xDE62EFB6 0x20C67C5B 0x3FCB0890: 0x1E4A3DF3 0xFB317C74 0xC0D86103 0x1D79ED56 0x72FE0862 0x3D38B0C8 0xD27EB587 0x0E0A4C40 0x3FCB08B0: 0xF643ADC0 0x56D114D7 0x703AF879 0xAC7F3075 0x89C78C23 0xEDA86814 0xF767B3E3 0x0528838F 0x3FCB08D0: 0x50ED4662 0x11FD38E7 0x8A5A83BB 0x658159BD 0x781AF696 0x8A700F79 0x526DDE23 0xC8472505 0x3FCB08F0: 0x21AACC02 0xCB89369E 0xB82E5BE2 0x4C6C9D7D 0x9E724D9B 0xDC1067F7 0x84478FBC 0x4E89C444 0x3FCB0910: 0x973F4229 0x49F93DA8 0xE30200F6 0xD1B5C391 0x8363A89F 0x2409E74C 0x3AFF7B52 0xCBEC2349 0x3FCB0930: 0xD38F6695 0xBC3EA980 0xF067EBB1 0x7F87D167 0x92B3823B 0x9F0617D7 0xA7537C57 0x12CAB3D4 0x3FCB0950: 0xC82EEE37 0x84D4B4BC 0xE1E2261C 0x488F0ADA 0x96EAF2FF 0x0BC493A0 0xCE614467 0x3829053D 0x3FCB0970: 0xA41156BE 0x2747B77D 0x64DEA90B 0xE704AB0A 0xE4B01006 0x8D51903C 0x56CD3CF2 0x07E0A8E8 0x3FCB0990: 0xD1DE05CE 0x33368522 0xD1889988 0x3A3097F4 0xB0796D09 0xC78948AA 0x6DEFC56E 0xD5C2E1D9 0x3FCB09B0: 0xFD6DD8FA 0xA957B675 0xC202D80D 0x733FF8F4 0xA1484913 0x0B9AFBA6 0x330C07EA 0x2C09AD4C 0x3FCB09D0: 0x3B1E08F7 0x3FCAE7D0 0x00000170 0xABBA1234 0x0000015C 0x3FCB00E0 0x00009C93 0x3FCA13C4 0x3FCB09F0: 0x3FCA13C4 0x3FCB09E4 0x3FCA13BC 0x00000018 0x00000000 0x00000000 0x3FCB09E4 0x00000000 0x3FCB0A10: 0x00000001 0x3FCAE7E0 0x706F6F6C 0x6B736154 0x00000000 0x00000000 0x3FCB07D0 0x00000005 0x3FCB0A30: 0x00000000 0x00000001 0x00000000 0x3FCAB444 0x4209AFF0 0x0017E38F 0x00000000 0x3FCA7BD0 ``` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/Logging/Logging.cpp | 41 +++++++++++- lib/Logging/Logging.h | 5 ++ lib/hal/HalSystem.cpp | 137 ++++++++++++++++++++++++++++++++++++++++ lib/hal/HalSystem.h | 29 +++++++++ platformio.ini | 1 + src/main.cpp | 5 ++ 6 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 lib/hal/HalSystem.cpp create mode 100644 lib/hal/HalSystem.h diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index 7d72c920..22c14db9 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -1,5 +1,21 @@ #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. @@ -9,7 +25,7 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) { } va_list args; va_start(args, format); - char buf[256]; + char buf[MAX_ENTRY_LEN]; char* c = buf; // add the timestamp { @@ -43,5 +59,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/hal/HalSystem.cpp b/lib/hal/HalSystem.cpp new file mode 100644 index 00000000..4e17d5bd --- /dev/null +++ b/lib/hal/HalSystem.cpp @@ -0,0 +1,137 @@ +#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" { + +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..93275082 --- /dev/null +++ b/lib/hal/HalSystem.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +extern "C" { + +void __real_panic_abort(const char* message); +void __wrap_panic_abort(const char* message); + +void __real_panic_print_backtrace(const void* frame, int core); +void __wrap_panic_print_backtrace(const void* frame, int core); +} + +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..8e45ab77 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 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(); From 8fbb37b359831d0e137d09336c2add402360b52d Mon Sep 17 00:00:00 2001 From: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:10:45 +0200 Subject: [PATCH 10/12] feat: footnote anchor navigation (#1245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Enable footnote anchor navigation in EPUB reader This PR extracts the core anchor-to-page mapping mechanism from PR #1143 (TOC fragment navigation) to provide immediate footnote navigation support. By merging this focused subset first, users get a complete footnote experience now while simplifying the eventual review and merge of the full #1143 PR. --- ## What this extracts from PR #1143 PR #1143 implements comprehensive TOC fragment navigation for EPUBs with multi-chapter spine files. This PR takes only the anchor resolution infrastructure: - Anchor-to-page mapping in section cache: During page layout, ChapterHtmlSlimParser records which page each HTML id attribute lands on, serializing the map into the .bin cache file. - Anchor resolution in `EpubReaderActivity`: When navigating to a footnote link with a fragment (e.g., `chapter2.xhtml#note1`), the reader resolves the anchor to a page number and jumps directly to it. - Section file format change: Bumped to version 15, adds anchor map offset in header. --- ## Simplified scope vs. PR #1143 To minimize conflicts and complexity, this PR differs from #1143 in key ways: * **Anchors tracked** * **Origin:** Only TOC anchors (passed via `std::set`) * **This branch:** All `id` attributes * **Page breaks** * **Origin**: Forces new page at TOC chapter boundaries * **This branch:** None — natural flow * **TOC integration** * **Origin**: `tocBoundaries`, `getTocIndexForPage()`, chapter skip * **This branch:** None — just footnote links * **Bug fix** * **This branch:** Fixed anchor page off-by-1/2 bug The anchor recording bug (recording page number before `makePages()` flushes previous block) was identified and fixed during this extraction. The fix uses a deferred `pendingAnchorId` pattern that records the anchor after page completion. --- ## Positioning for future merge Changes are structured to minimize conflicts when #1143 eventually merges: - `ChapterHtmlSlimParser.cpp` `startElement()`: Both branches rewrite the same if `(!idAttr.empty())` block. The merged version will combine both approaches (TOC anchors get page breaks + immediate recording; footnote anchors get deferred recording). - `EpubReaderActivity.cpp` `render()`: The `pendingAnchor` resolution block is positioned at the exact same insertion point where #1143 places its `pendingTocIndex` block (line 596, right after `nextPageNumber` assignment). During merge, both blocks will sit side-by-side. --- ## Why merge separately? 1. Immediate user value: Footnote navigation works now without waiting for the full TOC overhaul 2. Easier review: ~100 lines vs. 500+ lines in #1143 3. Bug fix included: The page recording bug is fixed here and will carry into #1143 4. Minimal conflicts: Structured for clean merge — both PRs touch the same files but in complementary ways --- ### AI Usage Did you use AI tools to help write this code? _**< YES >**_ Done by Claude Opus 4.6 --- lib/Epub/Epub/Section.cpp | 60 ++++++++++++++++--- lib/Epub/Epub/Section.h | 5 ++ .../Epub/parsers/ChapterHtmlSlimParser.cpp | 22 ++++++- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 7 +++ src/activities/reader/EpubReaderActivity.cpp | 19 +++++- src/activities/reader/EpubReaderActivity.h | 3 + 6 files changed, 106 insertions(+), 10 deletions(-) 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/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/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 059d3ea1..d2af6751 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -595,6 +595,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 @@ -790,12 +800,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); @@ -809,6 +825,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 91c6f049..316677ba 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; From a59af01583714a32ba586248735663c5421f10f5 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Fri, 6 Mar 2026 22:05:23 +0100 Subject: [PATCH 11/12] fix: dump crash log without usb plugged, bump release log to INFO (#1332) ## Summary Follow-up https://github.com/crosspoint-reader/crosspoint-reader/pull/1145 - Fix log not being record without USB connected - Bump release log to INFO for more logging details --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- lib/Logging/Logging.cpp | 3 --- lib/hal/HalSystem.cpp | 3 +++ lib/hal/HalSystem.h | 9 --------- platformio.ini | 2 +- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index 22c14db9..d7f83606 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -20,9 +20,6 @@ void addToLogRingBuffer(const char* message) { // 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[MAX_ENTRY_LEN]; diff --git a/lib/hal/HalSystem.cpp b/lib/hal/HalSystem.cpp index 4e17d5bd..606bf80d 100644 --- a/lib/hal/HalSystem.cpp +++ b/lib/hal/HalSystem.cpp @@ -17,6 +17,9 @@ 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; diff --git a/lib/hal/HalSystem.h b/lib/hal/HalSystem.h index 93275082..e9df1b9d 100644 --- a/lib/hal/HalSystem.h +++ b/lib/hal/HalSystem.h @@ -3,15 +3,6 @@ #include #include -extern "C" { - -void __real_panic_abort(const char* message); -void __wrap_panic_abort(const char* message); - -void __real_panic_print_backtrace(const void* frame, int core); -void __wrap_panic_print_backtrace(const void* frame, int core); -} - namespace HalSystem { struct StackFrame { uint32_t sp; diff --git a/platformio.ini b/platformio.ini index 8e45ab77..4ba1c750 100644 --- a/platformio.ini +++ b/platformio.ini @@ -78,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 From 65e6caea575843a16816e83fb94f6a939a57135f Mon Sep 17 00:00:00 2001 From: Jonasz Potoniec Date: Sat, 7 Mar 2026 03:22:52 +0100 Subject: [PATCH 12/12] chore: Polish localization for STR_DELETE (#1323) --- lib/I18n/translations/polish.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index a69dc1b2..91228b08 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -301,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 | "