mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Merge branch 'master' into sd-card-fonts
# Conflicts: # src/JsonSettingsIO.cpp # src/SettingsList.h # src/activities/settings/SettingsActivity.cpp
This commit is contained in:
commit
8e184724d1
103 files changed
+5512
-643
No files matched your search
+1
-1
@@ -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).
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
freetype-py==2.5.1
|
||||
fonttools>=4.62.1
|
||||
freetype-py>=2.5.1
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] = {
|
||||
{"¥", "¥"}, {"ÿ", "ÿ"}, {"ζ", "ζ"}, {"‍", "\u200D"}, {"‌", "\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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -290,3 +290,4 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак"
|
||||
STR_LINK: "[спасылка]"
|
||||
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
||||
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
|
||||
@@ -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ó"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
Reference in new issue
Block a user