Merge remote-tracking branch 'origin/develop' into feat-touch

# Conflicts:
#	freeink-sdk
#	lib/I18n/translations/slovak.yaml
#	platformio.ini
#	src/components/icons/bookmark.h
This commit is contained in:
Justin Mitchell
2026-06-28 02:20:24 -04:00
60 changed files with 998 additions and 227 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized). * Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable. * Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification. * Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first. * Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage). * No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected. * Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file). * Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage) * lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables) * lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit) * src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager) * freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections * .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
### Hardware Abstraction Layer (HAL) ### Hardware Abstraction Layer (HAL)
+39 -10
View File
@@ -122,19 +122,43 @@ A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin. CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
1. Install the plugin in Calibre: #### Installing the Plugin in Calibre
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
- Download the zip file.
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
2. On the device: File Transfer -> Calibre Wireless, then join a network. If you don't already have the plugin installed:
3. Make sure your computer is on the same Wi-Fi network. 1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
2. Download the zip file.
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
4. Restart Calibre.
4. In Calibre, click "Send to device" to transfer books. #### Configuring the CrossPoint Plugin in Calibre
1. In Calibre select Preferences.
2. In the Preferences dialog select Plugins.
3. In Plugins search for "crosspoint".
4. Click on "Customize plugin".
5. Update the value for "Host" to match the IP for your device.
6. Leave the other settings as they are.
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
8. Restart Calibre.
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
#### Uploading Books
To upload a book using the CrossPoint plugin in Calibre:
1. On the device: File Transfer -> Calibre Wireless, then join a network.
2. Select one or more books.
3. Right-click on that selection.
4. Select "Send to Device" > "Send to main memory"
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
#### Removing a Book
Books cannot be removed from your device through Calibre. Use the web interface instead.
### 3.6 Settings ### 3.6 Settings
@@ -150,6 +174,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Cover" - The book cover image (Note: this is experimental and may not work as expected) - "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen - "None" - A blank screen
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise - "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected: - **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
@@ -162,6 +187,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion - "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion - "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
- **Status Bar**: Configure the status bar displayed while reading: - **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar - "None" - No status bar
@@ -527,6 +554,8 @@ On the **Xteink X3**, the gyroscope can be used to turn pages by tilting the dev
When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position. When reading an EPUB that contains footnotes, you can navigate to the footnote text by selecting the footnote reference in the book. From the footnote, you can return to your original reading position.
If the device goes to sleep or you close the book while viewing a footnote, the book reopens to your original reading position, not the footnote.
### System Navigation ### System Navigation
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen. * **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
+2 -2
View File
@@ -4,7 +4,7 @@
.DESCRIPTION .DESCRIPTION
Formats all C/C++ source and header files in the repository, excluding Formats all C/C++ source and header files in the repository, excluding
generated, vendored, and build directories (open-x4-sdk, builtinFonts, generated, vendored, and build directories (freeink-sdk, builtinFonts,
hyphenation tries, uzlib, .pio, *.generated.h). hyphenation tries, uzlib, .pio, *.generated.h).
The clang-format binary path is resolved once and cached in The clang-format binary path is resolved once and cached in
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
$clangFormat = Resolve-ClangFormat $clangFormat = Resolve-ClangFormat
$exclude = @( $exclude = @(
'open-x4-sdk' 'freeink-sdk'
'lib\EpdFont\builtinFonts' 'lib\EpdFont\builtinFonts'
'lib\Epub\Epub\hyphenation\generated' 'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib' 'lib\uzlib'
+3 -3
View File
@@ -8,7 +8,7 @@ At a high level, it is firmware that uses an activity-driven application archite
```mermaid ```mermaid
graph TD graph TD
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk] A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
B --> C[lib/hal wrappers] B --> C[lib/hal wrappers]
C --> D[src/main.cpp runtime loop] C --> D[src/main.cpp runtime loop]
D --> E[Activities layer] D --> E[Activities layer]
@@ -195,10 +195,10 @@ When editing related source assets, regenerate via normal build steps/scripts.
- `src/`: app orchestration, settings/state, and activity implementations - `src/`: app orchestration, settings/state, and activity implementations
- `src/network/`: web server and OTA/update networking - `src/network/`: web server and OTA/update networking
- `src/components/`: theming and shared UI components - `src/components/`: theming and shared UI components
- `lib/hal/`: hardware abstraction wrappers around open-x4-sdk - `lib/hal/`: hardware abstraction wrappers around freeink-sdk
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation - `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.) - `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery) - `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
- `docs/`: user and technical documentation - `docs/`: user and technical documentation
## Embedded constraints that shape design ## Embedded constraints that shape design
+20
View File
@@ -19,6 +19,26 @@ const SdCardFontFileInfo* SdCardFontFamilyInfo::findClosestReaderSize(const uint
const uint8_t style) const { const uint8_t style) const {
if (files.empty()) return nullptr; if (files.empty()) return nullptr;
// Collect sizes matching the requested style, sorted ascending.
std::vector<uint8_t> sizes;
for (const auto& f : files) {
if (f.style != style) continue;
sizes.push_back(f.pointSize);
}
if (sizes.empty()) return nullptr;
std::sort(sizes.begin(), sizes.end());
// When the family provides at least 4 sizes, use ordinal (index-based)
// selection so custom-built font sets (e.g. 10/12/14/16) map SMALL to
// the smallest file, not to a hardcoded 12pt target.
if (sizes.size() >= 4) {
uint8_t idx = fontSizeEnum;
if (idx >= sizes.size()) idx = sizes.size() - 1;
return findFile(sizes[idx], style);
}
// Fewer sizes than enum slots (e.g. CJK packs with only 2-3 sizes):
// fall back to closest-match against the built-in reader targets.
uint8_t target = 14; uint8_t target = 14;
switch (fontSizeEnum) { switch (fontSizeEnum) {
case 0: case 0:
+3 -2
View File
@@ -40,7 +40,8 @@ INTERVAL_PRESETS = {
"ascii": [(0x0020, 0x007E)], "ascii": [(0x0020, 0x007E)],
"latin1": [(0x0080, 0x00FF)], "latin1": [(0x0080, 0x00FF)],
"latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F), "latin-ext": [(0x0020, 0x007E), (0x0080, 0x00FF), (0x0100, 0x024F),
(0x1E00, 0x1EFF), (0x2000, 0x206F), (0xFB00, 0xFB06)], (0x02B0, 0x02FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
(0xFB00, 0xFB06)],
"greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)], "greek": [(0x0370, 0x03FF), (0x1F00, 0x1FFF)],
"cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)], "cyrillic": [(0x0400, 0x04FF), (0x0500, 0x052F)],
"hebrew": [(0x0590, 0x05FF), (0xFB1D, 0xFB4F)], "hebrew": [(0x0590, 0x05FF), (0xFB1D, 0xFB4F)],
@@ -62,7 +63,7 @@ INTERVAL_PRESETS = {
# Composite preset for English-language literary fiction including scifi/popsci. # Composite preset for English-language literary fiction including scifi/popsci.
# Greek for physics terms, math operators, geometric shapes, uncommon # Greek for physics terms, math operators, geometric shapes, uncommon
# dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats. # dialogue punctuation, CJK quote marks, miscellaneous symbols (♪♫♬), dingbats.
"reading": [(0x0020, 0x024F), (0x0300, 0x036F), (0x0370, 0x03FF), "reading": [(0x0020, 0x024F), (0x02B0, 0x02FF), (0x0300, 0x036F), (0x0370, 0x03FF),
(0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F), (0x0400, 0x04FF), (0x1E00, 0x1EFF), (0x2000, 0x206F),
(0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F), (0x2070, 0x209F), (0x20A0, 0x20CF), (0x2150, 0x218F),
(0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F), (0x2190, 0x21FF), (0x2200, 0x22FF), (0x2500, 0x257F),
+20 -3
View File
@@ -6,6 +6,20 @@
#include <new> #include <new>
namespace {
template <typename Predicate>
void renderFilteredPageElements(const std::vector<std::shared_ptr<PageElement>>& elements, GfxRenderer& renderer,
const int fontId, const int xOffset, const int yOffset, Predicate&& predicate) {
for (const auto& element : elements) {
if (predicate(*element)) {
element->render(renderer, fontId, xOffset, yOffset);
}
}
}
} // namespace
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
} }
@@ -93,9 +107,12 @@ std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(HalFile& fil
} }
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
for (auto& element : elements) { renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, [](const PageElement&) { return true; });
element->render(renderer, fontId, xOffset, yOffset); }
}
void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset,
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
} }
bool Page::serialize(HalFile& file) const { bool Page::serialize(HalFile& file) const {
+1
View File
@@ -88,6 +88,7 @@ class Page {
} }
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(HalFile& file) const; bool serialize(HalFile& file) const;
static std::unique_ptr<Page> deserialize(HalFile& file); static std::unique_ptr<Page> deserialize(HalFile& file);
+10 -3
View File
@@ -1006,7 +1006,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
int advance = int advance =
renderer.getKerning(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]), renderer.getKerning(fontId, lastCodepoint(reorderedWordsScratch[wordIdx]),
firstCodepoint(reorderedWordsScratch[wordIdx + 1]), reorderedStylesScratch[wordIdx]); firstCodepoint(reorderedWordsScratch[wordIdx + 1]), reorderedStylesScratch[wordIdx]);
if (reorderedWordsScratch[wordIdx] == " " && reorderedContinuesScratch[wordIdx] && // wordIdx > 0 mirrors the gap accounting above (which skips index 0): a leading
// no-break space must not receive justifyExtra, or the line over-stretches by one
// gap and the last word is pushed past the right margin (issue #2185).
if (wordIdx > 0 && reorderedWordsScratch[wordIdx] == " " && reorderedContinuesScratch[wordIdx] &&
effectiveAlignment == CssTextAlign::Justify && !isLastLine) { effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
advance += reorderedJustifyExtra; advance += reorderedJustifyExtra;
} }
@@ -1048,7 +1051,8 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
// Cross-boundary kerning for continuation words // Cross-boundary kerning for continuation words
int advance = renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]), int advance = renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]); firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
if (lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] && // wordIdx > 0: see the LTR branch — a leading no-break space is not a justifiable gap.
if (wordIdx > 0 && lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
effectiveAlignment == CssTextAlign::Justify && !isLastLine) { effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
advance += justifyExtra; advance += justifyExtra;
} }
@@ -1086,7 +1090,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
int advance = wordWidths[lastBreakAt + wordIdx]; int advance = wordWidths[lastBreakAt + wordIdx];
advance += renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]), advance += renderer.getKerning(fontId, lastCodepoint(lineWords[wordIdx]),
firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]); firstCodepoint(lineWords[wordIdx + 1]), lineWordStyles[wordIdx]);
if (lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] && // wordIdx > 0 mirrors the gap accounting above (which skips index 0): a leading
// no-break space must not receive justifyExtra, or the line over-stretches by one
// gap and the last word is pushed past the right margin (issue #2185).
if (wordIdx > 0 && lineWords[wordIdx] == " " && continuesVec[lastBreakAt + wordIdx] &&
effectiveAlignment == CssTextAlign::Justify && !isLastLine) { effectiveAlignment == CssTextAlign::Justify && !isLastLine) {
advance += justifyExtra; advance += justifyExtra;
} }
@@ -317,6 +317,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
const bool isTocAnchor = const bool isTocAnchor =
std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end(); std::find(self->tocAnchors.begin(), self->tocAnchors.end(), idValue) != self->tocAnchors.end();
if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) { if (isTocAnchor || (!isNonNavigableInlineElement(name) && self->anchorData.size() < MAX_ANCHORS_PER_CHAPTER)) {
// Flush a displaced anchor before overwriting. Consecutive non-block elements
// (e.g. <aside id="fn1">text</aside><aside id="fn2">) with no intervening block
// never trigger startNewTextBlock, so fn1 gets silently overwritten. That leaves
// fn1 missing from the anchor map -> getPageForAnchor returns nullopt -> reader
// lands at page 0 (section start) instead of the footnote.
if (!self->pendingAnchorId.empty()) {
self->flushPendingAnchor();
}
self->pendingAnchorId = idValue; self->pendingAnchorId = idValue;
} }
} else if (strcmp(atts[i], "dir") == 0) { } else if (strcmp(atts[i], "dir") == 0) {
+22 -14
View File
@@ -3,6 +3,7 @@
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
#include <string_view>
#include <vector> #include <vector>
namespace FsHelpers { namespace FsHelpers {
@@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) {
} }
std::string normalisePath(const std::string& path) { std::string normalisePath(const std::string& path) {
std::vector<std::string> components; std::vector<std::string_view> components;
std::string component; components.reserve(8); // Eight nested folders is more than we might expect
for (const auto c : path) { size_t start = 0;
if (c == '/') { for (size_t i = 0; i <= path.length(); ++i) {
if (!component.empty()) { if (i == path.length() || path[i] == '/') {
if (i > start) {
std::string_view component(path.data() + start, i - start);
if (component == "..") { if (component == "..") {
if (!components.empty()) { if (!components.empty()) {
components.pop_back(); components.pop_back();
@@ -49,23 +52,28 @@ std::string normalisePath(const std::string& path) {
} else { } else {
components.push_back(component); components.push_back(component);
} }
component.clear();
} }
} else { start = i + 1;
component += c;
} }
} }
if (!component.empty()) { if (components.empty()) {
components.push_back(component); return "";
}
size_t total_len = 0;
for (const auto& c : components) {
total_len += c.length() + 1;
} }
std::string result; std::string result;
for (const auto& c : components) { result.reserve(total_len - 1);
if (!result.empty()) {
result += "/"; for (size_t i = 0; i < components.size(); ++i) {
if (i > 0) {
result += '/';
} }
result += c; result.append(components[i].data(), components[i].length());
} }
return result; return result;
+209 -15
View File
@@ -679,8 +679,10 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con
} }
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
for (int fillY = y; fillY < y + height; fillY++) { if (state) {
drawLine(x, fillY, x + width - 1, fillY, state); fillRectImpl<Color::Black>(x, y, width, height);
} else {
fillRectImpl<Color::White>(x, y, width, height);
} }
} }
@@ -712,26 +714,194 @@ void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) con
} }
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const { void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
if (color == Color::Clear) { switch (color) {
} else if (color == Color::Black) { case Color::Clear:
fillRect(x, y, width, height, true); break;
} else if (color == Color::White) { case Color::Black:
fillRect(x, y, width, height, false); fillRectImpl<Color::Black>(x, y, width, height);
} else if (color == Color::LightGray) { break;
for (int fillY = y; fillY < y + height; fillY++) { case Color::White:
for (int fillX = x; fillX < x + width; fillX++) { fillRectImpl<Color::White>(x, y, width, height);
drawPixelDither<Color::LightGray>(fillX, fillY); break;
case Color::LightGray:
fillRectImpl<Color::LightGray>(x, y, width, height);
break;
case Color::DarkGray:
fillRectImpl<Color::DarkGray>(x, y, width, height);
break;
}
}
template <Color C>
void GfxRenderer::fillRectImpl(const int x, const int y, const int width, const int height) const {
if constexpr (C == Color::Clear) return;
if (width <= 0 || height <= 0) return;
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
// Clip in logical space.
const int screenW = getScreenWidth();
const int screenH = getScreenHeight();
const int lx0 = std::max(0, x);
const int ly0 = std::max(0, y);
const int lx1 = std::min(screenW, x + width);
const int ly1 = std::min(screenH, y + height);
if (lx0 >= lx1 || ly0 >= ly1) return;
// Rotate the two opposing logical corners into physical-framebuffer space.
// The bounding rect in physical space is the rect we need to fill — rotation
// is rigid (no shear/stretch) so the bbox of the two corners IS the rect.
int paX, paY, pbX, pbY;
rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight);
rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight);
const int phyX0 = std::min(paX, pbX);
const int phyX1 = std::max(paX, pbX); // inclusive
int phyY0 = std::min(paY, pbY);
int phyY1 = std::max(paY, pbY);
// Strip mode: clip Y range to the active band and redirect writes.
uint8_t* target = getWriteTarget();
const int originY = getWriteOriginY();
const int writeRows = getWriteRows();
phyY0 = std::max(phyY0, originY);
phyY1 = std::min(phyY1, originY + writeRows - 1);
if (phyY0 > phyY1) return;
// Bit/byte layout: MSB-first within a byte, so phyX → bit (7 - (phyX & 7)).
// Head and tail masks cover only the in-rect bits of the first/last byte.
const int byteStart = phyX0 >> 3;
const int byteEnd = phyX1 >> 3; // inclusive
const uint8_t headMask = static_cast<uint8_t>(0xFFu >> (phyX0 & 7));
const uint8_t tailMask = static_cast<uint8_t>(0xFFu << (7 - (phyX1 & 7)));
const int32_t panelStride = static_cast<int32_t>(panelWidthBytes);
if constexpr (C == Color::Black || C == Color::White) {
// Solid fill. Framebuffer: 0 = black, 1 = white.
const uint8_t fillByte = (C == Color::Black) ? 0x00u : 0xFFu;
for (int py = phyY0; py <= phyY1; ++py) {
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
if (byteStart == byteEnd) {
const uint8_t mask = headMask & tailMask;
if constexpr (C == Color::Black) {
row[byteStart] &= static_cast<uint8_t>(~mask);
} else {
row[byteStart] |= mask;
}
} else {
if constexpr (C == Color::Black) {
row[byteStart] &= static_cast<uint8_t>(~headMask);
if (byteEnd > byteStart + 1) {
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
}
row[byteEnd] &= static_cast<uint8_t>(~tailMask);
} else {
row[byteStart] |= headMask;
if (byteEnd > byteStart + 1) {
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
}
row[byteEnd] |= tailMask;
}
} }
} }
} else if (color == Color::DarkGray) { } else {
for (int fillY = y; fillY < y + height; fillY++) { // Dither (LightGray / DarkGray). Both patterns have period 2 in logical
for (int fillX = x; fillX < x + width; fillX++) { // (x, y), so per physical row we precompute one byte that represents the
drawPixelDither<Color::DarkGray>(fillX, fillY); // pattern across an 8-pixel stretch — every full byte in the row uses
// that same value.
//
// dlxPerPhyX / dlyPerPhyX: how logical (x, y) change as phyX increments
// along a physical row. Derived from inverting rotateCoordinates.
int dlxPerPhyX = 0, dlyPerPhyX = 0;
switch (orientation) {
case Portrait:
dlxPerPhyX = 0;
dlyPerPhyX = 1;
break;
case PortraitInverted:
dlxPerPhyX = 0;
dlyPerPhyX = -1;
break;
case LandscapeClockwise:
dlxPerPhyX = -1;
dlyPerPhyX = 0;
break;
case LandscapeCounterClockwise:
dlxPerPhyX = 1;
dlyPerPhyX = 0;
break;
}
// The dither pattern has period 2 in logical space, and each orientation
// maps py to logical coords with a fixed parity relationship. The
// blackMask byte therefore repeats with period 2 in py. Precompute both
// variants outside the row loop to eliminate the per-row switch + 8-bit
// construction loop.
uint8_t blackMasks[2];
for (int parityIdx = 0; parityIdx < 2; ++parityIdx) {
const int samplePy = phyY0 + parityIdx;
int lxBase = 0, lyBase = 0;
switch (orientation) {
case Portrait:
lxBase = panelHeight - 1 - samplePy;
lyBase = byteStart * 8;
break;
case PortraitInverted:
lxBase = samplePy;
lyBase = panelWidth - 1 - byteStart * 8;
break;
case LandscapeClockwise:
lxBase = panelWidth - 1 - byteStart * 8;
lyBase = panelHeight - 1 - samplePy;
break;
case LandscapeCounterClockwise:
lxBase = byteStart * 8;
lyBase = samplePy;
break;
}
uint8_t mask = 0;
for (int b = 0; b < 8; ++b) {
const int lx = lxBase + b * dlxPerPhyX;
const int ly = lyBase + b * dlyPerPhyX;
bool isBlack;
if constexpr (C == Color::LightGray) {
isBlack = ((lx & 1) == 0) && ((ly & 1) == 0);
} else { // DarkGray
isBlack = (((lx + ly) & 1) == 0);
}
if (isBlack) mask |= static_cast<uint8_t>(1u << (7 - b));
}
blackMasks[samplePy & 1] = mask;
}
for (int py = phyY0; py <= phyY1; ++py) {
const uint8_t blackMask = blackMasks[py & 1];
const uint8_t whiteMask = static_cast<uint8_t>(~blackMask);
// Dither writes BOTH inks (the slow path called drawPixel for every
// pixel — setting or clearing — so we must do the same). Inside the
// rect mask: write whiteMask (1s where white, 0s where black). Outside
// the rect mask: leave the framebuffer untouched.
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
if (byteStart == byteEnd) {
const uint8_t rectMask = headMask & tailMask;
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~rectMask) | (rectMask & whiteMask));
} else {
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~headMask) | (headMask & whiteMask));
if (byteEnd > byteStart + 1) {
// Period 2, so every full byte in this row is exactly whiteMask.
memset(row + byteStart + 1, whiteMask, byteEnd - byteStart - 1);
}
row[byteEnd] = static_cast<uint8_t>((row[byteEnd] & ~tailMask) | (tailMask & whiteMask));
} }
} }
} }
} }
template void GfxRenderer::fillRectImpl<Color::Black>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::White>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::LightGray>(int, int, int, int) const;
template void GfxRenderer::fillRectImpl<Color::DarkGray>(int, int, int, int) const;
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height, void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
const int radius, const Color color) const { const int radius, const Color color) const {
if (radius <= 0 || color == Color::Clear) { if (radius <= 0 || color == Color::Clear) {
@@ -1673,6 +1843,30 @@ size_t GfxRenderer::getBufferSize() const { return frameBufferSize; }
// unused // unused
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } // void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
void GfxRenderer::displayGrayscaleBase(HalDisplay::RefreshMode fallback) const {
display.displayGrayscaleBase(fallback, fadingFix);
}
void GfxRenderer::preconditionGrayscale() const { display.preconditionGrayscale(); }
void GfxRenderer::preconditionGrayscale(int x, int y, int w, int h) const {
if (w <= 0 || h <= 0) return;
// Rotate the logical rect's opposite corners to physical panel coords; the
// physical bbox stays axis-aligned for all four orientations.
int ax, ay, bx, by;
rotateCoordinates(orientation, x, y, &ax, &ay, panelWidth, panelHeight);
rotateCoordinates(orientation, x + w - 1, y + h - 1, &bx, &by, panelWidth, panelHeight);
int x0 = ax < bx ? ax : bx, x1 = ax > bx ? ax : bx;
int y0 = ay < by ? ay : by, y1 = ay > by ? ay : by;
if (x0 < 0) x0 = 0;
if (y0 < 0) y0 = 0;
if (x1 >= panelWidth) x1 = panelWidth - 1;
if (y1 >= panelHeight) y1 = panelHeight - 1;
if (x1 < x0 || y1 < y0) return;
display.preconditionGrayscale(static_cast<uint16_t>(x0), static_cast<uint16_t>(y0),
static_cast<uint16_t>(x1 - x0 + 1), static_cast<uint16_t>(y1 - y0 + 1));
}
void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); } void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); }
void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); } void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); }
+16
View File
@@ -91,6 +91,12 @@ class GfxRenderer {
void drawPixelDither(int x, int y) const; void drawPixelDither(int x, int y) const;
template <Color color> template <Color color>
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
// Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's
// two opposing corners into physical-framebuffer space once, then walks each
// physical row with head-mask / middle memset / tail-mask byte writes — no
// per-pixel rotation, no per-pixel RMW.
template <Color color>
void fillRectImpl(int x, int y, int width, int height) const;
public: public:
explicit GfxRenderer(HalDisplay& halDisplay) explicit GfxRenderer(HalDisplay& halDisplay)
@@ -247,6 +253,16 @@ class GfxRenderer {
// Grayscale functions // Grayscale functions
void setRenderMode(const RenderMode mode) { this->renderMode = mode; } void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
RenderMode getRenderMode() const { return renderMode; } RenderMode getRenderMode() const { return renderMode; }
// Grayscale preconditioning settle pass (no-op on X4). The rect overload
// takes the gray region in LOGICAL screen coordinates and rotates it to the
// panel; the no-arg overload settles the full frame. Call after the BW base
// frame is displayed and before the grayscale planes are written.
void preconditionGrayscale() const;
void preconditionGrayscale(int x, int y, int w, int h) const;
// Display the framebuffer as the base frame for a grayscale overlay that
// follows (X3: OEM differential base waveform; others: plain display with
// `fallback`).
void displayGrayscaleBase(HalDisplay::RefreshMode fallback = HalDisplay::HALF_REFRESH) const;
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer() const;
+4
View File
@@ -127,6 +127,7 @@ STR_PAGE_TURN: "Перагортванне"
STR_PORTRAIT: "Партрэт" STR_PORTRAIT: "Партрэт"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Інверсія" STR_INVERTED: "Інверсія"
STR_ORIENTATION_INVERTED: "Партрэт 180°"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Наперад" STR_PREV_NEXT: "Назад/Наперад"
STR_NEXT_PREV: "Наперад/Назад" STR_NEXT_PREV: "Наперад/Назад"
@@ -177,6 +178,9 @@ STR_EXIT: "« Выхад"
STR_HOME: "« Галоўная" STR_HOME: "« Галоўная"
STR_SELECT: "Абраць" STR_SELECT: "Абраць"
STR_TOGGLE: "Выбар" STR_TOGGLE: "Выбар"
STR_TOGGLE_BOOKMARK: "Пераключыць закладку"
STR_BOOKMARK_REMOVED: "Закладка выдалена."
STR_HOLD_OPEN_TO_DELETE: "Утрымлівайце Адкрыць, каб выдаліць"
STR_CONFIRM: "Пацв." STR_CONFIRM: "Пацв."
STR_CANCEL: "Адмена" STR_CANCEL: "Адмена"
STR_CONNECT: "Падкл." STR_CONNECT: "Падкл."
+4 -2
View File
@@ -137,6 +137,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
@@ -176,8 +177,7 @@ STR_DOWNLOADING: "S'està baixant..."
STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Sense nom" STR_UNNAMED: "Sense nom"
STR_HOLD_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar" STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre."
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor" STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed" STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
@@ -193,6 +193,7 @@ STR_HOME: "« Inici"
STR_SELECT: "Selecciona" STR_SELECT: "Selecciona"
STR_SELECTED: "Seleccionat" STR_SELECTED: "Seleccionat"
STR_TOGGLE: "Canvia" STR_TOGGLE: "Canvia"
STR_TOGGLE_BOOKMARK: "Commuta punt de llibre"
STR_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
STR_CONNECT: "Connecta" STR_CONNECT: "Connecta"
@@ -237,6 +238,7 @@ STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_BOOKMARKS: "Punts de llibre" STR_BOOKMARKS: "Punts de llibre"
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre." STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida" STR_QUICK_RESUME: "Represa ràpida"
+4
View File
@@ -132,6 +132,7 @@ STR_PAGE_TURN: "Otáčení stránek"
STR_PORTRAIT: "Na výšku" STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček" STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný" STR_INVERTED: "Invertovaný"
STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček" STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_PREV_NEXT: "Předchozí/Další" STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí" STR_NEXT_PREV: "Další/Předchozí"
@@ -182,6 +183,9 @@ STR_EXIT: "« Konec"
STR_HOME: "« Domů" STR_HOME: "« Domů"
STR_SELECT: "Vybrat" STR_SELECT: "Vybrat"
STR_TOGGLE: "Přepnout" STR_TOGGLE: "Přepnout"
STR_TOGGLE_BOOKMARK: "Přepnout záložku"
STR_BOOKMARK_REMOVED: "Záložka odstraněna."
STR_HOLD_OPEN_TO_DELETE: "Podržte Otevřít pro smazání"
STR_CONFIRM: "Potvrdit" STR_CONFIRM: "Potvrdit"
STR_CANCEL: "Zrušit" STR_CANCEL: "Zrušit"
STR_CONNECT: "Připojit" STR_CONNECT: "Připojit"
+4
View File
@@ -137,6 +137,7 @@ STR_PAGE_TURN: "Sideskift"
STR_PORTRAIT: "Portræt" STR_PORTRAIT: "Portræt"
STR_LANDSCAPE_CW: "Liggende med uret" STR_LANDSCAPE_CW: "Liggende med uret"
STR_INVERTED: "Inverteret" STR_INVERTED: "Inverteret"
STR_ORIENTATION_INVERTED: "Portræt 180°"
STR_LANDSCAPE_CCW: "Liggende mod uret" STR_LANDSCAPE_CCW: "Liggende mod uret"
STR_PREV_NEXT: "Forrige/Næste" STR_PREV_NEXT: "Forrige/Næste"
STR_NEXT_PREV: "Næste/Forrige" STR_NEXT_PREV: "Næste/Forrige"
@@ -188,6 +189,9 @@ STR_HOME: "« Hjem"
STR_SELECT: "Vælg" STR_SELECT: "Vælg"
STR_SELECTED: "Valgt" STR_SELECTED: "Valgt"
STR_TOGGLE: "Skift" STR_TOGGLE: "Skift"
STR_TOGGLE_BOOKMARK: "Skift bogmærke"
STR_BOOKMARK_REMOVED: "Bogmærke fjernet."
STR_HOLD_OPEN_TO_DELETE: "Hold Åbn nede for at slette"
STR_CONFIRM: "Bekræft" STR_CONFIRM: "Bekræft"
STR_CANCEL: "Annuller" STR_CANCEL: "Annuller"
STR_CONNECT: "Forbind" STR_CONNECT: "Forbind"
+5 -1
View File
@@ -136,7 +136,8 @@ STR_SLEEP: "Slaap"
STR_PAGE_TURN: "Pagina omslaan" STR_PAGE_TURN: "Pagina omslaan"
STR_PORTRAIT: "Staand" STR_PORTRAIT: "Staand"
STR_LANDSCAPE_CW: "Liggend (rechtsom)" STR_LANDSCAPE_CW: "Liggend (rechtsom)"
STR_INVERTED: "Omgekeerd" STR_INVERTED: "Geïnverteerd"
STR_ORIENTATION_INVERTED: "Staand 180°"
STR_LANDSCAPE_CCW: "Liggend (linksom)" STR_LANDSCAPE_CCW: "Liggend (linksom)"
STR_PREV_NEXT: "Vorige/Volgende" STR_PREV_NEXT: "Vorige/Volgende"
STR_NEXT_PREV: "Volgende/Vorige" STR_NEXT_PREV: "Volgende/Vorige"
@@ -188,6 +189,9 @@ STR_HOME: "« Home"
STR_SELECT: "Kies" STR_SELECT: "Kies"
STR_SELECTED: "Geselecteerd" STR_SELECTED: "Geselecteerd"
STR_TOGGLE: "Wissel" STR_TOGGLE: "Wissel"
STR_TOGGLE_BOOKMARK: "Bladwijzer wisselen"
STR_BOOKMARK_REMOVED: "Bladwijzer verwijderd."
STR_HOLD_OPEN_TO_DELETE: "Houd Openen ingedrukt om te verwijderen"
STR_CONFIRM: "Bevestig" STR_CONFIRM: "Bevestig"
STR_CANCEL: "Annuleer" STR_CANCEL: "Annuleer"
STR_CONNECT: "Verbind" STR_CONNECT: "Verbind"
+4 -2
View File
@@ -140,6 +140,7 @@ STR_FORCE_REFRESH: "Refresh Screen"
STR_PORTRAIT: "Portrait" STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW" STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted" STR_INVERTED: "Inverted"
STR_ORIENTATION_INVERTED: "Portrait 180°"
STR_LANDSCAPE_CCW: "Landscape CCW" STR_LANDSCAPE_CCW: "Landscape CCW"
STR_PREV_NEXT: "Prev/Next" STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev" STR_NEXT_PREV: "Next/Prev"
@@ -180,8 +181,7 @@ STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed" STR_DOWNLOAD_FAILED: "Download failed"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Unnamed" STR_UNNAMED: "Unnamed"
STR_HOLD_CONFIRM_TO_DELETE: "Hold Confirm to Delete" STR_HOLD_OPEN_TO_DELETE: "Hold Open to Delete"
STR_BOOKMARK_INSTRUCTIONS: "Hold Confirm from the reader to create a bookmark."
STR_NO_SERVER_URL: "No server URL configured" STR_NO_SERVER_URL: "No server URL configured"
STR_FETCH_FEED_FAILED: "Failed to fetch feed" STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed" STR_PARSE_FEED_FAILED: "Failed to parse feed"
@@ -199,6 +199,7 @@ STR_HOME: "« Home"
STR_SELECT: "Select" STR_SELECT: "Select"
STR_SELECTED: "Selected" STR_SELECTED: "Selected"
STR_TOGGLE: "Toggle" STR_TOGGLE: "Toggle"
STR_TOGGLE_BOOKMARK: "Toggle Bookmark"
STR_CONFIRM: "Confirm" STR_CONFIRM: "Confirm"
STR_CANCEL: "Cancel" STR_CANCEL: "Cancel"
STR_CONNECT: "Connect" STR_CONNECT: "Connect"
@@ -261,6 +262,7 @@ STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_BOOKMARKS: "Bookmarks" STR_BOOKMARKS: "Bookmarks"
STR_BOOKMARK_ADDED: "Bookmark added." STR_BOOKMARK_ADDED: "Bookmark added."
STR_BOOKMARK_REMOVED: "Bookmark removed."
STR_OPDS_BROWSER: "OPDS Browser" STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Search" STR_SEARCH: "Search"
STR_COVER_CUSTOM: "Cover + Custom" STR_COVER_CUSTOM: "Cover + Custom"
+5 -1
View File
@@ -131,7 +131,8 @@ STR_SLEEP: "Lepotila"
STR_PAGE_TURN: "Sivunkääntö" STR_PAGE_TURN: "Sivunkääntö"
STR_PORTRAIT: "Pysty" STR_PORTRAIT: "Pysty"
STR_LANDSCAPE_CW: "Vaaka myötäpäivään" STR_LANDSCAPE_CW: "Vaaka myötäpäivään"
STR_INVERTED: "Käännetty" STR_INVERTED: "Käänteinen"
STR_ORIENTATION_INVERTED: "Pysty 180°"
STR_LANDSCAPE_CCW: "Vaaka vastapäivään" STR_LANDSCAPE_CCW: "Vaaka vastapäivään"
STR_PREV_NEXT: "Edell/Seur" STR_PREV_NEXT: "Edell/Seur"
STR_NEXT_PREV: "Seur/Edell" STR_NEXT_PREV: "Seur/Edell"
@@ -182,6 +183,9 @@ STR_EXIT: "« Poistu"
STR_HOME: "« Koti" STR_HOME: "« Koti"
STR_SELECT: "Valitse" STR_SELECT: "Valitse"
STR_TOGGLE: "Vaihda" STR_TOGGLE: "Vaihda"
STR_TOGGLE_BOOKMARK: "Vaihda kirjanmerkki"
STR_BOOKMARK_REMOVED: "Kirjanmerkki poistettu."
STR_HOLD_OPEN_TO_DELETE: "Pidä Avaa painettuna poistaaksesi"
STR_CONFIRM: "Vahvista" STR_CONFIRM: "Vahvista"
STR_CANCEL: "Peruuta" STR_CANCEL: "Peruuta"
STR_CONNECT: "Yhdistä" STR_CONNECT: "Yhdistä"
+4
View File
@@ -137,6 +137,7 @@ STR_PAGE_TURN: "Page suivante"
STR_PORTRAIT: "Portrait" STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Paysage" STR_LANDSCAPE_CW: "Paysage"
STR_INVERTED: "Inversé" STR_INVERTED: "Inversé"
STR_ORIENTATION_INVERTED: "Portrait 180°"
STR_LANDSCAPE_CCW: "Paysage inversé" STR_LANDSCAPE_CCW: "Paysage inversé"
STR_PREV_NEXT: "Préc/Suiv" STR_PREV_NEXT: "Préc/Suiv"
STR_NEXT_PREV: "Suiv/Préc" STR_NEXT_PREV: "Suiv/Préc"
@@ -188,6 +189,9 @@ STR_HOME: "« Accueil"
STR_SELECT: "OK" STR_SELECT: "OK"
STR_SELECTED: "Sélectionné" STR_SELECTED: "Sélectionné"
STR_TOGGLE: "Modifier" STR_TOGGLE: "Modifier"
STR_TOGGLE_BOOKMARK: "Basculer le marque-page"
STR_BOOKMARK_REMOVED: "Marque-page supprimé."
STR_HOLD_OPEN_TO_DELETE: "Maintenir Ouvrir pour supprimer"
STR_CONFIRM: "Confirmer" STR_CONFIRM: "Confirmer"
STR_CANCEL: "Annuler" STR_CANCEL: "Annuler"
STR_CONNECT: "Connecter" STR_CONNECT: "Connecter"
+8 -5
View File
@@ -71,7 +71,7 @@ STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_TOUCH_READER_CONTROLS: "Touch-Steuerung (Lesen)" STR_TOUCH_READER_CONTROLS: "Touch-Steuerung (Lesen)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck" STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
STR_LONG_PRESS_BEHAVIOR_OFF: "AUS" STR_LONG_PRESS_BEHAVIOR_OFF: "Aus"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen" STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapitel überspringen"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ausrichtung ändern"
STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich" STR_FONT_PREVIEW_TEXT: "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich"
@@ -80,8 +80,9 @@ STR_FONT_SIZE: "Schriftgröße"
STR_LINE_SPACING: "Lese-Zeilenabstand" STR_LINE_SPACING: "Lese-Zeilenabstand"
STR_SCREEN_MARGIN: "Lese-Seitenränder" STR_SCREEN_MARGIN: "Lese-Seitenränder"
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung" STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
STR_LONG_PRESS_MENU: "Menütaste lang drücken"
STR_HYPHENATION: "Silbentrennung" STR_HYPHENATION: "Silbentrennung"
STR_TIME_TO_SLEEP: "Standby nach" STR_TIME_TO_SLEEP: "Standby-Modus nach"
STR_REFRESH_FREQ: "Anti-Ghosting nach" STR_REFRESH_FREQ: "Anti-Ghosting nach"
STR_KOREADER_SYNC: "KOReader-Synchr." STR_KOREADER_SYNC: "KOReader-Synchr."
STR_CHECK_UPDATES: "Nach Updates suchen" STR_CHECK_UPDATES: "Nach Updates suchen"
@@ -130,7 +131,8 @@ STR_PAGE_TURN: "Umblättern"
STR_FORCE_REFRESH: "Bildschirm regenerieren" STR_FORCE_REFRESH: "Bildschirm regenerieren"
STR_PORTRAIT: "Hochformat" STR_PORTRAIT: "Hochformat"
STR_LANDSCAPE_CW: "Querformat rechts" STR_LANDSCAPE_CW: "Querformat rechts"
STR_INVERTED: "Hochformat 180°" STR_INVERTED: "Invertiert"
STR_ORIENTATION_INVERTED: "Hochformat 180°"
STR_LANDSCAPE_CCW: "Querformat links" STR_LANDSCAPE_CCW: "Querformat links"
STR_PREV_NEXT: "Zurück/Weiter" STR_PREV_NEXT: "Zurück/Weiter"
STR_NEXT_PREV: "Weiter/Zurück" STR_NEXT_PREV: "Weiter/Zurück"
@@ -169,8 +171,7 @@ STR_DOWNLOADING: "Herunterladen…"
STR_DOWNLOAD_FAILED: "Ladefehler" STR_DOWNLOAD_FAILED: "Ladefehler"
STR_ERROR_MSG: "Fehler:" STR_ERROR_MSG: "Fehler:"
STR_UNNAMED: "Unbenannt" STR_UNNAMED: "Unbenannt"
STR_HOLD_CONFIRM_TO_DELETE: "Halte Bestätigen zum Löschen" STR_HOLD_OPEN_TO_DELETE: "Halte Öffnen zum Löschen"
STR_BOOKMARK_INSTRUCTIONS: "Halte Bestätigen im Lesemodus um ein Lesezeichen anzulegen."
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert" STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
STR_FETCH_FEED_FAILED: "Feedfehler" STR_FETCH_FEED_FAILED: "Feedfehler"
STR_PARSE_FEED_FAILED: "Feed-Format ungültig" STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
@@ -188,6 +189,7 @@ STR_HOME: "« Start"
STR_SELECT: "Auswahl" STR_SELECT: "Auswahl"
STR_SELECTED: "Ausgewählt" STR_SELECTED: "Ausgewählt"
STR_TOGGLE: "Ändern" STR_TOGGLE: "Ändern"
STR_TOGGLE_BOOKMARK: "Lesezeichen umschalten"
STR_CONFIRM: "Bestätigen" STR_CONFIRM: "Bestätigen"
STR_CANCEL: "Abbrechen" STR_CANCEL: "Abbrechen"
STR_CONNECT: "Verbinden" STR_CONNECT: "Verbinden"
@@ -251,6 +253,7 @@ STR_QUICK_RESUME_TIMEOUT: "Schnelles Fortsetzen nach Timeout"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_BOOKMARKS: "Lesezeichen" STR_BOOKMARKS: "Lesezeichen"
STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt." STR_BOOKMARK_ADDED: "Lesezeichen hinzugefügt."
STR_BOOKMARK_REMOVED: "Lesezeichen entfernt."
STR_SEARCH: "Suche" STR_SEARCH: "Suche"
STR_OPDS_BROWSER: "OPDS-Browser" STR_OPDS_BROWSER: "OPDS-Browser"
STR_COVER_CUSTOM: "Cover + Eigenes" STR_COVER_CUSTOM: "Cover + Eigenes"
+12 -3
View File
@@ -135,7 +135,8 @@ STR_PAGE_TURN: "העברת דף"
STR_FORCE_REFRESH: "רענון מסך מלא" STR_FORCE_REFRESH: "רענון מסך מלא"
STR_PORTRAIT: "לאורך" STR_PORTRAIT: "לאורך"
STR_LANDSCAPE_CW: "לרוחב (ימינה)" STR_LANDSCAPE_CW: "לרוחב (ימינה)"
STR_INVERTED: "הפוך" STR_INVERTED: יפוך צבעים"
STR_ORIENTATION_INVERTED: "לאורך 180°"
STR_LANDSCAPE_CCW: "לרוחב (שמאלה)" STR_LANDSCAPE_CCW: "לרוחב (שמאלה)"
STR_PREV_NEXT: "הקודם/הבא" STR_PREV_NEXT: "הקודם/הבא"
STR_NEXT_PREV: "הבא/הקודם" STR_NEXT_PREV: "הבא/הקודם"
@@ -190,6 +191,7 @@ STR_HOME: "מסך הבית »"
STR_SELECT: "בחר" STR_SELECT: "בחר"
STR_SELECTED: "נבחר" STR_SELECTED: "נבחר"
STR_TOGGLE: "בחר" STR_TOGGLE: "בחר"
STR_TOGGLE_BOOKMARK: "הוסף/הסר סימנייה"
STR_CONFIRM: "אישור" STR_CONFIRM: "אישור"
STR_CANCEL: "ביטול" STR_CANCEL: "ביטול"
STR_CONNECT: "התחבר" STR_CONNECT: "התחבר"
@@ -361,8 +363,7 @@ STR_FRONT_BTN_FOLLOW_ORIENTATION: "התאמת לחצנים קדמיים לכיו
STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים" STR_REMOVE_READ_FROM_RECENTS: "הסר ספרים שנקראו מרשימת האחרונים"
STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'" STR_MOVE_FINISHED_TO_READ: "העבר ספרים שהסתיימו לתיקיית 'נקראו'"
STR_DISABLED: "מבוטל" STR_DISABLED: "מבוטל"
STR_HOLD_CONFIRM_TO_DELETE: "החזק לחוץ על אישור כדי למחוק" STR_HOLD_OPEN_TO_DELETE: "לחיצה ארוכה על 'פתח' כדי למחוק"
STR_BOOKMARK_INSTRUCTIONS: "החזק לחוץ על כפתור אישור בזמן הקריאה כדי להוסיף סימנייה"
STR_CLOCK: "שעון" STR_CLOCK: "שעון"
STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC" STR_CLOCK_UTC_OFFSET: "הפרש זמן UTC"
STR_CLOCK_FORMAT: "תבנית השעון" STR_CLOCK_FORMAT: "תבנית השעון"
@@ -380,6 +381,14 @@ STR_CLOCK_SYNC_NO_WIFI_HINT: "התחבר תחילה לרשת אלחוטית, ו
STR_CLOCK_SYNCED: "השעון סונכרן" STR_CLOCK_SYNCED: "השעון סונכרן"
STR_BOOKMARKS: "סימניות" STR_BOOKMARKS: "סימניות"
STR_BOOKMARK_ADDED: "הסימנייה התווספה" STR_BOOKMARK_ADDED: "הסימנייה התווספה"
STR_BOOKMARK_REMOVED: "הסימנייה הוסרה"
STR_QUICK_RESUME: "חזרה מהירה" STR_QUICK_RESUME: "חזרה מהירה"
STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?" STR_REMOVE_FROM_RECENTS: "להסיר מרשימת הספרים האחרונים?"
STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?" STR_CONFIRM_DELETE_BOOKMARK: "למחוק סימנייה זו?"
STR_LONG_PRESS_MENU: "לחיצה ארוכה על אישור"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "סימנייה"
STR_PWR_BTN_FOOTNOTE_BACK: "חזרה מהירה מהערות שוליים"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
STR_SLEEP_NEVER: "אף פעם"
STR_SLEEP_TIMER_STEP_HINT: "שמאל/ימין: 1 דק' למעלה/למטה: 5 דק'"
+5 -1
View File
@@ -133,7 +133,8 @@ STR_SLEEP: "Alvás"
STR_PAGE_TURN: "Lapozás" STR_PAGE_TURN: "Lapozás"
STR_PORTRAIT: "Álló" STR_PORTRAIT: "Álló"
STR_LANDSCAPE_CW: "Fekvő jobbra" STR_LANDSCAPE_CW: "Fekvő jobbra"
STR_INVERTED: "Fordított" STR_INVERTED: "Invertált"
STR_ORIENTATION_INVERTED: "Álló 180°"
STR_LANDSCAPE_CCW: "Fekvő balra" STR_LANDSCAPE_CCW: "Fekvő balra"
STR_PREV_NEXT: "Előző/Következő" STR_PREV_NEXT: "Előző/Következő"
STR_NEXT_PREV: "Következő/Előző" STR_NEXT_PREV: "Következő/Előző"
@@ -185,6 +186,9 @@ STR_HOME: "« Főoldal"
STR_SELECT: "Kiválasztás" STR_SELECT: "Kiválasztás"
STR_SELECTED: "Kiválasztva" STR_SELECTED: "Kiválasztva"
STR_TOGGLE: "Váltás" STR_TOGGLE: "Váltás"
STR_TOGGLE_BOOKMARK: "Könyvjelző váltása"
STR_BOOKMARK_REMOVED: "Könyvjelző eltávolítva."
STR_HOLD_OPEN_TO_DELETE: "Tartsa lenyomva a Megnyitás gombot a törléshez"
STR_CONFIRM: "Megerősítés" STR_CONFIRM: "Megerősítés"
STR_CANCEL: "Mégse" STR_CANCEL: "Mégse"
STR_CONNECT: "Csatlakozás" STR_CONNECT: "Csatlakozás"
+6 -4
View File
@@ -138,7 +138,8 @@ STR_PAGE_TURN: "Cambio pagina"
STR_FORCE_REFRESH: "Refresh" STR_FORCE_REFRESH: "Refresh"
STR_PORTRAIT: "Verticale" STR_PORTRAIT: "Verticale"
STR_LANDSCAPE_CW: "Orizzontale Dx" STR_LANDSCAPE_CW: "Orizzontale Dx"
STR_INVERTED: "Capovolto" STR_INVERTED: "Invertito"
STR_ORIENTATION_INVERTED: "Verticale 180°"
STR_LANDSCAPE_CCW: "Orizzontale Sx" STR_LANDSCAPE_CCW: "Orizzontale Sx"
STR_PREV_NEXT: "Prec/Succ" STR_PREV_NEXT: "Prec/Succ"
STR_NEXT_PREV: "Succ/Prec" STR_NEXT_PREV: "Succ/Prec"
@@ -193,6 +194,7 @@ STR_HOME: "« Home"
STR_SELECT: "Seleziona" STR_SELECT: "Seleziona"
STR_SELECTED: "Selezionato" STR_SELECTED: "Selezionato"
STR_TOGGLE: "Cambia" STR_TOGGLE: "Cambia"
STR_TOGGLE_BOOKMARK: "Attiva/disattiva segnalibro"
STR_CONFIRM: "Conferma" STR_CONFIRM: "Conferma"
STR_CANCEL: "Annulla" STR_CANCEL: "Annulla"
STR_CONNECT: "Connetti" STR_CONNECT: "Connetti"
@@ -358,9 +360,9 @@ STR_FIRMWARE_WRITE_FAILED: "Aggiornamento firmware non riuscito"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!" STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Non spegnere il dispositivo!"
STR_RECOVERY_MODE: "Modalità ripristino" STR_RECOVERY_MODE: "Modalità ripristino"
STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo" STR_RECOVERY_MODE_HINT: "Metti firmware.bin nella scheda SD e selezionalo"
STR_BOOKMARK_INSTRUCTIONS: "Tieni premuto Conferma nel lettore per creare un segnalibro"
STR_BOOKMARKS: "Segnalibri" STR_BOOKMARKS: "Segnalibri"
STR_BOOKMARK_ADDED: "Segnalibro aggiunto" STR_BOOKMARK_ADDED: "Segnalibro aggiunto"
STR_BOOKMARK_REMOVED: "Segnalibro rimosso"
STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?" STR_CONFIRM_DELETE_BOOKMARK: "Eliminare questo segnalibro?"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova" STR_CLOCK_SYNC_NO_WIFI_HINT: "Connettiti prima al Wi-Fi e poi riprova"
STR_CLOCK_SYNC_OK: "Orologio sincronizzato" STR_CLOCK_SYNC_OK: "Orologio sincronizzato"
@@ -375,10 +377,10 @@ STR_CLOCK_SYNCING: "Sincronizzazione con il server NTP..."
STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita" STR_CLOCK_SYNC_FAIL: "Sincronizzazione non riuscita"
STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso" STR_CLOCK_SYNC_NOW: "Sincronizza l'orologio adesso"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso" STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi non connesso"
STR_HOLD_CONFIRM_TO_DELETE: "Tieni premuto Conferma per cancellare" STR_HOLD_OPEN_TO_DELETE: "Tieni premuto Apri per eliminare"
STR_NEXT_FIELD: "Succ." STR_NEXT_FIELD: "Succ."
STR_CURRENT_TIME: "Ora attuale: " STR_CURRENT_TIME: "Ora attuale: "
STR_DISABLED: "Disattivato" STR_DISABLED: "Disattivato"
STR_BOOKMARK_OPTION: "Segnalibro" STR_BOOKMARK_OPTION: "Segnalibro"
STR_KOSYNC: "KOSync" STR_KOSYNC: "KOSync"
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note" STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
+5 -1
View File
@@ -127,7 +127,8 @@ STR_SLEEP: "Ұйқы"
STR_PAGE_TURN: "Бет аудару" STR_PAGE_TURN: "Бет аудару"
STR_PORTRAIT: "Тік бағдар" STR_PORTRAIT: "Тік бағдар"
STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)" STR_LANDSCAPE_CW: "Көлденең (сағат бағытымен)"
STR_INVERTED: "Төңкерілген" STR_INVERTED: "Инверсия"
STR_ORIENTATION_INVERTED: "Тік бағдар 180°"
STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)" STR_LANDSCAPE_CCW: "Көлденең (сағат тіліне қарсы)"
STR_PREV_NEXT: "Алдыңғы/Келесі" STR_PREV_NEXT: "Алдыңғы/Келесі"
STR_NEXT_PREV: "Келесі/Алдыңғы" STR_NEXT_PREV: "Келесі/Алдыңғы"
@@ -178,6 +179,9 @@ STR_EXIT: "« Шығу"
STR_HOME: "« Басты" STR_HOME: "« Басты"
STR_SELECT: "Таңдау" STR_SELECT: "Таңдау"
STR_TOGGLE: "Ауыстыру" STR_TOGGLE: "Ауыстыру"
STR_TOGGLE_BOOKMARK: "Бетбелгіні ауыстыру"
STR_BOOKMARK_REMOVED: "Бетбелгі жойылды."
STR_HOLD_OPEN_TO_DELETE: "Жою үшін Ашу түймесін ұстап тұрыңыз"
STR_CONFIRM: "Растау" STR_CONFIRM: "Растау"
STR_CANCEL: "Болдырмау" STR_CANCEL: "Болдырмау"
STR_CONNECT: "Қосылу" STR_CONNECT: "Қосылу"
+5 -1
View File
@@ -133,7 +133,8 @@ STR_SLEEP: "Miegas"
STR_PAGE_TURN: "Versti psl." STR_PAGE_TURN: "Versti psl."
STR_PORTRAIT: "Stačias" STR_PORTRAIT: "Stačias"
STR_LANDSCAPE_CW: "Gulsčias (P)" STR_LANDSCAPE_CW: "Gulsčias (P)"
STR_INVERTED: "Apverstas" STR_INVERTED: "Invertuotas"
STR_ORIENTATION_INVERTED: "Stačias 180°"
STR_LANDSCAPE_CCW: "Gulsčias (A)" STR_LANDSCAPE_CCW: "Gulsčias (A)"
STR_PREV_NEXT: "Atgal/Pirmyn" STR_PREV_NEXT: "Atgal/Pirmyn"
STR_NEXT_PREV: "Pirmyn/Atgal" STR_NEXT_PREV: "Pirmyn/Atgal"
@@ -185,6 +186,9 @@ STR_HOME: "« Pradžia"
STR_SELECT: "Rinktis" STR_SELECT: "Rinktis"
STR_SELECTED: "Pasirinkta" STR_SELECTED: "Pasirinkta"
STR_TOGGLE: "Keisti" STR_TOGGLE: "Keisti"
STR_TOGGLE_BOOKMARK: "Perjungti žymę"
STR_BOOKMARK_REMOVED: "Žymė pašalinta."
STR_HOLD_OPEN_TO_DELETE: "Laikykite Atidaryti, kad ištrintumėte"
STR_CONFIRM: "Gerai" STR_CONFIRM: "Gerai"
STR_CANCEL: "Atšaukti" STR_CANCEL: "Atšaukti"
STR_CONNECT: "Jungtis" STR_CONNECT: "Jungtis"
+5 -1
View File
@@ -137,7 +137,8 @@ STR_PAGE_TURN: "Nast. str."
STR_FORCE_REFRESH: "Odśwież ekran" STR_FORCE_REFRESH: "Odśwież ekran"
STR_PORTRAIT: "Pionowo" STR_PORTRAIT: "Pionowo"
STR_LANDSCAPE_CW: "Poziomo P" STR_LANDSCAPE_CW: "Poziomo P"
STR_INVERTED: "Odwrócony" STR_INVERTED: "Inwersja"
STR_ORIENTATION_INVERTED: "Pionowo 180°"
STR_LANDSCAPE_CCW: "Poziomo L" STR_LANDSCAPE_CCW: "Poziomo L"
STR_PREV_NEXT: "Poprz./Nast." STR_PREV_NEXT: "Poprz./Nast."
STR_NEXT_PREV: "Nast./Poprz." STR_NEXT_PREV: "Nast./Poprz."
@@ -192,6 +193,9 @@ STR_HOME: "« Home"
STR_SELECT: "Wybierz" STR_SELECT: "Wybierz"
STR_SELECTED: "Wybrano" STR_SELECTED: "Wybrano"
STR_TOGGLE: "Zmień" STR_TOGGLE: "Zmień"
STR_TOGGLE_BOOKMARK: "Przełącz zakładkę"
STR_BOOKMARK_REMOVED: "Zakładka usunięta."
STR_HOLD_OPEN_TO_DELETE: "Przytrzymaj Otwórz, aby usunąć"
STR_CONFIRM: "Potwierdź" STR_CONFIRM: "Potwierdź"
STR_CANCEL: "Anuluj" STR_CANCEL: "Anuluj"
STR_CONNECT: "Połącz" STR_CONNECT: "Połącz"
+4
View File
@@ -132,6 +132,7 @@ STR_PAGE_TURN: "Virar página"
STR_PORTRAIT: "Retrato" STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H" STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido" STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Retrato 180°"
STR_LANDSCAPE_CCW: "Paisagem AH" STR_LANDSCAPE_CCW: "Paisagem AH"
STR_PREV_NEXT: "Ant/Próx" STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant" STR_NEXT_PREV: "Próx/Ant"
@@ -182,6 +183,9 @@ STR_EXIT: "« Sair"
STR_HOME: "« Início" STR_HOME: "« Início"
STR_SELECT: "Escolher" STR_SELECT: "Escolher"
STR_TOGGLE: "Alternar" STR_TOGGLE: "Alternar"
STR_TOGGLE_BOOKMARK: "Alternar marcador"
STR_BOOKMARK_REMOVED: "Marcador removido."
STR_HOLD_OPEN_TO_DELETE: "Mantenha Abrir pressionado para excluir"
STR_CONFIRM: "Confirmar" STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar" STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar" STR_CONNECT: "Conectar"
+4
View File
@@ -137,6 +137,7 @@ STR_PAGE_TURN: "Răsfoire pagină"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Orizontal dreapta" STR_LANDSCAPE_CW: "Orizontal dreapta"
STR_INVERTED: "Invers" STR_INVERTED: "Invers"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Orizontal stânga" STR_LANDSCAPE_CCW: "Orizontal stânga"
STR_PREV_NEXT: "Înainte/Înapoi" STR_PREV_NEXT: "Înainte/Înapoi"
STR_NEXT_PREV: "Înapoi/Înainte" STR_NEXT_PREV: "Înapoi/Înainte"
@@ -188,6 +189,9 @@ STR_HOME: "« Acasă"
STR_SELECT: "Selectează" STR_SELECT: "Selectează"
STR_SELECTED: "Selectat" STR_SELECTED: "Selectat"
STR_TOGGLE: "Schimbă" STR_TOGGLE: "Schimbă"
STR_TOGGLE_BOOKMARK: "Comută marcajul"
STR_BOOKMARK_REMOVED: "Marcaj eliminat."
STR_HOLD_OPEN_TO_DELETE: "Țineți apăsat Deschideți pentru a șterge"
STR_CONFIRM: "Confirmă" STR_CONFIRM: "Confirmă"
STR_CANCEL: "Anulare" STR_CANCEL: "Anulare"
STR_CONNECT: "Conectare" STR_CONNECT: "Conectare"
+4 -2
View File
@@ -141,6 +141,7 @@ STR_FORCE_REFRESH: "Обновление экрана"
STR_PORTRAIT: "Портрет" STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Ландшафт (CW)" STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Инверсия" STR_INVERTED: "Инверсия"
STR_ORIENTATION_INVERTED: "Портрет 180°"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)" STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_PREV_NEXT: "Назад/Вперёд" STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад" STR_NEXT_PREV: "Вперёд/Назад"
@@ -181,8 +182,7 @@ STR_DOWNLOADING: "Загрузка..."
STR_DOWNLOAD_FAILED: "Ошибка загрузки" STR_DOWNLOAD_FAILED: "Ошибка загрузки"
STR_ERROR_MSG: "Ошибка:" STR_ERROR_MSG: "Ошибка:"
STR_UNNAMED: "Без имени" STR_UNNAMED: "Без имени"
STR_HOLD_CONFIRM_TO_DELETE: "Удерживайте ОТКРЫТЬ для удаления закладки" STR_HOLD_OPEN_TO_DELETE: "Удерживайте Открыть для удаления"
STR_BOOKMARK_INSTRUCTIONS: "Удерживайте ВЫБРАТЬ для добавления закладки"
STR_NO_SERVER_URL: "URL сервера не настроен" STR_NO_SERVER_URL: "URL сервера не настроен"
STR_FETCH_FEED_FAILED: "Не удалось получить ленту" STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту" STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
@@ -200,6 +200,7 @@ STR_HOME: "« Главная"
STR_SELECT: "Выбрать" STR_SELECT: "Выбрать"
STR_SELECTED: "Выбран" STR_SELECTED: "Выбран"
STR_TOGGLE: "Выбор" STR_TOGGLE: "Выбор"
STR_TOGGLE_BOOKMARK: "Переключить закладку"
STR_CONFIRM: "Подтв." STR_CONFIRM: "Подтв."
STR_CANCEL: "Отмена" STR_CANCEL: "Отмена"
STR_CONNECT: "Подкл." STR_CONNECT: "Подкл."
@@ -262,6 +263,7 @@ STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_BOOKMARKS: "Закладки" STR_BOOKMARKS: "Закладки"
STR_BOOKMARK_ADDED: "Закладка добавлена" STR_BOOKMARK_ADDED: "Закладка добавлена"
STR_BOOKMARK_REMOVED: "Закладка удалена"
STR_OPDS_BROWSER: "OPDS браузер" STR_OPDS_BROWSER: "OPDS браузер"
STR_SEARCH: "Поиск" STR_SEARCH: "Поиск"
STR_COVER_CUSTOM: "Обложка + Свой" STR_COVER_CUSTOM: "Обложка + Свой"
+5 -3
View File
@@ -138,7 +138,8 @@ STR_PAGE_TURN: "Otáčanie stránok"
STR_FORCE_REFRESH: "Obnoviť obrazovku" STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_PORTRAIT: "Na výšku" STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek" STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
STR_INVERTED: "Obrátený" STR_INVERTED: "Invertovaný"
STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek" STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca" STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca" STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
@@ -177,8 +178,7 @@ STR_DOWNLOADING: "Sťahovanie..."
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo" STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
STR_ERROR_MSG: "Chyba:" STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepomenované" STR_UNNAMED: "Nepomenované"
STR_HOLD_CONFIRM_TO_DELETE: "Podrž potvrdiť pre vymazanie" STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
STR_BOOKMARK_INSTRUCTIONS: "Podrž tlačidlo Potvrdiť pre vytvorenie záložky."
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera" STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo" STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo" STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
@@ -196,6 +196,7 @@ STR_HOME: "« Domov"
STR_SELECT: "Vybrať" STR_SELECT: "Vybrať"
STR_SELECTED: "Vybrané" STR_SELECTED: "Vybrané"
STR_TOGGLE: "Prepnúť" STR_TOGGLE: "Prepnúť"
STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
STR_CONFIRM: "Potvrdiť" STR_CONFIRM: "Potvrdiť"
STR_CANCEL: "Zrušiť" STR_CANCEL: "Zrušiť"
STR_CONNECT: "Pripojiť" STR_CONNECT: "Pripojiť"
@@ -258,6 +259,7 @@ STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá" STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
STR_BOOKMARKS: "Záložky" STR_BOOKMARKS: "Záložky"
STR_BOOKMARK_ADDED: "Záložka pridaná." STR_BOOKMARK_ADDED: "Záložka pridaná."
STR_BOOKMARK_REMOVED: "Záložka odstránená."
STR_OPDS_BROWSER: "Prehliadač OPDS" STR_OPDS_BROWSER: "Prehliadač OPDS"
STR_SEARCH: "Hľadať" STR_SEARCH: "Hľadať"
STR_COVER_CUSTOM: "Obálka + Vlastné" STR_COVER_CUSTOM: "Obálka + Vlastné"
+5 -1
View File
@@ -133,7 +133,8 @@ STR_SLEEP: "Spanje"
STR_PAGE_TURN: "Obračanje strani" STR_PAGE_TURN: "Obračanje strani"
STR_PORTRAIT: "Pokončno" STR_PORTRAIT: "Pokončno"
STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)" STR_LANDSCAPE_CW: "Ležeče (v smeri urinega kazalca)"
STR_INVERTED: "Obrnjeno" STR_INVERTED: "Invertirano"
STR_ORIENTATION_INVERTED: "Pokončno 180°"
STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)" STR_LANDSCAPE_CCW: "Ležeče (proti smeri urinega kazalca)"
STR_PREV_NEXT: "Nazaj/Naprej" STR_PREV_NEXT: "Nazaj/Naprej"
STR_NEXT_PREV: "Naprej/Nazaj" STR_NEXT_PREV: "Naprej/Nazaj"
@@ -185,6 +186,9 @@ STR_HOME: "« Domov"
STR_SELECT: "Izberi" STR_SELECT: "Izberi"
STR_SELECTED: "Izbrano" STR_SELECTED: "Izbrano"
STR_TOGGLE: "Preklopi" STR_TOGGLE: "Preklopi"
STR_TOGGLE_BOOKMARK: "Preklopi zaznamek"
STR_BOOKMARK_REMOVED: "Zaznamek odstranjen."
STR_HOLD_OPEN_TO_DELETE: "Držite Odpri za brisanje"
STR_CONFIRM: "Potrdi" STR_CONFIRM: "Potrdi"
STR_CANCEL: "Prekliči" STR_CANCEL: "Prekliči"
STR_CONNECT: "Poveži" STR_CONNECT: "Poveži"
+4 -2
View File
@@ -139,6 +139,7 @@ STR_FORCE_REFRESH: "Refrescar pant."
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horizontal (horario)" STR_LANDSCAPE_CW: "Horizontal (horario)"
STR_INVERTED: "Invertido" STR_INVERTED: "Invertido"
STR_ORIENTATION_INVERTED: "Al revés"
STR_LANDSCAPE_CCW: "Horizontal (antihorario)" STR_LANDSCAPE_CCW: "Horizontal (antihorario)"
STR_PREV_NEXT: "Ant./Sig." STR_PREV_NEXT: "Ant./Sig."
STR_NEXT_PREV: "Sig./Ant." STR_NEXT_PREV: "Sig./Ant."
@@ -179,8 +180,7 @@ STR_DOWNLOADING: "Descargando..."
STR_DOWNLOAD_FAILED: "Fallo de descarga" STR_DOWNLOAD_FAILED: "Fallo de descarga"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Sin nombre" STR_UNNAMED: "Sin nombre"
STR_HOLD_CONFIRM_TO_DELETE: "Mantenga pulsado Confirmar para borrar" STR_HOLD_OPEN_TO_DELETE: "Mantenga pulsado Abrir para borrar"
STR_BOOKMARK_INSTRUCTIONS: "Mantenga pulsado Confirmar en el lector para crear un marcador."
STR_NO_SERVER_URL: "No se configuró URL de servidor" STR_NO_SERVER_URL: "No se configuró URL de servidor"
STR_FETCH_FEED_FAILED: "Fallo al obtener el feed" STR_FETCH_FEED_FAILED: "Fallo al obtener el feed"
STR_PARSE_FEED_FAILED: "Fallo al procesar el feed" STR_PARSE_FEED_FAILED: "Fallo al procesar el feed"
@@ -198,6 +198,7 @@ STR_HOME: "« Inicio"
STR_SELECT: "Selecc." STR_SELECT: "Selecc."
STR_SELECTED: "Seleccionado" STR_SELECTED: "Seleccionado"
STR_TOGGLE: "Cambiar" STR_TOGGLE: "Cambiar"
STR_TOGGLE_BOOKMARK: "Alternar marcador"
STR_CONFIRM: "Confirmar" STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar" STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar" STR_CONNECT: "Conectar"
@@ -261,6 +262,7 @@ STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_BOOKMARKS: "Marcadores" STR_BOOKMARKS: "Marcadores"
STR_BOOKMARK_ADDED: "Marcador añadido." STR_BOOKMARK_ADDED: "Marcador añadido."
STR_BOOKMARK_REMOVED: "Marcador eliminado."
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Buscar" STR_SEARCH: "Buscar"
STR_COVER_CUSTOM: "Portada + Pers." STR_COVER_CUSTOM: "Portada + Pers."
+4 -2
View File
@@ -139,6 +139,7 @@ STR_FORCE_REFRESH: "Uppdatera skärmen"
STR_PORTRAIT: "Porträtt" STR_PORTRAIT: "Porträtt"
STR_LANDSCAPE_CW: "Landskap medurs" STR_LANDSCAPE_CW: "Landskap medurs"
STR_INVERTED: "Inverterad" STR_INVERTED: "Inverterad"
STR_ORIENTATION_INVERTED: "Porträtt 180°"
STR_LANDSCAPE_CCW: "Landskap moturs" STR_LANDSCAPE_CCW: "Landskap moturs"
STR_PREV_NEXT: "Förra/Nästa" STR_PREV_NEXT: "Förra/Nästa"
STR_NEXT_PREV: "Nästa/Förra" STR_NEXT_PREV: "Nästa/Förra"
@@ -177,8 +178,7 @@ STR_DOWNLOADING: "Laddar ner…"
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades" STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
STR_ERROR_MSG: "Fel:" STR_ERROR_MSG: "Fel:"
STR_UNNAMED: "Ej namngiven" STR_UNNAMED: "Ej namngiven"
STR_HOLD_CONFIRM_TO_DELETE: "Håll ned Bekräfta för att radera" STR_HOLD_OPEN_TO_DELETE: "Håll ned Öppna för att radera"
STR_BOOKMARK_INSTRUCTIONS: "Håll Bekräfta i läsaren för att skapa ett bokmärke."
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad" STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde" STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde" STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
@@ -196,6 +196,7 @@ STR_HOME: "« Hem"
STR_SELECT: "Välj " STR_SELECT: "Välj "
STR_SELECTED: "Vald" STR_SELECTED: "Vald"
STR_TOGGLE: "Växla" STR_TOGGLE: "Växla"
STR_TOGGLE_BOOKMARK: "Växla bokmärke"
STR_CONFIRM: "Bekräfta" STR_CONFIRM: "Bekräfta"
STR_CANCEL: "Avbryt" STR_CANCEL: "Avbryt"
STR_CONNECT: "Anslut" STR_CONNECT: "Anslut"
@@ -259,6 +260,7 @@ STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_BOOKMARKS: "Bokmärken" STR_BOOKMARKS: "Bokmärken"
STR_BOOKMARK_ADDED: "Bokmärke tillagt." STR_BOOKMARK_ADDED: "Bokmärke tillagt."
STR_BOOKMARK_REMOVED: "Bokmärke borttaget."
STR_OPDS_BROWSER: "OPDS-webbläsare" STR_OPDS_BROWSER: "OPDS-webbläsare"
STR_SEARCH: "Sök" STR_SEARCH: "Sök"
STR_COVER_CUSTOM: "Omslag + Valfri" STR_COVER_CUSTOM: "Omslag + Valfri"
+5 -1
View File
@@ -131,7 +131,8 @@ STR_SLEEP: "Uyku"
STR_PAGE_TURN: "Sayfa Çevirme" STR_PAGE_TURN: "Sayfa Çevirme"
STR_PORTRAIT: "Dikey" STR_PORTRAIT: "Dikey"
STR_LANDSCAPE_CW: "Yatay (Saat Yönü)" STR_LANDSCAPE_CW: "Yatay (Saat Yönü)"
STR_INVERTED: "Ters" STR_INVERTED: "Negatif"
STR_ORIENTATION_INVERTED: "Dikey 180°"
STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)" STR_LANDSCAPE_CCW: "Yatay (Saat Yönü Tersi)"
STR_PREV_NEXT: "Önceki/Sonraki" STR_PREV_NEXT: "Önceki/Sonraki"
STR_NEXT_PREV: "Sonraki/Önceki" STR_NEXT_PREV: "Sonraki/Önceki"
@@ -182,6 +183,9 @@ STR_EXIT: "« Çıkış"
STR_HOME: "« Ana Sayfa" STR_HOME: "« Ana Sayfa"
STR_SELECT: "Seç" STR_SELECT: "Seç"
STR_TOGGLE: "Değiştir" STR_TOGGLE: "Değiştir"
STR_TOGGLE_BOOKMARK: "Yer imini değiştir"
STR_BOOKMARK_REMOVED: "Yer imi kaldırıldı."
STR_HOLD_OPEN_TO_DELETE: "Silmek için Aç düğmesini basılı tutun"
STR_CONFIRM: "Onayla" STR_CONFIRM: "Onayla"
STR_CANCEL: "İptal" STR_CANCEL: "İptal"
STR_CONNECT: "Bağlan" STR_CONNECT: "Bağlan"
+5 -3
View File
@@ -138,7 +138,8 @@ STR_PAGE_TURN: "Наст. сторінка"
STR_FORCE_REFRESH: "Оновити екран" STR_FORCE_REFRESH: "Оновити екран"
STR_PORTRAIT: "Книжкова" STR_PORTRAIT: "Книжкова"
STR_LANDSCAPE_CW: "Альбом. за год." STR_LANDSCAPE_CW: "Альбом. за год."
STR_INVERTED: "Перевернутий" STR_INVERTED: "Інверсія"
STR_ORIENTATION_INVERTED: "Книжкова 180°"
STR_LANDSCAPE_CCW: "Альбом. проти год." STR_LANDSCAPE_CCW: "Альбом. проти год."
STR_PREV_NEXT: "Попер/Наст" STR_PREV_NEXT: "Попер/Наст"
STR_NEXT_PREV: "Наст/Попер" STR_NEXT_PREV: "Наст/Попер"
@@ -177,8 +178,7 @@ STR_DOWNLOADING: "Завантаження..."
STR_DOWNLOAD_FAILED: "Завантаження не вдалося" STR_DOWNLOAD_FAILED: "Завантаження не вдалося"
STR_ERROR_MSG: "Помилка:" STR_ERROR_MSG: "Помилка:"
STR_UNNAMED: "Без назви" STR_UNNAMED: "Без назви"
STR_HOLD_CONFIRM_TO_DELETE: "Утрим. Підтвердити, щоб Видалити" STR_HOLD_OPEN_TO_DELETE: "Утримуйте Відкрити, щоб видалити"
STR_BOOKMARK_INSTRUCTIONS: "Утрим. Підтвердити в книзі, щоб створ. закладку"
STR_NO_SERVER_URL: "URL сервера не налаштовано" STR_NO_SERVER_URL: "URL сервера не налаштовано"
STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку" STR_FETCH_FEED_FAILED: "Не вдалося отримати стрічку"
STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку" STR_PARSE_FEED_FAILED: "Не вдалося розпарсити стрічку"
@@ -196,6 +196,7 @@ STR_HOME: "« Додому"
STR_SELECT: "Вибрати" STR_SELECT: "Вибрати"
STR_SELECTED: "Вибрано" STR_SELECTED: "Вибрано"
STR_TOGGLE: "Обрати" STR_TOGGLE: "Обрати"
STR_TOGGLE_BOOKMARK: "Перемкнути закладку"
STR_CONFIRM: "Підтвердити" STR_CONFIRM: "Підтвердити"
STR_CANCEL: "Скасувати" STR_CANCEL: "Скасувати"
STR_CONNECT: "Приєдн." STR_CONNECT: "Приєдн."
@@ -258,6 +259,7 @@ STR_SUNLIGHT_FADING_FIX: "Виправлення вицвітання на со
STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки" STR_REMAP_FRONT_BUTTONS: "Налаштувати передні кнопки"
STR_BOOKMARKS: "Закладки" STR_BOOKMARKS: "Закладки"
STR_BOOKMARK_ADDED: "Закладку додано" STR_BOOKMARK_ADDED: "Закладку додано"
STR_BOOKMARK_REMOVED: "Закладку видалено"
STR_OPDS_BROWSER: "Браузер OPDS" STR_OPDS_BROWSER: "Браузер OPDS"
STR_SEARCH: "Пошук" STR_SEARCH: "Пошук"
STR_COVER_CUSTOM: "Обкл. + власне" STR_COVER_CUSTOM: "Обкл. + власне"
+4 -2
View File
@@ -142,6 +142,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
STR_PORTRAIT: "Vertical" STR_PORTRAIT: "Vertical"
STR_LANDSCAPE_CW: "Horitzontal horari" STR_LANDSCAPE_CW: "Horitzontal horari"
STR_INVERTED: "Invertit" STR_INVERTED: "Invertit"
STR_ORIENTATION_INVERTED: "Vertical 180°"
STR_LANDSCAPE_CCW: "Horitzontal antihorari" STR_LANDSCAPE_CCW: "Horitzontal antihorari"
STR_PREV_NEXT: "Anterior/Següent" STR_PREV_NEXT: "Anterior/Següent"
STR_NEXT_PREV: "Següent/Anterior" STR_NEXT_PREV: "Següent/Anterior"
@@ -181,8 +182,7 @@ STR_DOWNLOADING: "S'està baixant..."
STR_DOWNLOAD_FAILED: "Ha fallat la baixada" STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
STR_ERROR_MSG: "Error:" STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Sense nom" STR_UNNAMED: "Sense nom"
STR_HOLD_CONFIRM_TO_DELETE: "Manteniu premut Confirma per esborrar" STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
STR_BOOKMARK_INSTRUCTIONS: "Manteniu premut Confirma al lector per crear un punt de llibre."
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor" STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed" STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed" STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
@@ -198,6 +198,7 @@ STR_HOME: "« Inici"
STR_SELECT: "Selecciona" STR_SELECT: "Selecciona"
STR_SELECTED: "Seleccionat" STR_SELECTED: "Seleccionat"
STR_TOGGLE: "Canvia" STR_TOGGLE: "Canvia"
STR_TOGGLE_BOOKMARK: "Canvia punt de llibre"
STR_CONFIRM: "Confirma" STR_CONFIRM: "Confirma"
STR_CANCEL: "Cancel·la" STR_CANCEL: "Cancel·la"
STR_CONNECT: "Connecta" STR_CONNECT: "Connecta"
@@ -241,6 +242,7 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals" STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_BOOKMARKS: "Punts de llibre" STR_BOOKMARKS: "Punts de llibre"
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre." STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat" STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida" STR_QUICK_RESUME: "Represa ràpida"
+5 -3
View File
@@ -138,7 +138,8 @@ STR_PAGE_TURN: "Lật trang"
STR_FORCE_REFRESH: "Làm tươi màn hình" STR_FORCE_REFRESH: "Làm tươi màn hình"
STR_PORTRAIT: "Dọc" STR_PORTRAIT: "Dọc"
STR_LANDSCAPE_CW: "Ngang (thuận)" STR_LANDSCAPE_CW: "Ngang (thuận)"
STR_INVERTED: "Lật ngược" STR_INVERTED: "Đảo màu"
STR_ORIENTATION_INVERTED: "Dọc 180°"
STR_LANDSCAPE_CCW: "Ngang (ngược)" STR_LANDSCAPE_CCW: "Ngang (ngược)"
STR_PREV_NEXT: "Trước/Sau" STR_PREV_NEXT: "Trước/Sau"
STR_NEXT_PREV: "Sau/Trước" STR_NEXT_PREV: "Sau/Trước"
@@ -177,8 +178,7 @@ STR_DOWNLOADING: "Đang tải về..."
STR_DOWNLOAD_FAILED: "Tải về thất bại" STR_DOWNLOAD_FAILED: "Tải về thất bại"
STR_ERROR_MSG: "Lỗi:" STR_ERROR_MSG: "Lỗi:"
STR_UNNAMED: "Không tên" STR_UNNAMED: "Không tên"
STR_HOLD_CONFIRM_TO_DELETE: "Giữ Xác nhận để xóa" STR_HOLD_OPEN_TO_DELETE: "Giữ Mở để xóa"
STR_BOOKMARK_INSTRUCTIONS: "Giữ Xác nhận trong trình đọc để tạo dấu trang."
STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ" STR_NO_SERVER_URL: "Chưa cấu hình URL máy chủ"
STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp" STR_FETCH_FEED_FAILED: "Không tải được nguồn cấp"
STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp" STR_PARSE_FEED_FAILED: "Không phân tích được nguồn cấp"
@@ -196,6 +196,7 @@ STR_HOME: "« Thư viện"
STR_SELECT: "Chọn" STR_SELECT: "Chọn"
STR_SELECTED: "Đã chọn" STR_SELECTED: "Đã chọn"
STR_TOGGLE: "Bật/Tắt" STR_TOGGLE: "Bật/Tắt"
STR_TOGGLE_BOOKMARK: "Bật/tắt dấu trang"
STR_CONFIRM: "Xác nhận" STR_CONFIRM: "Xác nhận"
STR_CANCEL: "Hủy" STR_CANCEL: "Hủy"
STR_CONNECT: "Kết nối" STR_CONNECT: "Kết nối"
@@ -258,6 +259,7 @@ STR_SUNLIGHT_FADING_FIX: "Khắc phục mờ dưới nắng"
STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước" STR_REMAP_FRONT_BUTTONS: "Gán lại nút mặt trước"
STR_BOOKMARKS: "Dấu trang" STR_BOOKMARKS: "Dấu trang"
STR_BOOKMARK_ADDED: "Đã thêm dấu trang." STR_BOOKMARK_ADDED: "Đã thêm dấu trang."
STR_BOOKMARK_REMOVED: "Đã xóa dấu trang."
STR_OPDS_BROWSER: "Trình duyệt OPDS" STR_OPDS_BROWSER: "Trình duyệt OPDS"
STR_SEARCH: "Tìm kiếm" STR_SEARCH: "Tìm kiếm"
STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh" STR_COVER_CUSTOM: "Ảnh bìa + Tùy chỉnh"
+122 -2
View File
@@ -164,6 +164,7 @@ namespace {
constexpr int MAX_MCU_HEIGHT = 16; constexpr int MAX_MCU_HEIGHT = 16;
constexpr size_t JPEG_DECODER_SIZE = 20 * 1024; constexpr size_t JPEG_DECODER_SIZE = 20 * 1024;
constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024; constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024;
constexpr uint32_t FP_ONE = 1UL << 16;
// Static file pointer for JPEGDEC open callback. // Static file pointer for JPEGDEC open callback.
// Safe in single-threaded embedded context; never accessed concurrently. // Safe in single-threaded embedded context; never accessed concurrently.
@@ -208,6 +209,9 @@ struct BmpConvertCtx {
bool needsScaling; bool needsScaling;
uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point
uint32_t scaleY_fp; uint32_t scaleY_fp;
bool smoothUpscale;
uint32_t smoothScaleX_fp;
uint32_t smoothScaleY_fp;
// Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels) // Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels)
// Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row // Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row
@@ -219,6 +223,13 @@ struct BmpConvertCtx {
std::unique_ptr<uint32_t[]> rowAccum; std::unique_ptr<uint32_t[]> rowAccum;
std::unique_ptr<uint32_t[]> rowCount; std::unique_ptr<uint32_t[]> rowCount;
int smoothNextOutY;
int smoothPrevY;
std::unique_ptr<uint8_t[]> smoothRows;
uint8_t* smoothPrevRow;
uint8_t* smoothCurrRow;
uint8_t* smoothOutRow;
std::unique_ptr<uint8_t[]> bmpRow; std::unique_ptr<uint8_t[]> bmpRow;
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer; std::unique_ptr<AtkinsonDitherer> atkinsonDitherer;
@@ -265,6 +276,88 @@ static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY)
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow); ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
} }
// Matches the progressive-JPEG smoothing used by JpegToFramebufferConverter, but stays
// local because cover generation streams dithered BMP rows instead of framebuffer pixels.
static uint32_t interpolationStep(const int srcSize, const int outSize) {
if (srcSize <= 1 || outSize <= 1) return 0;
return (static_cast<uint32_t>(srcSize - 1) << 16) / static_cast<uint32_t>(outSize - 1);
}
static uint32_t interpolatedSourceFp(const int outIndex, const int outSize, const int srcSize, const uint32_t step) {
if (srcSize <= 1 || outSize <= 1) return 0;
if (outIndex >= outSize - 1) return static_cast<uint32_t>(srcSize - 1) << 16;
return static_cast<uint32_t>(outIndex) * step;
}
static void scaleRowLinear(BmpConvertCtx* ctx, const uint8_t* srcRow, uint8_t* dstRow) {
for (int outX = 0; outX < ctx->outWidth; outX++) {
const uint32_t srcX_fp = interpolatedSourceFp(outX, ctx->outWidth, ctx->srcWidth, ctx->smoothScaleX_fp);
const int x0 = srcX_fp >> 16;
const int x1 = (x0 + 1 < ctx->srcWidth) ? (x0 + 1) : x0;
const uint32_t fx = srcX_fp & (FP_ONE - 1);
dstRow[outX] = static_cast<uint8_t>((srcRow[x0] * (FP_ONE - fx) + srcRow[x1] * fx) >> 16);
}
}
static void writeBlendedRow(BmpConvertCtx* ctx, const uint8_t* row0, const uint8_t* row1, const uint32_t fy,
const int outY) {
const uint32_t invFy = FP_ONE - fy;
for (int outX = 0; outX < ctx->outWidth; outX++) {
ctx->smoothOutRow[outX] = static_cast<uint8_t>((row0[outX] * invFy + row1[outX] * fy) >> 16);
}
writeOutputRow(ctx, ctx->smoothOutRow, outY);
}
static void processSmoothSourceRow(BmpConvertCtx* ctx, const uint8_t* srcRow, const int srcY) {
scaleRowLinear(ctx, srcRow, ctx->smoothCurrRow);
if (ctx->smoothPrevY < 0) {
uint8_t* tmp = ctx->smoothPrevRow;
ctx->smoothPrevRow = ctx->smoothCurrRow;
ctx->smoothCurrRow = tmp;
ctx->smoothPrevY = srcY;
if (ctx->srcHeight <= 1) {
while (ctx->smoothNextOutY < ctx->outHeight) {
writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY);
ctx->smoothNextOutY++;
}
return;
}
return;
}
while (ctx->smoothNextOutY < ctx->outHeight) {
const uint32_t srcY_fp =
interpolatedSourceFp(ctx->smoothNextOutY, ctx->outHeight, ctx->srcHeight, ctx->smoothScaleY_fp);
const int y0 = srcY_fp >> 16;
const int y1 = (y0 + 1 < ctx->srcHeight) ? (y0 + 1) : y0;
if (y1 > srcY) break;
const uint8_t* row0 = (y0 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow;
const uint8_t* row1 = (y1 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow;
writeBlendedRow(ctx, row0, row1, srcY_fp & (FP_ONE - 1), ctx->smoothNextOutY);
ctx->smoothNextOutY++;
}
uint8_t* tmp = ctx->smoothPrevRow;
ctx->smoothPrevRow = ctx->smoothCurrRow;
ctx->smoothCurrRow = tmp;
ctx->smoothPrevY = srcY;
}
static void finishSmoothUpscale(BmpConvertCtx* ctx) {
if (ctx->smoothPrevY < 0) {
LOG_ERR("JPG", "No progressive rows decoded for smoothing");
ctx->error = true;
return;
}
while (ctx->smoothNextOutY < ctx->outHeight) {
writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY);
ctx->smoothNextOutY++;
}
}
// Flush one scaled output row from Y-axis accumulators and advance currentOutY // Flush one scaled output row from Y-axis accumulators and advance currentOutY
static void flushScaledRow(BmpConvertCtx* ctx) { static void flushScaledRow(BmpConvertCtx* ctx) {
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow); memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
@@ -344,7 +437,9 @@ int bmpDrawCallback(JPEGDRAW* pDraw) {
for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) { for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) {
const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth; const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth;
if (!ctx->needsScaling) { if (ctx->smoothUpscale) {
processSmoothSourceRow(ctx, srcRow, y);
} else if (!ctx->needsScaling) {
// 1:1 — outWidth == srcWidth, write directly // 1:1 — outWidth == srcWidth, write directly
writeOutputRow(ctx, srcRow, y); writeOutputRow(ctx, srcRow, y);
} else { } else {
@@ -472,6 +567,9 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
needsScaling = true; needsScaling = true;
} }
const bool smoothUpscale =
progressiveDecode && needsScaling && scaleSrcWidth <= outWidth && scaleSrcHeight <= outHeight;
// Write BMP header with output dimensions // Write BMP header with output dimensions
int bytesPerRow; int bytesPerRow;
if (USE_8BIT_OUTPUT && !oneBit) { if (USE_8BIT_OUTPUT && !oneBit) {
@@ -496,6 +594,11 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
ctx.needsScaling = needsScaling; ctx.needsScaling = needsScaling;
ctx.scaleX_fp = scaleX_fp; ctx.scaleX_fp = scaleX_fp;
ctx.scaleY_fp = scaleY_fp; ctx.scaleY_fp = scaleY_fp;
ctx.smoothUpscale = smoothUpscale;
ctx.smoothScaleX_fp = interpolationStep(ctx.srcWidth, outWidth);
ctx.smoothScaleY_fp = interpolationStep(ctx.srcHeight, outHeight);
ctx.smoothNextOutY = 0;
ctx.smoothPrevY = -1;
ctx.error = false; ctx.error = false;
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale // MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale
@@ -512,7 +615,20 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
return false; return false;
} }
if (needsScaling) { if (smoothUpscale) {
// One contiguous allocation avoids three heap blocks while keeping smoothing line-buffered.
const size_t smoothRowsBytes = static_cast<size_t>(outWidth) * 3;
ctx.smoothRows = makeUniqueNoThrow<uint8_t[]>(smoothRowsBytes);
if (!ctx.smoothRows) {
LOG_ERR("JPG", "OOM: progressive smoothing buffers");
return false;
}
ctx.smoothPrevRow = ctx.smoothRows.get();
ctx.smoothCurrRow = ctx.smoothPrevRow + outWidth;
ctx.smoothOutRow = ctx.smoothCurrRow + outWidth;
LOG_DBG("JPG", "Progressive smoothing: %dx%d -> %dx%d, buffers=%u bytes", ctx.srcWidth, ctx.srcHeight, outWidth,
outHeight, static_cast<unsigned>(smoothRowsBytes));
} else if (needsScaling) {
ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth); ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth);
ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth); ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth);
if (!ctx.rowAccum || !ctx.rowCount) { if (!ctx.rowAccum || !ctx.rowCount) {
@@ -549,6 +665,10 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
rc = jpeg->decode(0, 0, 0); rc = jpeg->decode(0, 0, 0);
if (rc == 1 && ctx.smoothUpscale && !ctx.error) {
finishSmoothUpscale(&ctx);
}
if (rc != 1 || ctx.error) { if (rc != 1 || ctx.error) {
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
return false; return false;
+21
View File
@@ -81,6 +81,27 @@ void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* m
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
} }
void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) {
// X3: a HALF fallback means the caller wants a clean base (e.g. the sleep
// cover, a full-screen swap from arbitrary prior content). Without this, the
// X3 grayscale base takes its gentle differential happy path and the prior
// home/reader frame ghosts through the soft aa_pre_bw_mid waveform. Forcing a
// resync makes displayGrayscaleBase clear first, matching displayBuffer(HALF).
// The reader's FAST path is deliberately left on the differential path so
// per-page grayscale stays cheap.
if (gpio.deviceIsX3() && fallback == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen);
}
void HalDisplay::preconditionGrayscale() { einkDisplay.preconditionGrayscale(); }
void HalDisplay::preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
einkDisplay.preconditionGrayscale(x, y, w, h);
}
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); } void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); }
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); } void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); }
+13
View File
@@ -47,6 +47,19 @@ class HalDisplay {
// Access to frame buffer // Access to frame buffer
uint8_t* getFrameBuffer() const; uint8_t* getFrameBuffer() const;
// X3 grayscale preconditioning (OEM "AA-pre-BW(mid)" settle pass), windowed
// to the gray region in physical panel coordinates (no-arg = full frame).
// Call after the BW base frame is displayed and before the grayscale planes
// are written; no-op on X4. See EInkDisplay::preconditionGrayscale.
void preconditionGrayscale();
void preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
// Display the framebuffer as the base frame for a grayscale overlay that
// follows. On X3, HALF fallback first requests a resync to match
// displayBuffer(HALF); FAST fallback keeps the OEM differential base waveform
// ("AA-pre-BW(mid)"). Other panels display normally with `fallback` mode.
void displayGrayscaleBase(RefreshMode fallback = HALF_REFRESH, bool turnOffScreen = false);
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer); void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer); void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
+3 -1
View File
@@ -4,7 +4,7 @@ build_cache_dir = .cache
extra_configs = platformio.local.ini extra_configs = platformio.local.ini
[crosspoint] [crosspoint]
version = 1.3.0 version = 1.4.1
[base] [base]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
@@ -35,6 +35,8 @@ build_flags =
# Increase PNG scanline buffer to support up to 2048px wide images # Increase PNG scanline buffer to support up to 2048px wide images
# Default is (320*4+1)*2=2562, we need more for larger images # Default is (320*4+1)*2=2562, we need more for larger images
-DPNG_MAX_BUFFERED_PIXELS=16416 -DPNG_MAX_BUFFERED_PIXELS=16416
-DFREEINK_DEVICE_X4=1
-DFREEINK_DEVICE_X3=1
-Wno-bidi-chars -Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity -Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
-fno-exceptions -fno-exceptions
+5 -3
View File
@@ -65,6 +65,8 @@ class CrossPointSettings {
XTC_STATUS_BAR_MODE_COUNT XTC_STATUS_BAR_MODE_COUNT
}; };
enum STATUS_BAR_CLOCK_MODE { STATUS_BAR_CLOCK_HIDE = 0, STATUS_BAR_CLOCK_RIGHT = 1, STATUS_BAR_CLOCK_LEFT = 2 };
enum ORIENTATION { enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default) PORTRAIT = 0, // 480x800 logical coordinates (current default)
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
@@ -188,7 +190,7 @@ class CrossPointSettings {
uint8_t statusBarBattery = 1; uint8_t statusBarBattery = 1;
uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE; uint8_t xtcStatusBarMode = XTC_STATUS_BAR_HIDE;
// Clock display in status bar (X3 only, requires DS3231 RTC) // Clock display in status bar (X3 only, requires DS3231 RTC)
uint8_t statusBarClock = 0; uint8_t statusBarClock = STATUS_BAR_CLOCK_HIDE;
// Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t. // Clock UTC offset in quarter-hour steps, biased by 48 so it fits in uint8_t.
// Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00. // Value 48 = UTC+0, 0 = UTC-12:00, 104 = UTC+14:00.
// Quarter-hour granularity supports oddball zones like Nepal (+5:45) and Chatham (+12:45). // Quarter-hour granularity supports oddball zones like Nepal (+5:45) and Chatham (+12:45).
@@ -238,8 +240,8 @@ class CrossPointSettings {
// Long-press page turn button behavior // Long-press page turn button behavior
uint8_t longPressButtonBehavior = OFF; uint8_t longPressButtonBehavior = OFF;
// Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values). // Long-press Confirm function in EPUB reader (cycles through LONG_PRESS_MENU_FUNCTION values).
// Defaults to Bookmark to preserve the upstream long-press-Confirm-adds-bookmark behavior. // Defaults to Disabled so shortcut-based bookmark toggling remains opt-in.
uint8_t longPressMenuFunction = LP_MENU_BOOKMARK; uint8_t longPressMenuFunction = LP_MENU_DISABLED;
// UI Theme // UI Theme
uint8_t uiTheme = LYRA; uint8_t uiTheme = LYRA;
// Sunlight fading compensation // Sunlight fading compensation
+6
View File
@@ -423,6 +423,9 @@ bool JsonSettingsIO::saveBookmarks(const std::vector<BookmarkEntry>& bookmarks,
obj["xpath"] = bookmark.xpath; obj["xpath"] = bookmark.xpath;
obj["percentage"] = bookmark.percentage; obj["percentage"] = bookmark.percentage;
obj["summary"] = bookmark.summary; obj["summary"] = bookmark.summary;
obj["si"] = bookmark.computedSpineIndex;
obj["pc"] = bookmark.computedChapterPageCount;
obj["pp"] = bookmark.computedChapterProgress;
} }
String json; String json;
@@ -447,6 +450,9 @@ bool JsonSettingsIO::loadBookmarks(std::vector<BookmarkEntry>& bookmarks, const
bookmark.xpath = obj["xpath"] | std::string(""); bookmark.xpath = obj["xpath"] | std::string("");
bookmark.percentage = obj["percentage"] | static_cast<float>(0); bookmark.percentage = obj["percentage"] | static_cast<float>(0);
bookmark.summary = obj["summary"] | std::string(""); bookmark.summary = obj["summary"] | std::string("");
bookmark.computedSpineIndex = obj["si"] | static_cast<uint16_t>(0);
bookmark.computedChapterPageCount = obj["pc"] | static_cast<uint16_t>(0);
bookmark.computedChapterProgress = obj["pp"] | static_cast<uint16_t>(0);
} }
LOG_DBG("BKM", "Loaded %zu bookmarks from file", bookmarks.size()); LOG_DBG("BKM", "Loaded %zu bookmarks from file", bookmarks.size());
+7 -5
View File
@@ -153,9 +153,10 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER), StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, SettingInfo::Enum(
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
"orientation", StrId::STR_CAT_READER), {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_ORIENTATION_INVERTED, StrId::STR_LANDSCAPE_CCW},
"orientation", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing,
"extraParagraphSpacing", StrId::STR_CAT_READER), "extraParagraphSpacing", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
@@ -246,8 +247,9 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
StrId::STR_CUSTOMISE_STATUS_BAR), StrId::STR_CUSTOMISE_STATUS_BAR),
// Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset). // Clock entries (web settings only; device UI uses ClockOffsetActivity for the offset).
// Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48. // Range 0..104 = quarter-hour steps from UTC-12:00 to UTC+14:00, biased by 48.
SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", SettingInfo::Enum(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock,
StrId::STR_CUSTOMISE_STATUS_BAR), {StrId::STR_HIDE, StrId::STR_DIR_LEFT, StrId::STR_DIR_RIGHT}, "statusBarClock",
StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1}, SettingInfo::Value(StrId::STR_CLOCK_UTC_OFFSET, &CrossPointSettings::clockUtcOffsetQ, {0, 104, 1},
"clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR), "clockUtcOffsetQ", StrId::STR_CUSTOMISE_STATUS_BAR),
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat, SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat,
+9 -1
View File
@@ -218,7 +218,15 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
renderer.invertScreen(); renderer.invertScreen();
} }
renderer.displayBuffer(HalDisplay::HALF_REFRESH); if (hasGreyscale) {
// OEM grayscale pipeline base: on X3 this displays the frame with the
// dedicated "AA-pre-BW(mid)" differential waveform, leaving every pixel
// in the calibrated state the gray nudge refresh expects; on X4 it is a
// plain HALF refresh (previous behavior).
renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH);
} else {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
if (hasGreyscale) { if (hasGreyscale) {
bitmap.rewindToData(); bitmap.rewindToData();
+146 -49
View File
@@ -41,6 +41,8 @@ namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
// pages per minute, first item is 1 to prevent division by zero if accessed // pages per minute, first item is 1 to prevent division by zero if accessed
constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12}; constexpr int PAGE_TURN_RATES[] = {1, 1, 3, 6, 12};
constexpr size_t initialBookmarkCacheCapacity = 16;
constexpr float bookmarkProgressEpsilon = 0.0001f;
int clampPercent(int percent) { int clampPercent(int percent) {
if (percent < 0) { if (percent < 0) {
@@ -63,6 +65,36 @@ bool isInReadFolder(const std::string& path) {
return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/'; return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/';
} }
struct ProgressRange {
float start;
float end;
};
ProgressRange getPageProgressRange(const std::shared_ptr<Epub>& epub, const int spineIndex, const int page,
const int pageCount) {
if (pageCount <= 1) {
return {epub->calculateProgress(spineIndex, 0.0f), epub->calculateProgress(spineIndex, 1.0f)};
}
const float step = 1.0f / static_cast<float>(pageCount - 1);
const float anchor = std::clamp(static_cast<float>(page) * step, 0.0f, 1.0f);
const float start = std::max(0.0f, anchor - (step * 0.5f));
const float end = std::min(1.0f, anchor + (step * 0.5f));
return {epub->calculateProgress(spineIndex, start), epub->calculateProgress(spineIndex, end)};
}
bool bookmarkMatchesProgress(const BookmarkEntry& bookmark, const int spineIndex, const int page, const int pageCount,
const ProgressRange& pageRange) {
if (bookmark.computedSpineIndex == spineIndex && bookmark.computedChapterPageCount == pageCount &&
bookmark.computedChapterProgress == page) {
return true;
}
const float bookmarkProgress = std::clamp(bookmark.percentage, 0.0f, 1.0f);
return bookmarkProgress + bookmarkProgressEpsilon >= pageRange.start &&
bookmarkProgress - bookmarkProgressEpsilon <= pageRange.end;
}
// Pick a non-colliding destination path inside /Read/ for a finished book. // Pick a non-colliding destination path inside /Read/ for a finished book.
// Mirrors the suffixing scheme used elsewhere: "name.epub" -> "name (2).epub", etc. // Mirrors the suffixing scheme used elsewhere: "name.epub" -> "name (2).epub", etc.
std::string buildReadFolderDestination(const std::string& srcPath) { std::string buildReadFolderDestination(const std::string& srcPath) {
@@ -165,6 +197,8 @@ void EpubReaderActivity::onEnter() {
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath()); RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
loadCachedBookmarks();
// Trigger first update // Trigger first update
requestUpdate(); requestUpdate();
} }
@@ -177,6 +211,14 @@ void EpubReaderActivity::onExit() {
APP_STATE.readerActivityLoadCount = 0; APP_STATE.readerActivityLoadCount = 0;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
// Leaving mid-footnote loses the in-RAM return stack on deep sleep; persist the
// pre-footnote position so the book reopens at the link origin, not the footnote.
if (footnoteDepth > 0 && epub) {
const SavedPosition& origin = savedPositions[0];
saveProgress(origin.spineIndex, origin.pageNumber, 0);
}
section.reset(); section.reset();
if (pendingReadFolderMove && epub) { if (pendingReadFolderMove && epub) {
const std::string srcPath = epub->getPath(); const std::string srcPath = epub->getPath();
@@ -277,7 +319,7 @@ void EpubReaderActivity::loop() {
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
startActivityForResult(std::make_unique<EpubReaderMenuActivity>( startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty()), SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()),
[this](const ActivityResult& result) { [this](const ActivityResult& result) {
// Always apply orientation change even if the menu was cancelled // Always apply orientation change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data); const auto& menu = std::get<MenuResult>(result.data);
@@ -499,6 +541,7 @@ void EpubReaderActivity::jumpToPercent(int percent) {
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
auto progressChangeResultHandler = [this](const ActivityResult& result) { auto progressChangeResultHandler = [this](const ActivityResult& result) {
loadCachedBookmarks();
if (!result.isCancelled) { if (!result.isCancelled) {
const auto& sync = std::get<ProgressChangeResult>(result.data); const auto& sync = std::get<ProgressChangeResult>(result.data);
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
@@ -614,6 +657,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
progressChangeResultHandler); progressChangeResultHandler);
break; break;
} }
case EpubReaderMenuActivity::MenuAction::TOGGLE_BOOKMARK: {
addBookmark();
break;
}
} }
} }
@@ -900,6 +947,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
return; return;
} }
updateBookmarkFlag();
{ {
auto p = section->loadPageFromSectionFile(); auto p = section->loadPageFromSectionFile();
if (!p) { if (!p) {
@@ -931,7 +980,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
if (showBookmarkMessage) { if (showBookmarkMessage) {
GUI.drawPopup(renderer, tr(STR_BOOKMARK_ADDED)); GUI.drawPopup(renderer, bookmarkRemoved ? tr(STR_BOOKMARK_REMOVED) : tr(STR_BOOKMARK_ADDED));
} }
} }
@@ -974,22 +1023,31 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) { const int orientedMarginLeft) {
const auto t0 = millis(); const auto t0 = millis();
const int fontId = SETTINGS.getReaderFontId();
// Font prewarm: scan pass accumulates text, then prewarm, then real render // Font prewarm: scan pass accumulates text, then prewarm, then real render
auto* fcm = renderer.getFontCacheManager(); auto* fcm = renderer.getFontCacheManager();
auto scope = fcm->createPrewarmScope(); auto scope = fcm->createPrewarmScope();
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); // scan pass
scope.endScanAndPrewarm(); scope.endScanAndPrewarm();
const auto tPrewarm = millis(); const auto tPrewarm = millis();
// Force special handling for pages with images when anti-aliasing is on const bool pageHasImages = page->hasImages();
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing; const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
auto renderGrayscalePass = [&]() {
if (needsTextGrayscale) {
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
} else {
page->renderImages(renderer, fontId, orientedMarginLeft, orientedMarginTop);
}
};
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
renderStatusBar(); renderStatusBar();
const auto tBwRender = millis(); const auto tBwRender = millis();
if (imagePageWithAA) { if (pageHasImages) {
// Double FAST_REFRESH with selective image blanking (pablohc's technique): // Double FAST_REFRESH with selective image blanking (pablohc's technique):
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust. // HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
// Instead, blank only the image area and do two fast refreshes. // Instead, blank only the image area and do two fast refreshes.
@@ -1002,7 +1060,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// Re-render page content to restore images into the blanked area // Re-render page content to restore images into the blanked area
// Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %) // Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %)
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
renderer.displayBuffer(HalDisplay::FAST_REFRESH); renderer.displayBuffer(HalDisplay::FAST_REFRESH);
} else { } else {
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
@@ -1026,7 +1084,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// per plane, but renderCharImpl culls out-of-band glyphs before decode so the // per plane, but renderCharImpl culls out-of-band glyphs before decode so the
// cost stays close to one render. Both text (drawPixel) and images // cost stays close to one render. Both text (drawPixel) and images
// (DirectPixelWriter) honor the active strip target. // (DirectPixelWriter) honor the active strip target.
if (SETTINGS.textAntiAliasing && renderer.supportsStripGrayscale()) { if (needsAnyGrayscale && renderer.supportsStripGrayscale()) {
constexpr int STRIP_ROWS = 80; constexpr int STRIP_ROWS = 80;
const int gh = renderer.getDisplayHeight(); const int gh = renderer.getDisplayHeight();
const int gwBytes = renderer.getDisplayWidthBytes(); const int gwBytes = renderer.getDisplayWidthBytes();
@@ -1042,7 +1100,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
renderer.beginStripTarget(scratch.get(), y, rows); renderer.beginStripTarget(scratch.get(), y, rows);
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderGrayscalePass();
renderer.endStripTarget(); renderer.endStripTarget();
renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows); renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows);
} }
@@ -1054,7 +1112,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS; const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
renderer.beginStripTarget(scratch.get(), y, rows); renderer.beginStripTarget(scratch.get(), y, rows);
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderGrayscalePass();
renderer.endStripTarget(); renderer.endStripTarget();
renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows); renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows);
} }
@@ -1079,22 +1137,28 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
} else { } else {
// Fallback path for a controller without strip support. grayscale rendering // Fallback path for a controller without strip support. grayscale rendering
// TODO: Only do this if font supports it // TODO: Only do this if font supports it
if (SETTINGS.textAntiAliasing) { if (needsAnyGrayscale) {
// Save the BW frame before the grayscale passes overwrite it, restore // Save the BW frame before the grayscale passes overwrite it, restore
// after. Only needed when grayscale actually renders. // after. Only needed when grayscale actually renders.
renderer.storeBwBuffer(); if (!renderer.storeBwBuffer()) {
LOG_ERR("ERS", "Failed to store BW buffer for grayscale render; skipping grayscale this page");
const auto tEnd = millis();
LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0,
tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0);
return;
}
const auto tBwStore = millis(); const auto tBwStore = millis();
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderGrayscalePass();
renderer.copyGrayscaleLsbBuffers(); renderer.copyGrayscaleLsbBuffers();
const auto tGrayLsb = millis(); const auto tGrayLsb = millis();
// Render and copy to MSB buffer // Render and copy to MSB buffer
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderGrayscalePass();
renderer.copyGrayscaleMsbBuffers(); renderer.copyGrayscaleMsbBuffers();
const auto tGrayMsb = millis(); const auto tGrayMsb = millis();
@@ -1112,8 +1176,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore,
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
} else { } else {
// No anti-aliasing: BW frame already displayed above, no grayscale to // No text AA and no images: BW frame already displayed above, no grayscale
// render, so no save/restore. // to render, so no save/restore.
const auto tEnd = millis(); const auto tEnd = millis();
LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0, LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums total=%lums", tPrewarm - t0,
tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0); tBwRender - tPrewarm, tDisplay - tBwRender, tEnd - t0);
@@ -1155,7 +1219,7 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle(); title = epub->getTitle();
} }
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset); GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked);
} }
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
@@ -1217,11 +1281,31 @@ void EpubReaderActivity::restoreSavedPosition() {
requestUpdate(); requestUpdate();
} }
void EpubReaderActivity::loadCachedBookmarks() {
cachedBookmarks.clear();
if (cachedBookmarks.capacity() < initialBookmarkCacheCapacity) {
cachedBookmarks.reserve(initialBookmarkCacheCapacity);
}
if (!epub) {
currentPageBookmarked = false;
return;
}
const std::string bmPath = BookmarkUtil::getBookmarkPath(epub->getPath());
if (Storage.exists(bmPath.c_str())) {
String json = Storage.readFile(bmPath.c_str());
if (!json.isEmpty()) {
JsonSettingsIO::loadBookmarks(cachedBookmarks, json.c_str());
}
}
updateBookmarkFlag();
}
void EpubReaderActivity::addBookmark() { void EpubReaderActivity::addBookmark() {
if (!section || !epub) { if (!section || !epub) {
return; return;
} }
LOG_DBG("ERS", "Adding bookmark at spine %d, page %d", currentSpineIndex, section ? section->currentPage : -1); LOG_DBG("ERS", "Toggle bookmark at spine %d, page %d", currentSpineIndex, section ? section->currentPage : -1);
int currentPage; int currentPage;
int pageCount; int pageCount;
{ {
@@ -1230,45 +1314,58 @@ void EpubReaderActivity::addBookmark() {
currentPage = section->currentPage; currentPage = section->currentPage;
} }
std::string pageText; SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
if (currentPage >= 0 && currentPage < pageCount) { const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, currentPage, pageCount);
pageText = section->getTextFromSectionFile();
const size_t bookmarkCountBeforeToggle = cachedBookmarks.size();
cachedBookmarks.erase(std::remove_if(cachedBookmarks.begin(), cachedBookmarks.end(),
[&](const BookmarkEntry& b) {
return bookmarkMatchesProgress(b, currentSpineIndex, currentPage, pageCount,
pageRange);
}),
cachedBookmarks.end());
if (cachedBookmarks.size() != bookmarkCountBeforeToggle) {
bookmarkRemoved = true;
currentPageBookmarked = false;
} else {
std::string pageText;
if (currentPage >= 0 && currentPage < pageCount) {
pageText = section->getTextFromSectionFile();
}
BookmarkEntry entry;
entry.percentage = progress.percentage;
entry.xpath = progress.xpath;
entry.summary = BookmarkUtil::sanitizeBookmarkSummary(pageText);
entry.computedSpineIndex = currentSpineIndex;
entry.computedChapterPageCount = pageCount;
entry.computedChapterProgress = currentPage;
cachedBookmarks.insert(cachedBookmarks.begin(), entry);
bookmarkRemoved = false;
currentPageBookmarked = true;
} }
SavedProgressPosition progress = ProgressMapper::toSavedProgress(epub, getCurrentPosition());
BookmarkEntry entry;
entry.percentage = progress.percentage;
entry.xpath = progress.xpath;
entry.summary = BookmarkUtil::sanitizeBookmarkSummary(pageText);
// Add bookmark
const std::string path = BookmarkUtil::getBookmarkPath(epub->getPath()); const std::string path = BookmarkUtil::getBookmarkPath(epub->getPath());
LOG_DBG("ERS", "Bookmark path: %s", path.c_str());
const std::string bookmarksDir = BookmarkUtil::getBookmarksDir(); const std::string bookmarksDir = BookmarkUtil::getBookmarksDir();
Storage.mkdir(bookmarksDir.c_str()); Storage.mkdir(bookmarksDir.c_str());
std::vector<BookmarkEntry> bookmarks; const bool ok = JsonSettingsIO::saveBookmarks(cachedBookmarks, path.c_str());
if (Storage.exists(path.c_str())) { if (!ok) {
LOG_DBG("ERS", "Existing bookmark file found, loading bookmarks"); LOG_ERR("ERS", "Failed to save bookmarks to: %s", path.c_str());
String json = Storage.readFile(path.c_str());
if (!json.isEmpty()) {
JsonSettingsIO::loadBookmarks(bookmarks, json.c_str());
}
} else {
LOG_DBG("ERS", "No existing bookmark file, starting with empty bookmark list");
} }
bookmarks.insert(bookmarks.begin(), entry);
LOG_DBG("ERS", "Saving bookmark to file: %s", path.c_str());
const bool ok = JsonSettingsIO::saveBookmarks(bookmarks, path.c_str());
if (ok) {
showBookmarkMessage = true;
} else {
LOG_ERR("ERS", "Failed to save bookmark to: %s", path.c_str());
}
requestUpdate(); requestUpdate();
} }
void EpubReaderActivity::updateBookmarkFlag() {
if (!section || !epub || cachedBookmarks.empty()) {
currentPageBookmarked = false;
return;
}
const ProgressRange pageRange =
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, section->pageCount, pageRange);
});
}
ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const { ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const {
ScreenshotInfo info; ScreenshotInfo info;
info.readerType = ScreenshotInfo::ReaderType::Epub; info.readerType = ScreenshotInfo::ReaderType::Epub;
@@ -5,6 +5,7 @@
#include <optional> #include <optional>
#include "BookmarkEntry.h"
#include "EpubReaderMenuActivity.h" #include "EpubReaderMenuActivity.h"
#include "ProgressMapper.h" #include "ProgressMapper.h"
#include "activities/Activity.h" #include "activities/Activity.h"
@@ -34,6 +35,9 @@ class EpubReaderActivity final : public Activity {
bool automaticPageTurnActive = false; bool automaticPageTurnActive = false;
bool showBookmarkMessage = false; bool showBookmarkMessage = false;
bool ignoreNextConfirmRelease = false; bool ignoreNextConfirmRelease = false;
bool currentPageBookmarked = false;
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
std::vector<BookmarkEntry> cachedBookmarks;
// Tracks whether this book is currently removed from Recent Books by the // Tracks whether this book is currently removed from Recent Books by the
// removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in). // removeReadBooksFromRecents feature (set at End-of-Book, cleared if paged back in).
bool recentsEntryRemoved = false; bool recentsEntryRemoved = false;
@@ -66,7 +70,9 @@ class EpubReaderActivity final : public Activity {
void applyOrientation(uint8_t orientation); void applyOrientation(uint8_t orientation);
void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
void pageTurn(bool isForwardTurn); void pageTurn(bool isForwardTurn);
void loadCachedBookmarks();
void addBookmark(); void addBookmark();
void updateBookmarkFlag();
// Footnote navigation // Footnote navigation
void navigateToHref(const std::string& href, bool savePosition = false); void navigateToHref(const std::string& href, bool savePosition = false);
@@ -39,14 +39,6 @@ void EpubReaderBookmarksActivity::onEnter() {
bookmarks.shrink_to_fit(); bookmarks.shrink_to_fit();
} else { } else {
JsonSettingsIO::loadBookmarks(bookmarks, json.c_str()); JsonSettingsIO::loadBookmarks(bookmarks, json.c_str());
// pre-compute bookmark page values for quicker rendering
for (auto& bookmark : bookmarks) {
CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer);
bookmark.computedSpineIndex = pos.spineIndex;
bookmark.computedChapterPageCount = pos.totalPages;
bookmark.computedChapterProgress = pos.pageNumber;
}
} }
} else { } else {
LOG_DBG("EPB", "No bookmark file found at %s, starting with empty bookmarks", path.c_str()); LOG_DBG("EPB", "No bookmark file found at %s, starting with empty bookmarks", path.c_str());
@@ -93,6 +85,14 @@ void EpubReaderBookmarksActivity::loop() {
selectorIndex--; selectorIndex--;
} }
if (bookmarks.empty()) {
ActivityResult result;
result.isCancelled = true;
setResult(std::move(result));
finish();
return;
}
requestUpdate(); requestUpdate();
confirmingDelete = DELETE_MODE_OFF; confirmingDelete = DELETE_MODE_OFF;
return; return;
@@ -203,9 +203,12 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) {
auto bookmark = bookmarks.at(confirmingDelete >= DELETE_MODE_DISPLAY ? selectorIndex : index); auto bookmark = bookmarks.at(confirmingDelete >= DELETE_MODE_DISPLAY ? selectorIndex : index);
auto tocIndex = epub->getTocIndexForSpineIndex(bookmark.computedSpineIndex); auto tocIndex = epub->getTocIndexForSpineIndex(bookmark.computedSpineIndex);
auto tocTitle = (tocIndex >= 0) ? (epub->getTocItem(tocIndex)).title : tr(STR_UNNAMED); auto tocTitle = (tocIndex >= 0) ? (epub->getTocItem(tocIndex)).title : tr(STR_UNNAMED);
return std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - " + std::string subtitle = std::to_string((int)(std::clamp(bookmark.percentage, 0.0f, 1.0f) * 100.0f + 0.5f)) + "% - ";
std::to_string(bookmark.computedChapterProgress + 1) + "/" + if (bookmark.computedChapterPageCount > 0) {
std::to_string(bookmark.computedChapterPageCount) + " - " + tocTitle; subtitle += std::to_string(bookmark.computedChapterProgress + 1) + "/" +
std::to_string(bookmark.computedChapterPageCount) + " - ";
}
return subtitle + tocTitle;
}; };
const auto getBookmarkIcon = [isPortrait](int index) { const auto getBookmarkIcon = [isPortrait](int index) {
// only enabled icon in portrait mode due to limitation with rotating icons for other orientations // only enabled icon in portrait mode due to limitation with rotating icons for other orientations
@@ -225,11 +228,8 @@ void EpubReaderBookmarksActivity::render(RenderLock&&) {
getBookmarkTitle, getBookmarkSubtitle, getBookmarkIcon); getBookmarkTitle, getBookmarkSubtitle, getBookmarkIcon);
GUI.drawHelpText(renderer, Rect{contentX, pageHeight - hintGutterBottom, contentWidth, LINE_HEIGHT}, GUI.drawHelpText(renderer, Rect{contentX, pageHeight - hintGutterBottom, contentWidth, LINE_HEIGHT},
tr(STR_HOLD_CONFIRM_TO_DELETE)); tr(STR_HOLD_OPEN_TO_DELETE));
} }
} else {
GUI.drawHelpText(renderer, Rect{contentX, LINE_HEIGHT * 2, contentWidth, LINE_HEIGHT},
tr(STR_BOOKMARK_INSTRUCTIONS));
} }
const auto backLabel = confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_CANCEL) : tr(STR_BACK); const auto backLabel = confirmingDelete >= DELETE_MODE_DISPLAY ? tr(STR_CANCEL) : tr(STR_BACK);
@@ -10,23 +10,27 @@
EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::string& title, const int currentPage, const int totalPages, const std::string& title, const int currentPage, const int totalPages,
const int bookProgressPercent, const uint8_t currentOrientation, const int bookProgressPercent, const uint8_t currentOrientation,
const bool hasFootnotes) const bool hasFootnotes, const bool hasBookmarks)
: Activity("EpubReaderMenu", renderer, mappedInput), : Activity("EpubReaderMenu", renderer, mappedInput),
menuItems(buildMenuItems(hasFootnotes)), menuItems(buildMenuItems(hasFootnotes, hasBookmarks)),
title(title), title(title),
pendingOrientation(currentOrientation), pendingOrientation(currentOrientation),
currentPage(currentPage), currentPage(currentPage),
totalPages(totalPages), totalPages(totalPages),
bookProgressPercent(bookProgressPercent) {} bookProgressPercent(bookProgressPercent) {}
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
bool hasBookmarks) {
std::vector<MenuItem> items; std::vector<MenuItem> items;
items.reserve(11); items.reserve(12);
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
if (hasFootnotes) { if (hasFootnotes) {
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
} }
items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS}); if (hasBookmarks) {
items.push_back({MenuAction::BOOKMARKS, StrId::STR_BOOKMARKS});
}
items.push_back({MenuAction::TOGGLE_BOOKMARK, StrId::STR_TOGGLE_BOOKMARK});
items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN});
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
@@ -18,6 +18,7 @@ class EpubReaderMenuActivity final : public Activity {
AUTO_PAGE_TURN, AUTO_PAGE_TURN,
ROTATE_SCREEN, ROTATE_SCREEN,
BOOKMARKS, BOOKMARKS,
TOGGLE_BOOKMARK,
SCREENSHOT, SCREENSHOT,
DISPLAY_QR, DISPLAY_QR,
GO_HOME, GO_HOME,
@@ -27,7 +28,7 @@ class EpubReaderMenuActivity final : public Activity {
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
const int currentPage, const int totalPages, const int bookProgressPercent, const int currentPage, const int totalPages, const int bookProgressPercent,
const uint8_t currentOrientation, const bool hasFootnotes); const uint8_t currentOrientation, const bool hasFootnotes, bool hasBookmarks);
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
@@ -40,7 +41,7 @@ class EpubReaderMenuActivity final : public Activity {
StrId labelId; StrId labelId;
}; };
static std::vector<MenuItem> buildMenuItems(bool hasFootnotes); static std::vector<MenuItem> buildMenuItems(bool hasFootnotes, bool hasBookmarks);
// Fixed menu layout // Fixed menu layout
const std::vector<MenuItem> menuItems; const std::vector<MenuItem> menuItems;
+13 -1
View File
@@ -300,7 +300,19 @@ void XtcReaderActivity::renderPage() {
} }
} }
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); if (pagesUntilFullRefresh <= 1) {
// Periodic ghost cleanup: scrub via the normal path, then run the
// settle flavor of the grayscale base pass (DTM planes are equal after
// the display sync, so only the gentle reinforcement cells fire).
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
renderer.preconditionGrayscale();
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else {
// OEM grayscale pipeline base: differential "AA-pre-BW(mid)" update as
// the page turn on X3; plain FAST refresh on X4 (previous behavior).
renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH);
pagesUntilFullRefresh--;
}
// Pass 2: LSB buffer - mark DARK gray only (XTH value 1) // Pass 2: LSB buffer - mark DARK gray only (XTH value 1)
// In LUT: 0 bit = apply gray effect, 1 bit = untouched // In LUT: 0 bit = apply gray effect, 1 bit = untouched
@@ -77,6 +77,9 @@ const StrId titleNames[TITLE_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrI
constexpr int XTC_STATUS_BAR_ITEMS = 3; constexpr int XTC_STATUS_BAR_ITEMS = 3;
const StrId xtcStatusBarNames[XTC_STATUS_BAR_ITEMS] = {StrId::STR_HIDE, StrId::STR_BOTTOM, StrId::STR_TOP}; const StrId xtcStatusBarNames[XTC_STATUS_BAR_ITEMS] = {StrId::STR_HIDE, StrId::STR_BOTTOM, StrId::STR_TOP};
constexpr int STATUS_BAR_CLOCK_ITEMS = 3;
const StrId statusBarClockNames[STATUS_BAR_CLOCK_ITEMS] = {StrId::STR_HIDE, StrId::STR_DIR_RIGHT, StrId::STR_DIR_LEFT};
const int verticalPreviewPadding = 50; const int verticalPreviewPadding = 50;
const int verticalPreviewTextPadding = 40; const int verticalPreviewTextPadding = 40;
} // namespace } // namespace
@@ -112,6 +115,10 @@ void StatusBarSettingsActivity::onEnter() {
SETTINGS.clockFormat = 0; SETTINGS.clockFormat = 0;
} }
if (SETTINGS.statusBarClock >= STATUS_BAR_CLOCK_ITEMS) {
SETTINGS.statusBarClock = CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE;
}
requestUpdate(); requestUpdate();
} }
@@ -196,7 +203,7 @@ void StatusBarSettingsActivity::handleSelection() {
SETTINGS.xtcStatusBarMode = (SETTINGS.xtcStatusBarMode + 1) % XTC_STATUS_BAR_ITEMS; SETTINGS.xtcStatusBarMode = (SETTINGS.xtcStatusBarMode + 1) % XTC_STATUS_BAR_ITEMS;
break; break;
case ITEM_CLOCK: case ITEM_CLOCK:
SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % 2; SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % STATUS_BAR_CLOCK_ITEMS;
break; break;
case ITEM_CLOCK_FORMAT: case ITEM_CLOCK_FORMAT:
SETTINGS.clockFormat = (SETTINGS.clockFormat + 1) % CLOCK_FORMAT_ITEMS; SETTINGS.clockFormat = (SETTINGS.clockFormat + 1) % CLOCK_FORMAT_ITEMS;
@@ -245,7 +252,7 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
case ITEM_XTC_STATUS_BAR: case ITEM_XTC_STATUS_BAR:
return I18N.get(xtcStatusBarNames[SETTINGS.xtcStatusBarMode]); return I18N.get(xtcStatusBarNames[SETTINGS.xtcStatusBarMode]);
case ITEM_CLOCK: case ITEM_CLOCK:
return SETTINGS.statusBarClock ? tr(STR_SHOW) : tr(STR_HIDE); return I18N.get(statusBarClockNames[SETTINGS.statusBarClock]);
case ITEM_CLOCK_FORMAT: { case ITEM_CLOCK_FORMAT: {
const uint8_t fmt = SETTINGS.clockFormat < CLOCK_FORMAT_ITEMS ? SETTINGS.clockFormat : 0; const uint8_t fmt = SETTINGS.clockFormat < CLOCK_FORMAT_ITEMS ? SETTINGS.clockFormat : 0;
return std::string(I18N.get(clockFormatNames[fmt])); return std::string(I18N.get(clockFormatNames[fmt]));
+4 -3
View File
@@ -209,9 +209,10 @@ int UITheme::getStatusBarHeight() {
const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); const ThemeMetrics& metrics = UITheme::getInstance().getMetrics();
// Add status bar margin // Add status bar margin
const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || const bool showStatusBar =
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
SETTINGS.statusBarBattery; SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
SETTINGS.statusBarClock != CrossPointSettings::STATUS_BAR_CLOCK_MODE::STATUS_BAR_CLOCK_HIDE;
const bool showProgressBar = const bool showProgressBar =
SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) + return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) +
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
// size: 32x32
static const uint8_t BookmarkIcon[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00,
0x03, 0x80, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x0F, 0x00,
0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x0F,
0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
// size: 16x16
static const uint8_t BookmarkStatusIcon[] = {0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F,
0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xF8,
0x0F, 0x78, 0x0E, 0x38, 0x0C, 0x18, 0x08, 0x08, 0x00, 0x00};
+66 -18
View File
@@ -14,6 +14,7 @@
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "components/TouchRegistry.h" #include "components/TouchRegistry.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "components/icons/bookmark.h"
#include "fontIds.h" #include "fontIds.h"
// Internal constants // Internal constants
@@ -21,6 +22,27 @@ namespace {
constexpr int homeMenuMargin = 20; constexpr int homeMenuMargin = 20;
constexpr int homeMarginTop = 30; constexpr int homeMarginTop = 30;
constexpr int subtitleY = 738; constexpr int subtitleY = 738;
constexpr int bookmarkStatusIconWidth = 16;
constexpr int bookmarkStatusIconHeight = 14;
constexpr int bookmarkStatusIconGap = 4;
constexpr int bookmarkStatusIconTopCrop = 2;
bool statusBarTextLaneVisible() {
return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE || SETTINGS.statusBarBattery ||
(SETTINGS.statusBarClock && halClock.isAvailable());
}
void drawBookmarkStatusIcon(const GfxRenderer& renderer, const int x, const int y) {
constexpr int bytesPerRow = bookmarkStatusIconWidth / 8;
for (int row = 0; row < bookmarkStatusIconHeight; ++row) {
for (int col = 0; col < bookmarkStatusIconWidth; ++col) {
const uint8_t byte = BookmarkStatusIcon[(row + bookmarkStatusIconTopCrop) * bytesPerRow + col / 8];
const uint8_t mask = 1U << (7 - (col % 8));
renderer.drawPixel(x + col, y + row, (byte & mask) != 0);
}
}
}
// This theme's metrics scaled by the board uiScale (lazily built once; uiScale is // This theme's metrics scaled by the board uiScale (lazily built once; uiScale is
// fixed at boot). Runtime draw code reads these so the UI grows on touch/high-PPI // fixed at boot). Runtime draw code reads these so the UI grows on touch/high-PPI
@@ -802,16 +824,21 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou
void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
const int pageCount, std::string title, const int paddingBottom, const int textYOffset, const int pageCount, std::string title, const int paddingBottom, const int textYOffset,
const bool fillMargin) const { const bool fillMargin, const bool isPageBookmarked) const {
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
&orientedMarginLeft); &orientedMarginLeft);
const bool showStatusBarTextLane = statusBarTextLaneVisible();
// Draw Progress Text // Draw Progress Text
const auto screenHeight = renderer.getScreenHeight(); const auto screenHeight = renderer.getScreenHeight();
auto textY = screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4; auto textY = screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4;
int progressTextWidth = 0;
const int leftClusterX = metrics.statusBarHorizontalMargin + orientedMarginLeft + 1;
const int rightClusterX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight;
int leftClusterWidth = 0;
int rightClusterWidth = 0;
if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) { if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) {
// Right aligned text for progress counter // Right aligned text for progress counter
@@ -825,11 +852,10 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage, pageCount); snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage, pageCount);
} }
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
renderer.drawText( renderer.drawText(SMALL_FONT_ID, rightClusterX - progressTextWidth, textY, progressStr);
SMALL_FONT_ID,
renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - progressTextWidth, textY, rightClusterWidth += progressTextWidth;
progressStr);
} }
// Draw Progress Bar // Draw Progress Bar
@@ -855,26 +881,50 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
// Draw Battery // Draw Battery
const bool showBatteryPercentage = const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
if (SETTINGS.statusBarBattery) { if (SETTINGS.statusBarBattery) {
GUI.drawBatteryLeft(renderer, GUI.drawBatteryLeft(renderer,
Rect{metrics.statusBarHorizontalMargin + orientedMarginLeft + 1, textY, metrics.batteryWidth, Rect{leftClusterX + leftClusterWidth, textY, metrics.batteryWidth, metrics.batteryHeight},
metrics.batteryHeight},
showBatteryPercentage); showBatteryPercentage);
int batteryWidth = metrics.batteryWidth;
if (showBatteryPercentage) {
const uint16_t percentage = powerManager.getBatteryPercentage();
// width of icon + spacing + text for layout purposes
batteryWidth +=
batteryPercentSpacing + renderer.getTextWidth(SMALL_FONT_ID, (std::to_string(percentage) + "%").c_str());
}
leftClusterWidth += batteryWidth;
} }
// Draw Clock (X3 only — DS3231 RTC) // Draw Clock (X3 only — DS3231 RTC)
int clockTextWidth = 0;
if (SETTINGS.statusBarClock && halClock.isAvailable()) { if (SETTINGS.statusBarClock && halClock.isAvailable()) {
char timeBuf[9]; char timeBuf[9];
if (halClock.formatTime(timeBuf, sizeof(timeBuf), SETTINGS.clockUtcOffsetQ, SETTINGS.clockFormat == 1)) { if (halClock.formatTime(timeBuf, sizeof(timeBuf), SETTINGS.clockUtcOffsetQ, SETTINGS.clockFormat == 1)) {
clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, timeBuf); int clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, timeBuf);
// Position to the left of the progress text (with a small gap) int clockX = 0;
const int clockX = renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - // Position to the left or right of the progress text (with a small gap)
progressTextWidth - (progressTextWidth > 0 ? 10 : 0) - clockTextWidth; if (SETTINGS.statusBarClock == CrossPointSettings::STATUS_BAR_CLOCK_LEFT) {
clockX = leftClusterX + leftClusterWidth + (leftClusterWidth > 0 ? 10 : 0);
leftClusterWidth += clockTextWidth + 10;
} else if (SETTINGS.statusBarClock == CrossPointSettings::STATUS_BAR_CLOCK_RIGHT) {
clockX = rightClusterX - rightClusterWidth - (rightClusterWidth > 0 ? 10 : 0) - clockTextWidth;
rightClusterWidth += clockTextWidth + 10;
}
renderer.drawText(SMALL_FONT_ID, clockX, textY, timeBuf); renderer.drawText(SMALL_FONT_ID, clockX, textY, timeBuf);
} }
} }
// Draw Bookmark
if (showStatusBarTextLane && isPageBookmarked) {
const int bookmarkGap = leftClusterWidth > 0 ? bookmarkStatusIconGap : 0;
const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap;
const int bookmarkY = textY + 5;
drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY);
leftClusterWidth += bookmarkStatusIconWidth + bookmarkGap;
}
// Draw Title // Draw Title
if (!title.empty()) { if (!title.empty()) {
textY -= textYOffset; textY -= textYOffset;
@@ -883,10 +933,8 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
const int rendererableScreenWidth = const int rendererableScreenWidth =
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight; renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0; const int titleMarginLeft = leftClusterWidth + 30;
const int titleMarginLeft = batterySize + 30; const int titleMarginRight = rightClusterWidth + 30;
const int clockReserve = clockTextWidth > 0 ? (clockTextWidth + 10) : 0;
const int titleMarginRight = progressTextWidth + clockReserve + 30;
// Attempt to center title on the screen, but if title is too wide then later we will center it within the // Attempt to center title on the screen, but if title is too wide then later we will center it within the
// available space. // available space.
+1 -1
View File
@@ -246,7 +246,7 @@ class BaseTheme {
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount,
std::string title, const int paddingBottom = 0, const int textYOffset = 0, std::string title, const int paddingBottom = 0, const int textYOffset = 0,
const bool fillMargin = true) const; const bool fillMargin = true, const bool isPageBookmarked = false) const;
void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false,
int contentStartX = 0, int contentWidth = 0) const; int contentStartX = 0, int contentWidth = 0) const;
+38 -23
View File
@@ -80,11 +80,11 @@
} }
.breadcrumb-inline .sep { .breadcrumb-inline .sep {
margin: 0 6px; margin: 0 6px;
color: var(--border-color); color: var(--label-color);
} }
.breadcrumb-inline .current { .breadcrumb-inline .current {
color: var(--title-color); color: var(--title-color);
font-weight: 500; font-weight: 600;
} }
.nav-links { .nav-links {
margin: 20px 0; margin: 20px 0;
@@ -960,12 +960,6 @@
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
} }
.contents-title {
font-size: 1.1em;
font-weight: 600;
color: var(--title-color);
margin: 0;
}
.summary-inline { .summary-inline {
color: var(--label-color); color: var(--label-color);
font-size: 0.9em; font-size: 0.9em;
@@ -1296,9 +1290,6 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
} }
.contents-title {
font-size: 1em;
}
.summary-inline { .summary-inline {
font-size: 0.8em; font-size: 0.8em;
} }
@@ -1501,7 +1492,6 @@
<div class="page-header"> <div class="page-header">
<div class="page-header-left"> <div class="page-header-left">
<h2>📁 File Manager</h2> <h2>📁 File Manager</h2>
<div class="breadcrumb-inline" id="directory-breadcrumbs"></div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
@@ -1523,7 +1513,7 @@
<div class="card"> <div class="card">
<div class="contents-header"> <div class="contents-header">
<h2 class="contents-title">Contents</h2> <div class="breadcrumb-inline" id="directory-breadcrumbs"></div>
<span class="summary-inline" id="folder-summary"></span> <span class="summary-inline" id="folder-summary"></span>
</div> </div>
@@ -1904,19 +1894,44 @@
const breadcrumbs = document.getElementById('directory-breadcrumbs'); const breadcrumbs = document.getElementById('directory-breadcrumbs');
const fileTable = document.getElementById('file-table'); const fileTable = document.getElementById('file-table');
let breadcrumbContent = '<span class="sep">/</span>'; const segments = currentPath.split('/').filter(Boolean);
if (currentPath === '/') { breadcrumbs.replaceChildren();
breadcrumbContent += '<span class="current">🏠</span>';
const appendSep = function() {
const sep = document.createElement('span');
sep.className = 'sep';
sep.textContent = '';
breadcrumbs.appendChild(sep);
};
const appendLink = function(label, href) {
const link = document.createElement('a');
link.href = href;
link.textContent = label;
breadcrumbs.appendChild(link);
};
const appendCurrent = function(label) {
const current = document.createElement('span');
current.className = 'current';
current.textContent = label;
breadcrumbs.appendChild(current);
};
if (segments.length === 0) {
appendCurrent('🏠 Home');
} else { } else {
breadcrumbContent += '<a href="/files">🏠</a>'; appendLink('🏠 Home', '/files');
const pathSegments = currentPath.split('/'); segments.forEach(function(segment, index) {
pathSegments.slice(1, pathSegments.length - 1).forEach(function(segment, index) { appendSep();
breadcrumbContent += '<span class="sep">/</span><a href="/files?path=' + encodeURIComponent(pathSegments.slice(0, index + 2).join('/')) + '">' + escapeHtml(segment) + '</a>'; if (index === segments.length - 1) {
appendCurrent(segment);
} else {
const path = '/' + segments.slice(0, index + 1).join('/');
appendLink(segment, '/files?path=' + encodeURIComponent(path));
}
}); });
breadcrumbContent += '<span class="sep">/</span>';
breadcrumbContent += '<span class="current">' + escapeHtml(pathSegments[pathSegments.length - 1]) + '</span>';
} }
breadcrumbs.innerHTML = breadcrumbContent;
let files = []; let files = [];
try { try {