Merge branch 'master' into sd-card-fonts

# Conflicts:
#	src/JsonSettingsIO.cpp
#	src/SettingsList.h
#	src/activities/settings/SettingsActivity.cpp
This commit is contained in:
Zach Nelson committed 2026-05-04 21:51:48 -05:00
commit 8e184724d1
103 files changed
+5512 -643

No files matched your search

+1 -1
View File
@@ -195,7 +195,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
- **OPDS Servers**: Manage one or more OPDS libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
- **Clear Reading Cache**: Clear the internal SD card cache.
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
+2 -1
View File
@@ -1 +1,2 @@
freetype-py==2.5.1
fonttools>=4.62.1
freetype-py>=2.5.1
+1 -1
View File
@@ -10,7 +10,7 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 21;
constexpr uint8_t SECTION_FILE_VERSION = 22;
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(uint32_t) + sizeof(uint32_t);
+59 -32
View File
@@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include "Epub/css/CssStyle.h"
@@ -8,6 +9,12 @@
* BlockStyle - Block-level styling properties
*/
struct BlockStyle {
// Upper bound (in em) for any single side's horizontal margin or padding.
// Some EPUBs apply huge em-based insets to chapter-opener classes; without a
// cap, effectiveWidth collapses to 1-2 words per line and justification dumps
// the remaining space into a single gap.
static constexpr float MAX_HORIZONTAL_INSET_EM = 2.0f;
CssTextAlign alignment = CssTextAlign::Justify;
// Spacing (in pixels)
@@ -23,42 +30,61 @@ struct BlockStyle {
bool textIndentDefined = false; // true if text-indent was explicitly set in CSS
bool textAlignDefined = false; // true if text-align was explicitly set in CSS
// Combined horizontal insets (margin + padding)
// Combined insets (margin + padding)
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
[[nodiscard]] int16_t totalHorizontalInset() const { return leftInset() + rightInset(); }
[[nodiscard]] int16_t topInset() const { return marginTop + paddingTop; }
[[nodiscard]] int16_t bottomInset() const { return marginBottom + paddingBottom; }
// Combine with another block style. Useful for parent -> child styles, where the child style should be
// applied on top of the parent's style to get the combined style.
BlockStyle getCombinedBlockStyle(const BlockStyle& child) const {
BlockStyle combinedBlockStyle;
// Return a copy with bottom margins/padding zeroed out.
[[nodiscard]] BlockStyle withoutBottom() const {
BlockStyle result = *this;
result.marginBottom = 0;
result.paddingBottom = 0;
return result;
}
combinedBlockStyle.marginTop = static_cast<int16_t>(child.marginTop + marginTop);
combinedBlockStyle.marginBottom = static_cast<int16_t>(child.marginBottom + marginBottom);
combinedBlockStyle.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
combinedBlockStyle.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
// Return a copy with bottom margins/padding collapsed (max) with the source's.
// Uses CSS margin collapsing: adjacent parent-child margins resolve to the larger value.
[[nodiscard]] BlockStyle addBottom(const BlockStyle& source) const {
BlockStyle result = *this;
result.marginBottom = std::max(marginBottom, source.marginBottom);
result.paddingBottom = static_cast<int16_t>(paddingBottom + source.paddingBottom);
return result;
}
combinedBlockStyle.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
combinedBlockStyle.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
combinedBlockStyle.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
combinedBlockStyle.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
// Text indent: use child's if defined
if (child.textIndentDefined) {
combinedBlockStyle.textIndent = child.textIndent;
combinedBlockStyle.textIndentDefined = true;
enum class CombineAxis : uint8_t {
Horizontal = 1, // margins left/right, padding left/right, text-align, text-indent
Vertical = 2, // margins top/bottom, padding top/bottom
};
// Combine this style's properties with a child style along the specified axis.
// Properties on the other axis are kept from the child unchanged.
[[nodiscard]] BlockStyle getCombinedBlockStyle(const BlockStyle& child, CombineAxis axis) const {
BlockStyle result = child;
if (axis == CombineAxis::Horizontal) {
result.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
result.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
result.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
result.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
if (!child.textIndentDefined && textIndentDefined) {
result.textIndent = textIndent;
result.textIndentDefined = true;
}
if (!child.textAlignDefined && textAlignDefined) {
result.alignment = alignment;
result.textAlignDefined = true;
}
} else {
combinedBlockStyle.textIndent = textIndent;
combinedBlockStyle.textIndentDefined = textIndentDefined;
result.marginTop = std::max(child.marginTop, marginTop);
result.marginBottom = std::max(child.marginBottom, marginBottom);
result.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
result.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
}
// Text align: use child's if defined
if (child.textAlignDefined) {
combinedBlockStyle.alignment = child.alignment;
combinedBlockStyle.textAlignDefined = true;
} else {
combinedBlockStyle.alignment = alignment;
combinedBlockStyle.textAlignDefined = textAlignDefined;
}
return combinedBlockStyle;
return result;
}
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
@@ -68,16 +94,17 @@ struct BlockStyle {
const uint16_t viewportWidth = 0) {
BlockStyle blockStyle;
const float vw = viewportWidth;
const auto maxHorizontalInsetPx = static_cast<int16_t>(emSize * MAX_HORIZONTAL_INSET_EM);
// Resolve all CssLength values to pixels using the current font's em size and viewport width
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw);
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw);
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw);
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw);
blockStyle.marginLeft = std::min(cssStyle.marginLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
blockStyle.marginRight = std::min(cssStyle.marginRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw);
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw);
blockStyle.paddingLeft = std::min(cssStyle.paddingLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
blockStyle.paddingRight = std::min(cssStyle.paddingRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
// For textIndent: if it's a percentage we can't resolve (no viewport width),
// leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used
+3 -4
View File
@@ -4,6 +4,7 @@
#include "htmlEntities.h"
#include <cstring>
#include <iterator>
struct EntityPair {
const char* key;
@@ -62,8 +63,6 @@ static constexpr EntityPair ENTITY_LOOKUP[] = {
{"&yen;", "¥"}, {"&yuml;", "ÿ"}, {"&zeta;", "ζ"}, {"&zwj;", "\u200D"}, {"&zwnj;", "\u200C"},
};
static const size_t ENTITY_LOOKUP_COUNT = sizeof(ENTITY_LOOKUP) / sizeof(ENTITY_LOOKUP[0]);
// Verify the table is sorted at compile time.
static constexpr int constexprStrcmp(const char* a, const char* b) {
for (size_t i = 0;; i++) {
@@ -73,7 +72,7 @@ static constexpr int constexprStrcmp(const char* a, const char* b) {
}
static constexpr bool isTableSorted() {
for (size_t i = 1; i < ENTITY_LOOKUP_COUNT; i++) {
for (size_t i = 1; i < std::size(ENTITY_LOOKUP); i++) {
if (constexprStrcmp(ENTITY_LOOKUP[i - 1].key, ENTITY_LOOKUP[i].key) >= 0) return false;
}
return true;
@@ -85,7 +84,7 @@ const char* lookupHtmlEntity(const char* entity, size_t len) {
if (entity == nullptr || len == 0) return nullptr;
size_t lo = 0;
size_t hi = ENTITY_LOOKUP_COUNT;
size_t hi = std::size(ENTITY_LOOKUP);
while (lo < hi) {
const size_t mid = lo + (hi - lo) / 2;
+122 -77
View File
@@ -8,42 +8,30 @@
#include <XmlParserUtils.h>
#include <expat.h>
#include <iterator>
#include "../../Epub.h"
#include "../Page.h"
#include "../converters/ImageDecoderFactory.h"
#include "../converters/ImageToFramebufferDecoder.h"
#include "../htmlEntities.h"
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
constexpr size_t PARSE_BUFFER_SIZE = 1024;
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
const char* BOLD_TAGS[] = {"b", "strong"};
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
const char* ITALIC_TAGS[] = {"i", "em"};
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
const char* UNDERLINE_TAGS[] = {"u", "ins"};
constexpr int NUM_UNDERLINE_TAGS = sizeof(UNDERLINE_TAGS) / sizeof(UNDERLINE_TAGS[0]);
const char* IMAGE_TAGS[] = {"img"};
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
const char* SKIP_TAGS[] = {"head"};
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
constexpr const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
constexpr const char* IMAGE_TAGS[] = {"img"};
constexpr const char* SKIP_TAGS[] = {"head"};
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
// given the start and end of a tag, check to see if it matches a known tag
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
for (int i = 0; i < possible_tag_count; i++) {
bool matches(const char* tag_name, const char* const* possible_tags, size_t count) {
for (size_t i = 0; i < count; i++) {
if (strcmp(tag_name, possible_tags[i]) == 0) {
return true;
}
@@ -70,7 +58,7 @@ bool isInternalEpubLink(const char* href) {
}
bool isHeaderOrBlock(const char* name) {
return matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
return matches(name, HEADER_TAGS, std::size(HEADER_TAGS)) || matches(name, BLOCK_TAGS, std::size(BLOCK_TAGS));
}
bool isTableStructuralTag(const char* name) {
@@ -131,10 +119,13 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
if (currentTextBlock) {
// already have a text block running and it is empty - just reuse it
if (currentTextBlock->isEmpty()) {
// Merge with existing block style to accumulate CSS styling from parent block elements.
// 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));
// The stack accumulates horizontal margins and text properties from ancestors.
// Vertical margins are per-element and not inherited through the stack, but
// container elements deposit their vertical margins on the empty block when they
// open. Merge those into the new style so the first child in a container inherits
// the container's vertical spacing.
const auto style = currentTextBlock->getBlockStyle();
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
if (!pendingAnchorId.empty()) {
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
@@ -268,7 +259,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
if (matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS))) {
std::string src;
std::string alt;
if (atts != nullptr) {
@@ -344,18 +335,29 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
const bool hasCssHeight = imgStyle.hasImageHeight();
const bool hasCssWidth = imgStyle.hasImageWidth();
// Compute effective container width for percentage-based image sizes.
// If the image is inside a block with horizontal margins/padding (e.g.
// <div style="margin: 1em 40%">), percentage widths like width:100%
// should resolve against the container width, not the full viewport.
int containerWidth = self->viewportWidth;
if (self->currentTextBlock) {
const int inset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
if (inset > 0 && inset < self->viewportWidth) {
containerWidth = self->viewportWidth - inset;
}
}
if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) {
// Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio
displayHeight = static_cast<int>(
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
displayWidth = static_cast<int>(
imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
displayWidth =
static_cast<int>(imgStyle.imageWidth.toPixels(emSize, static_cast<float>(containerWidth)) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
if (displayWidth < 1) displayWidth = 1;
if (displayWidth > self->viewportWidth || displayHeight > self->viewportHeight) {
float scaleX = (displayWidth > self->viewportWidth)
? static_cast<float>(self->viewportWidth) / displayWidth
: 1.0f;
if (displayWidth > containerWidth || displayHeight > self->viewportHeight) {
float scaleX =
(displayWidth > containerWidth) ? static_cast<float>(containerWidth) / displayWidth : 1.0f;
float scaleY = (displayHeight > self->viewportHeight)
? static_cast<float>(self->viewportHeight) / displayHeight
: 1.0f;
@@ -380,8 +382,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth < 1) displayWidth = 1;
}
if (displayWidth > self->viewportWidth) {
displayWidth = self->viewportWidth;
if (displayWidth > containerWidth) {
displayWidth = containerWidth;
// Rescale height to preserve aspect ratio when width is clamped
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
@@ -390,10 +392,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (displayWidth < 1) displayWidth = 1;
LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight);
} else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) {
// Use CSS width (resolve % against viewport width) and derive height from aspect ratio
displayWidth = static_cast<int>(
imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth;
// Use CSS width (resolve % against container width) and derive height from aspect ratio
displayWidth =
static_cast<int>(imgStyle.imageWidth.toPixels(emSize, static_cast<float>(containerWidth)) + 0.5f);
if (displayWidth > containerWidth) displayWidth = containerWidth;
if (displayWidth < 1) displayWidth = 1;
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
@@ -407,8 +409,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (displayHeight < 1) displayHeight = 1;
LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight);
} else {
// Scale to fit viewport while maintaining aspect ratio
int maxWidth = self->viewportWidth;
// Scale to fit container while maintaining aspect ratio
int maxWidth = containerWidth;
int maxHeight = self->viewportHeight;
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
@@ -429,9 +431,24 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->startNewTextBlock(parentBlockStyle);
}
// Apply vertical margins from the container to the image.
// Top margin lives on the empty text block (deposited via vertical merge
// in startNewTextBlock). Bottom margin was stripped by withoutBottom() for
// deferred application at element close, so read it from the stack.
int16_t imageMarginTop = 0;
int16_t imageMarginBottom = 0;
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
const auto& bs = self->currentTextBlock->getBlockStyle();
imageMarginTop = bs.topInset();
if (self->blockStyleStack.size() > 1) {
imageMarginBottom = self->blockStyleStack.back().bottomInset();
}
}
// Create page for image - only break if image won't fit remaining space
if (self->currentPage && !self->currentPage->elements.empty() &&
(self->currentPageNextY + displayHeight > self->viewportHeight)) {
(self->currentPageNextY + imageMarginTop + displayHeight + imageMarginBottom >
self->viewportHeight)) {
self->completePageFn(std::move(self->currentPage), self->xpathParagraphIndex);
self->completedPageCount++;
self->currentPage.reset(new Page());
@@ -449,6 +466,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->currentPageNextY = 0;
}
// Apply top margin from container block
self->currentPageNextY += imageMarginTop;
// Create ImageBlock and add to page
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight);
if (!imageBlock) {
@@ -462,7 +482,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
self->currentPage->elements.push_back(pageImage);
self->currentPageNextY += displayHeight;
self->currentPageNextY += displayHeight + imageMarginBottom;
// The image consumed the empty block's accumulated vertical spacing.
// Reset the block so the Vertical merge in startNewTextBlock doesn't
// re-apply the same margins to the next text paragraph.
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
BlockStyle resetStyle;
resetStyle.alignment = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(self->paragraphAlignment);
self->currentTextBlock->setBlockStyle(resetStyle);
}
self->depth += 1;
return;
@@ -480,7 +511,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
// Fallback to alt text if image processing fails
if (!alt.empty()) {
alt = "[Image: " + alt + "]";
self->startNewTextBlock(centeredBlockStyle);
self->startNewTextBlock(self->blockStyleStack.back()
.getCombinedBlockStyle(centeredBlockStyle, BlockStyle::CombineAxis::Horizontal)
.withoutBottom());
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
@@ -496,7 +529,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
}
}
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
if (matches(name, SKIP_TAGS, std::size(SKIP_TAGS))) {
// start skip
self->skipUntilDepth = self->depth;
self->depth += 1;
@@ -563,33 +596,39 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
if (matches(name, HEADER_TAGS, std::size(HEADER_TAGS))) {
self->currentCssStyle = cssStyle;
auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth);
headerBlockStyle.textAlignDefined = true;
if (self->embeddedStyle && cssStyle.hasTextAlign()) {
headerBlockStyle.alignment = cssStyle.textAlign;
}
self->startNewTextBlock(headerBlockStyle);
const auto accumulated =
self->blockStyleStack.back().getCombinedBlockStyle(headerBlockStyle, BlockStyle::CombineAxis::Horizontal);
self->blockStyleStack.push_back(accumulated);
self->startNewTextBlock(accumulated.withoutBottom());
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
self->updateEffectiveInlineStyle();
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
} else if (matches(name, BLOCK_TAGS, std::size(BLOCK_TAGS))) {
if (strcmp(name, "br") == 0) {
if (self->partWordBufferIndex > 0) {
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
self->flushPartWordBuffer();
}
self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
self->startNewTextBlock(self->blockStyleStack.back().withoutBottom());
} else {
self->currentCssStyle = cssStyle;
self->startNewTextBlock(userAlignmentBlockStyle);
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
BlockStyle::CombineAxis::Horizontal);
self->blockStyleStack.push_back(accumulated);
self->startNewTextBlock(accumulated.withoutBottom());
self->updateEffectiveInlineStyle();
if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
}
}
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
@@ -611,7 +650,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
}
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
@@ -633,7 +672,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
}
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
} else if (matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
@@ -895,12 +934,12 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
// Flush buffer with current style BEFORE any style changes
if (self->partWordBufferIndex > 0) {
// Flush if style will change OR if we're closing a block/structural element
const bool isInlineTag =
!headerOrBlockTag && !tableStructuralTag && !matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || tableStructuralTag ||
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
const bool isInlineTag = !headerOrBlockTag && !tableStructuralTag &&
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag ||
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
if (shouldFlush) {
self->flushPartWordBuffer();
@@ -975,29 +1014,35 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->currentCssStyle.reset();
self->updateEffectiveInlineStyle();
// Reset alignment on empty text blocks to prevent stale alignment from bleeding
// into the next sibling element. This fixes issue #1026 where an empty <h1> (default
// Center) followed by an image-only <p> causes Center to persist through the chain
// of empty block reuse into subsequent text paragraphs.
// Margins/padding are preserved so parent element spacing still accumulates correctly.
if (self->currentTextBlock && self->currentTextBlock->isEmpty()) {
auto style = self->currentTextBlock->getBlockStyle();
style.textAlignDefined = false;
style.alignment = (self->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(self->paragraphAlignment);
self->currentTextBlock->setBlockStyle(style);
// br is self-closing and not a container — it doesn't push/pop the stack.
if (strcmp(name, "br") != 0 && self->blockStyleStack.size() > 1) {
// Apply closing element's bottom margin to the current text block so
// container spacing appears after the element's content (on the last child),
// not on the first child via the empty-block merge in startNewTextBlock.
if (self->currentTextBlock) {
const auto style = self->currentTextBlock->getBlockStyle();
self->currentTextBlock->setBlockStyle(style.addBottom(self->blockStyleStack.back()));
}
self->blockStyleStack.pop_back();
}
}
}
bool ChapterHtmlSlimParser::parseAndBuildPages() {
// Initialize block style stack with a root entry representing "no ancestor block elements".
// The user's paragraph alignment is set as the default so child elements without explicit
// text-align inherit it correctly through getCombinedBlockStyle.
BlockStyle rootBlockStyle;
rootBlockStyle.alignment = (this->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(this->paragraphAlignment);
blockStyleStack.clear();
blockStyleStack.reserve(8);
blockStyleStack.push_back(rootBlockStyle);
auto paragraphAlignmentBlockStyle = BlockStyle();
paragraphAlignmentBlockStyle.textAlignDefined = true;
// Resolve None sentinel to Justify for initial block (no CSS context yet)
const auto align = (this->paragraphAlignment == static_cast<uint8_t>(CssTextAlign::None))
? CssTextAlign::Justify
: static_cast<CssTextAlign>(this->paragraphAlignment);
const auto align = rootBlockStyle.alignment;
paragraphAlignmentBlockStyle.alignment = align;
startNewTextBlock(paragraphAlignmentBlockStyle);
@@ -62,6 +62,7 @@ class ChapterHtmlSlimParser {
bool hasUnderline = false, underline = false;
};
std::vector<StyleStackEntry> inlineStyleStack;
std::vector<BlockStyle> blockStyleStack; // accumulated block styles from open ancestor elements
CssStyle currentCssStyle;
bool effectiveBold = false;
bool effectiveItalic = false;
+18
View File
@@ -86,4 +86,22 @@ std::string extractFolderPath(const std::string& filePath) {
return filePath.substr(0, lastSlash);
}
void sanitizePathComponentForFat32(const char* input, char* output, size_t maxLen) {
if (maxLen == 0) {
return;
}
size_t i = 0;
for (; i < maxLen - 1 && input[i] != '\0'; i++) {
const char c = input[i];
if (c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' ||
c == ' ' || (c > 0x00 && c <= 0x1f)) {
output[i] = '-';
} else {
output[i] = c;
}
}
output[i] = '\0';
}
} // namespace FsHelpers
+6
View File
@@ -57,4 +57,10 @@ bool hasMarkdownExtension(std::string_view fileName);
std::string extractFolderPath(const std::string& filePath);
/**
* Sanitize a filename/path component for FAT32 in a caller-provided buffer.
* Replaces invalid path characters, spaces, and control characters with '-'.
*/
void sanitizePathComponentForFat32(const char* input, char* output, size_t maxLen);
} // namespace FsHelpers
+39 -10
View File
@@ -6,6 +6,8 @@
#include <SdCardFont.h>
#include <Utf8.h>
#include <algorithm>
#include "FontCacheManager.h"
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
@@ -524,6 +526,41 @@ void GfxRenderer::fillRectDither(const int x, const int y, const int width, cons
}
}
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
const int radius, const Color color) const {
if (radius <= 0 || color == Color::Clear) {
return;
}
const int rr = radius - 1;
const int rr2 = rr * rr;
for (int dy = 0; dy < radius; dy++) {
for (int dx = 0; dx < radius; dx++) {
const int tx = rr - dx;
const int ty = rr - dy;
if (tx * tx + ty * ty > rr2) {
if (color == Color::White || color == Color::Black) {
bool state = color == Color::Black;
drawPixel(x + dx, y + dy, state); // top-left
drawPixel(x + width - 1 - dx, y + dy, state); // top-right
drawPixel(x + dx, y + height - 1 - dy, state); // bottom-left
drawPixel(x + width - 1 - dx, y + height - 1 - dy, state); // bottom-right
} else if (color == Color::LightGray) {
drawPixelDither<Color::LightGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::LightGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::LightGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
} else if (color == Color::DarkGray) {
drawPixelDither<Color::DarkGray>(x + dx, y + dy); // top-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + dy); // top-right
drawPixelDither<Color::DarkGray>(x + dx, y + height - 1 - dy); // bottom-left
drawPixelDither<Color::DarkGray>(x + width - 1 - dx, y + height - 1 - dy); // bottom-right
}
}
}
}
}
template <Color color>
void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) const {
if (maxRadius <= 0) return;
@@ -878,16 +915,8 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi
j = i;
}
// Sort nodes by X (simple bubble sort, numPoints is small)
for (int i = 0; i < nodes - 1; i++) {
for (int k = i + 1; k < nodes; k++) {
if (nodeX[i] > nodeX[k]) {
int temp = nodeX[i];
nodeX[i] = nodeX[k];
nodeX[k] = temp;
}
}
}
// Sort nodes by X
std::sort(nodeX, nodeX + nodes);
// Fill between pairs of nodes
for (int i = 0; i < nodes - 1; i += 2) {
+1
View File
@@ -115,6 +115,7 @@ class GfxRenderer {
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool roundTopLeft,
bool roundTopRight, bool roundBottomLeft, bool roundBottomRight, bool state) const;
void maskRoundedRectOutsideCorners(int x, int y, int width, int height, int radius, Color color = Color::White) const;
void fillRect(int x, int y, int width, int height, bool state = true) const;
void fillRectDither(int x, int y, int width, int height, Color color) const;
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, Color color) const;
+12 -45
View File
@@ -1,17 +1,12 @@
#include "I18n.h"
#include <HalStorage.h>
#include <HardwareSerial.h>
#include <Serialization.h>
#include <cstddef>
#include <cstring>
#include "I18nStrings.h"
using namespace i18n_strings;
// Settings file path
static constexpr const char* SETTINGS_FILE = "/.crosspoint/language.bin";
static constexpr uint8_t SETTINGS_VERSION = 1;
I18n& I18n::getInstance() {
static I18n instance;
return instance;
@@ -24,8 +19,12 @@ const char* I18n::get(StrId id) const {
}
// Use generated helper function - no hardcoded switch needed!
const char* const* strings = getStringArray(_language);
return strings[index];
const LangStrings lang = getLanguageStrings(_language);
// If bit 15 of the offset is set, apply the offset to the English lookup table
const uint16_t off = lang.offsets[index];
if (off & 0x8000) return STRINGS_EN_DATA + (off & 0x7FFF);
return lang.data + off;
}
void I18n::setLanguage(Language lang) {
@@ -33,7 +32,6 @@ void I18n::setLanguage(Language lang) {
return;
}
_language = lang;
saveSettings();
}
const char* I18n::getLanguageName(Language lang) const {
@@ -44,42 +42,11 @@ const char* I18n::getLanguageName(Language lang) const {
return LANGUAGE_NAMES[index];
}
void I18n::saveSettings() {
Storage.mkdir("/.crosspoint");
FsFile file;
if (!Storage.openFileForWrite("I18N", SETTINGS_FILE, file)) {
Serial.printf("[I18N] Failed to save settings\n");
return;
}
serialization::writePod(file, SETTINGS_VERSION);
serialization::writePod(file, static_cast<uint8_t>(_language));
file.close();
Serial.printf("[I18N] Settings saved: language=%d\n", static_cast<int>(_language));
}
void I18n::loadSettings() {
FsFile file;
if (!Storage.openFileForRead("I18N", SETTINGS_FILE, file)) {
Serial.printf("[I18N] No settings file, using default (English)\n");
return;
}
uint8_t version;
serialization::readPod(file, version);
if (version != SETTINGS_VERSION) {
Serial.printf("[I18N] Settings version mismatch\n");
return;
}
uint8_t lang;
serialization::readPod(file, lang);
if (lang < static_cast<size_t>(Language::_COUNT)) {
_language = static_cast<Language>(lang);
Serial.printf("[I18N] Loaded language: %d\n", static_cast<int>(_language));
Language I18n::languageFromCode(const char* code) {
for (uint8_t i = 0; i < getLanguageCount(); i++) {
if (strcmp(code, LANGUAGE_CODES[i]) == 0) return static_cast<Language>(i);
}
return Language::EN;
}
// Generate character set for a specific language
+1 -3
View File
@@ -23,9 +23,7 @@ class I18n {
Language getLanguage() const { return _language; }
void setLanguage(Language lang);
const char* getLanguageName(Language lang) const;
void saveSettings();
void loadSettings();
static Language languageFromCode(const char* code);
// Get all unique characters used in a specific language
// Returns a sorted string of unique characters
+1
View File
@@ -290,3 +290,4 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак"
STR_LINK: "[спасылка]"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
+5 -1
View File
@@ -73,7 +73,10 @@ STR_IMAGES_SUPPRESS: "Suprimir"
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
STR_ORIENTATION: "Orientació de lectura"
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
STR_LONG_PRESS_SKIP: "Pressió llarga omet el capítol"
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
STR_FONT_FAMILY: "Tipus de lletra"
STR_FONT_SIZE: "Mida de la lletra (UI)"
STR_LINE_SPACING: "Interlineat del lector"
@@ -290,3 +293,4 @@ 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)"
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
+6 -1
View File
@@ -69,7 +69,10 @@ STR_TEXT_AA: "Vyhlazování textu"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Přeskočení kapitoly"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Změna orientace"
STR_FONT_FAMILY: "Rodina písem čtečky"
STR_FONT_SIZE: "Velikost písma rozhraní"
STR_LINE_SPACING: "Řádkování čtečky"
@@ -202,6 +205,7 @@ STR_FILTER_CONTRAST: "Kontrast"
STR_UI_THEME: "Šablona rozhraní"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
@@ -261,3 +265,4 @@ STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl"
STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
+5 -1
View File
@@ -73,7 +73,10 @@ STR_IMAGES_SUPPRESS: "Skjul"
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
STR_ORIENTATION: "Læseretning"
STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)"
STR_LONG_PRESS_SKIP: "Langt tryk spring kapitel over"
STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Cambio de orientación"
STR_FONT_FAMILY: "Læser skrifttype"
STR_FONT_SIZE: "Læser skriftstørrelse"
STR_LINE_SPACING: "Linjeafstand"
@@ -290,3 +293,4 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
STR_TILT_PAGE_TURN: "Vip for at vende side"
+6 -2
View File
@@ -73,7 +73,10 @@ STR_IMAGES_SUPPRESS: "Verbergen"
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
STR_ORIENTATION: "Leesstand"
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
STR_LONG_PRESS_SKIP: "Hoofdstuk overslaan (lang indrukken)"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_FAMILY: "Lettertype lezer"
STR_FONT_SIZE: "Lettergrootte lezer"
STR_LINE_SPACING: "Regelafstand lezer"
@@ -289,4 +292,5 @@ STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
+20 -1
View File
@@ -73,7 +73,10 @@ STR_IMAGES_SUPPRESS: "Suppress"
STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_FAMILY: "Reader Font Family"
STR_FONT_SIZE: "Reader Font Size"
STR_LINE_SPACING: "Reader Line Spacing"
@@ -168,6 +171,7 @@ STR_NO_UPDATE: "No update available"
STR_UPDATE_FAILED: "Update failed"
STR_UPDATE_COMPLETE: "Update complete"
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
STR_RESTARTING_HINT: "Restarting... If device does not restart, hold the power button for a few seconds."
STR_NO_ENTRIES: "No entries found"
STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed"
@@ -228,6 +232,7 @@ STR_BATTERY: "Battery"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
@@ -304,6 +309,7 @@ STR_CRASH_TITLE: "System Crash"
STR_CRASH_DESCRIPTION: "A detailed report was saved to crash_report.txt. Please include this file in your bug report."
STR_CRASH_REASON: "Crash reason:"
STR_CRASH_NO_REASON: "(No reason was recorded)"
STR_TILT_PAGE_TURN: "Tilt Page Turn"
STR_KB_HINT_MOVE_CURSOR: "Press LEFT or RIGHT to move cursor"
STR_KB_HINT_RETURN_CURSOR: "Press LEFT to return to cursor position"
STR_KB_HINT_HIDE_PASSWORD: "Hold RIGHT then press [***] to hide password"
@@ -319,3 +325,16 @@ STR_KB_HINT_SECONDARY_CHAR: "Hold SELECT for secondary char"
STR_KB_HINT_UPPER_SECONDARY: "Hold SELECT for UPPERCASE or secondary char"
STR_KB_HINT_LOWER_SECONDARY: "Hold SELECT for lowercase or secondary char"
STR_KB_HINT_URL_SNIPPETS: "Press URL for snippets"
STR_SD_FIRMWARE_UPDATE: "SD Card Firmware Update"
STR_SELECT_FIRMWARE_FILE: "Select firmware file (.bin)"
STR_NO_BIN_FILES: "No .bin files found"
STR_VALIDATING_FIRMWARE: "Validating firmware..."
STR_INVALID_FIRMWARE: "Invalid firmware file"
STR_FIRMWARE_TOO_LARGE: "Firmware too large for partition"
STR_FIRMWARE_TOO_SMALL: "Firmware file is too small"
STR_FIRMWARE_UPDATE_PROMPT: "Update firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Cannot open file"
STR_FIRMWARE_WRITE_FAILED: "Firmware write failed"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Do not power off!"
STR_RECOVERY_MODE: "Recovery Mode"
STR_RECOVERY_MODE_HINT: "Place firmware.bin on SD card root and select it"
+5 -1
View File
@@ -69,7 +69,10 @@ STR_TEXT_AA: "Tekstin reunanpehmennys"
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
STR_ORIENTATION: "Lukusuunta"
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
STR_LONG_PRESS_SKIP: "Pitkä painallus: lukuhyppy"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
STR_FONT_FAMILY: "Lukijan fonttiperhe"
STR_FONT_SIZE: "Käyttöliittymän fonttikoko"
STR_LINE_SPACING: "Lukijan riviväli"
@@ -260,3 +263,4 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
Loaded 20 of 103 files, more files were not shown because too many files have changed in this diff. Show more