Merge branch 'master' of https://github.com/crosspoint-reader/crosspoint-reader into feat-page-overlay
This commit is contained in:
+14
-14
@@ -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
|
||||
|
||||
@@ -15,6 +15,7 @@ This guide explains the multi-language support system in CrossPoint Reader.
|
||||
- Ukrainian
|
||||
- Polish
|
||||
- Danish
|
||||
- Turkish
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-1
@@ -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
@@ -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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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 | "
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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, "", "", ""};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user