Merge branch 'master' of https://github.com/crosspoint-reader/crosspoint-reader into feat-page-overlay

This commit is contained in:
jpirnay
2026-03-07 10:22:18 +01:00
41 changed files with 924 additions and 193 deletions
+14 -14
View File
@@ -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
+1
View File
@@ -15,6 +15,7 @@ This guide explains the multi-language support system in CrossPoint Reader.
- Ukrainian
- Polish
- Danish
- Turkish
---
+2 -1
View File
@@ -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)
+8 -13
View File
@@ -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";
+52 -8
View File
@@ -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> 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<uint32_t>(0)); // Placeholder for LUT offset
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(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<uint16_t>(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<Page> 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<Page> Section::loadPageFromSectionFile() {
file.close();
return page;
}
std::optional<uint16_t> 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;
}
+5
View File
@@ -1,6 +1,8 @@
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <string>
#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<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Look up the page number for an anchor id from the section cache file.
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
};
@@ -1,5 +1,6 @@
#include "JpegToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <JPEGDEC.h>
@@ -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);
}
@@ -1,5 +1,6 @@
#include "PngToFramebufferConverter.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Logging.h>
@@ -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);
}
@@ -133,11 +133,21 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> 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<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
return;
}
makePages();
}
// Record deferred anchor after previous block is flushed
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(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<uint16_t>(completedPageCount)});
pendingAnchorId.clear();
}
completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset();
currentTextBlock.reset();
}
@@ -1003,6 +1022,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
if (currentPageNextY + lineHeight > viewportHeight) {
completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset(new Page());
currentPageNextY = 0;
}
@@ -5,6 +5,7 @@
#include <climits>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#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<std::pair<std::string, uint16_t>> 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<TextBlock> line);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
};
+43 -1
View File
@@ -1,8 +1,12 @@
#include "FsHelpers.h"
#include <cctype>
#include <cstring>
#include <vector>
std::string FsHelpers::normalisePath(const std::string& path) {
namespace FsHelpers {
std::string normalisePath(const std::string& path) {
std::vector<std::string> 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<unsigned char>(fileName[offset + i])) !=
tolower(static_cast<unsigned char>(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
+56 -5
View File
@@ -1,7 +1,58 @@
#pragma once
#include <string>
#include <WString.h>
class FsHelpers {
public:
static std::string normalisePath(const std::string& path);
};
#include <string>
#include <string_view>
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
+30
View File
@@ -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)"
+5
View File
@@ -92,6 +92,10 @@ STR_STATUS_BAR: "Status Bar"
STR_HIDE_BATTERY: "Ukryj % baterii"
STR_EXTRA_SPACING: "Dodatkowe odstępy paragrafów"
STR_TEXT_AA: "Wygładzanie tekstu"
STR_IMAGES: "Obrazki"
STR_IMAGES_DISPLAY: "Pokazuj"
STR_IMAGES_PLACEHOLDER: "Ramki"
STR_IMAGES_SUPPRESS: "Pomijaj"
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
STR_ORIENTATION: "Układ czytania"
STR_FRONT_BTN_LAYOUT: "Układ przednich przycisków"
@@ -297,6 +301,7 @@ STR_GO_TO_PERCENT: "Idź do %"
STR_GO_HOME_BUTTON: "Wróć do głównego ekranu"
STR_SYNC_PROGRESS: "Postęp synchronizacji"
STR_DELETE_CACHE: "Usuń pamięć podręczną książek"
STR_DELETE: "Usuń"
STR_DISPLAY_QR: "Pokaż stronę jako kod QR"
STR_CHAPTER_PREFIX: "Rozdział: "
STR_PAGES_SEPARATOR: " stron | "
+7
View File
@@ -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"
+35 -7
View File
@@ -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)"
+344
View File
@@ -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"
+39 -5
View File
@@ -1,15 +1,28 @@
#include "Logging.h"
#include <string>
#define MAX_ENTRY_LEN 256
#define MAX_LOG_LINES 16
// Simple ring buffer log, useful for error reporting when we encounter a crash
RTC_NOINIT_ATTR char logMessages[MAX_LOG_LINES][MAX_ENTRY_LEN];
RTC_NOINIT_ATTR size_t logHead = 0;
void addToLogRingBuffer(const char* message) {
// Add the message to the ring buffer, overwriting old messages if necessary
strncpy(logMessages[logHead], message, MAX_ENTRY_LEN - 1);
logMessages[logHead][MAX_ENTRY_LEN - 1] = '\0';
logHead = (logHead + 1) % MAX_LOG_LINES;
}
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
// provide the format string for the message itself.
void logPrintf(const char* level, const char* origin, const char* format, ...) {
if (!logSerial) {
return; // Serial not initialized, skip logging
}
va_list args;
va_start(args, format);
char buf[256];
char buf[MAX_ENTRY_LEN];
char* c = buf;
// add the timestamp
{
@@ -43,5 +56,26 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
// add the user message
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
va_end(args);
logSerial.print(buf);
if (logSerial) {
logSerial.print(buf);
}
addToLogRingBuffer(buf);
}
std::string getLastLogs() {
std::string output;
for (size_t i = 0; i < MAX_LOG_LINES; i++) {
size_t idx = (logHead + i) % MAX_LOG_LINES;
if (logMessages[idx][0] != '\0') {
output += logMessages[idx];
}
}
return output;
}
void clearLastLogs() {
for (size_t i = 0; i < MAX_LOG_LINES; i++) {
logMessages[i][0] = '\0';
}
logHead = 0;
}
+5
View File
@@ -2,6 +2,8 @@
#include <HardwareSerial.h>
#include <string>
/*
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); }
+3 -12
View File
@@ -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;
-10
View File
@@ -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
+8 -3
View File
@@ -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);
+140
View File
@@ -0,0 +1,140 @@
#include "HalSystem.h"
#include <string>
#include "Arduino.h"
#include "HalStorage.h"
#include "Logging.h"
#include "esp_debug_helpers.h"
#include "esp_private/esp_cpu_internal.h"
#include "esp_private/esp_system_attr.h"
#include "esp_private/panic_internal.h"
#define MAX_PANIC_STACK_DEPTH 32
RTC_NOINIT_ATTR char panicMessage[256];
RTC_NOINIT_ATTR HalSystem::StackFrame panicStack[MAX_PANIC_STACK_DEPTH];
extern "C" {
void __real_panic_abort(const char* message);
void __real_panic_print_backtrace(const void* frame, int core);
static DRAM_ATTR const char PANIC_REASON_UNKNOWN[] = "(unknown panic reason)";
void IRAM_ATTR __wrap_panic_abort(const char* message) {
if (!message) message = PANIC_REASON_UNKNOWN;
// IRAM-safe bounded copy (strncpy is not IRAM-safe in panic context)
int i = 0;
for (; i < (int)sizeof(panicMessage) - 1 && message[i]; i++) {
panicMessage[i] = message[i];
}
panicMessage[i] = '\0';
__real_panic_abort(message);
}
void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
if (!frame) {
__real_panic_print_backtrace(frame, core);
return;
}
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
// Copied from components/esp_system/port/arch/riscv/panic_arch.c
uint32_t sp = (uint32_t)((RvExcFrame*)frame)->sp;
const int per_line = 8;
int depth = 0;
for (int x = 0; x < 1024; x += per_line * sizeof(uint32_t)) {
uint32_t* spp = (uint32_t*)(sp + x);
// panic_print_hex(sp + x);
// panic_print_str(": ");
panicStack[depth].sp = sp + x;
for (int y = 0; y < per_line; y++) {
// panic_print_str("0x");
// panic_print_hex(spp[y]);
// panic_print_str(y == per_line - 1 ? "\r\n" : " ");
panicStack[depth].spp[y] = spp[y];
}
depth++;
if (depth >= MAX_PANIC_STACK_DEPTH) {
break;
}
}
__real_panic_print_backtrace(frame, core);
}
}
namespace HalSystem {
void begin() {
// This is mostly for the first boot, we need to initialize the panic info and logs to empty state
// If we reboot from a panic state, we want to keep the panic info until we successfully dump it to the SD card, use
// `clearPanic()` to clear it after dumping
if (!isRebootFromPanic()) {
clearPanic();
}
}
void checkPanic() {
if (isRebootFromPanic()) {
auto panicInfo = getPanicInfo(true);
auto file = Storage.open("/crash_report.txt", O_WRITE | O_CREAT | O_TRUNC);
if (file) {
file.write(panicInfo.c_str(), panicInfo.size());
file.close();
LOG_INF("SYS", "Dumped panic info to SD card");
} else {
LOG_ERR("SYS", "Failed to open crash_report.txt for writing");
}
}
}
void clearPanic() {
panicMessage[0] = '\0';
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
clearLastLogs();
}
std::string getPanicInfo(bool full) {
if (!full) {
return panicMessage;
} else {
std::string info;
info += "CrossPoint version: " CROSSPOINT_VERSION;
info += "\n\nPanic reason: " + std::string(panicMessage);
info += "\n\nLast logs:\n" + getLastLogs();
info += "\n\nStack memory:\n";
auto toHex = [](uint32_t value) {
char buffer[9];
snprintf(buffer, sizeof(buffer), "%08X", value);
return std::string(buffer);
};
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
if (panicStack[i].sp == 0) {
break;
}
info += "0x" + toHex(panicStack[i].sp) + ": ";
for (size_t j = 0; j < 8; j++) {
info += "0x" + toHex(panicStack[i].spp[j]) + " ";
}
info += "\n";
}
return info;
}
}
bool isRebootFromPanic() {
const auto resetReason = esp_reset_reason();
return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP;
}
} // namespace HalSystem
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
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
+2 -1
View File
@@ -35,6 +35,7 @@ build_flags =
# Default is (320*4+1)*2=2562, we need more for larger images
-DPNG_MAX_BUFFERED_PIXELS=16416
-Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort
build_unflags =
-std=gnu++11
@@ -77,7 +78,7 @@ build_flags =
${base.build_flags}
-DCROSSPOINT_VERSION=\"${crosspoint.version}\"
-DENABLE_SERIAL_LOG
-DLOG_LEVEL=0 ; Set log level to error for release builds
-DLOG_LEVEL=1 ; Set log level to info for release builds
[env:gh_release_rc]
extends = base
+1 -1
View File
@@ -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)
+4 -7
View File
@@ -1,6 +1,7 @@
#include "RecentBooksStore.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <HalStorage.h>
#include <JsonSettingsIO.h>
#include <Logging.h>
@@ -9,8 +10,6 @@
#include <algorithm>
#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, "", "", ""};
+5 -6
View File
@@ -1,6 +1,7 @@
#include "SleepActivity.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -18,7 +19,6 @@
#include "components/UITheme.h"
#include "fontIds.h"
#include "images/Logo120.h"
#include "util/StringUtils.h"
namespace {
@@ -211,7 +211,7 @@ void SleepActivity::renderCustomSleepScreen() const {
continue;
}
if (filename.substr(filename.length() - 4) != ".bmp") {
if (!FsHelpers::hasBmpExtension(filename)) {
LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name);
file.close();
continue;
@@ -378,8 +378,7 @@ void SleepActivity::renderCoverSleepScreen() const {
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
// Check if the current book is XTC, TXT, or EPUB
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
if (FsHelpers::hasXtcExtension(APP_STATE.openEpubPath)) {
// Handle XTC file
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastXtc.load()) {
@@ -393,7 +392,7 @@ void SleepActivity::renderCoverSleepScreen() const {
}
coverBmpPath = lastXtc.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) {
} else if (FsHelpers::hasTxtExtension(APP_STATE.openEpubPath)) {
// Handle TXT file - looks for cover image in the same folder
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastTxt.load()) {
@@ -407,7 +406,7 @@ void SleepActivity::renderCoverSleepScreen() const {
}
coverBmpPath = lastTxt.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
} else if (FsHelpers::hasEpubExtension(APP_STATE.openEpubPath)) {
// Handle EPUB file
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
// Skip loading css since we only need metadata here
+6 -6
View File
@@ -1,6 +1,7 @@
#include "FileBrowserActivity.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -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());
}
+3 -4
View File
@@ -2,6 +2,7 @@
#include <Bitmap.h>
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -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()) {
@@ -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;
+18 -1
View File
@@ -597,6 +597,16 @@ void EpubReaderActivity::render(RenderLock&& lock) {
section->currentPage = nextPageNumber;
}
if (!pendingAnchor.empty()) {
if (const auto page = section->getPageForAnchor(pendingAnchor)) {
section->currentPage = *page;
LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page);
} else {
LOG_DBG("ERS", "Anchor '%s' not found in section %d", pendingAnchor.c_str(), currentSpineIndex);
}
pendingAnchor.clear();
}
// handles changes in reader settings and reset to approximate position based on cached progress
if (cachedChapterTotalPageCount > 0) {
// only goes to relative position if spine index matches cached value
@@ -792,12 +802,18 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s
LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage);
}
// Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1")
std::string anchor;
const auto hashPos = hrefStr.find('#');
if (hashPos != std::string::npos && hashPos + 1 < hrefStr.size()) {
anchor = hrefStr.substr(hashPos + 1);
}
// Check for same-file anchor reference (#anchor only)
bool sameFile = !hrefStr.empty() && hrefStr[0] == '#';
int targetSpineIndex;
if (sameFile) {
// Same file — navigate to page 0 of current spine item
targetSpineIndex = currentSpineIndex;
} else {
targetSpineIndex = epub->resolveHrefToSpineIndex(hrefStr);
@@ -811,6 +827,7 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s
{
RenderLock lock(*this);
pendingAnchor = std::move(anchor);
currentSpineIndex = targetSpineIndex;
nextPageNumber = 0;
section.reset();
@@ -11,6 +11,9 @@ class EpubReaderActivity final : public Activity {
std::unique_ptr<Section> 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;
+5 -7
View File
@@ -1,5 +1,6 @@
#include "ReaderActivity.h"
#include <FsHelpers.h>
#include <HalStorage.h>
#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<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!Storage.exists(path.c_str())) {
+5 -6
View File
@@ -1,5 +1,6 @@
#include "UITheme.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <Logging.h>
@@ -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;
+1 -1
View File
@@ -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();
+5
View File
@@ -6,6 +6,7 @@
#include <HalGPIO.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <HalSystem.h>
#include <I18n.h>
#include <Logging.h>
#include <SPI.h>
@@ -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();
+2 -7
View File
@@ -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<void(F
root.close();
}
bool CrossPointWebServer::isEpubFile(const String& filename) const {
String lower = filename;
lower.toLowerCase();
return lower.endsWith(".epub");
}
bool CrossPointWebServer::isEpubFile(const String& filename) const { return FsHelpers::hasEpubExtension(filename); }
void CrossPointWebServer::handleFileList() const {
sendHtmlContent(server.get(), FilesPageHtml, sizeof(FilesPageHtml));
+15 -19
View File
@@ -6,8 +6,6 @@
#include <Logging.h>
#include <esp_task_wdt.h>
#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";
}
-28
View File
@@ -2,8 +2,6 @@
#include <Utf8.h>
#include <cstring>
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
-8
View File
@@ -1,7 +1,5 @@
#pragma once
#include <WString.h>
#include <string>
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