Compare commits

...
Author SHA1 Message Date
Justin Mitchell 4e12083566 Add anti-aliasing mode translations (Full/Fast)
Add STR_TEXT_AA_FULL and STR_TEXT_AA_FAST translation strings across all supported languages. Also includes a complete retranslation of the Slovak language file.
2026-07-06 19:04:00 -04:00
Justin Mitchell cf4f7be8af Reformat anti-aliasing condition for readability
Consolidate multi-line conditional statement in GfxRenderer onto fewer lines without changing logic
2026-07-06 19:01:21 -04:00
Justin Mitchell e69241ef54 Reverts UI font change and adds setting for AA sd fonts 2026-07-06 18:59:25 -04:00
Justin Mitchell 72b99a2a9c Enable compression for ui fonts
Add --compress flag to font generation command and regenerate the font bitmap data with compression enabled. This reduces the bitmap array size while maintaining the same 2-bit mode and character intervals.
2026-07-05 19:46:43 -04:00
Justin Mitchell 62448b3c08 Add anti-aliased font rendering with dithering
Replace solid black rendering of anti-aliased font edges with period-2 dithering patterns. Full coverage pixels render as solid ink, while partial coverage pixels use checkerboard patterns (50% for dark grey, 25% for light grey) matching fillRectDither behavior. This prevents edge pixels from bolding to black on BW displays and produces proper anti-aliasing for 2-bit fonts.
2026-07-05 16:06:56 -04:00
Tom-Inge Larsen 3e1dd31e53 feat(reader): End of Book next-book suggestions (#2499) (#2532) 2026-07-04 23:12:59 +03:00
685d4e88f9 feat: Lazy incremental EPUB section indexing (#2452)
Co-authored-by: Uri Tauber <uritaube@gmail.com>
Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-07-04 21:21:24 +03:00
Jakub Sypiański 79b4d63ee2 fix: settings persist on font clear, reserve() before push_back, minor (#2519) 2026-07-03 18:58:24 +03:00
Leopoldo Pla SempereandJiangoJ 57447a56a6 feat(epub): improve text-decoration support (#2397)
Co-authored-by: JiangoJ <jiangj2620@gmail.com>
2026-07-02 12:35:24 +03:00
Rahat Ahmed 5c86bfe1fd feat(network): add device serial number to web UI (#2506) 2026-07-02 00:53:55 +03:00
Justin Mitchell 4cdefb137c Update README with firmware flashing warnings and crosspet status
Removed warning about flashing Papyrix on USB-locked devices and updated crosspet status to unmaintained.
2026-07-01 14:22:27 -04:00
Justin Mitchell e7f3406704 fix: Add socket module import to build-sd-fonts script (#2504) (#2505) 2026-07-01 12:10:15 -04:00
Bastian 6f08a88b12 fix: changing translation in czech to fix overflowing navigation (#2502) 2026-07-01 11:58:24 -04:00
Tom-Inge LarsenandClaude Opus 4.8 e4d3981469 fix(reader): correct slider side-button direction and legend on X3 (#2402) (#2428)
## Summary

* **What is the goal of this PR?** Fixes #2402. On the X3, the "Time to
Sleep"
picker's 5-minute side buttons were inverted (left increased, right
decreased)
  and the on-screen legend didn't match the physical buttons.
* **What changes are included?**
  * Flip the large-step (±5 min) direction on X3 so the left side button
decreases and the right increases, matching the layout. X4 is unchanged.
* Add a device-specific step-hint string
(`STR_SLEEP_TIMER_STEP_HINT_X3`)
selected via `gpio.deviceIsX3()`. X3 shows `+/-: 1 min Side buttons: 5
min`;
    X4 keeps the original `Left/Right: 1 min  Up/Down: 5 min`.
* Same fix applied to both slider pickers that read the raw up/down side
    buttons: the Time-to-Sleep picker and the "Go to %" picker
(`EpubReaderPercentSelectionActivity`), each with its own X3 hint string
    (`STR_SLEEP_TIMER_STEP_HINT_X3`, `STR_PERCENT_STEP_HINT_X3`).

## Additional Context

* **Root cause:** the X3's side buttons sit one on each edge of the
screen
(power on top), whereas the X4 has a vertical up/down rocker on the
right
edge. So `BTN_UP` is physically the *left* button on X3 but the *top*
button
  on X4. The picker mapped up→+5 / down→−5 unconditionally, which reads
naturally on the X4 rocker but inverted on the X3's left/right buttons.
The
  static legend ("Up/Down: 5 min") had the same X3-only mismatch.
* The codebase already detects the device at runtime and handles this
layout
difference elsewhere (e.g. `LyraTheme::drawSideButtonHints` draws "Up on
left,
Down on right" for X3), so this reuses the same `gpio.deviceIsX3()`
signal.
* Swept the codebase for other side-button sliders:
`ClockOffsetActivity` already
uses the `Next/Previous` (`ButtonNavigator`) abstraction, which resolves
to
left=decrement / right=increment on X3, so it needs no change. List/page
navigation (Wi-Fi, KOReader sync, BMP viewer) and the keyboard cursor
are also
unaffected. The two slider pickers above were the only ones using raw
up/down.
* **X4 is untouched** — same direction, same legend wording as before.
* **Tested on X3 hardware:** left side button now decreases, right
increases, and
  the legend matches. X4 not yet tested on device (no unit on hand); its
  behaviour and strings are unchanged from master.
* **Translations:** the new `STR_SLEEP_TIMER_STEP_HINT_X3` and
`STR_PERCENT_STEP_HINT_X3` were added to all 26 languages, but the
non-English
  ones are AI-generated and would benefit from a native-speaker pass —
particularly Hebrew (RTL ordering with a leading `+/-`), Kazakh, and
Vietnamese.

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:06:31 -04:00
Bastian c9d27826f9 docs: update TOC in userguide (#2479) 2026-06-30 08:48:21 +03:00
Bastian 13bd00bf9d fix: sort sleep screen menu options to be more logically consistent (#2480) 2026-06-30 08:47:25 +03:00
Uri Tauberandpablohc fef39da23c feat: Selection Popup (#2358)
Co-authored-by: pablohc <pablonoviello@outlook.com>
2026-06-29 16:59:25 -04:00
Uri Tauber fd43ca2fe1 chore: Initial multi-core compatibility (#2294) 2026-06-29 21:57:21 +03:00
Pietro Campagnano 28255061fb fix(icons): align home menu icons with their labels (#2470) 2026-06-29 13:59:54 -04:00
Julia Nguyen 43ff1b6644 Merge remote-tracking branch 'origin/master' into develop 2026-06-29 12:29:58 -04:00
Егор Мартынов 2754a5ff01 feat: add Vollkorn font (#2473)
CI (build) / clang-format (push) Canceled after 0s
CI (build) / cppcheck (push) Canceled after 0s
CI (build) / build (push) Canceled after 0s
CI (build) / unit-tests (push) Canceled after 0s
CI (build) / Test Status (push) Canceled after 0s
2026-06-29 15:54:01 +03:00
Justin Mitchell 6edeed73c5 fix: x4 sleep/boot ghosting (#2471) 2026-06-29 12:04:33 +03:00
Justin Mitchell 8ac302018c fix: x4 ghosting by bumping sdk (#2469) 2026-06-28 22:38:35 +03:00
Rodrigo Matsuura a11517a62d fix: update Portuguese (Brasil) translations (#2458) 2026-06-28 15:07:52 +03:00
95 changed files with 2897 additions and 969 deletions
+1 -5
View File
@@ -67,10 +67,6 @@ USB port or browser before assuming the device is locked. Only reach for the unl
> Flashing any other firmware on a USB-locked device may **permanently brick the device** or leave it **permanently
> stuck on that firmware with no recovery path**. Once USB flashing is re-locked, your only way back is via OTA, and if
> the firmware you flashed doesn't support OTA, **there is no way out**.
>
> **The Papyrix fork has removed OTA update support from its code.** If you flash Papyrix onto a
> USB-locked unit, you will have **zero update or recovery path** and will be stuck on it forever. **Do not flash
> Papyrix (or any other unsupported firmware) on a locked device.**
## Install firmware
@@ -249,7 +245,7 @@ One of the best things about open source is that anyone can take the code in a d
- [papyrix-reader](https://github.com/bigbag/papyrix-reader) — Adds FB2 and MD format support. Actively maintained with Arabic script support. Custom themes via SD card.
- [crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.
- ~~[crosspet](https://github.com/trilwu/crosspet) — A Vietnamese fork that adds a Tamagotchi-style virtual chicken that grows based on your reading milestones (pages read, streaks, care). Also: Flashcards, Weather, Pomodoro timer, and mini-games.~~ (Unmaintained)
- [crosspoint-reader-cjk](https://github.com/aBER0724/crosspoint-reader-cjk) — Purpose-built for Chinese, Japanese, and Korean reading.
+21 -12
View File
@@ -5,6 +5,7 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [CrossPoint User Guide](#crosspoint-user-guide)
- [1. Hardware Overview](#1-hardware-overview)
- [Button Layout](#button-layout)
- [Taking a Screenshot](#taking-a-screenshot)
- [2. Power \& Startup](#2-power--startup)
- [Power On / Off](#power-on--off)
- [First Launch](#first-launch)
@@ -14,7 +15,11 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.3 Browse Files Screen](#33-browse-files-screen)
- [3.4 Recent Books Screen](#34-recent-books-screen)
- [3.5 File Transfer Screen](#35-file-transfer-screen)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [3.5.1 Calibre Wireless Transfers](#351-calibre-wireless-transfers)
- [Installing the Plugin in Calibre](#installing-the-plugin-in-calibre)
- [Configuring the CrossPoint Plugin in Calibre](#configuring-the-crosspoint-plugin-in-calibre)
- [Uploading Books](#uploading-books)
- [Removing a Book](#removing-a-book)
- [3.6 Settings](#36-settings)
- [3.6.1 Display](#361-display)
- [3.6.2 Reader](#362-reader)
@@ -23,21 +28,25 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
- [3.7 Sleep Screen](#37-sleep-screen)
- [Cover settings](#cover-settings)
- [Custom images](#custom-images)
- [3.8 Custom Fonts (SD Card)](#38-custom-fonts-sd-card)
- [4. Reading Mode](#4-reading-mode)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [Auto Page Turn](#auto-page-turn)
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
- [Footnote Navigation](#footnote-navigation)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [Page Turning](#page-turning)
- [Chapter Navigation](#chapter-navigation)
- [Auto Page Turn](#auto-page-turn)
- [Tilt Page Turn (X3 only)](#tilt-page-turn-x3-only)
- [Footnote Navigation](#footnote-navigation)
- [System Navigation](#system-navigation)
- [Supported Languages](#supported-languages)
- [5. Reader Menu](#5-reader-menu)
- [5.1 Chapter Selection](#51-chapter-selection)
- [5.2 Bookmarks](#52-bookmarks)
- [6. Current Limitations & Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues & Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
- [5.1 Chapter Selection](#51-chapter-selection)
- [5.2 Bookmarks](#52-bookmarks)
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
## 1. Hardware Overview
+5 -3
View File
@@ -90,13 +90,13 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 25
### Version 28
Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt.
Version 25 includes:
Version 28 includes:
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
image rendering mode, and Focus Reading
@@ -105,6 +105,8 @@ Version 25 includes:
- paragraph and list-item LUTs used by KOReader sync page refinement
- optional per-word Focus Reading split metadata
- per-page footnote entries
- serialized word style bits for underline, strikethrough, superscript, and
subscript
ImHex pattern:
@@ -113,7 +115,7 @@ import std.mem;
import std.string;
import std.core;
#define EXPECTED_VERSION 25
#define EXPECTED_VERSION 28
#define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
+1 -1
View File
@@ -1,7 +1,7 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const {
// Extract font style bits (ignore UNDERLINE bit for font selection)
// Extract font style bits; render-time overlay bits do not affect font selection.
const bool hasBold = (style & BOLD) != 0;
const bool hasItalic = (style & ITALIC) != 0;
+4
View File
@@ -17,6 +17,7 @@ class EpdFontFamily {
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
};
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
@@ -27,6 +28,9 @@ class EpdFontFamily {
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
static constexpr bool hasTextDecoration(const Style style) {
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
}
private:
const EpdFont* regular;
+35 -7
View File
@@ -33,6 +33,7 @@ import sys
import tempfile
import threading
import time
import socket
import urllib.request
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
@@ -49,17 +50,44 @@ INSTANCE_DIR = SCRIPT_DIR / "instanced_fonts"
DEFAULT_FALLBACK_FONT = EPDFONTS_DIR / "builtinFonts/source/NotoSans/NotoSans-Regular.ttf"
def download_font(url: str, dest: Path) -> Path:
"""Download a font file if not already cached. Returns the local path."""
_orig_getaddrinfo = socket.getaddrinfo
def _ipv4_only_getaddrinfo(*args, **kwargs):
"""getaddrinfo variant that drops AAAA records (IPv4 only)."""
return [ai for ai in _orig_getaddrinfo(*args, **kwargs) if ai[0] == socket.AF_INET]
def download_font(url: str, dest: Path, retries: int = 3) -> Path:
"""Download a font file if not already cached. Returns the local path.
Some sources (e.g. mirrors.ctan.org) are round-robin redirectors that land
on a different mirror each request; a mirror may advertise an IPv6 address a
host without an IPv6 route cannot reach ([Errno 101] Network is unreachable).
Retry on failure, forcing IPv4 resolution after the first attempt.
"""
if dest.exists():
return dest
dest.parent.mkdir(parents=True, exist_ok=True)
print(f" Downloading {dest.name}...")
try:
urllib.request.urlretrieve(url, dest)
except Exception as e:
dest.unlink(missing_ok=True)
raise RuntimeError(f"Failed to download {url}: {e}") from e
last_err = None
for attempt in range(1, retries + 1):
force_ipv4 = attempt > 1
if force_ipv4:
socket.getaddrinfo = _ipv4_only_getaddrinfo
try:
urllib.request.urlretrieve(url, dest)
break
except Exception as e: # noqa: BLE001 - reported via RuntimeError below
last_err = e
dest.unlink(missing_ok=True)
if attempt < retries:
print(f" Attempt {attempt} failed ({e}); retrying (IPv4-only)...")
finally:
if force_ipv4:
socket.getaddrinfo = _orig_getaddrinfo
else:
raise RuntimeError(f"Failed to download {url}: {last_err}") from last_err
size_kb = dest.stat().st_size / 1024
print(f" Downloaded {dest.name} ({size_kb:.0f} KB)")
return dest
+10 -1
View File
@@ -132,7 +132,16 @@ families:
bold: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Bold.ttf"}
italic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-Italic.ttf"}
bolditalic: {url: "https://raw.githubusercontent.com/impallari/Libre-Baskerville/master/fonts/ttf/LibreBaskerville-BoldItalic.ttf"}
- name: Vollkorn
description: "A serif for bread and butter use by Friedrich Althausen (Latin, Greek, Cyrillic)"
intervals: latin-ext,greek,cyrillic
sizes: [12, 14, 16, 18]
styles:
regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 400}}
bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn%5Bwght%5D.ttf", variable: {wght: 700}}
italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 400}}
bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/vollkorn/Vollkorn-Italic%5Bwght%5D.ttf", variable: {wght: 700}}
# ── Sans-serif ─────────────────────────────────────────────────────────
+513 -109
View File
@@ -2,6 +2,7 @@
#include <HalStorage.h>
#include <Logging.h>
#include <Memory.h>
#include <Serialization.h>
#include "Epub/css/CssParser.h"
@@ -10,34 +11,59 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
// v27: words NFC-composed at layout time; bump invalidates NFD section caches.
constexpr uint8_t SECTION_FILE_VERSION = 27;
// v28: text decoration bits now include line-through in serialized wordStyles.
constexpr uint8_t SECTION_FILE_VERSION = 28;
// Written into the version field while a build is in progress; patched to
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
// as unknown and clears -- so an incomplete file is never mistaken for a valid one.
constexpr uint8_t SECTION_FILE_INCOMPLETE_VERSION = 0;
// Written when a build is suspended partway (reader exited or device slept mid-build).
// The file carries valid pages 0..pageCount-1, all LUTs, and a trailer with the parse
// watermark (bytesConsumed, totalBytes) appended after the li LUT. loadSectionFile
// accepts it so a resume shows those pages instantly; the reader extends it by
// rebuilding in the background. Uses the same header layout as SECTION_FILE_VERSION,
// so finalized files are untouched by this feature; older firmware treats the sentinel
// as an unknown version and rebuilds, which is a safe downgrade.
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t) + sizeof(uint32_t);
struct PageLutEntry {
uint32_t fileOffset;
uint16_t paragraphIndex;
uint16_t listItemIndex;
};
} // namespace
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
// constructed/destroyed where the parser's full definition is visible.
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub),
spineIndex(spineIndex),
renderer(renderer),
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
// Suspend any in-progress build so every section.reset() / navigation / sleep path
// persists the pages already laid out as a partial .bin instead of discarding them
// (no-op once a build has completed or never started).
Section::~Section() { suspendBuild(); }
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
if (!file) {
LOG_ERR("SCT", "File not open for writing page %d", pageCount);
LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_);
return 0;
}
const uint32_t position = file.position();
if (!page->serialize(file)) {
LOG_ERR("SCT", "Failed to serialize page %d", pageCount);
LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_);
return 0;
}
LOG_DBG("SCT", "Page %d processed", pageCount);
LOG_DBG("SCT", "Page %d processed", builtPageCount_);
pageCount++;
builtPageCount_++;
// pageCount is the pages available to read: a rebuild over a partial only raises it
// once it has laid out more pages than the partial already covers.
if (builtPageCount_ > pageCount) {
pageCount = builtPageCount_;
}
return position;
}
@@ -56,7 +82,9 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
// Written as the incomplete sentinel; finalizeBuild() patches it to
// SECTION_FILE_VERSION as the last step, committing the file.
serialization::writePod(file, SECTION_FILE_INCOMPLETE_VERSION);
serialization::writePod(file, fontId);
serialization::writePod(file, lineCompression);
serialization::writePod(file, extraParagraphSpacing);
@@ -83,16 +111,18 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
}
// Match parameters
bool filePartial = false;
{
uint8_t version;
serialization::readPod(file, version);
if (version != SECTION_FILE_VERSION) {
if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) {
// Explicit close() required: member variable persists beyond function scope
file.close();
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
clearCache();
return false;
}
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
int fileFontId;
uint16_t fileViewportWidth, fileViewportHeight;
@@ -127,14 +157,42 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
}
serialization::readPod(file, pageCount);
if (filePartial) {
// A partial's pageCount is the watermark of a suspended build. Read the watermark
// trailer (appended after the li LUT) so estimatedTotalPages can extrapolate.
uint32_t liLutOffset = 0;
file.seek(HEADER_SIZE - sizeof(uint32_t));
serialization::readPod(file, liLutOffset);
const uint32_t trailerOffset = liLutOffset + static_cast<uint32_t>(pageCount) * sizeof(uint16_t);
const bool trailerValid =
pageCount > 0 && liLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size();
if (!trailerValid) {
file.close();
LOG_ERR("SCT", "Deserialization failed: malformed partial section");
clearCache();
pageCount = 0;
return false;
}
file.seek(trailerOffset);
serialization::readPod(file, partialBytesConsumed_);
serialization::readPod(file, partialTotalBytes_);
partial_ = true;
partialPageCount_ = pageCount;
}
// Explicit close() required: member variable persists beyond function scope
file.close();
LOG_DBG("SCT", "Deserialization succeeded: %d pages", pageCount);
LOG_DBG("SCT", "Deserialization succeeded: %d pages%s", pageCount, filePartial ? " (partial)" : "");
return true;
}
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
bool Section::clearCache() const {
const std::string tmpBin = binTmpPath();
if (Storage.exists(tmpBin.c_str())) {
Storage.remove(tmpBin.c_str());
}
if (!Storage.exists(filePath.c_str())) {
LOG_DBG("SCT", "Cache does not exist, no action needed");
return true;
@@ -154,8 +212,43 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
const uint8_t imageRendering, const bool focusReadingEnabled,
const std::function<void()>& popupFn) {
// One-shot build: start, then lay out the whole section in a single pass.
if (!startBuild(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled, popupFn)) {
return false;
}
if (!buildSomeMore(0)) { // 0 = build to completion
return false;
}
return buildComplete_;
}
bool Section::startBuild(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight,
const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering,
const bool focusReadingEnabled, const std::function<void()>& popupFn) {
if (build_) {
LOG_ERR("SCT", "startBuild called while a build is already active");
return false;
}
buildComplete_ = false;
builtPageCount_ = 0;
// Pages from a loaded partial stay readable (from filePath) while this build writes
// to the tmp .bin, so availability never drops below the partial's watermark.
pageCount = partial_ ? partialPageCount_ : 0;
// Remove a stale tmp .bin from a crash-interrupted build; this build recreates it.
{
const std::string staleTmp = binTmpPath();
if (Storage.exists(staleTmp.c_str())) {
Storage.remove(staleTmp.c_str());
}
}
const auto localPath = epub->getSpineItem(spineIndex).href;
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
const auto htmlDir = epub->getCachePath() + "/html";
const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html";
const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html";
// Create cache directory if it doesn't exist
{
@@ -163,62 +256,101 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
Storage.mkdir(sectionsDir.c_str());
}
// Retry logic for SD card timing issues
bool success = false;
uint32_t fileSize = 0;
for (int attempt = 0; attempt < 3 && !success; attempt++) {
if (attempt > 0) {
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
delay(50); // Brief delay before retry
// Reuse the previously unzipped HTML if we already have it. The unzipped HTML is keyed only on the
// book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation
// that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip
// inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so
// even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a
// reopen skip the multi-second inflate. If htmlPath exists it is known-complete.
const bool reusedHtml = Storage.exists(htmlPath.c_str());
bool htmlCached = reusedHtml;
if (reusedHtml) {
LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str());
} else {
Storage.mkdir(htmlDir.c_str());
// Retry logic for SD card timing issues
bool streamed = false;
uint32_t fileSize = 0;
for (int attempt = 0; attempt < 3 && !streamed; attempt++) {
if (attempt > 0) {
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
delay(50); // Brief delay before retry
}
// Remove any incomplete file from previous attempt before retrying
if (Storage.exists(tmpHtmlPath.c_str())) {
Storage.remove(tmpHtmlPath.c_str());
}
HalFile tmpHtml;
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
continue;
}
// Larger chunks mean far fewer SD writes inflating the HTML; a 1KB chunk turned a 584KB
// single-spine novel into ~570 tiny writes (multi-second). 8KB keeps the transient buffers
// small while cutting the write count 8x.
streamed = epub->readItemContentsToStream(localPath, tmpHtml, 8192);
fileSize = tmpHtml.size();
// Explicitly close() file before calling Storage.remove()
tmpHtml.close();
// If streaming failed, remove the incomplete file immediately
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
Storage.remove(tmpHtmlPath.c_str());
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
}
}
// Remove any incomplete file from previous attempt before retrying
if (Storage.exists(tmpHtmlPath.c_str())) {
Storage.remove(tmpHtmlPath.c_str());
if (!streamed) {
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
return false;
}
HalFile tmpHtml;
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
continue;
}
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
fileSize = tmpHtml.size();
// Explicitly close() file before calling Storage.remove()
tmpHtml.close();
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
// If streaming failed, remove the incomplete file immediately
if (!success && Storage.exists(tmpHtmlPath.c_str())) {
Storage.remove(tmpHtmlPath.c_str());
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
// Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are
// valid regardless of whether the layout build finishes, so reopening (even a window-only spine
// that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp.
if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) {
htmlCached = true;
} else {
LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp");
}
}
if (!success) {
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
return false;
}
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
if (!Storage.openFileForWrite("SCT", filePath, file)) {
if (!Storage.openFileForWrite("SCT", binTmpPath(), file)) {
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
return false;
}
// Header is written with the incomplete-version sentinel; finalizeBuild() commits it.
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
std::vector<PageLutEntry> lut = {};
auto ctx = makeUniqueNoThrow<BuildContext>();
if (!ctx) {
LOG_ERR("SCT", "OOM: BuildContext");
file.close();
Storage.remove(binTmpPath().c_str());
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
return false;
}
// htmlCached == "htmlPath is the live cache" (reused, or just promoted). finalizeBuild/abandonBuild
// then leave the cached HTML alone; only an un-promoted temp (rename failed) is theirs to clean up.
ctx->reusedHtml = htmlCached;
ctx->htmlPath = htmlPath;
ctx->tmpHtmlPath = tmpHtmlPath;
ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath;
// Derive the content base directory and image cache path prefix for the parser
size_t lastSlash = localPath.find_last_of('/');
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
const size_t lastSlash = localPath.find_last_of('/');
ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
CssParser* cssParser = nullptr;
if (embeddedStyle) {
cssParser = epub->getCssParser();
if (cssParser) {
if (!cssParser->loadFromCache()) {
LOG_ERR("SCT", "Failed to load CSS from cache");
}
ctx->cssParser = epub->getCssParser();
if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
LOG_ERR("SCT", "Failed to load CSS from cache");
}
}
@@ -235,104 +367,367 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
}
}
ChapterHtmlSlimParser visitor(
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled, focusReadingEnabled,
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
// The parser stores the path/contentBase/imageBasePath by reference, so they must
// live in the BuildContext (which outlives the parser). The page-complete callback
// captures the BuildContext pointer to append to its in-RAM LUT; build_ owns the
// context for the parser's whole lifetime.
BuildContext* ctxPtr = ctx.get();
ctx->parser = makeUniqueNoThrow<ChapterHtmlSlimParser>(
epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
viewportWidth, viewportHeight, hyphenationEnabled, focusReadingEnabled,
[this, ctxPtr](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
ctxPtr->lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
},
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), popupFn, cssParser);
Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages();
Storage.remove(tmpHtmlPath.c_str());
if (!success) {
LOG_ERR("SCT", "Failed to parse XML and build pages");
// Explicitly close() file before calling Storage.remove()
embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn,
ctxPtr->cssParser);
if (!ctx->parser) {
LOG_ERR("SCT", "OOM: ChapterHtmlSlimParser");
if (ctx->cssParser) ctx->cssParser->clear();
file.close();
Storage.remove(filePath.c_str());
if (cssParser) {
cssParser->clear();
}
Storage.remove(binTmpPath().c_str());
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
return false;
}
Hyphenator::setPreferredLanguage(epub->getLanguage());
build_ = std::move(ctx);
if (!build_->parser->beginParse()) {
LOG_ERR("SCT", "Failed to begin parse");
abandonBuild();
return false;
}
build_->totalBytes = build_->parser->parseTotalBytes();
return true;
}
bool Section::buildSomeMore(const int maxPages) {
if (!build_ || !build_->parser) {
LOG_ERR("SCT", "buildSomeMore with no active build");
return false;
}
// Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial,
// pageCount stays pinned at the partial's watermark until the build passes it, which
// would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark.
const int startCount = builtPageCount_;
for (;;) {
const auto status = build_->parser->parseStep();
if (status == ChapterHtmlSlimParser::ParseStatus::Error) {
LOG_ERR("SCT", "Parse error during incremental build");
abandonBuild();
return false;
}
if (status == ChapterHtmlSlimParser::ParseStatus::Done) {
return finalizeBuild();
}
// ParseStatus::More: yield once we've laid out the requested number of pages.
if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) {
build_->bytesConsumed = build_->parser->parseBytesConsumed();
return true;
}
}
}
bool Section::hasHtmlCache() const {
const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html";
return Storage.exists(htmlPath.c_str());
}
std::optional<uint16_t> Section::findAnchorDuringBuild(const std::string& anchor) const {
if (!build_ || !build_->parser) return std::nullopt;
for (const auto& [key, page] : build_->parser->getAnchors()) {
if (key == anchor) return page;
}
return std::nullopt;
}
std::optional<uint16_t> Section::findAnchor(const std::string& anchor) const {
if (const auto page = findAnchorDuringBuild(anchor)) {
return page;
}
// Fall back to the on-disk anchor map: a finalized section, or a partial whose map
// covers everything up to its watermark (nullopt past it -- build further and retry).
return getPageForAnchor(anchor);
}
uint16_t Section::estimatedTotalPages() const {
// Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA
// damping is needed. Also the best guess while a rebuild is running but hasn't laid out
// enough pages yet to extrapolate from its own progress.
const auto partialEstimate = [this]() -> uint16_t {
if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) {
return pageCount;
}
const uint64_t est = static_cast<uint64_t>(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_;
if (est <= pageCount) return pageCount;
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
};
if (!build_) {
return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact
}
const uint32_t consumed = build_->bytesConsumed;
const uint32_t total = build_->totalBytes;
if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate();
// Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This
// re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses
// dense vs sparse regions of the chapter.
const uint64_t raw = static_cast<uint64_t>(builtPageCount_) * total / consumed;
// Damp that jitter with an exponential moving average. Step it once per build advance (keyed on
// bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how
// often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so
// the average settles onto the true count (and finalizeBuild then returns the exact pageCount).
constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle
if (build_->smoothedEstimate <= 0) {
build_->smoothedEstimate = static_cast<float>(raw); // seed on the first estimate
} else if (consumed != build_->smoothedAtConsumed) {
build_->smoothedEstimate += ALPHA * (static_cast<float>(raw) - build_->smoothedEstimate);
}
build_->smoothedAtConsumed = consumed;
const uint64_t est = static_cast<uint64_t>(build_->smoothedEstimate + 0.5f);
if (est <= pageCount) return pageCount; // never fewer than the pages already available
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
}
// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built
// page count and table offsets, stamp `version` as the commit point, then swap the tmp
// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer
// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate
// the total page count. The parser must still be alive (anchors are read from it).
// On failure the tmp is removed and any pre-existing file at filePath is left intact.
bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) {
const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION);
const auto failCommit = [this]() {
// Explicit close() required before remove (member variable, O_RDWR handle).
file.close();
Storage.remove(binTmpPath().c_str());
return false;
};
const uint32_t lutOffset = file.position();
bool hasFailedLutRecords = false;
// Write LUT
for (const auto& entry : lut) {
for (const auto& entry : build_->lut) {
if (entry.fileOffset == 0) {
hasFailedLutRecords = true;
break;
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
return failCommit();
}
serialization::writePod(file, entry.fileOffset);
}
if (hasFailedLutRecords) {
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
// Explicitly close() file before calling Storage.remove()
file.close();
Storage.remove(filePath.c_str());
return false;
}
// Write anchor-to-page map for fragment navigation (e.g. footnote targets)
// Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a
// partial, skip anchors that landed on the incomplete trailing page the suspend drops.
const uint32_t anchorMapOffset = file.position();
const auto& anchors = visitor.getAnchors();
serialization::writePod(file, static_cast<uint16_t>(anchors.size()));
const auto& anchors = build_->parser->getAnchors();
uint16_t anchorCount = 0;
for (const auto& [anchor, page] : anchors) {
if (!asPartial || page < builtPageCount_) anchorCount++;
}
serialization::writePod(file, anchorCount);
for (const auto& [anchor, page] : anchors) {
if (asPartial && page >= builtPageCount_) continue;
serialization::writeString(file, anchor);
serialization::writePod(file, page);
}
const uint32_t paragraphLutOffset = file.position();
serialization::writePod(file, static_cast<uint16_t>(lut.size()));
for (const auto& entry : lut) {
serialization::writePod(file, static_cast<uint16_t>(build_->lut.size()));
for (const auto& entry : build_->lut) {
serialization::writePod(file, entry.paragraphIndex);
}
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
for (const auto& entry : lut) {
for (const auto& entry : build_->lut) {
serialization::writePod(file, entry.listItemIndex);
}
// Patch header with final pageCount, lutOffset, anchorMapOffset, paragraphLutOffset, and liLutOffset
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(pageCount));
serialization::writePod(file, pageCount);
if (asPartial) {
// Watermark trailer, located on load as liLutOffset + pageCount * sizeof(uint16_t).
serialization::writePod(file, bytesConsumed);
serialization::writePod(file, totalBytes);
}
// Patch header with the built page count and section offsets...
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(builtPageCount_));
serialization::writePod(file, builtPageCount_);
serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset);
serialization::writePod(file, paragraphLutOffset);
serialization::writePod(file, liLutFileOffset);
// ...then commit by overwriting the sentinel version with the real one. Writing the
// version last makes it the commit point: a crash before here leaves version 0.
file.seek(0);
serialization::writePod(file, version);
// Explicit close() required: member variable persists beyond function scope
file.close();
if (cssParser) {
cssParser->clear();
// Swap into place. A crash between remove and rename loses the old file but keeps a
// fully-committed tmp; the next build just removes it and rebuilds.
if (Storage.exists(filePath.c_str())) {
Storage.remove(filePath.c_str());
}
if (!Storage.rename(binTmpPath().c_str(), filePath.c_str())) {
LOG_ERR("SCT", "Failed to move built section into place");
Storage.remove(binTmpPath().c_str());
return false;
}
return true;
}
std::unique_ptr<Page> Section::loadPageFromSectionFile() {
if (!Storage.openFileForRead("SCT", filePath, file)) {
bool Section::finalizeBuild() {
// Flush the trailing page (emits the last page via the completePageFn into the LUT).
build_->parser->finishParse();
if (!build_->reusedHtml) {
// Parse succeeded: promote the freshly unzipped HTML to the persistent cache so future
// rebuilds skip zip inflation. If promotion fails, drop the temp -- the build still succeeded.
if (!Storage.rename(build_->tmpHtmlPath.c_str(), build_->htmlPath.c_str())) {
LOG_DBG("SCT", "Failed to promote HTML cache, removing temp");
Storage.remove(build_->tmpHtmlPath.c_str());
}
}
const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0);
if (build_->cssParser) build_->cssParser->clear();
build_.reset();
if (!committed) {
// commitBuildFile removed filePath before the failed swap, so nothing valid remains.
partial_ = false;
partialPageCount_ = 0;
pageCount = 0;
builtPageCount_ = 0;
return false;
}
buildComplete_ = true;
partial_ = false;
partialPageCount_ = 0;
pageCount = builtPageCount_;
return true;
}
void Section::suspendBuild() {
if (!build_) return;
// Only worth persisting if this build produced pages a pre-existing partial doesn't
// already cover; otherwise keep the older (bigger) partial and just drop the tmp.
const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_);
bool committed = false;
if (worthKeeping) {
// Capture the parse watermark and commit before tearing the parser down (the anchor
// map is read from it). The incomplete trailing page is intentionally not flushed:
// only fully laid-out pages are persisted, and the rebuild re-derives the rest.
const uint32_t consumed = static_cast<uint32_t>(build_->parser->parseBytesConsumed());
committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes);
if (committed) {
partial_ = true;
partialPageCount_ = builtPageCount_;
partialBytesConsumed_ = consumed;
partialTotalBytes_ = build_->totalBytes;
LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_);
}
}
if (build_->parser) build_->parser->abortParse();
if (build_->cssParser) build_->cssParser->clear();
if (!committed && file) {
// Explicit close() required before remove (member variable, O_RDWR handle).
file.close();
Storage.remove(binTmpPath().c_str());
}
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
Storage.remove(build_->tmpHtmlPath.c_str());
}
build_.reset();
buildComplete_ = false;
pageCount = partial_ ? partialPageCount_ : 0;
builtPageCount_ = 0;
}
void Section::abandonBuild() {
if (!build_) return;
if (build_->parser) build_->parser->abortParse();
if (build_->cssParser) build_->cssParser->clear();
if (file) {
// Explicit close() required before remove (member variable, O_RDWR handle).
file.close();
Storage.remove(binTmpPath().c_str());
}
// A parse error would recur against the same HTML, so drop any partial too -- resuming
// from it would just re-enter the failing build every open.
if (Storage.exists(filePath.c_str())) {
Storage.remove(filePath.c_str());
}
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
Storage.remove(build_->tmpHtmlPath.c_str());
}
build_.reset();
buildComplete_ = false;
partial_ = false;
partialPageCount_ = 0;
pageCount = 0;
builtPageCount_ = 0;
}
std::unique_ptr<Page> Section::loadPageDuringBuild(const int page) {
if (!build_ || page < 0 || page >= static_cast<int>(build_->lut.size()) || !file) {
return nullptr;
}
const uint32_t pos = build_->lut[page].fileOffset;
if (pos == 0) {
return nullptr;
}
// The .bin is open O_RDWR for the build. Read the already-written page, then restore
// the write cursor so the next onPageComplete keeps appending where it left off.
const uint32_t writePos = file.position();
file.seek(pos);
auto p = Page::deserialize(file);
file.seek(writePos);
return p;
}
// Read a page from the committed file at filePath (finalized section or partial from a
// previous session). Uses a local handle so it is safe while a build holds the member
// `file` open on the tmp .bin.
std::unique_ptr<Page> Section::loadPageAt(const int page) const {
HalFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return nullptr;
}
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
uint32_t lutOffset;
serialization::readPod(file, lutOffset);
file.seek(lutOffset + sizeof(uint32_t) * currentPage);
serialization::readPod(f, lutOffset);
f.seek(lutOffset + sizeof(uint32_t) * page);
uint32_t pagePos;
serialization::readPod(file, pagePos);
file.seek(pagePos);
serialization::readPod(f, pagePos);
f.seek(pagePos);
auto page = Page::deserialize(file);
// Explicit close() required: member variable persists beyond function scope
file.close();
return page;
return Page::deserialize(f);
// No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
}
std::unique_ptr<Page> Section::loadPage(const int page) {
if (page < 0) {
return nullptr;
}
if (build_ && page < static_cast<int>(build_->lut.size())) {
return loadPageDuringBuild(page);
}
// Not (yet) in the active build: serve from the file on disk -- a finalized section,
// or a partial from a previous session whose pages the rebuild hasn't reached again.
const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount);
if (page >= onDisk) {
return nullptr;
}
return loadPageAt(page);
}
std::string Section::getTextFromSectionFile() {
std::string fullText;
auto p = this->loadPageFromSectionFile();
auto p = loadPage(currentPage);
if (p) {
for (const auto& el : p->elements) {
if (el->getTag() == TAG_PageLine) {
@@ -361,6 +756,15 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
return std::nullopt;
}
// Only a finalized section's count is the chapter total; a partial's count is just the
// suspended build's watermark, which would skew progress mapping. Callers fall back to
// their own estimates.
uint8_t version;
serialization::readPod(f, version);
if (version != SECTION_FILE_VERSION) {
return std::nullopt;
}
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
uint16_t count;
serialization::readPod(f, count);
+105 -7
View File
@@ -3,11 +3,14 @@
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "Epub.h"
class Page;
class GfxRenderer;
class ChapterHtmlSlimParser;
class CssParser;
class Section {
std::shared_ptr<Epub> epub;
@@ -21,16 +24,67 @@ class Section {
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
uint32_t onPageComplete(std::unique_ptr<Page> page);
// Page-offset table entry, kept in RAM while an incremental build is running so
// already-built pages can be located in the partially-written .bin.
struct PageLutEntry {
uint32_t fileOffset;
uint16_t paragraphIndex;
uint16_t listItemIndex;
};
// Held only while an incremental build is in progress (see startBuild). Carries the
// live parser plus the strings it references (the parser stores them by reference)
// and the in-RAM page-offset table.
struct BuildContext {
std::unique_ptr<ChapterHtmlSlimParser> parser;
std::vector<PageLutEntry> lut;
std::string parsePath;
std::string contentBase;
std::string imageBasePath;
std::string htmlPath;
std::string tmpHtmlPath;
bool reusedHtml = false;
CssParser* cssParser = nullptr;
// HTML byte progress, for estimating the section's total page count while it's still building.
uint32_t bytesConsumed = 0;
uint32_t totalBytes = 0;
// Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its
// last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions;
// the EMA is stepped once per build advance (not per redraw) to damp that wobble.
float smoothedEstimate = 0;
uint32_t smoothedAtConsumed = 0;
};
std::unique_ptr<BuildContext> build_;
bool buildComplete_ = false;
// Pages laid out by the active build (== build_->lut.size()). Distinct from pageCount,
// which is the pages *available to read* and also counts a loaded partial file's pages.
uint16_t builtPageCount_ = 0;
// A partial section file (suspended build from a previous session) is loaded at filePath.
// Its pages 0..partialPageCount_-1 are readable while a rebuild extends past them.
bool partial_ = false;
uint16_t partialPageCount_ = 0;
// Parse watermark from the partial's trailer, for estimating the total page count.
uint32_t partialBytesConsumed_ = 0;
uint32_t partialTotalBytes_ = 0;
bool finalizeBuild();
// Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the
// header, stamp the version byte, and swap the tmp .bin over filePath.
bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes);
// Builds write here and are swapped over filePath only on commit, so a prior
// partial/finalized file stays readable while a rebuild is in progress.
std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const;
// Read a page already laid out by the in-progress build (page < build LUT size), from
// the partially-written tmp .bin without disturbing the build's write cursor.
std::unique_ptr<Page> loadPageDuringBuild(int page);
public:
uint16_t pageCount = 0;
int currentPage = 0;
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
: epub(epub),
spineIndex(spineIndex),
renderer(renderer),
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
~Section() = default;
// Constructor and destructor are out-of-line: BuildContext holds a unique_ptr to the
// forward-declared ChapterHtmlSlimParser, whose full definition is only visible in the .cpp.
explicit Section(const std::shared_ptr<Epub>& epub, int spineIndex, GfxRenderer& renderer);
~Section();
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, bool focusReadingEnabled);
@@ -39,12 +93,56 @@ class Section {
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, bool focusReadingEnabled,
const std::function<void()>& popupFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Incremental build: lay out the section a few pages at a time so a large chapter
// can show its first page immediately and keep the UI responsive while the rest
// builds. createSectionFile() above is the one-shot wrapper over these.
// if (!startBuild(...)) fail;
// each tick: buildSomeMore(N); render up to pageCount; when isBuildComplete() stop.
bool startBuild(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
uint8_t imageRendering, bool focusReadingEnabled, const std::function<void()>& popupFn = nullptr);
// Lay out up to maxPages more pages (maxPages <= 0 = build to completion). Returns
// false on error (the build is abandoned). Sets isBuildComplete() when finished.
bool buildSomeMore(int maxPages);
bool isBuilding() const { return static_cast<bool>(build_); }
bool isBuildComplete() const { return buildComplete_; }
// Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based
// estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine
// is still building, so "page X of Y" / progress don't read off the small build watermark.
uint16_t estimatedTotalPages() const;
void abandonBuild();
// Persist an in-progress build as a partial section file (version sentinel + LUTs +
// watermark trailer) instead of discarding it, so the next open of this spine can show
// its pages instantly and only rebuild in the background. Called by the destructor, so
// any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a
// pre-existing partial when it covers more pages than this build reached.
void suspendBuild();
// True when a partial file was loaded: pageCount is a watermark, not the chapter total.
bool isPartial() const { return partial_; }
// Unified page read: from the active build if it has reached the page, otherwise from
// the on-disk file (finalized section, or a partial the rebuild hasn't caught up to).
std::unique_ptr<Page> loadPage(int page);
std::string getTextFromSectionFile();
// Resolve an anchor from the in-progress build first, then the on-disk anchor map
// (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
bool hasHtmlCache() const;
// Look up the page number for an anchor id from the section cache file.
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
// Look up an anchor among the pages built so far by the in-progress build, so an anchor jump
// (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the
// whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build.
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
// Get the page count from the section cache file without fully loading it.
std::optional<uint16_t> getCachedPageCount() const;
+71 -5
View File
@@ -21,6 +21,39 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
const bool scanning = renderer.isFontCacheScanning();
const int ascender = renderer.getFontAscenderSize(fontId);
struct DecorationLineTracker {
EpdFontFamily::Style style;
int yOffset;
int startX = -1;
int endX = -1;
int yPos = 0;
bool active() const { return startX != -1; }
void reset() {
startX = -1;
endX = -1;
yPos = 0;
}
};
DecorationLineTracker decorationLines[] = {
{EpdFontFamily::UNDERLINE, ascender + 2},
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
};
const auto flushDecoration = [&](DecorationLineTracker& line) {
if (line.active()) {
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
line.reset();
}
};
const auto flushDecorations = [&]() {
for (auto& line : decorationLines) {
flushDecoration(line);
}
};
for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i];
@@ -59,18 +92,51 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir);
}
if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) {
if (scanning) {
continue;
}
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
const std::string& w = words[i];
int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
const int underlineY = wordY + ascender + 2;
int lineStartX = wordX;
int lineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
underlineWidth = (underlineWidth + 1) / 2;
lineWidth = (lineWidth + 1) / 2;
}
renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true);
// Do not decorate the synthetic em-space used for paragraph indentation.
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
static_cast<uint8_t>(w[2]) == 0x83) {
const char* visibleText = w.c_str() + 3;
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
lineWidth = (lineWidth + 1) / 2;
}
}
for (auto& line : decorationLines) {
if ((currentStyle & line.style) == 0) {
flushDecoration(line);
continue;
}
const int lineY = wordY + line.yOffset;
if (line.active() && line.yPos != lineY) {
flushDecoration(line);
}
if (!line.active()) {
line.startX = lineStartX;
line.yPos = lineY;
}
line.endX = lineStartX + lineWidth;
}
} else {
flushDecorations();
}
}
flushDecorations();
}
bool TextBlock::serialize(HalFile& file) const {
+15 -13
View File
@@ -67,13 +67,6 @@ constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKe
[](char a, char b) { return asciiToLower(a) == b; });
}
// Case-insensitive ASCII substring search. Only needed by text-decoration,
// which accepts multi-value strings like "underline solid red".
constexpr bool icontainsAscii(std::string_view value, std::string_view lowercaseKeyword) {
return std::search(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
[](char a, char b) { return asciiToLower(a) == b; }) != value.end();
}
// Walk s and invoke fn(token) for each non-empty run between delimiters.
// Tokens are boundary-trimmed and yielded as string_views into s; no
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
@@ -252,11 +245,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
}
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
// text-decoration can have multiple space-separated values
if (icontainsAscii(val, "underline")) {
return CssTextDecoration::Underline;
}
return CssTextDecoration::None;
// text-decoration can have multiple space-separated values. Compare whole tokens
// so malformed values like "notunderline" do not accidentally enable a line.
CssTextDecoration result = CssTextDecoration::None;
bool explicitNone = false;
forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
if (iequalsAscii(token, "none")) {
explicitNone = true;
} else if (iequalsAscii(token, "underline")) {
result = result | CssTextDecoration::Underline;
} else if (iequalsAscii(token, "line-through")) {
result = result | CssTextDecoration::LineThrough;
}
});
return explicitNone ? CssTextDecoration::None : result;
}
CssLength CssParser::interpretLength(std::string_view val) {
@@ -868,7 +870,7 @@ bool CssParser::loadFromCache() {
rulesBySelector_.clear();
return false;
}
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
style.textDecoration = static_cast<CssTextDecoration>(enumVal & CSS_TEXT_DECORATION_MASK);
if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear();
+1 -1
View File
@@ -33,7 +33,7 @@
class CssParser {
public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 6;
static constexpr uint8_t CSS_CACHE_VERSION = 7;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default;
+13 -2
View File
@@ -52,8 +52,19 @@ enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
// Font weight options - CSS supports 100-900, we simplify to normal/bold
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
// Text decoration options
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
// Text decoration options. Values are bit flags so CSS can combine multiple line decorations.
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
}
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
}
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
// Display options - only None and Block are relevant for e-ink rendering
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
+137 -93
View File
@@ -34,6 +34,7 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
constexpr const char* IMAGE_TAGS[] = {"img"};
constexpr const char* SKIP_TAGS[] = {"head"};
@@ -89,13 +90,50 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const
}
}
// Update effective bold/italic/underline based on block style and inline style stack
EpdFontFamily::Style ChapterHtmlSlimParser::fontStyleForTextDecoration(const CssTextDecoration decoration) {
EpdFontFamily::Style style = EpdFontFamily::REGULAR;
if ((decoration & CssTextDecoration::Underline) != CssTextDecoration::None) {
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::UNDERLINE);
}
if ((decoration & CssTextDecoration::LineThrough) != CssTextDecoration::None) {
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::STRIKETHROUGH);
}
return style;
}
void ChapterHtmlSlimParser::applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css) {
if (css.hasTextDecoration()) {
entry.hasTextDecoration = true;
entry.textDecoration = css.textDecoration;
}
}
void ChapterHtmlSlimParser::pushDecorationStyleEntry(const CssTextDecoration defaultDecoration,
const CssStyle& cssStyle) {
StyleStackEntry entry;
entry.depth = depth;
entry.hasTextDecoration = true;
entry.textDecoration = cssStyle.hasTextDecoration() ? cssStyle.textDecoration : defaultDecoration;
if (cssStyle.hasFontWeight()) {
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
}
if (cssStyle.hasFontStyle()) {
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
applyDirectionToEntry(entry, cssStyle);
inlineStyleStack.push_back(entry);
updateEffectiveInlineStyle();
}
// Update effective bold/italic/decorations based on block style and inline style stack
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
// Start with block-level styles
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
effectiveUnderline =
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
effectiveTextDecoration =
currentCssStyle.hasTextDecoration() ? currentCssStyle.textDecoration : CssTextDecoration::None;
effectiveDirectionDefined = currentCssStyle.hasDirection();
effectiveDirection = currentCssStyle.direction;
effectiveSup = false;
@@ -109,8 +147,10 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
if (entry.hasItalic) {
effectiveItalic = entry.italic;
}
if (entry.hasUnderline) {
effectiveUnderline = entry.underline;
// CSS line decorations propagate through descendants; child entries add
// their own lines but cannot cancel an ancestor's already active line.
if (entry.hasTextDecoration) {
effectiveTextDecoration = effectiveTextDecoration | entry.textDecoration;
}
if (entry.hasDirection) {
effectiveDirectionDefined = true;
@@ -164,7 +204,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
// Determine font style from depth-based tracking and CSS effective style
const bool isBold = boldUntilDepth < depth || effectiveBold;
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
// Combine style flags using bitwise OR
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
@@ -174,9 +213,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
if (isItalic) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
}
if (isUnderline) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
}
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | fontStyleForTextDecoration(effectiveTextDecoration));
if (effectiveSup) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
} else if (effectiveSub) {
@@ -420,14 +457,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
headerStyle.bold = false;
headerStyle.hasItalic = true;
headerStyle.italic = true;
headerStyle.hasUnderline = true;
headerStyle.underline = false;
self->inlineStyleStack.push_back(headerStyle);
self->updateEffectiveInlineStyle();
const CssTextDecoration savedTextDecoration = self->effectiveTextDecoration;
self->effectiveTextDecoration = CssTextDecoration::None;
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
}
self->effectiveTextDecoration = savedTextDecoration;
self->nextWordContinues = false;
self->inlineStyleStack.pop_back();
self->updateEffectiveInlineStyle();
@@ -760,12 +798,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->currentFootnote.number[0] = '\0';
self->currentFootnoteLinkTextLen = 0;
// Apply underline style to visually indicate the link
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
// Apply underline style to visually indicate the link.
StyleStackEntry entry;
entry.depth = self->depth;
entry.hasUnderline = true;
entry.underline = true;
entry.hasTextDecoration = true;
entry.textDecoration = CssTextDecoration::Underline;
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
@@ -837,23 +874,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->flushPartWordBuffer();
self->nextWordContinues = true;
}
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
// Push inline style entry for underline tag
StyleStackEntry entry;
entry.depth = self->depth; // Track depth for matching pop
entry.hasUnderline = true;
entry.underline = true;
if (cssStyle.hasFontWeight()) {
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
self->pushDecorationStyleEntry(CssTextDecoration::Underline, cssStyle);
} else if (matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
self->nextWordContinues = true;
}
if (cssStyle.hasFontStyle()) {
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
self->pushDecorationStyleEntry(CssTextDecoration::LineThrough, cssStyle);
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
@@ -870,10 +898,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
if (cssStyle.hasTextDecoration()) {
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyTextDecorationToEntry(entry, cssStyle);
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
@@ -893,10 +918,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
}
if (cssStyle.hasTextDecoration()) {
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyTextDecorationToEntry(entry, cssStyle);
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
@@ -935,10 +957,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
if (cssStyle.hasTextDecoration()) {
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyTextDecorationToEntry(entry, cssStyle);
applyDirectionToEntry(entry, cssStyle);
if (cssStyle.hasVerticalAlign()) {
if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
@@ -1150,9 +1169,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic;
const bool headerOrBlockTag = isHeaderOrBlock(name);
const bool tableStructuralTag = isTableStructuralTag(name);
@@ -1171,7 +1189,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag ||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) ||
matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS)) || tableStructuralTag ||
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
if (shouldFlush) {
@@ -1230,11 +1249,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->italicUntilDepth = INT_MAX;
}
// Leaving underline tag
if (self->underlineUntilDepth == self->depth) {
self->underlineUntilDepth = INT_MAX;
}
// Pop from inline style stack if we pushed an entry at this depth
// This handles all inline elements: b, i, u, span, etc.
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
@@ -1261,7 +1275,9 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
}
}
bool ChapterHtmlSlimParser::parseAndBuildPages() {
ChapterHtmlSlimParser::~ChapterHtmlSlimParser() { abortParse(); }
bool ChapterHtmlSlimParser::beginParse() {
// Initialize block style stack with a root entry representing "no ancestor block elements".
// The user's paragraph alignment is set as the default so child elements without explicit
// text-align inherit it correctly through getCombinedBlockStyle.
@@ -1279,67 +1295,78 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
paragraphAlignmentBlockStyle.alignment = align;
startNewTextBlock(paragraphAlignmentBlockStyle);
XML_Parser parser = XML_ParserCreate(nullptr);
int done;
if (!parser) {
xmlParser_ = XML_ParserCreate(nullptr);
if (!xmlParser_) {
LOG_ERR("EHP", "Couldn't allocate memory for parser");
return false;
}
// Handle HTML entities (like &nbsp;) that aren't in XML spec or DTD
// Using DefaultHandlerExpand preserves normal entity expansion from DOCTYPE
XML_SetDefaultHandlerExpand(parser, defaultHandlerExpand);
XML_SetDefaultHandlerExpand(xmlParser_, defaultHandlerExpand);
HalFile file;
if (!Storage.openFileForRead("EHP", filepath, file)) {
destroyXmlParser(parser);
if (!Storage.openFileForRead("EHP", filepath, parseFile_)) {
destroyXmlParser(xmlParser_);
xmlParser_ = nullptr;
return false;
}
// Get file size to decide whether to show indexing popup.
if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) {
if (popupFn && parseFile_.size() >= MIN_SIZE_FOR_POPUP) {
popupFn();
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
XML_SetUserData(xmlParser_, this);
XML_SetElementHandler(xmlParser_, startElement, endElement);
XML_SetCharacterDataHandler(xmlParser_, characterData);
// Compute the time taken to parse and build pages
const uint32_t chapterStartTime = millis();
do {
void* const buf = XML_GetBuffer(parser, PARSE_BUFFER_SIZE);
if (!buf) {
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
destroyXmlParser(parser);
file.close();
return false;
}
parseStartTime_ = millis();
return true;
}
const size_t len = file.read(buf, PARSE_BUFFER_SIZE);
ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() {
void* const buf = XML_GetBuffer(xmlParser_, PARSE_BUFFER_SIZE);
if (!buf) {
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
return ParseStatus::Error;
}
if (len == 0 && file.available() > 0) {
LOG_ERR("EHP", "File read error");
destroyXmlParser(parser);
file.close();
return false;
}
const size_t len = parseFile_.read(buf, PARSE_BUFFER_SIZE);
done = file.available() == 0;
if (len == 0 && parseFile_.available() > 0) {
LOG_ERR("EHP", "File read error");
return ParseStatus::Error;
}
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
destroyXmlParser(parser);
file.close();
return false;
}
} while (!done);
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - chapterStartTime);
const int done = parseFile_.available() == 0;
destroyXmlParser(parser);
file.close();
if (XML_ParseBuffer(xmlParser_, static_cast<int>(len), done) == XML_STATUS_ERROR) {
LOG_ERR("EHP", "Parse error at line %lu:\n%s", XML_GetCurrentLineNumber(xmlParser_),
XML_ErrorString(XML_GetErrorCode(xmlParser_)));
return ParseStatus::Error;
}
return done ? ParseStatus::Done : ParseStatus::More;
}
void ChapterHtmlSlimParser::abortParse() {
if (xmlParser_) {
destroyXmlParser(xmlParser_);
xmlParser_ = nullptr;
}
// Only close the file if it was successfully opened in beginParse()
if (parseFile_.isOpen()) {
parseFile_.close();
}
}
bool ChapterHtmlSlimParser::finishParse() {
if (xmlParser_) {
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - parseStartTime_);
destroyXmlParser(xmlParser_);
xmlParser_ = nullptr;
}
parseFile_.close();
// Process last page if there is still text
if (currentTextBlock) {
@@ -1357,6 +1384,23 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return true;
}
bool ChapterHtmlSlimParser::parseAndBuildPages() {
if (!beginParse()) {
return false;
}
for (;;) {
const ParseStatus status = parseStep();
if (status == ParseStatus::Error) {
abortParse();
return false;
}
if (status == ParseStatus::Done) {
break;
}
}
return finishParse();
}
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
+38 -4
View File
@@ -1,5 +1,6 @@
#pragma once
#include <HalStorage.h>
#include <expat.h>
#include <climits>
@@ -31,7 +32,6 @@ class ChapterHtmlSlimParser {
int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX;
int underlineUntilDepth = INT_MAX;
// buffer for building up words from characters, will auto break if longer than this
// leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
@@ -60,7 +60,8 @@ class ChapterHtmlSlimParser {
int depth = 0;
bool hasBold = false, bold = false;
bool hasItalic = false, italic = false;
bool hasUnderline = false, underline = false;
bool hasTextDecoration = false;
CssTextDecoration textDecoration = CssTextDecoration::None;
bool hasDirection = false;
CssTextDirection direction = CssTextDirection::Ltr;
bool hasSup = false, sup = false;
@@ -71,7 +72,7 @@ class ChapterHtmlSlimParser {
CssStyle currentCssStyle;
bool effectiveBold = false;
bool effectiveItalic = false;
bool effectiveUnderline = false;
CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
bool effectiveDirectionDefined = false;
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
bool effectiveSup = false;
@@ -96,12 +97,25 @@ class ChapterHtmlSlimParser {
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
int wordsExtractedInBlock = 0;
// Resumable parse state. The one-shot parseAndBuildPages() drives these
// internally; the incremental section builder drives them across render ticks
// so a large single chapter can yield between pages instead of blocking the UI
// until the whole thing is laid out. parseFile_ and the expat parser stay alive
// for the lifetime of the parse so it can be paused and resumed at buffer
// boundaries.
XML_Parser xmlParser_ = nullptr;
HalFile parseFile_;
uint32_t parseStartTime_ = 0;
void updateEffectiveInlineStyle();
void startNewTextBlock(const BlockStyle& blockStyle);
void flushPendingAnchor();
void flushPartWordBuffer();
void makePages();
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
void emitHorizontalRule(const BlockStyle& blockStyle);
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
@@ -141,8 +155,28 @@ class ChapterHtmlSlimParser {
imageBasePath(imageBasePath),
tocAnchors(std::move(tocAnchors)) {}
~ChapterHtmlSlimParser() = default;
~ChapterHtmlSlimParser();
// One-shot parse: builds every page before returning (begin + step* + finish).
bool parseAndBuildPages();
// Resumable parse, for the incremental section builder. Drive as:
// if (!beginParse()) fail;
// loop: switch (parseStep()) { More: keep going / yield; Done: finishParse(); Error: abortParse(); }
// Pages are emitted via completePageFn as they complete during parseStep(), so
// the caller can stop once enough pages are built and resume on a later tick.
enum class ParseStatus { More, Done, Error };
bool beginParse();
ParseStatus parseStep();
bool finishParse(); // flush the trailing page and tear down; returns true
void abortParse(); // tear down without flushing (error / abandon)
void addLineToPage(std::shared_ptr<TextBlock> line);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
// between beginParse() and finishParse()/abortParse().
size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : 0; }
size_t parseTotalBytes() { return parseFile_ ? parseFile_.size() : 0; }
};
+48 -40
View File
@@ -79,6 +79,53 @@ std::string normalisePath(const std::string& path) {
return result;
}
bool naturalLess(const std::string& str1, const std::string& str2) {
// Naive natural sort: numeric-aware, case-insensitive
const char* s1 = str1.c_str();
const char* s2 = str2.c_str();
// ctype functions require unsigned char values: passing a negative char (UTF-8
// bytes above 0x7f with signed char) is undefined behavior
const auto isDigit = [](const char c) { return isdigit(static_cast<unsigned char>(c)) != 0; };
// Iterate while both strings have characters
while (*s1 && *s2) {
// Check if both are at the start of a number
if (isDigit(*s1) && isDigit(*s2)) {
// Skip leading zeros and track them
while (*s1 == '0') s1++;
while (*s2 == '0') s2++;
// Count digits to compare lengths first
int len1 = 0, len2 = 0;
while (isDigit(s1[len1])) len1++;
while (isDigit(s2[len2])) len2++;
// Different length so return smaller integer value
if (len1 != len2) return len1 < len2;
// Same length so compare digit by digit
for (int i = 0; i < len1; i++) {
if (s1[i] != s2[i]) return s1[i] < s2[i];
}
// Numbers equal so advance pointers
s1 += len1;
s2 += len2;
} else {
// Regular case-insensitive character comparison
const int c1 = tolower(static_cast<unsigned char>(*s1));
const int c2 = tolower(static_cast<unsigned char>(*s2));
if (c1 != c2) return c1 < c2;
s1++;
s2++;
}
}
// One string is prefix of other
return *s1 == '\0' && *s2 != '\0';
}
void sortFileList(std::vector<std::string>& strs) {
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
// Directories first
@@ -86,46 +133,7 @@ void sortFileList(std::vector<std::string>& strs) {
bool isDir2 = str2.back() == '/';
if (isDir1 != isDir2) return isDir1;
// Start naive natural sort
const char* s1 = str1.c_str();
const char* s2 = str2.c_str();
// Iterate while both strings have characters
while (*s1 && *s2) {
// Check if both are at the start of a number
if (isdigit(*s1) && isdigit(*s2)) {
// Skip leading zeros and track them
while (*s1 == '0') s1++;
while (*s2 == '0') s2++;
// Count digits to compare lengths first
int len1 = 0, len2 = 0;
while (isdigit(s1[len1])) len1++;
while (isdigit(s2[len2])) len2++;
// Different length so return smaller integer value
if (len1 != len2) return len1 < len2;
// Same length so compare digit by digit
for (int i = 0; i < len1; i++) {
if (s1[i] != s2[i]) return s1[i] < s2[i];
}
// Numbers equal so advance pointers
s1 += len1;
s2 += len2;
} else {
// Regular case-insensitive character comparison
char c1 = tolower(*s1);
char c2 = tolower(*s2);
if (c1 != c2) return c1 < c2;
s1++;
s2++;
}
}
// One string is prefix of other
return *s1 == '\0' && *s2 != '\0';
return naturalLess(str1, str2);
});
}
+4
View File
@@ -11,6 +11,10 @@ std::string decodeUriEscapes(const std::string& path);
std::string normalisePath(const std::string& path);
// Numeric-aware, case-insensitive comparison ("2" < "10"). Returns true when str1 orders
// before str2. Same ordering sortFileList applies within the file/directory groups.
bool naturalLess(const std::string& str1, const std::string& str2);
void sortFileList(std::vector<std::string>& strs);
/**
+29 -4
View File
@@ -282,8 +282,17 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3);
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
// Black (also paints over the grays in BW mode)
renderer.drawPixel(screenX, screenY, pixelState);
// Default: gray pixels paint solid black. Required when grayscale
// passes follow (their LUT lightens pixels it assumes were just
// driven solid black), and it is the plain non-AA look otherwise.
// Fast AA: partial (anti-aliased edge) coverage dithers with the
// same period-2 patterns as fillRectDither (dark grey = 50%
// checkerboard, light grey = 25%), approximating AA in a single
// BW pass. Skipped pixels leave the background untouched.
if (!renderer.isFastAntiAliasing() || bmpVal == 0 || (bmpVal == 1 && ((screenX + screenY) & 1) == 0) ||
(bmpVal == 2 && (screenX & 1) == 0 && (screenY & 1) == 0)) {
renderer.drawPixel(screenX, screenY, pixelState);
}
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
// Light gray (also mark the MSB if it's going to be a dark gray too)
// Dedicated X3 gray LUTs now provide proper 4-level gray on both devices
@@ -1054,8 +1063,24 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
}
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width);
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int size) const {
// Plot the icon pixel-by-pixel through drawPixel (which applies the orientation
// transform) instead of the byte-aligned framebuffer blit. The blit snaps the
// icon's position to 8px (one byte) along the rotated axis, which prevents it
// from aligning with adjacent text; per-pixel plotting is pixel-precise.
// Icons are square and 1bpp (MSB-first, bit==0 = ink). The (size-1-row, col)
// mapping reproduces the Portrait orientation the blit produced; drawIcon is
// only called by the UI themes, which all render in forced Portrait.
const int rowBytes = (size + 7) / 8;
for (int row = 0; row < size; row++) {
for (int col = 0; col < size; col++) {
const uint8_t byte = bitmap[row * rowBytes + (col >> 3)];
const bool ink = ((byte >> (7 - (col & 7))) & 1) == 0;
if (ink) {
drawPixel(x + (size - 1 - row), y + col, true);
}
}
}
}
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
+10 -1
View File
@@ -44,6 +44,13 @@ class GfxRenderer {
RenderMode renderMode;
Orientation orientation;
bool fadingFix;
// Fast text anti-aliasing: when set, BW-mode rendering of 2-bit glyphs
// dithers partial-coverage (gray) pixels instead of painting them solid
// black, approximating AA in a single BW pass with no grayscale refresh.
// Must stay false when grayscale LSB/MSB passes follow the BW render: the
// gray LUT lightens pixels it assumes were just driven solid black, and
// dither-skipped white pixels would take its white-drive frames unopposed.
bool fastAntiAliasing = false;
uint8_t* frameBuffer = nullptr;
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
@@ -183,7 +190,7 @@ class GfxRenderer {
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
bool roundBottomLeft, bool roundBottomRight, Color color) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawIcon(const uint8_t bitmap[], int x, int y, int size) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
float cropY = 0) const;
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
@@ -224,6 +231,8 @@ class GfxRenderer {
// Grayscale functions
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
RenderMode getRenderMode() const { return renderMode; }
void setFastAntiAliasing(const bool enabled) { this->fastAntiAliasing = enabled; }
bool isFastAntiAliasing() const { return fastAntiAliasing; }
// 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
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Рэжым вокладкі сну"
STR_HIDE_BATTERY: "Схаваць % батарэі"
STR_EXTRA_SPACING: "Дадат. інтэрвал абзаца"
STR_TEXT_AA: "Згладжванне тэксту"
STR_TEXT_AA_FULL: "Поўнае"
STR_TEXT_AA_FAST: "Хуткае"
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
STR_ORIENTATION: "Арыентацыя чытання"
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
@@ -236,7 +238,6 @@ STR_CHAPTER_PREFIX: "Раздзел:"
STR_PAGES_SEPARATOR: "стар. |"
STR_BOOK_PREFIX: "Кніга:"
STR_CALIBRE_URL_HINT: "Для Calibre дадайце /opds да URL"
STR_PERCENT_STEP_HINT: "Улева/Управа: 1% Уверх/Уніз: 10%"
STR_SYNCING_TIME: "Сінхранізацыя часу..."
STR_CALC_HASH: "Вылічэнне хэша дакумента..."
STR_HASH_FAILED: "Не ўдалося вылічыць хэш дакумента"
@@ -296,7 +297,8 @@ STR_NO_FOOTNOTES: "На гэтай старонцы няма зносак"
STR_LINK: "[спасылка]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі"
STR_SLEEP_TIMER_STEP_HINT: "Улева/Управа: 1 хв Уверх/Уніз: 5 хв"
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
STR_HIDE_BATTERY: "Oculta el % de bateria"
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
STR_TEXT_AA: "Antialiàsing del text"
STR_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Ràpid"
STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra"
@@ -272,7 +274,6 @@ STR_CHAPTER_PREFIX: "Capítol: "
STR_PAGES_SEPARATOR: " pàgines | "
STR_BOOK_PREFIX: "Llibre: "
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
STR_SYNCING_TIME: "S'està sincronitzant el temps..."
STR_CALC_HASH: "S'està calculant el hash del document..."
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
@@ -305,7 +306,8 @@ STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
STR_LINK: "[enllaç]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Mai"
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min"
STR_STEP_HINT_FRONT: "Botons frontals:"
STR_STEP_HINT_SIDE: "Botons laterals:"
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
+5 -3
View File
@@ -58,13 +58,15 @@ STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odeslat do zařízení“"
STR_CALIBRE_INSTRUCTION_4: "„Při odesílání ponechat tuto obrazovku otevřenou“"
STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čtečka"
STR_CAT_CONTROLS: "Ovládací prvky"
STR_CAT_CONTROLS: "Ovládání"
STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
STR_HIDE_BATTERY: "Skrýt baterii %"
STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
STR_TEXT_AA: "Vyhlazování textu"
STR_TEXT_AA_FULL: "Plné"
STR_TEXT_AA_FAST: "Rychlé"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
@@ -243,7 +245,6 @@ STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |"
STR_BOOK_PREFIX: "Kniha:"
STR_CALIBRE_URL_HINT: "Pro Calibre přidejte /opds do URL adresy"
STR_PERCENT_STEP_HINT: "Vlevo/Vpravo: 1 % Nahoru/Dolů: 10 %"
STR_SYNCING_TIME: "Čas synchronizace..."
STR_CALC_HASH: "Výpočet hashe dokumentu..."
STR_HASH_FAILED: "Nepodařilo se vypočítat hash dokumentu"
@@ -273,5 +274,6 @@ STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
STR_SLEEP_TIMER_STEP_HINT: "Vlevo/Vpravo: 1 min Nahoru/Dolů: 5 min"
STR_STEP_HINT_FRONT: "Přední tlačítka:"
STR_STEP_HINT_SIDE: "Boční tlačítka:"
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Hvile-skærm omslag-tilstand"
STR_HIDE_BATTERY: "Skjul batteri %"
STR_EXTRA_SPACING: "Ekstra afsnitsafstand"
STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_TEXT_AA_FULL: "Fuld"
STR_TEXT_AA_FAST: "Hurtig"
STR_IMAGES: "Billeder"
STR_IMAGES_DISPLAY: "Vis"
STR_IMAGES_PLACEHOLDER: "Pladsholder"
@@ -266,7 +268,6 @@ STR_CHAPTER_PREFIX: "Kapitel: "
STR_PAGES_SEPARATOR: " sider | "
STR_BOOK_PREFIX: "Bog: "
STR_CALIBRE_URL_HINT: "Tilføj /opds til din URL for Calibre"
STR_PERCENT_STEP_HINT: "Venstre/Højre: 1% Op/Ned: 10%"
STR_SYNCING_TIME: "Synkroniserer tid..."
STR_CALC_HASH: "Beregner dokument-hash..."
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
@@ -298,7 +299,8 @@ STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig"
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Højre: 1 min Op/Ned: 5 min"
STR_STEP_HINT_FRONT: "Frontknapper:"
STR_STEP_HINT_SIDE: "Sideknapper:"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Slaapscherm omslag-modus"
STR_HIDE_BATTERY: "Batterij % verbergen"
STR_EXTRA_SPACING: "Extra regelafstand alinea"
STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_TEXT_AA_FULL: "Volledig"
STR_TEXT_AA_FAST: "Snel"
STR_IMAGES: "Afbeeldingen"
STR_IMAGES_DISPLAY: "Weergave"
STR_IMAGES_PLACEHOLDER: "Placeholder"
@@ -266,7 +268,6 @@ STR_CHAPTER_PREFIX: "Hoofdstuk: "
STR_PAGES_SEPARATOR: " pagina's | "
STR_BOOK_PREFIX: "Boek: "
STR_CALIBRE_URL_HINT: "Voeg voor Calibre /opds toe aan de URL"
STR_PERCENT_STEP_HINT: "Links/Rechts: 1% Omhoog/Omlaag: 10%"
STR_SYNCING_TIME: "Tijd synchroniseren..."
STR_CALC_HASH: "Document-hash berekenen..."
STR_HASH_FAILED: "Document-hash berekenen mislukt"
@@ -298,7 +299,8 @@ STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nooit"
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 min Omhoog/Omlaag: 5 min"
STR_STEP_HINT_FRONT: "Voorknoppen:"
STR_STEP_HINT_SIDE: "Zijknoppen:"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
+6 -2
View File
@@ -66,10 +66,14 @@ STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
STR_HIDE_BATTERY: "Hide Battery %"
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
STR_TEXT_AA: "Text Anti-Aliasing"
STR_TEXT_AA_FULL: "Full"
STR_TEXT_AA_FAST: "Fast"
STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Display"
STR_IMAGES_PLACEHOLDER: "Placeholder"
STR_IMAGES_SUPPRESS: "Suppress"
STR_EOB_HOME: "Home"
STR_EOB_CONTINUE_WITH: "Continue with"
STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
@@ -297,7 +301,6 @@ STR_CHAPTER_PREFIX: "Chapter: "
STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Book: "
STR_CALIBRE_URL_HINT: "For Calibre, add /opds to your URL"
STR_PERCENT_STEP_HINT: "Left/Right: 1% Up/Down: 10%"
STR_SYNCING_TIME: "Syncing time..."
STR_CALC_HASH: "Calculating document hash..."
STR_HASH_FAILED: "Failed to calculate document hash"
@@ -332,7 +335,8 @@ STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Take screenshot"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Never"
STR_SLEEP_TIMER_STEP_HINT: "Left/Right: 1 min Up/Down: 5 min"
STR_STEP_HINT_FRONT: "Front buttons:"
STR_STEP_HINT_SIDE: "Side buttons:"
STR_ADD_SERVER: "Add Server"
STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Lepotilanäytön kansitila"
STR_HIDE_BATTERY: "Piilota akun %"
STR_EXTRA_SPACING: "Kappaleiden lisäväli"
STR_TEXT_AA: "Tekstin reunanpehmennys"
STR_TEXT_AA_FULL: "Täysi"
STR_TEXT_AA_FAST: "Nopea"
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
STR_ORIENTATION: "Lukusuunta"
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
@@ -241,7 +243,6 @@ STR_CHAPTER_PREFIX: "Luku: "
STR_PAGES_SEPARATOR: " sivua | "
STR_BOOK_PREFIX: "Kirja: "
STR_CALIBRE_URL_HINT: "Calibrelle lisää /opds osoitteeseen"
STR_PERCENT_STEP_HINT: "Vasen/Oikea: 1% Ylös/Alas: 10%"
STR_SYNCING_TIME: "Synkronoidaan aikaa..."
STR_CALC_HASH: "Lasketaan dokumentin tarkistussummaa..."
STR_HASH_FAILED: "Dokumentin tiivisteen laskenta epäonnistui"
@@ -271,5 +272,6 @@ STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan"
STR_SLEEP_TIMER_STEP_HINT: "Vasen/Oikea: 1 min Ylös/Alas: 5 min"
STR_STEP_HINT_FRONT: "Etupainikkeet:"
STR_STEP_HINT_SIDE: "Sivupainikkeet:"
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Mode écran de veille"
STR_HIDE_BATTERY: "Masquer % batterie"
STR_EXTRA_SPACING: "Espacement paragraphes"
STR_TEXT_AA: "Lissage du texte"
STR_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Rapide"
STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Affichage"
STR_IMAGES_PLACEHOLDER: "Espace réservé"
@@ -267,7 +269,6 @@ STR_CHAPTER_PREFIX: "Chapitre : "
STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Livre : "
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à lURL"
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
STR_SYNCING_TIME: "Synchro de lheure…"
STR_CALC_HASH: "Calcul hash doc…"
STR_HASH_FAILED: "Échec calcul hash"
@@ -299,7 +300,8 @@ STR_NO_FOOTNOTES: "Aucune note sur cette page"
STR_LINK: "[lien]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais"
STR_SLEEP_TIMER_STEP_HINT: "Gauche/Droite : 1 min Haut/Bas : 5 min"
STR_STEP_HINT_FRONT: "Boutons avant :"
STR_STEP_HINT_SIDE: "Boutons latéraux :"
STR_SCREENSHOT_BUTTON: "Capture d'écran"
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
STR_HIDE_BATTERY: "Batterie % ausblenden"
STR_EXTRA_SPACING: "Absatzabstand"
STR_TEXT_AA: "Schriftglättung"
STR_TEXT_AA_FULL: "Voll"
STR_TEXT_AA_FAST: "Schnell"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
@@ -288,7 +290,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " Seiten | "
STR_BOOK_PREFIX: "Buch: "
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
STR_SYNCING_TIME: "Zeit synchronisieren…"
STR_CALC_HASH: "Dokument-Hash berechnen…"
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
@@ -322,7 +323,8 @@ STR_NO_FOOTNOTES: "Keine Fußnoten auf dieser Seite"
STR_LINK: "[Link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u Min."
STR_SLEEP_NEVER: "Nie"
STR_SLEEP_TIMER_STEP_HINT: "Links/Rechts: 1 Min. Hoch/Runter: 5 Min."
STR_STEP_HINT_FRONT: "Vordere Tasten:"
STR_STEP_HINT_SIDE: "Seitentasten:"
STR_ADD_SERVER: "Server hinzufügen"
STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
+4 -2
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "תצוגת כריכה במצב שינה"
STR_HIDE_BATTERY: "הסתר אחוזי סוללה"
STR_EXTRA_SPACING: "מרווח פסקאות מוגדל"
STR_TEXT_AA: "החלקת קצוות טקסט"
STR_TEXT_AA_FULL: "מלאה"
STR_TEXT_AA_FAST: "מהירה"
STR_IMAGES: "תמונות"
STR_IMAGES_DISPLAY: "הצג תמונות"
STR_IMAGES_PLACEHOLDER: "סמן"
@@ -268,7 +270,6 @@ STR_CHAPTER_PREFIX: "פרק: "
STR_PAGES_SEPARATOR: " דפים | "
STR_BOOK_PREFIX: "ספר: "
STR_CALIBRE_URL_HINT: "עבור Calibre, הוסף /opds לכתובת ה-URL"
STR_PERCENT_STEP_HINT: "שמאל/ימין: 1% למעלה/למטה: 10%"
STR_SYNCING_TIME: "מסנכרן שעון..."
STR_CALC_HASH: "מחשב האש..."
STR_HASH_FAILED: "חישוב האש נכשל"
@@ -390,4 +391,5 @@ STR_BOOKMARK_OPTION: "סימנייה"
STR_PWR_BTN_FOOTNOTE_BACK: "חזרה מהירה מהערות שוליים"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u דקות"
STR_SLEEP_NEVER: "אף פעם"
STR_SLEEP_TIMER_STEP_HINT: "שמאל/ימין: 1 דק' למעלה/למטה: 5 דק'"
STR_STEP_HINT_FRONT: "לחצנים קדמיים:"
STR_STEP_HINT_SIDE: "לחצני צד:"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Alvásképernyő borítómód"
STR_HIDE_BATTERY: "Akkumulátor % elrejtése"
STR_EXTRA_SPACING: "Extra bekezdésköz"
STR_TEXT_AA: "Szöveg élsimítás"
STR_TEXT_AA_FULL: "Teljes"
STR_TEXT_AA_FAST: "Gyors"
STR_IMAGES: "Képek"
STR_IMAGES_DISPLAY: "Megjelenítés"
STR_IMAGES_PLACEHOLDER: "Helyőrző"
@@ -263,7 +265,6 @@ STR_CHAPTER_PREFIX: "Fejezet: "
STR_PAGES_SEPARATOR: " oldal | "
STR_BOOK_PREFIX: "Könyv: "
STR_CALIBRE_URL_HINT: "Calibre esetén adj /opds-t az URL-hez"
STR_PERCENT_STEP_HINT: "Bal/Jobb: 1% Fel/Le: 10%"
STR_SYNCING_TIME: "Idő szinkronizálása..."
STR_CALC_HASH: "Dokumentum hash kiszámítása..."
STR_HASH_FAILED: "Dokumentum hash kiszámítása sikertelen"
@@ -295,7 +296,8 @@ STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha"
STR_SLEEP_TIMER_STEP_HINT: "Bal/Jobb: 1 perc Fel/Le: 5 perc"
STR_STEP_HINT_FRONT: "Elülső gombok:"
STR_STEP_HINT_SIDE: "Oldalsó gombok:"
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Modalità copertina"
STR_HIDE_BATTERY: "Nascondi % batteria"
STR_EXTRA_SPACING: "Spaziatura extra paragrafi"
STR_TEXT_AA: "Anti-aliasing testo"
STR_TEXT_AA_FULL: "Completo"
STR_TEXT_AA_FAST: "Veloce"
STR_IMAGES: "Immagini"
STR_IMAGES_DISPLAY: "Visualizza"
STR_IMAGES_PLACEHOLDER: "Segnaposto"
@@ -274,7 +276,6 @@ STR_CHAPTER_PREFIX: "Capitolo: "
STR_PAGES_SEPARATOR: " pagine | "
STR_BOOK_PREFIX: "Libro: "
STR_CALIBRE_URL_HINT: "Per Calibre, aggiungere /opds all'URL"
STR_PERCENT_STEP_HINT: "Sinistra/Destra: 1% Su/Giù: 10%"
STR_SYNCING_TIME: "Sincronizzazione orario..."
STR_CALC_HASH: "Calcolo hash documento..."
STR_HASH_FAILED: "Impossibile calcolare l'hash del documento"
@@ -307,7 +308,8 @@ STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Mai"
STR_SLEEP_TIMER_STEP_HINT: "Sinistra/Destra: 1 min Su/Giù: 5 min"
STR_STEP_HINT_FRONT: "Pulsanti frontali:"
STR_STEP_HINT_SIDE: "Pulsanti laterali:"
STR_SCREENSHOT_BUTTON: "Screenshot"
STR_ADD_SERVER: "Aggiungi server"
STR_SERVER_NAME: "Nome server"
+4 -2
View File
@@ -64,6 +64,8 @@ STR_SLEEP_COVER_MODE: "Ұйқы экраны мұқаба режимі"
STR_HIDE_BATTERY: "Батарея % жасыру"
STR_EXTRA_SPACING: "Қосымша абзац аралығы"
STR_TEXT_AA: "Мәтін сырғытпасы"
STR_TEXT_AA_FULL: "Толық"
STR_TEXT_AA_FAST: "Жылдам"
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
STR_ORIENTATION: "Оқу бағдары"
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
@@ -237,7 +239,6 @@ STR_CHAPTER_PREFIX: "Тарау: "
STR_PAGES_SEPARATOR: " бет | "
STR_BOOK_PREFIX: "Кітап: "
STR_CALIBRE_URL_HINT: "Calibre үшін URL-ге /opds қосыңыз"
STR_PERCENT_STEP_HINT: "Сол/Оң: 1% Жоғары/Төмен: 10%"
STR_SYNCING_TIME: "Уақыт синхрондалуда..."
STR_CALC_HASH: "Құжат хэші есептелуде..."
STR_HASH_FAILED: "Құжат хэшін есептеу сәтсіз"
@@ -294,7 +295,8 @@ STR_NO_FOOTNOTES: "Бұл бетте түсіндірме жазбалар жо
STR_LINK: "[сілтеме]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан"
STR_SLEEP_TIMER_STEP_HINT: "Сол/Оң: 1 мин Жоғары/Төмен: 5 мин"
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
STR_STEP_HINT_SIDE: "Бүйір түймелері:"
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Viršelio režimas"
STR_HIDE_BATTERY: "Slėpti baterijos %"
STR_EXTRA_SPACING: "Tarpai tarp pastraipų"
STR_TEXT_AA: "Teksto glotninimas"
STR_TEXT_AA_FULL: "Pilnas"
STR_TEXT_AA_FAST: "Greitas"
STR_IMAGES: "Paveikslėliai"
STR_IMAGES_DISPLAY: "Rodyti"
STR_IMAGES_PLACEHOLDER: "Vietaženklis"
@@ -263,7 +265,6 @@ STR_CHAPTER_PREFIX: "Sk: "
STR_PAGES_SEPARATOR: " psl. | "
STR_BOOK_PREFIX: "Kn: "
STR_CALIBRE_URL_HINT: "Pridėkite /opds prie URL"
STR_PERCENT_STEP_HINT: "K/D: 1% | V/A: 10%"
STR_SYNCING_TIME: "Sinchr. laikas..."
STR_CALC_HASH: "Skaičiuojama maiša..."
STR_HASH_FAILED: "Maišos klaida"
@@ -295,7 +296,8 @@ STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
STR_LINK: "[nuoroda]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Niekada"
STR_SLEEP_TIMER_STEP_HINT: "Kairė/Dešinė: 1 min. Viršus/Apačia: 5 min."
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Okładki wygaszacza"
STR_HIDE_BATTERY: "Ukryj % baterii"
STR_EXTRA_SPACING: "Dodatkowe odstępy paragrafów"
STR_TEXT_AA: "Wygładzanie tekstu"
STR_TEXT_AA_FULL: "Pełne"
STR_TEXT_AA_FAST: "Szybkie"
STR_IMAGES: "Obrazki"
STR_IMAGES_DISPLAY: "Pokazuj"
STR_IMAGES_PLACEHOLDER: "Ramki"
@@ -275,7 +277,6 @@ STR_CHAPTER_PREFIX: "Rozdział: "
STR_PAGES_SEPARATOR: " stron | "
STR_BOOK_PREFIX: "Książka: "
STR_CALIBRE_URL_HINT: "Dla Calibre, dodaj /opds do adresu URL"
STR_PERCENT_STEP_HINT: "Lewo/Prawo: 1% Góra/Dół: 10%"
STR_SYNCING_TIME: "Synchronizacja czasu..."
STR_CALC_HASH: "Obliczanie sumy kontrolnej..."
STR_HASH_FAILED: "Błąd obliczania sumy kontrolnej"
@@ -308,7 +309,8 @@ STR_NO_FOOTNOTES: "Brak przypisów na tej stronie"
STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nigdy"
STR_SLEEP_TIMER_STEP_HINT: "Lewo/Prawo: 1 min Góra/Dół: 5 min"
STR_STEP_HINT_FRONT: "Przyciski przednie:"
STR_STEP_HINT_SIDE: "Przyciski boczne:"
STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu"
STR_ADD_SERVER: "Dodaj serwer"
STR_SERVER_NAME: "Nazwa serwera"
+138 -28
View File
@@ -19,7 +19,7 @@ STR_END_OF_BOOK: "Fim do livro"
STR_EMPTY_CHAPTER: "Capítulo vazio"
STR_INDEXING: "Indexando"
STR_MEMORY_ERROR: "Erro de memória"
STR_PAGE_LOAD_ERROR: "Erro página"
STR_PAGE_LOAD_ERROR: "Erro ao carregar pág."
STR_EMPTY_FILE: "Arquivo vazio"
STR_OUT_OF_BOUNDS: "Fora dos limites"
STR_LOADING: "Carregando..."
@@ -32,8 +32,8 @@ STR_CONNECTING: "Conectando..."
STR_CONNECTED: "Conectado!"
STR_CONNECTION_FAILED: "Falha na conexão"
STR_FORGET_NETWORK: "Esquecer rede?"
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
STR_SAVE_PASSWORD: "Salvar senha na próxima vez?"
STR_PRESS_OK_SCAN: "Pressione OK para buscar novamente"
STR_JOIN_NETWORK: "Entrar em uma rede"
STR_CREATE_HOTSPOT: "Criar hotspot"
STR_JOIN_DESC: "Conecte-se a uma rede Wi-Fi existente"
@@ -41,7 +41,7 @@ STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas entrarem"
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
STR_HOTSPOT_MODE: "Modo hotspot"
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi-Fi"
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
STR_OPEN_URL_HINT: "Abra esta URL no seu navegador"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
STR_CALIBRE_WIRELESS: "Calibre sem fio"
@@ -50,30 +50,38 @@ STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi-Fi"
STR_TO_PREFIX: "para"
STR_CALIBRE_RECEIVING: "Recebendo:"
STR_CALIBRE_RECEIVED: "Recebido:"
STR_CALIBRE_RECEIVING: "Recebendo: "
STR_CALIBRE_RECEIVED: "Recebido: "
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja na mesma rede Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o dispositivo\""
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar ao dispositivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha esta tela aberta durante o envio\""
STR_CAT_DISPLAY: "Tela"
STR_CAT_READER: "Leitor"
STR_CAT_CONTROLS: "Controles"
STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Tela de repouso"
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
STR_SLEEP_COVER_MODE: "Modo capa tela repouso"
STR_HIDE_BATTERY: "Ocultar % da bateria"
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
STR_TEXT_AA: "Suavização de texto"
STR_TEXT_AA_FULL: "Completa"
STR_TEXT_AA_FAST: "Rápida"
STR_IMAGES: "Imagens"
STR_IMAGES_DISPLAY: "Exibição"
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
STR_IMAGES_SUPPRESS: "Ocultar"
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
STR_ORIENTATION: "Orientação de leitura"
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
STR_LONG_PRESS_BEHAVIOR: "Comportamento do botão de premir e segurar"
STR_LONG_PRESS_BEHAVIOR_OFF: "Desligado"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
STR_LONG_PRESS_BEHAVIOR: "Comportamento de Pressionar e segurar"
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
STR_LONG_PRESS_BEHAVIOR_SKIP: "Pular capítulo"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
STR_FONT_PREVIEW_TEXT: "Vejo galã sexy pôr quinze kiwis à força em baú achatado"
STR_LONG_PRESS_MENU: "Pressionar e segurar Menu"
STR_FONT_PREVIEW_TEXT: "Um pequeno jabuti xereta viu dez cegonhas felizes. "
STR_FONT_FAMILY: "Fonte do leitor"
STR_FONT_SIZE: "Tam. fonte UI"
STR_LINE_SPACING: "Espaçamento entre linhas"
@@ -81,6 +89,7 @@ STR_SCREEN_MARGIN: "Margens da tela"
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
STR_HYPHENATION: "Hifenização"
STR_TIME_TO_SLEEP: "Tempo para repousar"
STR_SHOW_HIDDEN_FILES: "Mostrar arquivos ocultos"
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
STR_MOVE_FINISHED_TO_READ: "Mover livros lidos para a pasta Read"
STR_REFRESH_FREQ: "Frequência atualização"
@@ -128,6 +137,7 @@ STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página"
STR_FORCE_REFRESH: "Atualizar tela"
STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido"
@@ -135,6 +145,9 @@ STR_ORIENTATION_INVERTED: "Retrato 180°"
STR_LANDSCAPE_CCW: "Paisagem AH"
STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Marcador"
STR_DISABLED: "Desabilitado"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Pequeno"
@@ -162,17 +175,21 @@ STR_UPDATING: "Atualizando..."
STR_NO_UPDATE: "Nenhuma atualização disponível"
STR_UPDATE_FAILED: "Falha na atualização"
STR_UPDATE_COMPLETE: "Atualização concluída"
STR_POWER_ON_HINT: "Pressione e segure o botão energia ligar novamente"
STR_NO_ENTRIES: "Nenhum entries encontrado"
STR_POWER_ON_HINT: "Pressione e segure o botão de energia para ligar novamente"
STR_RESTARTING_HINT: "Reiniciando... Se não reiniciar, pressione e segure o botão de energia por alguns segundos."
STR_NO_ENTRIES: "Nenhuma entrada encontrada"
STR_DOWNLOADING: "Baixando..."
STR_DOWNLOAD_FAILED: "Falha no download"
STR_ERROR_MSG: "Erro:"
STR_UNNAMED: "Sem nome"
STR_HOLD_OPEN_TO_DELETE: "Segure 'Abrir' para excluir"
STR_NO_SERVER_URL: "Nenhum URL servidor configurado"
STR_FETCH_FEED_FAILED: "Falha ao buscar o feed"
STR_PARSE_FEED_FAILED: "Falha ao interpretar o feed"
STR_NETWORK_PREFIX: "Rede:"
STR_IP_ADDRESS_PREFIX: "Endereço IP:"
STR_NEXT_PAGE: "Próxima Página »"
STR_PREV_PAGE: "« Página Anterior"
STR_NETWORK_PREFIX: "Rede: "
STR_IP_ADDRESS_PREFIX: "Endereço IP: "
STR_ERROR_GENERAL_FAILURE: "Erro: falha geral"
STR_ERROR_NETWORK_NOT_FOUND: "Erro: rede não encontrada"
STR_ERROR_CONNECTION_TIMEOUT: "Erro: tempo limite conexão"
@@ -181,10 +198,9 @@ STR_BACK: "« Voltar"
STR_EXIT: "« Sair"
STR_HOME: "« Início"
STR_SELECT: "Escolher"
STR_SELECTED: "Selecionado"
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_CANCEL: "Cancelar"
STR_CONNECT: "Conectar"
@@ -193,6 +209,8 @@ STR_DOWNLOAD: "Baixar"
STR_RETRY: "Tentar novamente"
STR_YES: "Sim"
STR_NO: "Não"
STR_SHOW: "Mostrar"
STR_HIDE: "Ocultar"
STR_STATE_ON: "LIG."
STR_STATE_OFF: "DESL."
STR_NOT_SET: "Não definido"
@@ -203,15 +221,51 @@ STR_DIR_DOWN: "Baixo"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
STR_FILTER_CONTRAST: "Contraste"
STR_UI_THEME: "Tema da interface"
STR_CUSTOMISE_STATUS_BAR: "Customizar Barra de Status"
STR_CHAPTER_PAGE_COUNT: "Páginas do Capítulo"
STR_BOOK_PROGRESS_PERCENTAGE: "Porcentagem de Progresso"
STR_PROGRESS_BAR: "Barra de Progresso"
STR_PROGRESS_BAR_THICKNESS: "Espessura da Barra de Progresso"
STR_PROGRESS_BAR_THIN: "Fino"
STR_PROGRESS_BAR_MEDIUM: "Médio"
STR_PROGRESS_BAR_THICK: "Grosso"
STR_BOOK: "Livro"
STR_CHAPTER: "Capítulo"
STR_EXAMPLE_CHAPTER: "Capítulo 21"
STR_EXAMPLE_BOOK: "Título do Livro"
STR_PREVIEW: "Prévia"
STR_TITLE: "Título"
STR_BATTERY: "Bateria"
STR_XTC_STATUS_BAR: "Barra de Status XTC"
STR_BOTTOM: "Baixo"
STR_TOP: "Topo"
STR_CLOCK: "Relógio"
STR_CLOCK_UTC_OFFSET: "Offset UTC do Relógio"
STR_CLOCK_FORMAT: "Formato do Relógio"
STR_CLOCK_FORMAT_24H: "24 horas"
STR_CLOCK_FORMAT_12H: "12 horas"
STR_CURRENT_TIME: "Hora atual:"
STR_NEXT_FIELD: "Próximo"
STR_CLOCK_SYNC: "Sincronizar Relógio"
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
STR_CLOCK_SYNCING: "Sincronizando via NTP..."
STR_CLOCK_SYNC_OK: "Relógio sincronizado"
STR_CLOCK_SYNC_FAIL: "Falha na sincronização"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi não conectado"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Conecte ao Wi-Fi primeiro, depois tente novamente."
STR_CLOCK_SYNCED: "Relógio sincronizado"
STR_UI_THEME: "Tema"
STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_THEME_LYRA_EXTENDED: "Lyra Estendido"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_QUICK_RESUME_TIMEOUT: "Retomada rápida após tempo limite"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_BOOKMARKS: "Marcadores"
STR_BOOKMARK_ADDED: "Marcador adicionado."
STR_BOOKMARK_REMOVED: "Marcador removido."
STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Busca"
STR_COVER_CUSTOM: "Capa + personalizado"
STR_QUICK_RESUME: "Retomada rápida"
STR_MENU_RECENT_BOOKS: "Livros recentes"
@@ -225,11 +279,11 @@ STR_CALIBRE_SETUP: "Configuração"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Limpar"
STR_DEFAULT_VALUE: "Padrão"
STR_REMAP_PROMPT: "Pressione um botão frontal cada função"
STR_REMAP_PROMPT: "Pressione um botão frontal para cada função"
STR_UNASSIGNED: "Não atribuído"
STR_ALREADY_ASSIGNED: "Já atribuído"
STR_REMAP_RESET_HINT: "Botão lateral cima: redefinir o disposição padrão"
STR_REMAP_CANCEL_HINT: "Botão lateral baixo: cancelar remapeamento"
STR_REMAP_RESET_HINT: "Botão lateral de cima: redefinir layout padrão"
STR_REMAP_CANCEL_HINT: "Botão lateral de baixo: cancelar remapeamento"
STR_HW_BACK_LABEL: "Voltar (1º botão)"
STR_HW_CONFIRM_LABEL: "Confirmar (2º botão)"
STR_HW_LEFT_LABEL: "Esquerda (3º botão)"
@@ -239,14 +293,15 @@ STR_GO_HOME_BUTTON: "Ir para o início"
STR_SYNC_PROGRESS: "Sincronizar progresso"
STR_DELETE_CACHE: "Excluir cache do livro"
STR_DELETE: "Excluir"
STR_CONFIRM_DELETE_BOOKMARK: "Excluir este marcador?"
STR_DISPLAY_QR: "Mostrar página como QR"
STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: "páginas |"
STR_BOOK_PREFIX: "Livro:"
STR_CALIBRE_URL_HINT: "Para o Calibre, adicione /opds ao seu URL"
STR_PERCENT_STEP_HINT: "Esq/Dir: 1% Cima/Baixo: 10%"
STR_SYNCING_TIME: "Sincronizando horário..."
STR_CALC_HASH: "Calculando hash documento..."
STR_HASH_FAILED: "Falha ao calcular o hash documento"
STR_CALC_HASH: "Calculando hash do documento..."
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
STR_FETCH_PROGRESS: "Buscando progresso remoto..."
STR_UPLOAD_PROGRESS: "Enviando progresso..."
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
@@ -264,14 +319,69 @@ STR_UPLOAD_PROMPT: "Enviar posição atual?"
STR_UPLOAD_SUCCESS: "Progresso enviado!"
STR_SYNC_FAILED_MSG: "Falha na sincronização"
STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso"
STR_SECTION_PREFIX: "Seção"
STR_SECTION_PREFIX: "Seção "
STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido"
STR_FOCUS_READING: "Leitura focada"
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "Retorno rápido de notas de rodapé"
STR_SET_SLEEP_COVER: "Definir capa"
STR_FOOTNOTES: "Notas de rodapé"
STR_NO_FOOTNOTES: "Nenhuma nota de rodapé nesta página"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Capturar tela"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nunca"
STR_SLEEP_TIMER_STEP_HINT: "Esq/Dir: 1 min Cima/Baixo: 5 min"
STR_STEP_HINT_FRONT: "Botões frontais:"
STR_STEP_HINT_SIDE: "Botões laterais:"
STR_ADD_SERVER: "Adicionar Servidor"
STR_SERVER_NAME: "Nome do Servidor"
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
STR_DELETE_SERVER: "Excluir Servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes"
STR_FONT_BROWSER: "Navegador de fontes"
STR_LOADING_FONT_LIST: "Carregando lista de fontes..."
STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível"
STR_FONT_INSTALLED: "Fonte instalada!"
STR_FONT_INSTALL_FAILED: "Falha na instalação da fonte"
STR_INSTALLED: "Instalada"
STR_DOWNLOAD_ALL: "Baixar tudo"
STR_UPDATE_ALL: "Atualizar tudo"
STR_UPDATE_AVAILABLE: "Atualizar"
STR_CRASH_TITLE: "Falha do sistema"
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi salvo em crash_report.txt. Por favor, inclua este arquivo no seu relatório de erro."
STR_CRASH_REASON: "Motivo da falha:"
STR_CRASH_NO_REASON: "(Nenhum motivo registrado)"
STR_TILT_PAGE_TURN: "Virar página por inclinação"
STR_KB_HINT_MOVE_CURSOR: "Pressione ESQ ou DIR para mover o cursor"
STR_KB_HINT_RETURN_CURSOR: "Pressione ESQ para retornar à posição do cursor"
STR_KB_HINT_HIDE_PASSWORD: "Segure DIR e pressione [***] para ocultar a senha"
STR_KB_HINT_SHOW_PASSWORD: "Segure DIR e pressione [abc] para exibir a senha"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pressione [***] para ocultar a senha"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pressione [abc] para exibir a senha"
STR_KB_HINT_EDIT_ENTRY: "Segure CIMA para editar a entrada"
STR_KB_TIPS: "Dicas:"
STR_KB_HINT_RETURN_KEYBOARD: "Pressione BAIXO para retornar ao teclado"
STR_KB_HINT_EXIT_URL_MODE: "Pressione ABC para sair do modo URL"
STR_KB_HINT_CLEAR_TEXT: "Segure DEL para limpar todo o texto"
STR_KB_HINT_SECONDARY_CHAR: "Segure SELECT para caractere secundário"
STR_KB_HINT_UPPER_SECONDARY: "Segure SELECT para MAIÚSCULO ou caractere secundário"
STR_KB_HINT_LOWER_SECONDARY: "Segure SELECT para minúsculo ou caractere secundário"
STR_KB_HINT_URL_SNIPPETS: "Pressione URL para atalhos"
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
STR_SELECT_FIRMWARE_FILE: "Selecione o arquivo de firmware (.bin)"
STR_NO_BIN_FILES: "Nenhum arquivo .bin encontrado"
STR_VALIDATING_FIRMWARE: "Validando firmware..."
STR_INVALID_FIRMWARE: "Arquivo de firmware inválido"
STR_FIRMWARE_TOO_LARGE: "Firmware grande demais para a partição"
STR_FIRMWARE_TOO_SMALL: "Arquivo de firmware pequeno demais"
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o arquivo"
STR_FIRMWARE_WRITE_FAILED: "Falha na gravação do firmware"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Não desligue o dispositivo!"
STR_RECOVERY_MODE: "Modo de Recuperação"
STR_RECOVERY_MODE_HINT: "Coloque o arquivo firmware.bin na raiz do cartão SD e selecione-o"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Mod ecran de repaus cu copertă"
STR_HIDE_BATTERY: "Ascunde procentul bateriei"
STR_EXTRA_SPACING: "Spaţiere suplimentară între paragrafe"
STR_TEXT_AA: "Anti-Aliasing text"
STR_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Rapid"
STR_IMAGES: "Imagini"
STR_IMAGES_DISPLAY: "Afişare"
STR_IMAGES_PLACEHOLDER: "Substituent"
@@ -266,7 +268,6 @@ STR_CHAPTER_PREFIX: "Capitol: "
STR_PAGES_SEPARATOR: " pagini | "
STR_BOOK_PREFIX: "Carte: "
STR_CALIBRE_URL_HINT: "Pentru Calibre, adăugaţi /opds la URL"
STR_PERCENT_STEP_HINT: "Stânga/Dreapta: 1% Sus/Jos: 10%"
STR_SYNCING_TIME: "Timp de sincronizare..."
STR_CALC_HASH: "Calcularea hash-ului documentului..."
STR_HASH_FAILED: "Eşec la calcularea hash-ului documentului"
@@ -298,7 +299,8 @@ STR_NO_FOOTNOTES: "Nicio notă de subsol"
STR_LINK: "[link]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată"
STR_SLEEP_TIMER_STEP_HINT: "Stânga/Dreapta: 1 min Sus/Jos: 5 min"
STR_STEP_HINT_FRONT: "Butoane frontale:"
STR_STEP_HINT_SIDE: "Butoane laterale:"
STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
+4 -2
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Режим обложки сна"
STR_HIDE_BATTERY: "Скрыть % батареи"
STR_EXTRA_SPACING: "Доп. интервал абзаца"
STR_TEXT_AA: "Сглаживание текста"
STR_TEXT_AA_FULL: "Полное"
STR_TEXT_AA_FAST: "Быстрое"
STR_IMAGES: "Изображения"
STR_IMAGES_DISPLAY: "Показать"
STR_IMAGES_PLACEHOLDER: "Заглушки"
@@ -298,7 +300,6 @@ STR_CHAPTER_PREFIX: "Глава: "
STR_PAGES_SEPARATOR: " стр. | "
STR_BOOK_PREFIX: "Книга: "
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
STR_SYNCING_TIME: "Синхронизация времени..."
STR_CALC_HASH: "Расчёт хэша документа..."
STR_HASH_FAILED: "Не удалось вычислить хэш документа"
@@ -332,7 +333,8 @@ STR_LINK: "[ссылка]"
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Никогда"
STR_SLEEP_TIMER_STEP_HINT: "Влево/Вправо: 1 мин Вверх/Вниз: 5 мин"
STR_STEP_HINT_FRONT: "Передние кнопки:"
STR_STEP_HINT_SIDE: "Боковые кнопки:"
STR_ADD_SERVER: "Добавить сервер"
STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
+375 -373
View File
@@ -1,381 +1,383 @@
_language_name: "Slovenčina"
_language_code: "SK"
_order: "24"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPÚŠŤANIE"
STR_SLEEPING: "SPÁNOK"
STR_ENTERING_SLEEP: "Prechod do režimu spánku"
STR_BROWSE_FILES: "Prehliadať súbory"
STR_FILE_TRANSFER: "Prenos súborov"
STR_SETTINGS_TITLE: "Nastavenia"
STR_CONTINUE_READING: "Pokračovať v čítaní"
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
STR_START_READING: "Začnite čítať nižšie"
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
STR_SELECT_CHAPTER: "Vybrať kapitolu"
STR_NO_CHAPTERS: "Žiadne kapitoly"
STR_END_OF_BOOK: "Koniec knihy"
STR_EMPTY_CHAPTER: "Prázdna kapitola"
STR_INDEXING: "Indexovanie"
STR_MEMORY_ERROR: "Chyba pamäte"
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
STR_EMPTY_FILE: "Prázdny súbor"
STR_OUT_OF_BOUNDS: "Mimo rozsahu"
STR_LOADING: "Načítava sa..."
STR_LOADING_POPUP: "Načítavanie"
STR_WIFI_NETWORKS: "Wi-Fi siete"
STR_NO_NETWORKS: "Nenašli sa žiadne siete"
STR_NETWORKS_FOUND: "Nájdených %zu sietí"
STR_SCANNING: "Skenovanie..."
STR_CONNECTING: "Pripájanie..."
STR_CONNECTED: "Pripojené!"
STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
STR_FORGET_NETWORK: "Zabudnúť sieť?"
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
STR_JOIN_NETWORK: "Pripojiť sa k sieti"
STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti"
STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní"
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti"
STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači"
STR_OR_HTTP_PREFIX: "alebo http://"
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
STR_TO_PREFIX: "pre"
STR_CALIBRE_RECEIVING: "Prijímanie:"
STR_CALIBRE_RECEIVED: "Prijaté:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čítačka"
STR_CAT_CONTROLS: "Ovládanie"
STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
STR_SLEEP_COVER_MODE: "Obrazovka spánku režim krytu"
STR_HIDE_BATTERY: "Skryť % batérie"
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
STR_TEXT_AA: "Vyhladzovanie textu"
STR_IMAGES: "Obrázky"
STR_IMAGES_DISPLAY: "Zobraz"
STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
STR_IMAGES_SUPPRESS: "Potlačiť"
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
STR_ORIENTATION: "Orientácia čítania"
STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii"
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
STR_FONT_FAMILY: "Rodina písiem čítačky"
STR_FONT_SIZE: "Veľkosť písma rozhrania"
STR_LINE_SPACING: "Riadkovanie čítačky"
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
STR_HYPHENATION: "Delenie slov"
STR_TIME_TO_SLEEP: "Čas do uspania"
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
STR_REFRESH_FREQ: "Frekvencia obnovovania"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
STR_LANGUAGE: "Jazyk"
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
STR_USERNAME: "Používateľské meno"
STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
STR_AUTHENTICATE: "Over"
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
STR_KOREADER_PASSWORD: "Heslo KOReader"
STR_FILENAME: "Názov súboru"
STR_BINARY: "Binárny"
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
STR_AUTHENTICATING: "Overovanie..."
STR_AUTH_SUCCESS: "Overenie úspešné!"
STR_KOREADER_AUTH: "Overenie KOReader"
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
STR_AUTH_FAILED: "Overenie zlyhalo"
STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
STR_ITEMS_REMOVED: "položiek odstránených"
STR_FAILED_LOWER: "zlyhalo"
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
STR_DARK: "Tmavý"
STR_LIGHT: "Svetlý"
STR_CUSTOM: "Vlastný"
STR_COVER: "Obálka"
STR_NONE_OPT: "Žiadny"
STR_FIT: "Prispôsobiť"
STR_CROP: "Orezať"
STR_NEVER: "Nikdy"
STR_IN_READER: "V čítačke"
STR_ALWAYS: "dy"
STR_IGNORE: "Ignorovať"
STR_SLEEP: "Spánok"
STR_PAGE_TURN: "Otáčanie stránok"
STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
_language_name: "Slovenčina"
_language_code: "SK"
_order: "24"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPÚŠŤANIE"
STR_SLEEPING: "SPÁNOK"
STR_ENTERING_SLEEP: "Prechod do režimu spánku"
STR_BROWSE_FILES: "Prehliadať súbory"
STR_FILE_TRANSFER: "Prenos súborov"
STR_SETTINGS_TITLE: "Nastavenia"
STR_CONTINUE_READING: "Pokračovať v čítaní"
STR_NO_OPEN_BOOK: "Žiadna otvorená kniha"
STR_START_READING: "Začnite čítať nižšie"
STR_NO_FILES_FOUND: "Neboli nájdené žiadne súbory"
STR_SELECT_CHAPTER: "Vybrať kapitolu"
STR_NO_CHAPTERS: "Žiadne kapitoly"
STR_END_OF_BOOK: "Koniec knihy"
STR_EMPTY_CHAPTER: "Prázdna kapitola"
STR_INDEXING: "Indexovanie"
STR_MEMORY_ERROR: "Chyba pamäte"
STR_PAGE_LOAD_ERROR: "Chyba načítania stránky"
STR_EMPTY_FILE: "Prázdny súbor"
STR_OUT_OF_BOUNDS: "Mimo rozsahu"
STR_LOADING: "Načítava sa..."
STR_LOADING_POPUP: "Načítavanie"
STR_WIFI_NETWORKS: "Wi-Fi siete"
STR_NO_NETWORKS: "Nenašli sa žiadne siete"
STR_NETWORKS_FOUND: "Nájdených %zu sietí"
STR_SCANNING: "Skenovanie..."
STR_CONNECTING: "Pripájanie..."
STR_CONNECTED: "Pripojené!"
STR_CONNECTION_FAILED: "Pripojenie zlyhalo"
STR_FORGET_NETWORK: "Zabudnúť sieť?"
STR_SAVE_PASSWORD: "Uložiť heslo na nabudúce?"
STR_PRESS_OK_SCAN: "Stlačte OK pre opätovné skenovanie"
STR_JOIN_NETWORK: "Pripojiť sa k sieti"
STR_CREATE_HOTSPOT: "Vytvoriť hotspot"
STR_JOIN_DESC: "Pripojiť sa k existujúcej Wi-Fi sieti"
STR_HOTSPOT_DESC: "Vytvoriť Wi-Fi sieť, ku ktorej sa môžu pripojiť ostatní"
STR_STARTING_HOTSPOT: "Spúšťanie hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Pripojte svoje zariadenie k tejto Wi-Fi sieti"
STR_OPEN_URL_HINT: "Otvorte túto URL adresu vo svojom prehliadači"
STR_OR_HTTP_PREFIX: "alebo http://"
STR_SCAN_QR_HINT: "alebo naskenujte QR kód telefónom:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Šifrované | + = Uložené"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Zadajte heslo Wi-Fi"
STR_TO_PREFIX: "pre"
STR_CALIBRE_RECEIVING: "Prijímanie:"
STR_CALIBRE_RECEIVED: "Prijaté:"
STR_CALIBRE_INSTRUCTION_1: "1) Nainštalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte v rovnakej Wi-Fi sieti"
STR_CALIBRE_INSTRUCTION_3: "3) V Calibre: „Odoslať do zariadenia“"
STR_CALIBRE_INSTRUCTION_4: "„Pri odosielaní nechajte túto obrazovku otvorenú“"
STR_CAT_DISPLAY: "Displej"
STR_CAT_READER: "Čítačka"
STR_CAT_CONTROLS: "Ovládanie"
STR_CAT_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_QUICK_RESUME_TIMEOUT: "Rýchle obnovenie pri nečinnosti"
STR_SLEEP_COVER_MODE: "Obrazovka spánku režim krytu"
STR_HIDE_BATTERY: "Skryť % batérie"
STR_EXTRA_SPACING: "Dodatočné medzery medzi odsekmi"
STR_TEXT_AA: "Vyhladzovanie textu"
STR_TEXT_AA_FULL: "Plné"
STR_TEXT_AA_FAST: "Rýchle"
STR_IMAGES: "Obrázky"
STR_IMAGES_DISPLAY: "Zobraz"
STR_IMAGES_PLACEHOLDER: "Rezervované miesto"
STR_IMAGES_SUPPRESS: "Potlačiť"
STR_SHORT_PWR_BTN: "Krátke stlačenie tlačidla napájania"
STR_ORIENTATION: "Orientácia čítania"
STR_SIDE_BTN_LAYOUT: "Rozloženie bočných tlačidiel (čítačka)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Prispôsobiť predné tlačidlá orientácii"
STR_LONG_PRESS_BEHAVIOR: "Správanie pri dlhom stlačení tlačidla"
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskočiť kapitolu"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Zmeniť orientáciu"
STR_FONT_PREVIEW_TEXT: "Vypätá dcéra grófa Maxwella s IQ nižším ako kôň núti čeľaď hrýzť hŕbu jabĺk"
STR_FONT_FAMILY: "Rodina písiem čítačky"
STR_FONT_SIZE: "Veľkosť písma rozhrania"
STR_LINE_SPACING: "Riadkovanie čítačky"
STR_SCREEN_MARGIN: "Okraj obrazovky čítačky"
STR_PARA_ALIGNMENT: "Zarovnanie odsekov čítačky"
STR_HYPHENATION: "Delenie slov"
STR_TIME_TO_SLEEP: "Čas do uspania"
STR_SHOW_HIDDEN_FILES: "Zobraz Skryté Súbory"
STR_REMOVE_READ_FROM_RECENTS: "Odstrániť prečítané knihy zo zoznamu nedávnych"
STR_MOVE_FINISHED_TO_READ: "Presunúť prečítané knihy do priečinka Read"
STR_REFRESH_FREQ: "Frekvencia obnovovania"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Skontrolovať aktualizácie"
STR_LANGUAGE: "Jazyk"
STR_CLEAR_READING_CACHE: "Vymazať vyrovnávaciu pamäť čítania"
STR_USERNAME: "Používateľské meno"
STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synchronizačného servera"
STR_DOCUMENT_MATCHING: "Párovanie dokumentov"
STR_AUTHENTICATE: "Overiť"
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
STR_KOREADER_PASSWORD: "Heslo KOReader"
STR_FILENAME: "Názov súboru"
STR_BINARY: "Binárny"
STR_SET_CREDENTIALS_FIRST: "Najprv nastavte prihlasovacie údaje"
STR_WIFI_CONN_FAILED: "Pripojenie k Wi-Fi zlyhalo"
STR_AUTHENTICATING: "Overovanie..."
STR_AUTH_SUCCESS: "Overenie úspešné!"
STR_KOREADER_AUTH: "Overenie KOReader"
STR_SYNC_READY: "Synchronizácia KOReader je pripravená na použitie"
STR_AUTH_FAILED: "Overenie zlyhalo"
STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_1: "Týmto vymažete všetky údaje kníh vo vyrovnávacej pamäti."
STR_CLEAR_CACHE_WARNING_2: "Všetok priebeh čítania bude stratený!"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude potrebné znova indexovať"
STR_CLEAR_CACHE_WARNING_4: "pri ich opätovnom otvorení."
STR_CLEARING_CACHE: "Mazanie vyrovnávacej pamäte..."
STR_CACHE_CLEARED: "Vyrovnávacia pamäť vymazaná"
STR_ITEMS_REMOVED: "položiek odstránených"
STR_FAILED_LOWER: "zlyhalo"
STR_CLEAR_CACHE_FAILED: "Vymazanie vyrovnávacej pamäte zlyhalo"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti nájdete v sériovom výstupe"
STR_DARK: "Tmavý"
STR_LIGHT: "Svetlý"
STR_CUSTOM: "Vlastný"
STR_COVER: "Obálka"
STR_NONE_OPT: "Žiadny"
STR_FIT: "Prispôsobiť"
STR_CROP: "Orezať"
STR_NEVER: "Nikdy"
STR_IN_READER: "V čítačke"
STR_ALWAYS: "Vždy"
STR_IGNORE: "Ignorovať"
STR_SLEEP: "Spánok"
STR_PAGE_TURN: "Otáčanie stránok"
STR_FORCE_REFRESH: "Obnoviť obrazovku"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šírku v smere hodinových ručičiek"
STR_INVERTED: "Invertovaný"
STR_ORIENTATION_INVERTED: "Na výšku 180°"
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
STR_DISABLED: "Vypnuté"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malý"
STR_MEDIUM: "Stredný"
STR_LARGE: "Veľký"
STR_X_LARGE: "Obrovský"
STR_TIGHT: "Tesný"
STR_NORMAL: "Normálny"
STR_WIDE: "Široký"
STR_JUSTIFY: "Zarovnať do bloku"
STR_ALIGN_LEFT: "Vľavo"
STR_CENTER: "Na stred"
STR_ALIGN_RIGHT: "Vpravo"
STR_PAGES_1: "1 strana"
STR_PAGES_5: "5 strán"
STR_PAGES_10: "10 strán"
STR_PAGES_15: "15 strán"
STR_PAGES_30: "30 strán"
STR_UPDATE: "Aktualizácia"
STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
STR_CURRENT_VERSION: "Aktuálna verzia:"
STR_NEW_VERSION: "Nová verzia:"
STR_UPDATING: "Aktualizácia..."
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
STR_UPDATE_FAILED: "Aktualizácia zlyhala"
STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
STR_DOWNLOADING: "Sťahovanie..."
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepomenované"
STR_LANDSCAPE_CCW: "Na šírku proti smeru hodinových ručičiek"
STR_PREV_NEXT: "Predchádzajúca/Nasledujúca"
STR_NEXT_PREV: "Nasledujúca/Predchádzajúca"
STR_DISABLED: "Vypnuté"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malý"
STR_MEDIUM: "Stredný"
STR_LARGE: "Veľký"
STR_X_LARGE: "Obrovský"
STR_TIGHT: "Tesný"
STR_NORMAL: "Normálny"
STR_WIDE: "Široký"
STR_JUSTIFY: "Zarovnať do bloku"
STR_ALIGN_LEFT: "Vľavo"
STR_CENTER: "Na stred"
STR_ALIGN_RIGHT: "Vpravo"
STR_PAGES_1: "1 strana"
STR_PAGES_5: "5 strán"
STR_PAGES_10: "10 strán"
STR_PAGES_15: "15 strán"
STR_PAGES_30: "30 strán"
STR_UPDATE: "Aktualizácia"
STR_CHECKING_UPDATE: "Kontrola aktualizácií…"
STR_NEW_UPDATE: "K dispozícii je nová aktualizácia!"
STR_CURRENT_VERSION: "Aktuálna verzia:"
STR_NEW_VERSION: "Nová verzia:"
STR_UPDATING: "Aktualizácia..."
STR_NO_UPDATE: "Nie je k dispozícii žiadna aktualizácia"
STR_UPDATE_FAILED: "Aktualizácia zlyhala"
STR_UPDATE_COMPLETE: "Aktualizácia dokončená"
STR_POWER_ON_HINT: "Stlačte a podržte tlačidlo napájania pre zapnutie"
STR_RESTARTING_HINT: "Reštartujem... Ak sa zariadenie nereštartuje, podrž tlačidlo na zapnutie niekoľko s."
STR_NO_ENTRIES: "Neboli nájdené žiadne položky"
STR_DOWNLOADING: "Sťahovanie..."
STR_DOWNLOAD_FAILED: "Sťahovanie zlyhalo"
STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepomenované"
STR_HOLD_OPEN_TO_DELETE: "Podržte Otvoriť pre vymazanie"
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
STR_NEXT_PAGE: "Ďaľsia strana »"
STR_PREV_PAGE: "« Predchádzajúca str."
STR_NETWORK_PREFIX: "Sieť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
STR_SD_CARD: "SD karta"
STR_BACK: "« Späť"
STR_EXIT: "« Koniec"
STR_HOME: "« Domov"
STR_SELECT: "Vybrať"
STR_SELECTED: "Vybrané"
STR_NO_SERVER_URL: "Nie je nakonfigurovaná URL adresa servera"
STR_FETCH_FEED_FAILED: "Načítanie kanála zlyhalo"
STR_PARSE_FEED_FAILED: "Spracovanie kanála zlyhalo"
STR_NEXT_PAGE: "Ďaľsia strana »"
STR_PREV_PAGE: "« Predchádzajúca str."
STR_NETWORK_PREFIX: "Sieť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_ERROR_GENERAL_FAILURE: "Chyba: Všeobecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Sieť nebola nájdená"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit pripojenia"
STR_SD_CARD: "SD karta"
STR_BACK: "« Späť"
STR_EXIT: "« Koniec"
STR_HOME: "« Domov"
STR_SELECT: "Vybrať"
STR_SELECTED: "Vybrané"
STR_TOGGLE: "Prepnúť"
STR_TOGGLE_BOOKMARK: "Prepnúť záložku"
STR_CONFIRM: "Potvrdiť"
STR_CANCEL: "Zrušiť"
STR_CONNECT: "Pripojiť"
STR_OPEN: "Otvoriť"
STR_DOWNLOAD: "Stiahnuť"
STR_RETRY: "Skúsiť znova"
STR_YES: "Áno"
STR_NO: "Nie"
STR_SHOW: "Zobraz"
STR_HIDE: "Skryť"
STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP"
STR_NOT_SET: "Nenastavené"
STR_DIR_LEFT: "Vľavo"
STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Hore"
STR_DIR_DOWN: "Dole"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
STR_PROGRESS_BAR: "Ukazovateľ čítania"
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
STR_PROGRESS_BAR_THIN: "Tenký"
STR_PROGRESS_BAR_MEDIUM: "Stredný"
STR_PROGRESS_BAR_THICK: "Hrubý"
STR_BOOK: "Kniha"
STR_CHAPTER: "Kapitola"
STR_EXAMPLE_CHAPTER: "Kapitola 21"
STR_EXAMPLE_BOOK: "Názov knihy"
STR_PREVIEW: "Ukážka"
STR_TITLE: "Názov"
STR_BATTERY: "Batéria"
STR_XTC_STATUS_BAR: "Stavový panel XTC"
STR_BOTTOM: "Dole"
STR_TOP: "Hore"
STR_CLOCK: "Hodiny"
STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
STR_CLOCK_FORMAT: "Formát času"
STR_CLOCK_FORMAT_24H: "24-hodinový"
STR_CLOCK_FORMAT_12H: "12-hodinový"
STR_CURRENT_TIME: "Aktuálny čas:"
STR_NEXT_FIELD: "Ďalej"
STR_CLOCK_SYNC: "Synchronizácia hodín"
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova."
STR_CLOCK_SYNCED: "Hodiny synchronizované"
STR_UI_THEME: "Téma rozhrania"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
STR_CONFIRM: "Potvrdiť"
STR_CANCEL: "Zrušiť"
STR_CONNECT: "Pripojiť"
STR_OPEN: "Otvoriť"
STR_DOWNLOAD: "Stiahnuť"
STR_RETRY: "Skúsiť znova"
STR_YES: "Áno"
STR_NO: "Nie"
STR_SHOW: "Zobraz"
STR_HIDE: "Skryť"
STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP"
STR_NOT_SET: "Nenastavené"
STR_DIR_LEFT: "Vľavo"
STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Hore"
STR_DIR_DOWN: "Dole"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Uprav status bar"
STR_CHAPTER_PAGE_COUNT: "Počítadlo strán kapitoly"
STR_BOOK_PROGRESS_PERCENTAGE: "Prečítané percent"
STR_PROGRESS_BAR: "Ukazovateľ čítania"
STR_PROGRESS_BAR_THICKNESS: "Hrúbka indikátora priebehu"
STR_PROGRESS_BAR_THIN: "Tenký"
STR_PROGRESS_BAR_MEDIUM: "Stredný"
STR_PROGRESS_BAR_THICK: "Hrubý"
STR_BOOK: "Kniha"
STR_CHAPTER: "Kapitola"
STR_EXAMPLE_CHAPTER: "Kapitola 21"
STR_EXAMPLE_BOOK: "Názov knihy"
STR_PREVIEW: "Ukážka"
STR_TITLE: "Názov"
STR_BATTERY: "Batéria"
STR_XTC_STATUS_BAR: "Stavový panel XTC"
STR_BOTTOM: "Dole"
STR_TOP: "Hore"
STR_CLOCK: "Hodiny"
STR_CLOCK_UTC_OFFSET: "UTC posun hodín"
STR_CLOCK_FORMAT: "Formát času"
STR_CLOCK_FORMAT_24H: "24-hodinový"
STR_CLOCK_FORMAT_12H: "12-hodinový"
STR_CURRENT_TIME: "Aktuálny čas:"
STR_NEXT_FIELD: "Ďalej"
STR_CLOCK_SYNC: "Synchronizácia hodín"
STR_CLOCK_SYNC_NOW: "Synchronizovať teraz"
STR_CLOCK_SYNCING: "Synchronizácia cez NTP..."
STR_CLOCK_SYNC_OK: "Hodiny synchronizované"
STR_CLOCK_SYNC_FAIL: "Synchronizácia zlyhala"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nie je pripojená"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Najprv sa pripojte k Wi-Fi a potom skúste znova."
STR_CLOCK_SYNCED: "Hodiny synchronizované"
STR_UI_THEME: "Téma rozhrania"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutia na slnku"
STR_REMAP_FRONT_BUTTONS: "Premapovať predné tlačidlá"
STR_BOOKMARKS: "Záložky"
STR_BOOKMARK_ADDED: "Záložka pridaná."
STR_BOOKMARK_REMOVED: "Záložka odstránená."
STR_OPDS_BROWSER: "Prehliadač OPDS"
STR_SEARCH: "Hľadať"
STR_COVER_CUSTOM: "Obálka + Vlastné"
STR_QUICK_RESUME: "Rýchle obnovenie"
STR_MENU_RECENT_BOOKS: "Nedávne knihy"
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
STR_FORGET_BUTTON: "Zabudnúť"
STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
STR_CALIBRE_SETUP: "Nastavenie"
STR_CALIBRE_STATUS: "Stav"
STR_CLEAR_BUTTON: "Vymazať"
STR_DEFAULT_VALUE: "Predvolené"
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
STR_UNASSIGNED: "Nepriradené"
STR_ALREADY_ASSIGNED: "Už priradené"
STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie"
STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie"
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
STR_GO_TO_PERCENT: "Prejsť na %"
STR_GO_HOME_BUTTON: "Prejsť na Domov"
STR_SYNC_PROGRESS: "Priebeh synchronizácie"
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
STR_DELETE: "Vymazať"
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
STR_DISPLAY_QR: "Zobraz stránku ako QR"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "strán |"
STR_BOOK_PREFIX: "Kniha:"
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
STR_PERCENT_STEP_HINT: "Vľavo/Vpravo: 1 % Hore/Dole: 10 %"
STR_SYNCING_TIME: "Čas synchronizácie..."
STR_CALC_HASH: "Výpočet hashu dokumentu..."
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
STR_PROGRESS_FOUND: "Priebeh nájdený!"
STR_REMOTE_LABEL: "Vzdialený:"
STR_LOCAL_LABEL: "Lokálny:"
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
STR_SECTION_PREFIX: "Sekcia"
STR_UPLOAD: "Nahrať"
STR_BOOK_S_STYLE: "Štýl knihy"
STR_EMBEDDED_STYLE: "Vložený štýl"
STR_FOCUS_READING: "Sústredené čítanie"
STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
STR_SET_SLEEP_COVER: "Nastav obal"
STR_FOOTNOTES: "Poznámky pod čiarou"
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
STR_SLEEP_TIMER_STEP_HINT: "Vľavo/Vpravo: 1 min Hore/Dole: 5 min"
STR_ADD_SERVER: "Pridať server"
STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem"
STR_FONT_BROWSER: "Prehliadač písiem"
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
STR_FONT_INSTALLED: "Písmo nainštalované!"
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
STR_INSTALLED: "Nainštalované"
STR_DOWNLOAD_ALL: "Stiahnuť všetko"
STR_UPDATE_ALL: "Aktualizovať všetko"
STR_UPDATE_AVAILABLE: "Aktualizovať"
STR_CRASH_TITLE: "Zlyhanie systému"
STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby."
STR_CRASH_REASON: "Dôvod zlyhania:"
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
STR_TILT_PAGE_TURN: "Otáčanie strán naklonením"
STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora"
STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora"
STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla"
STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky"
STR_KB_TIPS: "Tipy:"
STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici"
STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL"
STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu"
STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak"
STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak"
STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak"
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru"
STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu"
STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý"
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
STR_RECOVERY_MODE: "Režim obnovenia"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
STR_OPDS_BROWSER: "Prehliadač OPDS"
STR_SEARCH: "Hľadať"
STR_COVER_CUSTOM: "Obálka + Vlastné"
STR_QUICK_RESUME: "Rýchle obnovenie"
STR_MENU_RECENT_BOOKS: "Nedávne knihy"
STR_REMOVE_FROM_RECENTS: "Odstrániť z nedávnych kníh?"
STR_NO_RECENT_BOOKS: "Žiadne nedávne knihy"
STR_CALIBRE_DESC: "Používať bezdrôtové prenosy zariadení Calibre"
STR_FORGET_AND_REMOVE: "Zabudnúť sieť a odstrániť uložené heslo?"
STR_FORGET_BUTTON: "Zabudnúť"
STR_CALIBRE_STARTING: "Spúšťanie Calibre..."
STR_CALIBRE_SETUP: "Nastavenie"
STR_CALIBRE_STATUS: "Stav"
STR_CLEAR_BUTTON: "Vymazať"
STR_DEFAULT_VALUE: "Predvolené"
STR_REMAP_PROMPT: "Stlačte predné tlačidlo pre každú funkciu"
STR_UNASSIGNED: "Nepriradené"
STR_ALREADY_ASSIGNED: "Už priradené"
STR_REMAP_RESET_HINT: "Bočné tlačidlo Hore: Obnoviť predvolené rozloženie"
STR_REMAP_CANCEL_HINT: "Bočné tlačidlo Dole: Zrušiť premapovanie"
STR_HW_BACK_LABEL: "Späť (1. tlačidlo)"
STR_HW_CONFIRM_LABEL: "Potvrdiť (2. tlačidlo)"
STR_HW_LEFT_LABEL: "Vľavo (3. tlačidlo)"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačidlo)"
STR_GO_TO_PERCENT: "Prejsť na %"
STR_GO_HOME_BUTTON: "Prejsť na Domov"
STR_SYNC_PROGRESS: "Priebeh synchronizácie"
STR_DELETE_CACHE: "Vymazať vyrovnávaciu pamäť knihy"
STR_DELETE: "Vymazať"
STR_CONFIRM_DELETE_BOOKMARK: "Vymazať záložku?"
STR_DISPLAY_QR: "Zobraz stránku ako QR"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "strán |"
STR_BOOK_PREFIX: "Kniha:"
STR_CALIBRE_URL_HINT: "Pre Calibre pridajte /opds na koniec URL adresy"
STR_SYNCING_TIME: "Čas synchronizácie..."
STR_CALC_HASH: "Výpočet hashu dokumentu..."
STR_HASH_FAILED: "Nepodarilo sa vypočítať hash dokumentu"
STR_FETCH_PROGRESS: "Načítavanie vzdialeného priebehu..."
STR_UPLOAD_PROGRESS: "Nahrávanie priebehu..."
STR_NO_CREDENTIALS_MSG: "Prihlasovacie údaje nie sú nastavené"
STR_KOREADER_SETUP_HINT: "Nastavte účet KOReader v Nastaveniach"
STR_PROGRESS_FOUND: "Priebeh nájdený!"
STR_REMOTE_LABEL: "Vzdialený:"
STR_LOCAL_LABEL: "Lokálny:"
STR_PAGE_OVERALL_FORMAT: "Strana %d, celkovo %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Strana %d/%d, celkovo %.2f%%"
STR_DEVICE_FROM_FORMAT: " Zo zariadenia: %s"
STR_APPLY_REMOTE: "Použiť vzdialený priebeh"
STR_UPLOAD_LOCAL: "Nahrať lokálny priebeh"
STR_NO_REMOTE_MSG: "Nenašiel sa žiadny vzdialený priebeh"
STR_UPLOAD_PROMPT: "Nahrať aktuálnu pozíciu?"
STR_UPLOAD_SUCCESS: "Priebeh nahraný!"
STR_SYNC_FAILED_MSG: "Synchronizácia zlyhala"
STR_SAVE_PROGRESS_FAILED: "Nepodarilo sa uložiť priebeh"
STR_SECTION_PREFIX: "Sekcia"
STR_UPLOAD: "Nahrať"
STR_BOOK_S_STYLE: "Štýl knihy"
STR_EMBEDDED_STYLE: "Vložený štýl"
STR_FOCUS_READING: "Sústredené čítanie"
STR_OPDS_SERVER_URL: "URL adresa OPDS servera"
STR_SET_SLEEP_COVER: "Nastav obal"
STR_FOOTNOTES: "Poznámky pod čiarou"
STR_NO_FOOTNOTES: "Žiadne poznámky pod čiarou"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Urobiť snímku obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
STR_STEP_HINT_FRONT: "Predné tlačidlá:"
STR_STEP_HINT_SIDE: "Bočné tlačidlá:"
STR_ADD_SERVER: "Pridať server"
STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem"
STR_FONT_BROWSER: "Prehliadač písiem"
STR_LOADING_FONT_LIST: "Načítava sa zoznam písiem..."
STR_NO_FONTS_AVAILABLE: "Nie sú dostupné žiadne písma"
STR_FONT_INSTALLED: "Písmo nainštalované!"
STR_FONT_INSTALL_FAILED: "Inštalácia písma zlyhala"
STR_INSTALLED: "Nainštalované"
STR_DOWNLOAD_ALL: "Stiahnuť všetko"
STR_UPDATE_ALL: "Aktualizovať všetko"
STR_UPDATE_AVAILABLE: "Aktualizovať"
STR_CRASH_TITLE: "Zlyhanie systému"
STR_CRASH_DESCRIPTION: "Podrobná správa bola uložená do súboru crash_report.txt. Priložte tento súbor k hláseniu chyby."
STR_CRASH_REASON: "Dôvod zlyhania:"
STR_CRASH_NO_REASON: "(Nebola zaznamenaná žiadna príčina)"
STR_TILT_PAGE_TURN: "Otáčanie strán naklonením"
STR_KB_HINT_MOVE_CURSOR: "Stlačte VĽAVO alebo VPRAVO pre presun kurzora"
STR_KB_HINT_RETURN_CURSOR: "Stlačte VĽAVO pre návrat na pozíciu kurzora"
STR_KB_HINT_HIDE_PASSWORD: "Podržte VPRAVO a potom stlačte [***] na skrytie hesla"
STR_KB_HINT_SHOW_PASSWORD: "Podržte VPRAVO a potom stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Stlačte [***] na skrytie hesla"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Stlačte [abc] na zobrazenie hesla"
STR_KB_HINT_EDIT_ENTRY: "Podržte HORE pre úpravu položky"
STR_KB_TIPS: "Tipy:"
STR_KB_HINT_RETURN_KEYBOARD: "Stlačte DOLE pre návrat ku klávesnici"
STR_KB_HINT_EXIT_URL_MODE: "Stlačte ABC pre ukončenie režimu URL"
STR_KB_HINT_CLEAR_TEXT: "Podržte DEL pre vymazanie celého textu"
STR_KB_HINT_SECONDARY_CHAR: "Podržte SELECT pre alternatívny znak"
STR_KB_HINT_UPPER_SECONDARY: "Podržte SELECT pre VEĽKÉ písmeno alebo alternatívny znak"
STR_KB_HINT_LOWER_SECONDARY: "Podržte SELECT pre malé písmeno alebo alternatívny znak"
STR_KB_HINT_URL_SNIPPETS: "Stlačte URL pre úryvky"
STR_SD_FIRMWARE_UPDATE: "Aktualizácia firmvéru z SD karty"
STR_SELECT_FIRMWARE_FILE: "Vyberte súbor firmvéru (.bin)"
STR_NO_BIN_FILES: "Nenašli sa žiadne súbory .bin"
STR_VALIDATING_FIRMWARE: "Overuje sa firmvér..."
STR_INVALID_FIRMWARE: "Neplatný súbor firmvéru"
STR_FIRMWARE_TOO_LARGE: "Firmvér je príliš veľký pre partíciu"
STR_FIRMWARE_TOO_SMALL: "Súbor firmvéru je príliš malý"
STR_FIRMWARE_UPDATE_PROMPT: "Aktualizovať firmvér?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Nie je možné otvoriť súbor"
STR_FIRMWARE_WRITE_FAILED: "Zápis firmvéru zlyhal"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nevypínajte zariadenie!"
STR_RECOVERY_MODE: "Režim obnovenia"
STR_RECOVERY_MODE_HINT: "Umiestnite firmware.bin do koreňového adresára SD karty a vyberte ho"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Način naslovnice v spanju"
STR_HIDE_BATTERY: "Skrij % baterije"
STR_EXTRA_SPACING: "Dodaten razmik med odstavki"
STR_TEXT_AA: "Glajenje besedila (AA)"
STR_TEXT_AA_FULL: "Polno"
STR_TEXT_AA_FAST: "Hitro"
STR_IMAGES: "Slike"
STR_IMAGES_DISPLAY: "Prikaži"
STR_IMAGES_PLACEHOLDER: "Oznaka mesta"
@@ -263,7 +265,6 @@ STR_CHAPTER_PREFIX: "Poglavje: "
STR_PAGES_SEPARATOR: " strani | "
STR_BOOK_PREFIX: "Knjiga: "
STR_CALIBRE_URL_HINT: "Za Calibre dodaj /opds svojemu URL-ju"
STR_PERCENT_STEP_HINT: "Levo/desno: 1% Gor/dol: 10%"
STR_SYNCING_TIME: "Sinhronizacija časa..."
STR_CALC_HASH: "Izračunavanje podpisa dokumenta..."
STR_HASH_FAILED: "Izračun podpisa dokumenta ni uspel"
@@ -295,7 +296,8 @@ STR_NO_FOOTNOTES: "Na tej strani ni opomb"
STR_LINK: "[povezava]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikoli"
STR_SLEEP_TIMER_STEP_HINT: "Levo/desno: 1 min Gor/dol: 5 min"
STR_STEP_HINT_FRONT: "Sprednji gumbi:"
STR_STEP_HINT_SIDE: "Stranski gumbi:"
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
+4 -2
View File
@@ -65,6 +65,8 @@ STR_SLEEP_COVER_MODE: "Modo de pantalla de suspensión"
STR_HIDE_BATTERY: "Ocultar % de batería"
STR_EXTRA_SPACING: "Espaciado entre párrafos"
STR_TEXT_AA: "Suavizado de texto"
STR_TEXT_AA_FULL: "Completo"
STR_TEXT_AA_FAST: "Rápido"
STR_IMAGES: "Imágenes"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Reemplazar"
@@ -297,7 +299,6 @@ STR_CHAPTER_PREFIX: "Cap.: "
STR_PAGES_SEPARATOR: " pág. | "
STR_BOOK_PREFIX: "Libro: "
STR_CALIBRE_URL_HINT: "Para Calibre, agregue /opds a su URL"
STR_PERCENT_STEP_HINT: "Izq./Dcha.: 1% | Subir/Bajar: 10%"
STR_SYNCING_TIME: "Tiempo de sincronización..."
STR_CALC_HASH: "Calculando hash del documento..."
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
@@ -331,7 +332,8 @@ STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
STR_LINK: "[enlace]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Nunca"
STR_SLEEP_TIMER_STEP_HINT: "Izq./Dcha.: 1 min. Subir/Bajar: 5 min."
STR_STEP_HINT_FRONT: "Botones frontales:"
STR_STEP_HINT_SIDE: "Botones laterales:"
STR_SCREENSHOT_BUTTON: "Tomar captura de pantalla"
STR_ADD_SERVER: "Agregar servidor"
STR_SERVER_NAME: "Nombre de servidor"
+4 -2
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
STR_HIDE_BATTERY: "Dölj batteriprocent"
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
STR_TEXT_AA: "Textkantutjämning"
STR_TEXT_AA_FULL: "Full"
STR_TEXT_AA_FAST: "Snabb"
STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Visa"
STR_IMAGES_PLACEHOLDER: "Platshållare"
@@ -295,7 +297,6 @@ STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " sidor | "
STR_BOOK_PREFIX: "Bok:"
STR_CALIBRE_URL_HINT: "För Calibre: lägg till /opds i din adress"
STR_PERCENT_STEP_HINT: "Vänster/Höger: 1% Upp/Ner 10%"
STR_SYNCING_TIME: "Synkroniserar tid…"
STR_CALC_HASH: "Beräknar dokumenthash"
STR_HASH_FAILED: "Misslyckades att beräkna dokumenthash"
@@ -328,7 +329,8 @@ STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
STR_LINK: "[länk]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig"
STR_SLEEP_TIMER_STEP_HINT: "Vänster/Höger: 1 min Upp/Ner: 5 min"
STR_STEP_HINT_FRONT: "Framknappar:"
STR_STEP_HINT_SIDE: "Sidoknappar:"
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
STR_ADD_SERVER: "Lägg till server"
STR_SERVER_NAME: "Servernamn"
+4 -2
View File
@@ -64,6 +64,8 @@ STR_SLEEP_COVER_MODE: "Uyku Ekranı Kapak Modu"
STR_HIDE_BATTERY: "Pil Yüzdesini Gizle"
STR_EXTRA_SPACING: "Ekstra Paragraf Boşluğu"
STR_TEXT_AA: "Metin Yumuşatma (AA)"
STR_TEXT_AA_FULL: "Tam"
STR_TEXT_AA_FAST: "Hızlı"
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
@@ -241,7 +243,6 @@ STR_CHAPTER_PREFIX: "Bölüm: "
STR_PAGES_SEPARATOR: " sayfa | "
STR_BOOK_PREFIX: "Kitap: "
STR_CALIBRE_URL_HINT: "Calibre için URL'nize /opds ekleyin"
STR_PERCENT_STEP_HINT: "Sol/Sağ: %1 Yukarı/Aşağı: %10"
STR_SYNCING_TIME: "Zaman senkronize ediliyor..."
STR_CALC_HASH: "Belge özeti hesaplanıyor..."
STR_HASH_FAILED: "Belge özeti hesaplanamadı"
@@ -289,7 +290,8 @@ STR_IMAGES_SUPPRESS: "Bastır"
STR_LINK: "[bağlantı]"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla"
STR_SLEEP_TIMER_STEP_HINT: "Sol/Sağ: 1 dak Yukarı/Aşağı: 5 dak"
STR_STEP_HINT_FRONT: "Ön tuşlar:"
STR_STEP_HINT_SIDE: "Yan tuşlar:"
STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme"
+4 -2
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Режим заповнення"
STR_HIDE_BATTERY: "Приховати % батареї"
STR_EXTRA_SPACING: "Додатковий інтервал між абзацами"
STR_TEXT_AA: "Згладжування тексту"
STR_TEXT_AA_FULL: "Повне"
STR_TEXT_AA_FAST: "Швидке"
STR_IMAGES: "Зображення"
STR_IMAGES_DISPLAY: "Показати"
STR_IMAGES_PLACEHOLDER: "Заглушка"
@@ -294,7 +296,6 @@ STR_CHAPTER_PREFIX: "Розділ: "
STR_PAGES_SEPARATOR: " сторінок | "
STR_BOOK_PREFIX: "Книга: "
STR_CALIBRE_URL_HINT: "Для Calibre додайте /opds до вашої URL"
STR_PERCENT_STEP_HINT: "Вліво/Вправо: 1% Вгору/Вниз: 10%"
STR_SYNCING_TIME: "Синхронізація часу..."
STR_CALC_HASH: "Обчислення хешу документа..."
STR_HASH_FAILED: "Не вдалося обчислити хеш документа"
@@ -329,7 +330,8 @@ STR_LINK: "[посилання]"
STR_SCREENSHOT_BUTTON: "Знімок екрана"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколи"
STR_SLEEP_TIMER_STEP_HINT: "Вліво/Вправо: 1 хв Вгору/Вниз: 5 хв"
STR_STEP_HINT_FRONT: "Передні кнопки:"
STR_STEP_HINT_SIDE: "Бічні кнопки:"
STR_ADD_SERVER: "Додати сервер"
STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
+4 -2
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
STR_HIDE_BATTERY: "Oculta el % de bateria"
STR_EXTRA_SPACING: "Espaiat de paràgraf extra"
STR_TEXT_AA: "Antialiàsing del text"
STR_TEXT_AA_FULL: "Complet"
STR_TEXT_AA_FAST: "Ràpid"
STR_IMAGES: "Imatges"
STR_IMAGES_DISPLAY: "Mostrar"
STR_IMAGES_PLACEHOLDER: "Text de mostra"
@@ -89,7 +91,8 @@ STR_HYPHENATION: "Partició de mots"
STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Mai"
STR_SLEEP_TIMER_STEP_HINT: "Esquerra/Dreta: 1 min Amunt/Avall: 5 min"
STR_STEP_HINT_FRONT: "Botons frontals:"
STR_STEP_HINT_SIDE: "Botons laterals:"
STR_SHOW_HIDDEN_FILES: "Mostra arxius ocults"
STR_REMOVE_READ_FROM_RECENTS: "Esborra els llibres llegits de la llista de recents"
STR_MOVE_FINISHED_TO_READ: "Mou els llibres acabats a la carpeta Read"
@@ -276,7 +279,6 @@ STR_CHAPTER_PREFIX: "Capítol: "
STR_PAGES_SEPARATOR: " pàgines | "
STR_BOOK_PREFIX: "Llibre: "
STR_CALIBRE_URL_HINT: "Per al Calibre, afegiu /opds a la URL"
STR_PERCENT_STEP_HINT: "Esquerra/Dreta: 1% Amunt/Avall: 10%"
STR_SYNCING_TIME: "S'està sincronitzant el temps..."
STR_CALC_HASH: "S'està calculant el hash del document..."
STR_HASH_FAILED: "No s'ha pogut calcular el hash del document"
+4 -2
View File
@@ -66,6 +66,8 @@ STR_SLEEP_COVER_MODE: "Kiểu ảnh bìa màn hình ngủ"
STR_HIDE_BATTERY: "Ẩn % pin"
STR_EXTRA_SPACING: "Giãn cách đoạn thêm"
STR_TEXT_AA: "Khử răng cưa chữ"
STR_TEXT_AA_FULL: "Đầy đủ"
STR_TEXT_AA_FAST: "Nhanh"
STR_IMAGES: "Hình ảnh"
STR_IMAGES_DISPLAY: "Hiển thị"
STR_IMAGES_PLACEHOLDER: "Khung thay thế"
@@ -294,7 +296,6 @@ STR_CHAPTER_PREFIX: "Chương: "
STR_PAGES_SEPARATOR: " trang | "
STR_BOOK_PREFIX: "Sách: "
STR_CALIBRE_URL_HINT: "Với Calibre, thêm /opds vào URL"
STR_PERCENT_STEP_HINT: "Trái/Phải: 1% Lên/Xuống: 10%"
STR_SYNCING_TIME: "Đang đồng bộ giờ..."
STR_CALC_HASH: "Đang tính mã băm tài liệu..."
STR_HASH_FAILED: "Không tính được mã băm tài liệu"
@@ -328,7 +329,8 @@ STR_LINK: "[liên kết]"
STR_SCREENSHOT_BUTTON: "Chụp màn hình"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u phút"
STR_SLEEP_NEVER: "Không bao giờ"
STR_SLEEP_TIMER_STEP_HINT: "Trái/Phải: 1 phút Lên/Xuống: 5 phút"
STR_STEP_HINT_FRONT: "Nút trước:"
STR_STEP_HINT_SIDE: "Nút bên:"
STR_ADD_SERVER: "Thêm máy chủ"
STR_SERVER_NAME: "Tên máy chủ"
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
+10 -5
View File
@@ -278,13 +278,18 @@ class XPathParagraphResolver final : public Print {
path.push_back({name, siblingIndex});
parentStates.emplace_back();
// Count both <p> and <li> as paragraph-like positions, matching how the section
// layout tracks them (xpathParagraphIndex and xpathListItemIndex). This ensures
// KOReader progress in list items maps to the correct XPath.
if (name == "p") {
paragraphCount++;
if (paragraphCount == targetParagraph) {
xpath = buildParagraphXPath(spineIndex, path, 0, 0);
stopped = true;
XML_StopParser(parser, XML_FALSE);
}
} else if (name == "li") {
paragraphCount++;
}
if (paragraphCount == targetParagraph) {
xpath = buildParagraphXPath(spineIndex, path, 0, 0);
stopped = true;
XML_StopParser(parser, XML_FALSE);
}
depth++;
+5 -4
View File
@@ -709,12 +709,13 @@ SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub
float intra =
(pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f;
result.percentage = epub->calculateProgress(pos.spineIndex, intra);
// Progress-based XPath correctly handles both <p> and <li> positions.
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
// Fall back to paragraph-index lookup when progress-based resolution fails.
if (result.xpath.empty() && pos.hasParagraphIndex && pos.paragraphIndex > 0) {
if (pos.hasParagraphIndex && pos.paragraphIndex > 0) {
result.xpath = ChapterXPathResolver::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex);
}
// Fall back to progress-based XPath, then synthetic progress mapping.
if (result.xpath.empty()) {
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
}
if (result.xpath.empty()) {
result.xpath = generateXPath(epub, pos.spineIndex, intra);
}
+5 -4
View File
@@ -10,7 +10,11 @@ OpdsParser::OpdsParser() {
if (!parser) {
errorOccured = true;
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
return;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
}
OpdsParser::~OpdsParser() { destroyXmlParser(parser); }
@@ -20,10 +24,6 @@ size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
if (errorOccured) return length;
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
const char* currentPos = reinterpret_cast<const char*>(xmlData);
size_t remaining = length;
constexpr size_t chunkSize = 1024;
@@ -54,6 +54,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
}
void OpdsParser::flush() {
if (errorOccured || !parser) return;
if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) {
errorOccured = true;
destroyXmlParser(parser);
+1 -1
View File
@@ -42,7 +42,7 @@ std::string Txt::getTitle() const {
// Remove .txt extension
if (FsHelpers::hasTxtExtension(filename)) {
filename = filename.substr(0, filename.length() - 4);
filename.resize(filename.length() - 4);
}
return filename;
+8 -1
View File
@@ -6,6 +6,7 @@
#include <Serialization.h>
#include <cstring>
#include <mutex>
#include <string>
#include "I18nKeys.h"
@@ -96,6 +97,7 @@ uint8_t CrossPointSettings::sleepTimeoutEnumToMinutes(const uint8_t legacyValue)
}
bool CrossPointSettings::saveToFile() const {
std::lock_guard<std::mutex> lock(_mutex);
Storage.mkdir("/.crosspoint");
return JsonSettingsIO::saveSettings(*this, SETTINGS_FILE_JSON);
}
@@ -106,7 +108,11 @@ bool CrossPointSettings::loadFromFile() {
String json = Storage.readFile(SETTINGS_FILE_JSON);
if (!json.isEmpty()) {
bool resave = false;
bool result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
bool result;
{
std::lock_guard<std::mutex> lock(_mutex);
result = JsonSettingsIO::loadSettings(*this, json.c_str(), &resave);
}
if (result && resave) {
if (saveToFile()) {
LOG_DBG("CPS", "Resaved settings to update format");
@@ -166,6 +172,7 @@ bool CrossPointSettings::loadFromBinaryFile() {
if (!Storage.openFileForRead("CPS", SETTINGS_FILE_BIN, inputFile)) {
return false;
}
std::lock_guard<std::mutex> lock(_mutex);
uint8_t version;
serialization::readPod(inputFile, version);
+15 -1
View File
@@ -3,9 +3,12 @@
#include <cstdint>
#include <iosfwd>
#include <mutex>
class CrossPointSettings {
private:
mutable std::mutex _mutex;
// Private constructor for singleton
CrossPointSettings() = default;
@@ -17,6 +20,10 @@ class CrossPointSettings {
CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
// Access the settings mutex for protecting multi-field reads/writes from other cores.
// Callers must not re-enter SETTINGS methods that lock _mutex while holding it.
std::mutex& getMutex() const { return _mutex; }
enum SLEEP_SCREEN_MODE {
DARK = 0,
LIGHT = 1,
@@ -166,6 +173,13 @@ class CrossPointSettings {
// Image rendering in EPUB reader
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
// Text anti-aliasing mode for 2-bit fonts. FULL runs the grayscale LUT
// passes after the BW page render (extra refresh per page turn). FAST
// approximates AA inside the single BW pass by dithering partial-coverage
// glyph pixels (GfxRenderer fast anti-aliasing flag). Value 1 must stay
// FULL: it was the "on" value of the old on/off toggle in saved settings.
enum TEXT_ANTIALIASING { TEXT_AA_OFF = 0, TEXT_AA_FULL = 1, TEXT_AA_FAST = 2, TEXT_ANTIALIASING_COUNT };
enum TILT_PAGE_TURN { TILT_OFF = 0, TILT_NORMAL = 1, TILT_NVERTED = 2, TILT_PAGE_TURN_COUNT };
enum QUICK_RESUME_SLEEP_SCREEN {
@@ -202,7 +216,7 @@ class CrossPointSettings {
uint8_t clockHasBeenSynced = 0;
// Text rendering settings
uint8_t extraParagraphSpacing = 1;
uint8_t textAntiAliasing = 1;
uint8_t textAntiAliasing = TEXT_AA_FULL;
// Short power button click behaviour
uint8_t shortPwrBtn = IGNORE;
// EPUB reading orientation settings
+4
View File
@@ -6,6 +6,7 @@
#include <Serialization.h>
#include <algorithm>
#include <mutex>
namespace {
constexpr uint8_t STATE_FILE_VERSION = 4;
@@ -32,6 +33,7 @@ void CrossPointState::pushRecentSleep(uint16_t idx) {
}
bool CrossPointState::saveToFile() const {
std::lock_guard<std::mutex> lock(_mutex);
Storage.mkdir("/.crosspoint");
return JsonSettingsIO::saveState(*this, STATE_FILE_JSON);
}
@@ -41,6 +43,7 @@ bool CrossPointState::loadFromFile() {
if (Storage.exists(STATE_FILE_JSON)) {
String json = Storage.readFile(STATE_FILE_JSON);
if (!json.isEmpty()) {
std::lock_guard<std::mutex> lock(_mutex);
return JsonSettingsIO::loadState(*this, json.c_str());
}
}
@@ -67,6 +70,7 @@ bool CrossPointState::loadFromBinaryFile() {
if (!Storage.openFileForRead("CPS", STATE_FILE_BIN, inputFile)) {
return false;
}
std::lock_guard<std::mutex> lock(_mutex);
uint8_t version;
serialization::readPod(inputFile, version);
+6
View File
@@ -1,12 +1,18 @@
#pragma once
#include <cstdint>
#include <mutex>
#include <string>
class CrossPointState {
mutable std::mutex _mutex;
// Static instance
static CrossPointState instance;
public:
// Access the state mutex for protecting multi-field reads/writes from other cores.
std::mutex& getMutex() const { return _mutex; }
static constexpr uint8_t SLEEP_RECENT_COUNT = 16;
std::string openEpubPath;
+2 -4
View File
@@ -5,6 +5,7 @@
#include <Logging.h>
#include <ObfuscationUtils.h>
#include <algorithm>
#include <cstring>
#include <string>
@@ -153,10 +154,6 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
// Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders.
doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN";
// Language -- managed by LanguageSelectActivity, not in SettingsList.
// Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders.
doc["language"] = (s.language < getLanguageCount()) ? LANGUAGE_CODES[s.language] : "EN";
String json;
serializeJson(doc, json);
return Storage.writeFile(path, json);
@@ -345,6 +342,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json)
store.recentBooks.clear();
JsonArray arr = doc["books"].as<JsonArray>();
store.recentBooks.reserve(std::min(arr.size(), (size_t)10));
for (JsonObject obj : arr) {
if (store.getCount() >= 10) break;
RecentBook book;
+5
View File
@@ -34,10 +34,12 @@ void SdCardFontSystem::begin(GfxRenderer& renderer) {
} else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName);
SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
}
} else {
LOG_DBG("SDFS", "SD font family not found on card: %s (clearing)", SETTINGS.sdFontFamilyName);
SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
}
}
@@ -76,6 +78,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
LOG_DBG("SDFS", "SD font family disappeared: %s (clearing)", wantedFamily);
manager_.unloadAll(renderer);
SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
return;
}
const auto* selected = family->findClosestReaderSize(sizeEnum);
@@ -96,10 +99,12 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) {
} else {
LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily);
SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
}
} else {
LOG_DBG("SDFS", "SD font family not found: %s (clearing)", wantedFamily);
SETTINGS.sdFontFamilyName[0] = '\0';
SETTINGS.saveToFile();
}
}
+7 -4
View File
@@ -104,8 +104,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
std::vector<SettingInfo> v = {
// --- Display ---
SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT,
StrId::STR_COVER_CUSTOM, StrId::STR_QUICK_RESUME},
{StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER,
StrId::STR_COVER_CUSTOM, StrId::STR_NONE_OPT, StrId::STR_QUICK_RESUME},
"sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
{StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
@@ -157,8 +157,11 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
"orientation", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing,
"extraParagraphSpacing", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
StrId::STR_CAT_READER),
// Option order must match TEXT_ANTIALIASING values (popup index is
// stored directly): OFF=0, FULL=1 (legacy toggle "on"), FAST=2.
SettingInfo::Enum(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing,
{StrId::STR_STATE_OFF, StrId::STR_TEXT_AA_FULL, StrId::STR_TEXT_AA_FAST}, "textAntiAliasing",
StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
"imageRendering", StrId::STR_CAT_READER),
+3
View File
@@ -6,6 +6,8 @@
#include <ObfuscationUtils.h>
#include <Serialization.h>
#include <algorithm>
// Initialize the static instance
WifiCredentialStore WifiCredentialStore::instance;
@@ -89,6 +91,7 @@ bool WifiCredentialStore::loadFromBinaryFile() {
serialization::readPod(file, count);
credentials.clear();
credentials.reserve(std::min<size_t>(count, MAX_NETWORKS));
for (uint8_t i = 0; i < count && i < MAX_NETWORKS; i++) {
WifiCredential cred;
serialization::readString(file, cred.ssid);
+13 -11
View File
@@ -19,12 +19,15 @@
#include "settings/SettingsActivity.h"
#include "util/FullScreenMessageActivity.h"
static portMUX_TYPE activityManagerSpinlock = portMUX_INITIALIZER_UNLOCKED;
void ActivityManager::begin() {
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
8192, // Stack size
this, // Parameters
1, // Priority
&renderTaskHandle // Task handle
xTaskCreatePinnedToCore(&renderTaskTrampoline, "ActivityManagerRender",
8192, // Stack size
this, // Parameters
1, // Priority
&renderTaskHandle, // Task handle
0 // Pin to core 0 (PRO_CPU)
);
assert(renderTaskHandle != nullptr && "Failed to create render task");
}
@@ -46,10 +49,10 @@ void ActivityManager::renderTaskLoop() {
}
// Notify any task blocked in requestUpdateAndWait() that the render is done.
TaskHandle_t waiter = nullptr;
taskENTER_CRITICAL(nullptr);
taskENTER_CRITICAL(&activityManagerSpinlock);
waiter = waitingTaskHandle;
waitingTaskHandle = nullptr;
taskEXIT_CRITICAL(nullptr);
taskEXIT_CRITICAL(&activityManagerSpinlock);
if (waiter) {
xTaskNotify(waiter, 1, eIncrement);
}
@@ -137,8 +140,7 @@ void ActivityManager::loop() {
}
}
if (requestedUpdate) {
requestedUpdate = false;
if (requestedUpdate.exchange(false)) {
// Using direct notification to signal the render task to update
// Increment counter so multiple rapid calls won't be lost
if (renderTaskHandle) {
@@ -280,7 +282,7 @@ void ActivityManager::requestUpdateAndWait() {
}
// Atomic section to perform checks
taskENTER_CRITICAL(nullptr);
taskENTER_CRITICAL(&activityManagerSpinlock);
auto currTaskHandler = xTaskGetCurrentTaskHandle();
auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex);
bool isRenderTask = (currTaskHandler == renderTaskHandle);
@@ -289,7 +291,7 @@ void ActivityManager::requestUpdateAndWait() {
if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) {
waitingTaskHandle = currTaskHandler;
}
taskEXIT_CRITICAL(nullptr);
taskEXIT_CRITICAL(&activityManagerSpinlock);
// Render task cannot call requestUpdateAndWait() or it will cause a deadlock
assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()");
+2 -1
View File
@@ -4,6 +4,7 @@
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <atomic>
#include <cassert>
#include <memory>
#include <string>
@@ -63,7 +64,7 @@ class ActivityManager {
// Whether to trigger a render after the current loop()
// This variable must only be set by the main loop, to avoid race conditions
bool requestedUpdate = false;
std::atomic<bool> requestedUpdate{false};
public:
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
+4
View File
@@ -43,6 +43,10 @@ struct PageResult {
struct ProgressChangeResult {
int spineIndex = 0;
int page = 0;
int totalPages = 0;
std::string xpath;
float percentage = 0.0f;
bool hasSavedProgress = false;
};
enum class NetworkMode;
+6 -8
View File
@@ -163,7 +163,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.invertScreen();
}
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
renderer.displayBuffer(HalDisplay::FULL_REFRESH);
}
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
@@ -219,13 +219,11 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
}
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);
// OEM grayscale pipeline base: use a full sleep-screen paint so the panel
// enters deep sleep from a clean B/W baseline before the gray nudge refresh.
renderer.displayGrayscaleBase(HalDisplay::FULL_REFRESH);
} else {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
renderer.displayBuffer(HalDisplay::FULL_REFRESH);
}
if (hasGreyscale) {
@@ -333,5 +331,5 @@ void SleepActivity::renderLastScreenSleepScreen() const {
void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen();
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
renderer.displayBuffer(HalDisplay::FULL_REFRESH);
}
+1 -1
View File
@@ -336,7 +336,7 @@ std::string getFileName(std::string filename) {
return filename.substr(0, pos);
}
std::string getFileExtension(std::string filename) {
std::string getFileExtension(const std::string& filename) {
if (filename.back() == '/') {
return "";
}
+115
View File
@@ -0,0 +1,115 @@
#include "EndOfBookOptions.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include "CrossPointSettings.h"
#include "ReaderUtils.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/ButtonNavigator.h"
#include "util/NextBookFinder.h"
namespace {
// Display name without the file extension, mirroring the file browser rows
std::string displayName(const std::string& filename) {
const auto pos = filename.rfind('.');
return filename.substr(0, pos);
}
} // namespace
void EndOfBookOptions::loadOnce(const std::string& currentBookPath) {
if (isLoaded.load(std::memory_order_acquire)) {
return;
}
folder = FsHelpers::extractFolderPath(currentBookPath);
names = NextBookFinder::findNextBooks(currentBookPath, MAX_SUGGESTIONS);
selector = 0;
// Release-publish so the main task, which gates all access on isLoaded, never
// observes a partially built list
isLoaded.store(true, std::memory_order_release);
}
bool EndOfBookOptions::menuActive() const { return isLoaded.load(std::memory_order_acquire) && !names.empty(); }
std::string EndOfBookOptions::fullPath(const size_t index) const {
if (index >= names.size()) {
return {};
}
return folder == "/" ? "/" + names[index] : folder + "/" + names[index];
}
EndOfBookOptions::Action EndOfBookOptions::handleMenuInput(const MappedInputManager& input, std::string* openPath) {
if (input.wasReleased(MappedInputManager::Button::Confirm)) {
if (selector < static_cast<int>(names.size())) {
if (openPath) {
*openPath = fullPath(selector);
}
return Action::OpenBook;
}
return Action::GoHome; // "Home" entry selected
}
// Short-press Back returns to the last page; a long press falls through to the
// reader's own handler (file browser). Home is reached through the list's Home entry.
if (input.wasReleased(MappedInputManager::Button::Back) && input.getHeldTime() < ReaderUtils::GO_HOME_MS) {
return Action::LastPage;
}
// Selection movement on the standard list navigation buttons (side Up/Down plus front
// Left/Right, orientation swap included). It follows the reader's page-turn semantics
// (press-triggered by default, release-triggered when a long-press behavior is
// configured, same rule as ReaderUtils::detectPageTurn). This matters on entry: with
// press-triggered turns, the press that turned the final page already fired in the
// reader, and its release must not double-fire into this menu.
const bool usePress = SETTINGS.longPressButtonBehavior == CrossPointSettings::OFF;
const auto triggered = [&](const MappedInputManager::Button button) {
return usePress ? input.wasPressed(button) : input.wasReleased(button);
};
const int itemCount = static_cast<int>(names.size()) + 1; // + "Home" entry
if (triggered(MappedInputManager::Button::NavPrevious)) {
selector = ButtonNavigator::previousIndex(selector, itemCount); // wraps to the bottom
return Action::Redraw;
}
if (triggered(MappedInputManager::Button::NavNext)) {
selector = ButtonNavigator::nextIndex(selector, itemCount); // wraps to the top
return Action::Redraw;
}
return Action::None;
}
void EndOfBookOptions::render(GfxRenderer& renderer, const MappedInputManager& input) const {
const auto& metrics = UITheme::getInstance().getMetrics();
if (!menuActive()) {
// No suggestions: the historical plain end screen. 3/8 of the screen height matches
// the previous fixed position on the 480x800 panel and scales to other resolutions.
renderer.drawCenteredText(UI_12_FONT_ID, renderer.getScreenHeight() * 3 / 8, tr(STR_END_OF_BOOK), true,
EpdFontFamily::BOLD);
return;
}
// Suggestion menu: title, list (+ Home entry) and button hints. The hints are drawn at
// the physical front buttons, which is a logical side/top edge in the rotated
// orientations — lay out inside the safe area so nothing hides behind them. Vertical
// positions derive from the safe-area height and font line heights so other panel
// resolutions scale (review request on #2532).
const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false);
const int titleY = safe.y + safe.height / 8;
const int subtitleY = titleY + renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing;
const int listTop = subtitleY + renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing * 2;
UITheme::drawCenteredText(renderer, safe, UI_12_FONT_ID, titleY, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
UITheme::drawCenteredText(renderer, safe, UI_10_FONT_ID, subtitleY, tr(STR_EOB_CONTINUE_WITH));
const int listHeight = safe.y + safe.height - listTop - metrics.verticalSpacing;
GUI.drawList(renderer, Rect{safe.x, listTop, safe.width, listHeight}, static_cast<int>(names.size()) + 1, selector,
[this](const int index) {
return index < static_cast<int>(names.size()) ? displayName(names[index])
: std::string(tr(STR_EOB_HOME));
});
const auto labels = input.mapLabels(tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <atomic>
#include <string>
#include <vector>
class GfxRenderer;
class MappedInputManager;
// Shared End-of-Book next-book menu for the EPUB and XTC readers. Collects up to
// MAX_SUGGESTIONS sibling books once per reader session, handles the menu input, and
// draws the end screen. With no suggestions the end screen keeps its historical
// plain-title look and behavior.
class EndOfBookOptions {
public:
enum class Action { None, Redraw, OpenBook, GoHome, LastPage };
static constexpr size_t MAX_SUGGESTIONS = 3;
// Scans the book's folder for suggestions; no-op when already loaded. Call ONLY from
// the reader's render() (the render task, serialized by RenderLock) — the loaded flag
// is the release/acquire publication point that lets the main task read the finished
// list safely.
void loadOnce(const std::string& currentBookPath);
// True when the suggestion menu is showing and should own the reader's input.
bool menuActive() const;
// Menu input handling, following the standard list idiom: side Up/Down and front
// Left/Right move the selection (wrapping), Confirm opens it (or Home), and a short
// Back press returns to the last page of the book. Fills openPath when the result is
// OpenBook. Returns Action::None when nothing relevant was pressed; callers continue
// their normal input path (keeping long-press Back to the file browser working).
Action handleMenuInput(const MappedInputManager& input, std::string* openPath);
// Draws the full end screen (plain title, or the suggestion menu) onto a cleared buffer.
void render(GfxRenderer& renderer, const MappedInputManager& input) const;
private:
std::string folder;
// Written by the render task in loadOnce(), immutable afterwards; the main task only
// reads it after isLoaded is observed true (acquire), so no further locking is needed.
std::vector<std::string> names;
int selector = 0;
std::atomic<bool> isLoaded{false};
std::string fullPath(size_t index) const;
};
+330 -107
View File
@@ -231,6 +231,30 @@ void EpubReaderActivity::onExit() {
}
}
void EpubReaderActivity::openReaderMenu() {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->estimatedTotalPages() : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->estimatedTotalPages() > 0) {
const float chapterProgress =
static_cast<float>(section->currentPage) / static_cast<float>(section->estimatedTotalPages());
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()),
[this](const ActivityResult& result) {
// Always apply orientation change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
toggleAutoPageTurn(menu.pageTurnOption);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
}
void EpubReaderActivity::loop() {
if (!epub) {
// Should never happen
@@ -238,6 +262,33 @@ void EpubReaderActivity::loop() {
return;
}
// Drive any in-progress incremental section build forward, off the page-turn critical path,
// but only within a small window ahead of the reader: an unbounded build monopolized the
// RenderLock and locked out page turns. The build follows the reader instead, and instant
// reopen comes from suspendBuild() persisting the laid-out pages as a partial on exit.
// Skip while the render mutex is busy so we never delay a pending render; re-check
// isBuilding() under the lock since render() may have just finished it.
if (section && section->isBuilding() && !RenderLock::peek() &&
static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) {
RenderLock lock;
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
// build between the outer isBuilding() check and acquiring the lock here, in which case
// buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task
// mutation, so it flags this as always true.
// cppcheck-suppress knownConditionTrueFalse
if (section->isBuilding()) {
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
LOG_ERR("ERS", "Background section build failed");
section.reset();
requestUpdate();
} else if (section->isBuildComplete() && applyDeferredReposition()) {
// The chapter re-paginated since the saved progress (settings changed): we now know the
// real page count, so re-render at the remapped page. No-op for an unchanged resume.
requestUpdate();
}
}
}
// End-of-Book screen reached (currentSpineIndex == spine count) means the book is
// finished. Two independent finished-book features key off this same condition.
const bool atEndOfBook = currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount();
@@ -298,6 +349,35 @@ void EpubReaderActivity::loop() {
requestUpdate();
}
// While the end screen suggestion menu is showing it owns Confirm/Back/navigation
// input. Anything it doesn't handle (e.g. long-press Back to the file browser) falls
// through to the regular handlers below; page turns are absorbed by the end-of-book
// block. A Confirm release after a long-press function (bookmark/sync) fired is left
// to the regular Confirm handler below, which consumes it via ignoreNextConfirmRelease.
if (atEndOfBook && endOfBookOptions.menuActive() &&
!(ignoreNextConfirmRelease && mappedInput.wasReleased(MappedInputManager::Button::Confirm))) {
std::string openPath;
switch (endOfBookOptions.handleMenuInput(mappedInput, &openPath)) {
case EndOfBookOptions::Action::OpenBook:
activityManager.goToReader(openPath);
return;
case EndOfBookOptions::Action::GoHome:
onGoHome();
return;
case EndOfBookOptions::Action::LastPage:
currentSpineIndex = std::max(epub->getSpineItemsCount() - 1, 0);
nextPageNumber = 0;
pendingPageJump = std::numeric_limits<uint16_t>::max();
requestUpdate();
return;
case EndOfBookOptions::Action::Redraw:
requestUpdate();
return;
case EndOfBookOptions::Action::None:
break;
}
}
// Enter reader menu activity on short-press Confirm. A long-press that fired a bound
// function (bookmark or KOReader sync) sets ignoreNextConfirmRelease so the release
// following the hold does not also open the menu.
@@ -305,26 +385,7 @@ void EpubReaderActivity::loop() {
if (ignoreNextConfirmRelease) {
ignoreNextConfirmRelease = false;
} else {
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
if (epub->getBookSize() > 0 && section && section->pageCount > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()),
[this](const ActivityResult& result) {
// Always apply orientation change even if the menu was cancelled
const auto& menu = std::get<MenuResult>(result.data);
applyOrientation(menu.orientation);
toggleAutoPageTurn(menu.pageTurnOption);
if (!result.isCancelled) {
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
}
});
openReaderMenu();
}
}
@@ -405,8 +466,14 @@ void EpubReaderActivity::loop() {
return;
}
// At end of the book, forward button goes home and back button returns to last page
// At end of the book with no suggestion menu, forward button goes home and back
// button returns to last page
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
if (endOfBookOptions.menuActive()) {
// Selection movement was handled above; absorb leftover page-turn triggers so
// e.g. "previous" at the top of the list doesn't jump back into the book
return;
}
if (nextTriggered) {
onGoHome();
} else {
@@ -537,11 +604,32 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
loadCachedBookmarks();
if (!result.isCancelled) {
const auto& sync = std::get<ProgressChangeResult>(result.data);
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
int targetSpineIndex = sync.spineIndex;
int targetPage = sync.page;
const int activeTotalPages = section ? section->estimatedTotalPages() : 0;
const bool cachedPageMatchesActiveSection = section && sync.totalPages > 0 &&
currentSpineIndex == sync.spineIndex && sync.page >= 0 &&
sync.page < sync.totalPages && activeTotalPages == sync.totalPages;
if (!cachedPageMatchesActiveSection && sync.hasSavedProgress) {
const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount;
CrossPointPosition fallback =
ProgressMapper::toCrossPoint(epub, {sync.xpath, sync.percentage}, renderer, currentSpineIndex, totalPages);
targetSpineIndex = fallback.spineIndex;
targetPage = fallback.pageNumber;
}
if (currentSpineIndex != targetSpineIndex) {
RenderLock lock(*this);
currentSpineIndex = sync.spineIndex;
nextPageNumber = sync.page;
currentSpineIndex = targetSpineIndex;
nextPageNumber = targetPage;
section.reset();
} else if (section && section->currentPage != targetPage) {
RenderLock lock(*this);
const int clampedTargetPage = std::max(0, targetPage);
section->currentPage = clampedTargetPage;
} else if (!section) {
nextPageNumber = targetPage;
}
}
};
@@ -661,7 +749,7 @@ bool EpubReaderActivity::launchKOReaderSync() {
if (!KOREADER_STORE.hasCredentials()) return false; // no-op: nothing to launch
const int currentPage = section ? section->currentPage : nextPageNumber;
const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount;
const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount;
std::optional<uint16_t> paragraphIndex;
if (section && currentPage >= 0 && currentPage < section->pageCount) {
const uint16_t paragraphPage =
@@ -759,7 +847,12 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption
void EpubReaderActivity::pageTurn(bool isForwardTurn) {
if (isForwardTurn) {
if (section->currentPage < section->pageCount - 1) {
// Advance within the section while there are (or may still be) more pages: either a built
// page ahead, or the section is still building (windowed), in which case more pages exist
// beyond the current watermark and render()'s ensure-built pump will lay them out. Only when
// the section is fully built AND we're on its last page do we move to the next spine -- using
// the live pageCount alone would mistake the build watermark for the end of a giant spine.
if (section->currentPage < section->pageCount - 1 || section->isBuilding()) {
section->currentPage++;
} else {
// We don't want to delete the section mid-render, so grab the semaphore
@@ -811,8 +904,11 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) {
// Sole load site: runs on the render task (serialized by RenderLock); the main
// task only reads the suggestions once the loaded flag is published
endOfBookOptions.loadOnce(epub->getPath());
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
endOfBookOptions.render(renderer, mappedInput);
renderer.displayBuffer();
automaticPageTurnActive = false;
showPendingSyncSaveError();
@@ -847,48 +943,130 @@ void EpubReaderActivity::render(RenderLock&& lock) {
LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex);
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_DBG("ERS", "Cache not found, building...");
// A finalized cache serves every page as-is. A partial cache (suspended build from a
// previous session) serves its pages instantly too, but a build must still run to lay
// out the rest -- it re-parses from the top in the background (HTML already cached,
// pages are deterministic) and finalizes, so the partial machinery retires itself.
const bool cacheLoaded = section->loadSectionFile(
SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing,
SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled);
if (cacheLoaded) {
// Matching render params means identical pagination, so the saved page number is valid
// as-is: consume any pending settings-change reposition. Without this, a chapter total
// saved while the section was still building (i.e. a watermark, not the real count)
// would remap the resume page against the finalized count and teleport the reader.
cachedChapterTotalPageCount = 0;
}
const bool cacheComplete = cacheLoaded && !section->isPartial();
if (!cacheComplete) {
if (section->isPartial()) {
LOG_DBG("ERS", "Partial cache found (%d pages), resuming build...", section->pageCount);
} else {
LOG_DBG("ERS", "Cache not found, building...");
}
GUI.drawPopup(renderer, tr(STR_INDEXING));
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
showPendingSyncSaveError();
return;
// Jumps that need the final pagination or the anchor map -- explicit page jumps,
// fragment anchors, percent jumps, and cross-setting progress repositioning -- can't
// resolve their landing page until the whole chapter is laid out, so they take the full
// (blocking) build with the indexing popup. Everything else -- plain forward reads, resume,
// and explicit page jumps -- only needs a specific page, so it builds incrementally to that
// page and finishes the rest in loop(). The settings-change reposition (cachedChapterTotal*)
// is NOT a full-build trigger: it's deferred to applyDeferredReposition() once the real page
// count is known, so it never blocks the first page.
// Only a percent jump truly needs the whole chapter up front (percent -> page needs the final
// page count). Anchor jumps (TOC / chapter select / footnotes) resolve incrementally below --
// the anchor is recorded as its page is laid out, so a chapter-top anchor lands on page 0
// without indexing the whole chapter.
const bool needsFullBuild = pendingPercentJump;
if (needsFullBuild) {
GUI.drawPopup(renderer, tr(STR_INDEXING));
// The popup's own refresh is a plain FAST, so force the page that replaces it onto the HALF
// ghost-cleanup path -- otherwise the "INDEXING" text ghosts under the rendered page.
pagesUntilFullRefresh = 1;
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
showPendingSyncSaveError();
return;
}
} else {
// Lay out just enough to show the landing page; loop() builds the rest behind it. Show the
// indexing popup up front only when the build will actually be slow: a large spine (its
// whole HTML must be inflated before page 1 can lay out -- the giant single-spine case), or
// a deep resume/jump that must lay out many pages to reach the landing page. Tiny sections
// build in a blink and stay popup-free.
const int target = pendingPageJump.has_value() ? *pendingPageJump : (nextPageNumber < 0 ? 0 : nextPageNumber);
const size_t spineBytes = epub->getCumulativeSpineItemSize(currentSpineIndex) -
(currentSpineIndex > 0 ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0);
// Popup only when the build will actually be slow: a big spine whose HTML still needs
// inflating (the multi-second cost), or a deep page target. A reopen with cached HTML builds
// fast, so no popup -- that's what made an already-indexed book look like it was reindexing.
// A partial cache that already covers the target page shows it instantly: never popup.
const bool willInflate = !section->hasHtmlCache();
const bool anchorJump = !pendingAnchor.empty();
bool showPopup;
if (anchorJump) {
// An anchor jump's cost is bounded by the anchor's page, not `target`. An anchor already
// in the on-disk map (partial or finalized cache) lands instantly: no popup. Otherwise it
// lies beyond the indexed watermark and the build may lay out the whole spine to find it,
// so gate on spine size alone -- laying out a big spine takes seconds even with cached
// HTML. Ordinary chapter-top TOC jumps resolve on page 0 and stay popup-free.
showPopup = !section->findAnchor(pendingAnchor).has_value() && spineBytes > BUILD_POPUP_BYTE_THRESHOLD;
} else {
const bool targetAvailable = target < static_cast<int>(section->pageCount);
showPopup = !targetAvailable &&
((spineBytes > BUILD_POPUP_BYTE_THRESHOLD && willInflate) || target > BUILD_POPUP_PAGE_THRESHOLD);
}
if (showPopup) {
GUI.drawPopup(renderer, tr(STR_INDEXING));
// HALF-clear the popup when the page replaces it, else "INDEXING" ghosts under the page.
pagesUntilFullRefresh = 1;
}
if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_ERR("ERS", "Failed to start section build");
section.reset();
showPendingSyncSaveError();
return;
}
while (!section->isBuildComplete() &&
(anchorJump ? !section->findAnchor(pendingAnchor) : static_cast<int>(section->pageCount) <= target)) {
// Anchor jump: build until the anchor's page is laid out (usually page 0), checking a
// partial's on-disk anchor map too so an already-indexed anchor resolves immediately.
// Otherwise: build until the target page exists. loop() builds the rest behind it.
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
LOG_ERR("ERS", "Failed during incremental section build");
section.reset();
showPendingSyncSaveError();
return;
}
}
}
} else {
LOG_DBG("ERS", "Cache found, skipping build...");
}
if (pendingPageJump.has_value()) {
if (*pendingPageJump >= section->pageCount && section->pageCount > 0) {
section->currentPage = section->pageCount - 1;
} else {
section->currentPage = *pendingPageJump;
}
section->currentPage = *pendingPageJump;
pendingPageJump.reset();
} else {
section->currentPage = nextPageNumber;
if (section->currentPage < 0) {
section->currentPage = 0;
} else if (section->currentPage >= section->pageCount && section->pageCount > 0) {
LOG_DBG("ERS", "Clamping cached page %d to %d", section->currentPage, section->pageCount - 1);
section->currentPage = section->pageCount - 1;
}
}
if (!pendingAnchor.empty()) {
if (const auto page = section->getPageForAnchor(pendingAnchor)) {
// Resolve from the pages laid out so far and/or the on-disk map (finalized or partial).
const auto page = section->findAnchor(pendingAnchor);
if (page) {
section->currentPage = *page;
LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page);
} else {
@@ -897,17 +1075,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
pendingAnchor.clear();
}
// handles changes in reader settings and reset to approximate position based on cached progress
if (cachedChapterTotalPageCount > 0) {
// only goes to relative position if spine index matches cached value
if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) {
float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount);
int newPage = static_cast<int>(progress * section->pageCount);
section->currentPage = newPage;
}
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
}
if (pendingPercentJump && section->pageCount > 0) {
// Apply the pending percent jump now that we know the new section's page count.
int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount));
@@ -919,6 +1086,57 @@ void EpubReaderActivity::render(RenderLock&& lock) {
}
}
// Extend the build to the requested page if needed (for partials and in-progress builds).
// This runs every render, so it covers both the first page and any forward turn that gets
// ahead of the background builder; pages already built do no work here.
while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) {
// Start a build to extend a partial toward the requested page.
if (!section->isBuilding() &&
!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering,
SETTINGS.focusReadingEnabled)) {
LOG_ERR("ERS", "Failed to start partial extension build");
section.reset();
showPendingSyncSaveError();
return;
}
// Extend until either the target page exists or the build completes.
while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) {
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
LOG_ERR("ERS", "Failed during incremental section build");
section.reset();
showPendingSyncSaveError();
return;
}
}
}
// For an in-progress incremental build, make sure the page we're about to show has been laid out.
if (section->isBuilding()) {
while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) {
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
LOG_ERR("ERS", "Failed during incremental section build");
section.reset();
showPendingSyncSaveError();
return;
}
}
}
// The requested page is now as built as it will get. If it still lands past the end,
// clamp to the last real page: the UINT16_MAX "last page" sentinel from backward chapter
// navigation, an explicit jump beyond a finished chapter, or a stale saved position.
// Guarded on !isBuilding() because a still-building section's pageCount is only the current
// watermark (not the final count) and has already been driven far enough by the loops above.
if (!section->isBuilding() && section->pageCount > 0 &&
section->currentPage >= static_cast<int>(section->pageCount)) {
section->currentPage = section->pageCount - 1;
}
// Apply a deferred settings-change reposition now that the real page count is known (a no-op for
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
applyDeferredReposition();
renderer.clearScreen();
if (section->pageCount == 0) {
@@ -944,9 +1162,14 @@ void EpubReaderActivity::render(RenderLock&& lock) {
updateBookmarkFlag();
{
auto p = section->loadPageFromSectionFile();
// Unified page read: the in-progress build's in-RAM table if it has reached the page,
// otherwise the on-disk file (finalized section, or a partial from a previous session).
auto p = section->loadPage(section->currentPage);
if (!p) {
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
// Abandon (not suspend) any active build BEFORE clearing: clearCache deletes the files,
// and the destructor's suspend would otherwise commit tables into a deleted handle.
section->abandonBuild();
section->clearCache();
section.reset();
requestUpdate(); // Try again after clearing cache
@@ -963,8 +1186,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
}
silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight);
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
saveProgress(currentSpineIndex, section->currentPage, section->estimatedTotalPages());
showPendingSyncSaveError();
@@ -978,36 +1200,28 @@ void EpubReaderActivity::render(RenderLock&& lock) {
}
}
void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) {
if (!epub || !section || section->pageCount < 2) {
return;
bool EpubReaderActivity::applyDeferredReposition() {
if (cachedChapterTotalPageCount == 0 || !section || section->isBuilding()) {
return false;
}
// Build the next chapter cache while the penultimate page is on screen.
if (section->currentPage != section->pageCount - 2) {
return;
}
const int nextSpineIndex = currentSpineIndex + 1;
if (nextSpineIndex < 0 || nextSpineIndex >= epub->getSpineItemsCount()) {
return;
}
Section nextSection(epub, nextSpineIndex, renderer);
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
return;
}
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex);
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
bool changed = false;
// Only remap when the chapter actually re-paginated (e.g. after a settings change). A plain
// resume has identical pagination, so section->pageCount == cachedChapterTotalPageCount and
// nothing moves.
if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) {
const float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount);
int newPage = static_cast<int>(progress * static_cast<float>(section->pageCount));
if (newPage < 0) newPage = 0;
if (section->pageCount > 0 && newPage >= static_cast<int>(section->pageCount)) {
newPage = section->pageCount - 1;
}
if (newPage != section->currentPage) {
section->currentPage = newPage;
changed = true;
}
}
cachedChapterTotalPageCount = 0; // consumed; don't read cached progress again
return changed;
}
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
@@ -1027,7 +1241,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const auto tPrewarm = millis();
const bool pageHasImages = page->hasImages();
const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
const bool needsTextGrayscale = SETTINGS.textAntiAliasing == CrossPointSettings::TEXT_AA_FULL;
const bool fastTextAA = SETTINGS.textAntiAliasing == CrossPointSettings::TEXT_AA_FAST;
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
auto renderGrayscalePass = [&]() {
if (needsTextGrayscale) {
@@ -1037,7 +1252,11 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
}
};
// Fast AA dithers 2-bit glyph edges in the BW pass itself, no grayscale
// refresh. Scoped to the page render so nothing else inherits the flag.
renderer.setFastAntiAliasing(fastTextAA);
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
renderer.setFastAntiAliasing(false);
renderStatusBar();
const auto tBwRender = millis();
@@ -1054,7 +1273,9 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// 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 %)
renderer.setFastAntiAliasing(fastTextAA);
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
renderer.setFastAntiAliasing(false);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
} else {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
@@ -1180,9 +1401,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
}
void EpubReaderActivity::renderStatusBar() const {
// Calculate progress in book
// Calculate progress in book. Use the estimated total while a giant spine is still building so
// "page X of Y" and the progress bar don't read off the small build watermark.
const int currentPage = section->currentPage + 1;
const float pageCount = section->pageCount;
const float pageCount = section->estimatedTotalPages();
const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0;
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
@@ -1213,7 +1435,8 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle();
}
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked);
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
section->isBuilding());
}
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
@@ -1304,7 +1527,7 @@ void EpubReaderActivity::addBookmark() {
int pageCount;
{
RenderLock lock(*this);
pageCount = section->pageCount;
pageCount = section->estimatedTotalPages();
currentPage = section->currentPage;
}
@@ -1353,10 +1576,10 @@ void EpubReaderActivity::updateBookmarkFlag() {
currentPageBookmarked = false;
return;
}
const ProgressRange pageRange =
getPageProgressRange(epub, currentSpineIndex, section->currentPage, section->pageCount);
const int pageCount = section->estimatedTotalPages();
const ProgressRange pageRange = getPageProgressRange(epub, currentSpineIndex, section->currentPage, pageCount);
currentPageBookmarked = std::any_of(cachedBookmarks.begin(), cachedBookmarks.end(), [&](const BookmarkEntry& b) {
return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, section->pageCount, pageRange);
return bookmarkMatchesProgress(b, currentSpineIndex, section->currentPage, pageCount, pageRange);
});
}
@@ -1369,9 +1592,9 @@ ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const {
}
if (section) {
info.currentPage = section->currentPage + 1;
info.totalPages = section->pageCount;
if (epub && epub->getBookSize() > 0 && section->pageCount > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
info.totalPages = section->estimatedTotalPages();
if (epub && epub->getBookSize() > 0 && info.totalPages > 0) {
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(info.totalPages);
int pct = static_cast<int>(epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f + 0.5f);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
@@ -1383,7 +1606,7 @@ ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const {
CrossPointPosition EpubReaderActivity::getCurrentPosition() const {
const int currentPage = section ? section->currentPage : nextPageNumber;
const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount;
const int totalPages = section ? section->estimatedTotalPages() : cachedChapterTotalPageCount;
std::optional<uint16_t> paragraphIndex;
if (section && currentPage >= 0 && currentPage < section->pageCount) {
const uint16_t paragraphPage =
+30 -1
View File
@@ -6,6 +6,7 @@
#include <optional>
#include "BookmarkEntry.h"
#include "EndOfBookOptions.h"
#include "EpubReaderMenuActivity.h"
#include "ProgressMapper.h"
#include "activities/Activity.h"
@@ -45,6 +46,8 @@ class EpubReaderActivity final : public Activity {
// Set when the reader is left at end-of-book and SETTINGS.moveFinishedToReadFolder is on.
// Consumed in onExit() to relocate the finished book into /Read/.
bool pendingReadFolderMove = false;
// Next-book suggestion menu for the End-of-Book screen
EndOfBookOptions endOfBookOptions;
// Footnote support
std::vector<FootnoteEntry> currentPageFootnotes;
@@ -59,11 +62,37 @@ class EpubReaderActivity final : public Activity {
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar() const;
void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight);
// Pages laid out per incremental-build pump: on the render path (catching up to the page
// being shown) and per loop() tick (background build of a large chapter). Kept small so a
// background build chunk never noticeably delays input or a pending render.
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
// How many pages to keep laid out ahead of the reader for a still-building section. A page
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
// -- a tiny buffer is enough. The background build stops once the watermark is this far
// ahead and resumes as the reader advances; building unbounded instead locked up input by
// monopolizing the RenderLock. A giant single-spine book therefore never finalizes its .bin
// in one sitting -- instant reopen comes from Section::suspendBuild() persisting the pages
// already laid out as a partial file on exit/sleep.
static constexpr int BUILD_WINDOW_AHEAD = 5;
// Show the indexing popup when an initial build must lay out more than this many pages up front
// (a deep resume/jump into a not-yet-built section), so it isn't a silent wait. Kept independent
// of the small look-ahead window so ordinary landings stay popup-free.
static constexpr int BUILD_POPUP_PAGE_THRESHOLD = 20;
// Also show the popup when first building a spine larger than this (uncompressed bytes): its
// whole HTML must be inflated before page 1 can lay out (the giant single-spine case), which is
// a multi-second wait. Normal chapters are well under this and stay popup-free.
static constexpr size_t BUILD_POPUP_BYTE_THRESHOLD = 96 * 1024;
// Remap the cached relative reading position once the section's real page count is known
// (used after a settings change re-paginates a chapter). Returns true if currentPage moved.
// No-op while the section is still building or when the pagination is unchanged (plain resume).
bool applyDeferredReposition();
bool saveProgress(int spineIndex, int currentPage, int pageCount);
// Jump to a percentage of the book (0-100), mapping it to spine and page.
void jumpToPercent(int percent);
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
// Opens the reader menu for the current position (short-press Confirm)
void openReaderMenu();
// Returns true if sync acted (launched, or surfaced a save error); false if it was a no-op
// because no KOReader credentials are stored.
bool launchKOReaderSync();
@@ -9,7 +9,6 @@
#include <algorithm>
#include "MappedInputManager.h"
#include "ProgressMapper.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -108,8 +107,17 @@ void EpubReaderBookmarksActivity::loop() {
return;
}
auto bookmark = bookmarks.at(selectorIndex);
CrossPointPosition pos = ProgressMapper::toCrossPoint(epub, {bookmark.xpath, bookmark.percentage}, renderer);
setResult(ProgressChangeResult{pos.spineIndex, pos.pageNumber});
ProgressChangeResult result{};
result.xpath = bookmark.xpath;
result.percentage = bookmark.percentage;
result.hasSavedProgress = true;
if (bookmark.computedChapterPageCount > 0 && bookmark.computedChapterProgress < bookmark.computedChapterPageCount &&
bookmark.computedSpineIndex < epub->getSpineItemsCount()) {
result.spineIndex = bookmark.computedSpineIndex;
result.page = bookmark.computedChapterProgress;
result.totalPages = bookmark.computedChapterPageCount;
}
setResult(std::move(result));
finish();
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
@@ -50,6 +50,8 @@ void EpubReaderMenuActivity::onEnter() {
void EpubReaderMenuActivity::onExit() { Activity::onExit(); }
void EpubReaderMenuActivity::loop() {
if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return;
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
@@ -64,14 +66,21 @@ void EpubReaderMenuActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto selectedAction = menuItems[selectedIndex].action;
if (selectedAction == MenuAction::ROTATE_SCREEN) {
// Cycle orientation preview locally; actual rotation happens on menu exit.
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
optionPopup.show(StrId::STR_ORIENTATION, orientationLabels.data(), static_cast<int>(orientationLabels.size()),
pendingOrientation, [this](int idx) {
pendingOrientation = idx;
requestUpdate();
});
requestUpdate();
return;
}
if (selectedAction == MenuAction::AUTO_PAGE_TURN) {
selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size();
optionPopup.show(I18N.get(StrId::STR_AUTO_TURN_PAGES_PER_MIN), pageTurnLabels.data(),
static_cast<int>(pageTurnLabels.size()), selectedPageTurnOption, [this](int idx) {
selectedPageTurnOption = idx;
requestUpdate();
});
requestUpdate();
return;
}
@@ -90,6 +99,8 @@ void EpubReaderMenuActivity::loop() {
}
void EpubReaderMenuActivity::render(RenderLock&&) {
if (optionPopup.processRender(renderer, mappedInput)) return;
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
@@ -6,6 +6,7 @@
#include <vector>
#include "activities/Activity.h"
#include "components/OptionPopup.h"
#include "util/ButtonNavigator.h"
class EpubReaderMenuActivity final : public Activity {
@@ -49,6 +50,7 @@ class EpubReaderMenuActivity final : public Activity {
int selectedIndex = 0;
ButtonNavigator buttonNavigator;
OptionPopup optionPopup;
std::string title = "Reader Menu";
uint8_t pendingOrientation = 0;
uint8_t selectedPageTurnOption = 0;
@@ -1,8 +1,11 @@
#include "EpubReaderPercentSelectionActivity.h"
#include <GfxRenderer.h>
#include <HalGPIO.h>
#include <I18n.h>
#include <cstdio>
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -51,8 +54,14 @@ void EpubReaderPercentSelectionActivity::loop() {
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
// On X3 the side buttons sit on the left/right edges of the screen rather than as a vertical up/down
// rocker (X4), so BTN_UP is physically the left button and BTN_DOWN the right one. Flip the large-step
// direction there so the left button decreases and the right button increases, matching the layout.
const int upDelta = gpio.deviceIsX3() ? -kLargeStep : kLargeStep;
const int downDelta = gpio.deviceIsX3() ? kLargeStep : -kLargeStep;
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this, upDelta] { adjustPercent(upDelta); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down},
[this, downDelta] { adjustPercent(downDelta); });
}
void EpubReaderPercentSelectionActivity::render(RenderLock&&) {
@@ -89,8 +98,13 @@ void EpubReaderPercentSelectionActivity::render(RenderLock&&) {
const int knobX = barX + 2 + fillWidth - 2;
renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true);
// Hint text for step sizes.
UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 30, tr(STR_PERCENT_STEP_HINT), true);
// Two-line step hint built from separate label + value strings (front buttons = fine step, side
// buttons = coarse step), so the layout doesn't depend on a separator hidden in translated text.
char line[64];
snprintf(line, sizeof(line), "%s %d%%", I18N.get(StrId::STR_STEP_HINT_FRONT), kSmallStep);
UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 30, line, true);
snprintf(line, sizeof(line), "%s %d%%", I18N.get(StrId::STR_STEP_HINT_SIDE), kLargeStep);
UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 52, line, true);
// Button hints follow the current front button layout.
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+");
+8
View File
@@ -2,6 +2,7 @@
#include <FsHelpers.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Memory.h>
#include "CrossPointSettings.h"
@@ -14,6 +15,7 @@
#include "XtcReaderActivity.h"
#include "activities/util/BmpViewerActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "components/UITheme.h"
bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasXtcExtension(path); }
@@ -35,6 +37,12 @@ std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
LOG_ERR("READER", "Failed to allocate EPUB object");
return nullptr;
}
// First open: building the spine/TOC index (book.bin) takes a couple of seconds. Show the
// indexing popup so it isn't a silent wait on the home screen. The cachePath/hash is known at
// construction, so this check is valid before load(); a cached open loads in a blink -> no popup.
if (!Storage.exists((epub->getCachePath() + "/book.bin").c_str())) {
GUI.drawPopup(renderer, tr(STR_INDEXING));
}
if (epub->load(true, SETTINGS.embeddedStyle == 0)) {
return epub;
}
+2 -1
View File
@@ -11,7 +11,8 @@ class Txt;
class ReaderActivity final : public Activity {
std::string initialBookPath;
std::string currentBookPath; // Track current book path for navigation
static std::unique_ptr<Epub> loadEpub(const std::string& path);
// Non-static (unlike the other loaders): draws the first-open indexing popup, which needs the renderer.
std::unique_ptr<Epub> loadEpub(const std::string& path);
static std::unique_ptr<Xtc> loadXtc(const std::string& path);
static std::unique_ptr<Txt> loadTxt(const std::string& path);
static bool isXtcFile(const std::string& path);
+5 -2
View File
@@ -400,13 +400,16 @@ void TxtReaderActivity::renderPage() {
renderLines(); // scan pass — text accumulated, no drawing
scope.endScanAndPrewarm();
// BW rendering
// BW rendering. Fast AA dithers 2-bit glyph edges in this single pass;
// scoped to the page text so nothing else inherits the flag.
renderer.setFastAntiAliasing(SETTINGS.textAntiAliasing == CrossPointSettings::TEXT_AA_FAST);
renderLines();
renderer.setFastAntiAliasing(false);
renderStatusBar();
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
if (SETTINGS.textAntiAliasing) {
if (SETTINGS.textAntiAliasing == CrossPointSettings::TEXT_AA_FULL) {
ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); });
}
// scope destructor clears font cache via FontCacheManager
+54 -12
View File
@@ -53,18 +53,52 @@ void XtcReaderActivity::onExit() {
xtc.reset();
}
void XtcReaderActivity::openChapterSelection() {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
startActivityForResult(std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
currentPage = std::get<PageResult>(result.data).page;
}
});
}
}
void XtcReaderActivity::loop() {
if (!xtc) {
return;
}
const bool atEndOfBook = currentPage >= xtc->getPageCount();
// While the end screen suggestion menu is showing it owns Confirm/Back/navigation
// input. Anything it doesn't handle (e.g. long-press Back to the file browser) falls
// through to the regular handlers below; page turns are absorbed by the end-of-book
// block.
if (atEndOfBook && endOfBookOptions.menuActive()) {
std::string openPath;
switch (endOfBookOptions.handleMenuInput(mappedInput, &openPath)) {
case EndOfBookOptions::Action::OpenBook:
activityManager.goToReader(openPath);
return;
case EndOfBookOptions::Action::GoHome:
onGoHome();
return;
case EndOfBookOptions::Action::LastPage:
currentPage = xtc->getPageCount() > 0 ? xtc->getPageCount() - 1 : 0;
requestUpdate();
return;
case EndOfBookOptions::Action::Redraw:
requestUpdate();
return;
case EndOfBookOptions::Action::None:
break;
}
}
// Enter chapter selection activity
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
startActivityForResult(
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
currentPage = std::get<PageResult>(result.data).page;
}
});
}
openChapterSelection();
}
// Long press BACK (1s+) goes to file selection
@@ -85,8 +119,14 @@ void XtcReaderActivity::loop() {
return;
}
// At end of the book, forward button goes home and back button returns to last page
// At end of the book with no suggestion menu, forward button goes home and back
// button returns to last page
if (currentPage >= xtc->getPageCount()) {
if (endOfBookOptions.menuActive()) {
// Selection movement was handled above; absorb leftover page-turn triggers so
// e.g. "previous" at the top of the list doesn't jump back into the book
return;
}
if (nextTriggered) {
onGoHome();
} else {
@@ -123,9 +163,11 @@ void XtcReaderActivity::render(RenderLock&&) {
// Bounds check
if (currentPage >= xtc->getPageCount()) {
// Show end of book screen
// Show end of book screen. Sole load site: runs on the render task (serialized by
// RenderLock); the main task only reads the suggestions once the flag is published.
endOfBookOptions.loadOnce(xtc->getPath());
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
endOfBookOptions.render(renderer, mappedInput);
renderer.displayBuffer();
return;
}
@@ -12,6 +12,7 @@
#include <string>
#include <utility>
#include "EndOfBookOptions.h"
#include "activities/Activity.h"
class XtcReaderActivity final : public Activity {
@@ -19,6 +20,8 @@ class XtcReaderActivity final : public Activity {
uint32_t currentPage = 0;
int pagesUntilFullRefresh = 0;
// Next-book suggestion menu for the End-of-Book screen
EndOfBookOptions endOfBookOptions;
enum class StatusBarOverlayPosition { Bottom, Top };
struct StatusBarInfo {
@@ -28,6 +31,8 @@ class XtcReaderActivity final : public Activity {
};
void renderPage();
// Opens chapter selection when the book has chapters (short-press Confirm); no-op otherwise
void openChapterSelection();
void renderStatusBarOverlay(StatusBarOverlayPosition position) const;
StatusBarInfo getStatusBarInfo() const;
void saveProgress() const;
+37 -4
View File
@@ -113,6 +113,8 @@ void SettingsActivity::onExit() {
}
void SettingsActivity::loop() {
if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return;
bool hasChangedCategory = false;
// Handle actions with early return
@@ -203,6 +205,18 @@ void SettingsActivity::toggleCurrentSetting() {
SETTINGS.*(setting.valuePtr) = !currentValue;
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
if (setting.enumValues.size() > 2) {
const auto valuePtr = setting.valuePtr;
optionPopup.show(setting.nameId, setting.enumValues.data(), static_cast<int>(setting.enumValues.size()),
currentValue, [this, valuePtr, sleepScreenChanged, quickResumeTimeoutChanged](int idx) {
SETTINGS.*valuePtr = idx;
syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged);
SETTINGS.saveToFile();
rebuildSettingsLists();
});
requestUpdate();
return;
}
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
} else if (setting.type == SettingType::ENUM && setting.valueGetter && setting.valueSetter) {
if (setting.nameId == StrId::STR_FONT_FAMILY) {
@@ -218,6 +232,23 @@ void SettingsActivity::toggleCurrentSetting() {
? static_cast<uint8_t>(setting.enumValues.size())
: static_cast<uint8_t>(setting.enumStringValues.size());
const uint8_t cur = setting.valueGetter();
if (totalValues > 2) {
const auto valueSetter = setting.valueSetter;
auto onSelect = [this, valueSetter, sleepScreenChanged, quickResumeTimeoutChanged](int idx) {
valueSetter(idx);
syncQuickResumeTimeoutForSleepScreen(sleepScreenChanged, quickResumeTimeoutChanged);
SETTINGS.saveToFile();
rebuildSettingsLists();
};
if (!setting.enumStringValues.empty()) {
optionPopup.show(setting.nameId, setting.enumStringValues, cur, std::move(onSelect));
} else {
optionPopup.show(setting.nameId, setting.enumValues.data(), static_cast<int>(setting.enumValues.size()), cur,
std::move(onSelect));
}
requestUpdate();
return;
}
setting.valueSetter((cur + 1) % totalValues);
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
@@ -305,10 +336,9 @@ void SettingsActivity::syncQuickResumeTimeoutForSleepScreen(bool sleepScreenChan
void SettingsActivity::openSleepTimeoutPicker() {
startActivityForResult(
std::make_unique<IntervalSelectionActivity>(
renderer, mappedInput, "SleepTimeoutInterval", StrId::STR_TIME_TO_SLEEP, StrId::STR_SLEEP_TIMER_STEP_HINT,
SETTINGS.sleepTimeoutMinutes, CrossPointSettings::MIN_SLEEP_TIMEOUT_MINUTES,
CrossPointSettings::MAX_SLEEP_TIMEOUT_MINUTES, 1, 5, StrId::STR_SLEEP_TIMER_VALUE_FORMAT, false, true,
StrId::STR_SLEEP_NEVER),
renderer, mappedInput, "SleepTimeoutInterval", StrId::STR_TIME_TO_SLEEP, SETTINGS.sleepTimeoutMinutes,
CrossPointSettings::MIN_SLEEP_TIMEOUT_MINUTES, CrossPointSettings::MAX_SLEEP_TIMEOUT_MINUTES, 1, 5,
StrId::STR_SLEEP_TIMER_VALUE_FORMAT, false, true, StrId::STR_SLEEP_NEVER),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
SETTINGS.sleepTimeoutMinutes = static_cast<uint8_t>(std::get<IntervalResult>(result.data).value);
@@ -319,6 +349,8 @@ void SettingsActivity::openSleepTimeoutPicker() {
}
void SettingsActivity::render(RenderLock&&) {
if (optionPopup.processRender(renderer, mappedInput)) return;
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
@@ -386,6 +418,7 @@ void SettingsActivity::render(RenderLock&&) {
: (selectedSettingIndex > 0 && (*currentSettings)[selectedSettingIndex - 1].nameId == StrId::STR_TIME_TO_SLEEP
? tr(STR_SELECT)
: tr(STR_TOGGLE));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -7,6 +7,7 @@
#include "CrossPointSettings.h"
#include "activities/Activity.h"
#include "components/OptionPopup.h"
#include "util/ButtonNavigator.h"
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING };
@@ -159,6 +160,8 @@ class SettingsActivity final : public Activity {
bool preserveQuickResumeTimeoutOn = false;
bool quickResumeTimeoutAutoEnabled = false;
OptionPopup optionPopup;
static constexpr int categoryCount = 4;
static const StrId categoryNames[categoryCount];
@@ -125,6 +125,8 @@ void StatusBarSettingsActivity::onEnter() {
void StatusBarSettingsActivity::onExit() { Activity::onExit(); }
void StatusBarSettingsActivity::loop() {
if (optionPopup.handleInput(mappedInput, [this] { requestUpdate(); })) return;
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
@@ -167,21 +169,35 @@ void StatusBarSettingsActivity::handleSelection() {
SETTINGS.statusBarBookProgressPercentage = (SETTINGS.statusBarBookProgressPercentage + 1) % 2;
break;
case ITEM_PROGRESS_BAR:
SETTINGS.statusBarProgressBar = (SETTINGS.statusBarProgressBar + 1) % PROGRESS_BAR_ITEMS;
break;
optionPopup.show(StrId::STR_PROGRESS_BAR, progressBarNames, PROGRESS_BAR_ITEMS, SETTINGS.statusBarProgressBar,
[this](int idx) {
SETTINGS.statusBarProgressBar = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_PROGRESS_BAR_THICKNESS:
SETTINGS.statusBarProgressBarThickness =
(SETTINGS.statusBarProgressBarThickness + 1) % PROGRESS_BAR_THICKNESS_ITEMS;
break;
optionPopup.show(StrId::STR_PROGRESS_BAR_THICKNESS, progressBarThicknessNames, PROGRESS_BAR_THICKNESS_ITEMS,
SETTINGS.statusBarProgressBarThickness, [this](int idx) {
SETTINGS.statusBarProgressBarThickness = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_TITLE:
SETTINGS.statusBarTitle = (SETTINGS.statusBarTitle + 1) % TITLE_ITEMS;
break;
optionPopup.show(StrId::STR_TITLE, titleNames, TITLE_ITEMS, SETTINGS.statusBarTitle, [this](int idx) {
SETTINGS.statusBarTitle = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_BATTERY:
SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2;
break;
case ITEM_XTC_STATUS_BAR:
SETTINGS.xtcStatusBarMode = (SETTINGS.xtcStatusBarMode + 1) % XTC_STATUS_BAR_ITEMS;
break;
optionPopup.show(StrId::STR_XTC_STATUS_BAR, xtcStatusBarNames, XTC_STATUS_BAR_ITEMS, SETTINGS.xtcStatusBarMode,
[this](int idx) {
SETTINGS.xtcStatusBarMode = idx;
SETTINGS.saveToFile();
});
return;
case ITEM_CLOCK:
SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % STATUS_BAR_CLOCK_ITEMS;
break;
@@ -202,6 +218,8 @@ void StatusBarSettingsActivity::handleSelection() {
}
void StatusBarSettingsActivity::render(RenderLock&&) {
if (optionPopup.processRender(renderer, mappedInput)) return;
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
@@ -4,6 +4,7 @@
#include <freertos/task.h>
#include "activities/Activity.h"
#include "components/OptionPopup.h"
#include "util/ButtonNavigator.h"
// Reader status bar configuration activity
@@ -19,6 +20,7 @@ class StatusBarSettingsActivity final : public Activity {
private:
ButtonNavigator buttonNavigator;
OptionPopup optionPopup;
int selectedIndex = 0;
// Decided in onEnter() based on halClock.isAvailable() so clock entries are hidden on X4.
@@ -1,6 +1,7 @@
#include "IntervalSelectionActivity.h"
#include <GfxRenderer.h>
#include <HalGPIO.h>
#include <I18n.h>
#include <algorithm>
@@ -25,6 +26,18 @@ void IntervalSelectionActivity::adjustValue(const int delta) {
requestUpdate();
}
void IntervalSelectionActivity::drawStepHintLine(const int y, const StrId labelId, const int step) {
char stepText[24];
if (valueFormatId != StrId::STR_NONE_OPT) {
snprintf(stepText, sizeof(stepText), I18N.get(valueFormatId), static_cast<unsigned int>(step));
} else {
snprintf(stepText, sizeof(stepText), "%d", step);
}
char line[64];
snprintf(line, sizeof(line), "%s %s", I18N.get(labelId), stepText);
renderer.drawCenteredText(SMALL_FONT_ID, y, line, true);
}
void IntervalSelectionActivity::loop() {
if (ignoreConfirmRelease) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
@@ -52,8 +65,15 @@ void IntervalSelectionActivity::loop() {
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustValue(-smallStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustValue(smallStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustValue(largeStep); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustValue(-largeStep); });
// On X3 the side buttons sit on the left/right edges of the screen rather than as a vertical up/down
// rocker (X4), so BTN_UP is physically the left button and BTN_DOWN the right one. Flip the large-step
// direction there so the left button decreases and the right button increases, matching the layout.
const int upDelta = gpio.deviceIsX3() ? -largeStep : largeStep;
const int downDelta = gpio.deviceIsX3() ? largeStep : -largeStep;
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this, upDelta] { adjustValue(upDelta); });
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down},
[this, downDelta] { adjustValue(downDelta); });
}
void IntervalSelectionActivity::render(RenderLock&&) {
@@ -88,7 +108,11 @@ void IntervalSelectionActivity::render(RenderLock&&) {
const int knobX = std::max(barX + 2, barX + 2 + fillWidth - 2);
renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true);
renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, I18N.get(stepHintId), true);
// Two-line step hint: front buttons do the small step, side buttons the large step. Built from
// separate label + value strings (rather than splitting one localized sentence) so the layout
// doesn't depend on translators preserving a hidden separator.
drawStepHintLine(barY + 30, StrId::STR_STEP_HINT_FRONT, smallStep);
drawStepHintLine(barY + 52, StrId::STR_STEP_HINT_SIDE, largeStep);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -11,13 +11,12 @@ class GfxRenderer;
class IntervalSelectionActivity final : public Activity {
public:
explicit IntervalSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const char* activityName,
StrId titleId, StrId stepHintId, int initialValue, int minValue, int maxValue,
int smallStep, int largeStep, StrId valueFormatId = StrId::STR_NONE_OPT,
StrId titleId, int initialValue, int minValue, int maxValue, int smallStep,
int largeStep, StrId valueFormatId = StrId::STR_NONE_OPT,
bool readerActivity = false, bool ignoreInitialConfirmRelease = false,
StrId maxBoundaryLabelId = StrId::STR_NONE_OPT)
: Activity(activityName, renderer, mappedInput),
titleId(titleId),
stepHintId(stepHintId),
valueFormatId(valueFormatId),
maxBoundaryLabelId(maxBoundaryLabelId),
value(initialValue),
@@ -35,7 +34,6 @@ class IntervalSelectionActivity final : public Activity {
private:
StrId titleId;
StrId stepHintId;
StrId valueFormatId;
StrId maxBoundaryLabelId;
int value;
@@ -49,4 +47,5 @@ class IntervalSelectionActivity final : public Activity {
void adjustValue(int delta);
int clampedValue(int candidate) const;
void drawStepHintLine(int y, StrId labelId, int step);
};
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include <I18n.h>
#include <functional>
#include <string>
#include <vector>
#include "GfxRenderer.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
class OptionPopup {
public:
void show(StrId titleId, const StrId* optionIds, int optionCount, int currentIndex,
std::function<void(int)> onSelect) {
title = I18N.get(titleId);
ownedStrings.resize(optionCount);
for (int i = 0; i < optionCount; i++) {
ownedStrings[i] = I18N.get(optionIds[i]);
}
selectedIndex = currentIndex;
onSelectCallback = std::move(onSelect);
active = true;
}
void show(const char* titleStr, const char* const* options, int optionCount, int currentIndex,
std::function<void(int)> onSelect) {
title = titleStr;
ownedStrings.resize(optionCount);
for (int i = 0; i < optionCount; i++) {
ownedStrings[i] = options[i];
}
selectedIndex = currentIndex;
onSelectCallback = std::move(onSelect);
active = true;
}
void show(StrId titleId, const std::vector<std::string>& options, int currentIndex,
std::function<void(int)> onSelect) {
title = I18N.get(titleId);
ownedStrings = options;
selectedIndex = currentIndex;
onSelectCallback = std::move(onSelect);
active = true;
}
bool handleInput(MappedInputManager& input, const std::function<void()>& requestUpdate) {
if (!active) return false;
const int count = static_cast<int>(ownedStrings.size());
if (input.wasPressed(MappedInputManager::Button::Up) || input.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex - 1 + count) % count;
requestUpdate();
return true;
} else if (input.wasPressed(MappedInputManager::Button::Down) ||
input.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % count;
requestUpdate();
return true;
} else if (input.wasPressed(MappedInputManager::Button::Confirm)) {
active = false;
if (onSelectCallback) onSelectCallback(selectedIndex);
requestUpdate();
return true;
} else if (input.wasPressed(MappedInputManager::Button::Back)) {
active = false;
requestUpdate();
return true;
}
return true;
}
bool processRender(GfxRenderer& renderer, const MappedInputManager& input) const {
if (!active) return false;
const auto popupLabels = input.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, popupLabels.btn1, popupLabels.btn2, popupLabels.btn3, popupLabels.btn4);
render(renderer);
renderer.displayBuffer();
return true;
}
void render(const GfxRenderer& renderer) const {
if (!active) return;
GUI.drawOptionPopup(renderer, title.c_str(), ownedStrings, selectedIndex);
}
bool isActive() const { return active; }
private:
bool active = false;
std::string title;
std::vector<std::string> ownedStrings;
int selectedIndex = 0;
std::function<void(int)> onSelectCallback;
};
+103 -3
View File
@@ -749,7 +749,7 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou
void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
const int pageCount, std::string title, const int paddingBottom, const int textYOffset,
const bool fillMargin, const bool isPageBookmarked) const {
const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated) const {
auto metrics = UITheme::getInstance().getMetrics();
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
@@ -769,12 +769,16 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
// Right aligned text for progress counter
char progressStr[32];
// Prefix the page count with "~" while a still-building spine only yields an estimated total.
const char* estimatePrefix = pageCountEstimated ? "~" : "";
if (SETTINGS.statusBarBookProgressPercentage && SETTINGS.statusBarChapterPageCount) {
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage, pageCount, bookProgress);
snprintf(progressStr, sizeof(progressStr), "%s%d/%d %.0f%%", estimatePrefix, currentPage, pageCount,
bookProgress);
} else if (SETTINGS.statusBarBookProgressPercentage) {
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
} else {
snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage, pageCount);
snprintf(progressStr, sizeof(progressStr), "%s%d/%d", estimatePrefix, currentPage, pageCount);
}
int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
@@ -1005,3 +1009,99 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch
rect.y + metrics.keyboardSecondaryLabelTopPadding, secondaryLabel, !invert);
}
}
void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, const std::vector<std::string>& options,
int selectedIndex) const {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
const int optionFontId = metrics.optionPopupUseSmallFont ? UI_10_FONT_ID : UI_12_FONT_ID;
const EpdFontFamily::Style optionStyle =
metrics.optionPopupOptionFontBold ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR;
const int itemSpacing = metrics.optionPopupItemSpacing;
const int innerPadding = metrics.optionPopupInnerPadding;
const int selectionHPadding = metrics.optionPopupSelectionHPadding;
const int selectionVPadding = metrics.optionPopupSelectionVPadding;
const int optionLineHeight = renderer.getLineHeight(optionFontId);
const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int rowHeight = optionLineHeight + selectionVPadding * 2;
int maxTextWidth = renderer.getTextWidth(UI_12_FONT_ID, title, EpdFontFamily::BOLD);
for (const auto& opt : options) {
int w = renderer.getTextWidth(optionFontId, opt.c_str(), optionStyle);
if (w > maxTextWidth) maxTextWidth = w;
}
const int optionCount = static_cast<int>(options.size());
const int listHeight = rowHeight * optionCount + itemSpacing * (optionCount - 1);
const int dialogW = std::min((maxTextWidth + innerPadding * 2 + selectionHPadding * 2) * 12 / 10,
pageWidth - metrics.optionPopupDialogSideMargin * 2);
const int contentHeight = titleLineHeight + metrics.optionPopupTitleGap + listHeight;
const int dialogH = contentHeight + innerPadding * 2;
const int dialogX = (pageWidth - dialogW) / 2;
const int dialogY = (pageHeight - dialogH) / 2;
const int frameThickness = metrics.popupFrameThickness;
const int frameRadius = metrics.popupCornerRadius;
if (frameRadius > 0) {
renderer.fillRoundedRect(dialogX - frameThickness, dialogY - frameThickness, dialogW + frameThickness * 2,
dialogH + frameThickness * 2, frameRadius + frameThickness, Color::White);
renderer.fillRoundedRect(dialogX, dialogY, dialogW, dialogH, frameRadius, Color::Black);
renderer.fillRoundedRect(dialogX + frameThickness, dialogY + frameThickness, dialogW - frameThickness * 2,
dialogH - frameThickness * 2,
frameRadius - frameThickness > 0 ? frameRadius - frameThickness : 0, Color::White);
} else {
renderer.fillRect(dialogX - frameThickness, dialogY - frameThickness, dialogW + frameThickness * 2,
dialogH + frameThickness * 2, true);
renderer.fillRect(dialogX, dialogY, dialogW, dialogH, false);
}
int y = dialogY + innerPadding;
renderer.drawCenteredText(UI_12_FONT_ID, y, title, true, EpdFontFamily::BOLD);
y += titleLineHeight;
if (metrics.optionPopupTitleSeparator) {
const int sepY = y + metrics.optionPopupTitleGap / 2;
renderer.drawLine(dialogX + innerPadding, sepY, dialogX + dialogW - innerPadding, sepY, true);
}
y += metrics.optionPopupTitleGap;
const int itemRectX = dialogX + innerPadding;
const int itemRectW = dialogW - innerPadding * 2;
const int selectionRadius = metrics.optionPopupSelectionRadius;
for (int i = 0; i < optionCount; i++) {
const int itemY = y + i * (rowHeight + itemSpacing);
const bool selected = (i == selectedIndex);
const char* labelText = options[i].c_str();
if (metrics.optionPopupDrawAllRows || selected) {
Color rowColor;
if (selected) {
rowColor = metrics.optionPopupSelectionLight ? Color::LightGray : Color::Black;
} else {
rowColor = Color::White;
}
if (selectionRadius > 0) {
renderer.fillRoundedRect(itemRectX, itemY, itemRectW, rowHeight, selectionRadius, rowColor);
} else {
renderer.fillRect(itemRectX, itemY, itemRectW, rowHeight, rowColor == Color::Black);
}
}
const int textW = renderer.getTextWidth(optionFontId, labelText, optionStyle);
const int textY = itemY + (rowHeight - optionLineHeight) / 2;
const int textX = itemRectX + (itemRectW - textW) / 2;
// Unselected items: text is dark (invert=true means draw on white bg).
// Selected on dark bg: text must be white (invert=false).
// Selected on light bg: text stays dark (invert=true).
const bool invertText = selected ? metrics.optionPopupSelectionLight : true;
renderer.drawText(optionFontId, textX, textY, labelText, invertText, optionStyle);
}
}
+29 -1
View File
@@ -94,6 +94,19 @@ struct ThemeMetrics {
bool popupProgressFillInverted;
bool popupProgressOutlineInverted;
int optionPopupItemSpacing;
int optionPopupInnerPadding;
int optionPopupSelectionHPadding;
int optionPopupSelectionVPadding;
int optionPopupTitleGap;
bool optionPopupUseSmallFont;
bool optionPopupOptionFontBold;
int optionPopupSelectionRadius;
bool optionPopupSelectionLight;
bool optionPopupDrawAllRows;
int optionPopupDialogSideMargin;
bool optionPopupTitleSeparator;
int textFieldHorizontalPadding;
int textFieldNormalThickness;
int textFieldCursorThickness;
@@ -167,6 +180,18 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
.popupProgressClampPercent = false,
.popupProgressFillInverted = true,
.popupProgressOutlineInverted = true,
.optionPopupItemSpacing = 6,
.optionPopupInnerPadding = 16,
.optionPopupSelectionHPadding = 8,
.optionPopupSelectionVPadding = 4,
.optionPopupTitleGap = 10,
.optionPopupUseSmallFont = true,
.optionPopupOptionFontBold = true,
.optionPopupSelectionRadius = 0,
.optionPopupSelectionLight = false,
.optionPopupDrawAllRows = false,
.optionPopupDialogSideMargin = 20,
.optionPopupTitleSeparator = true,
.textFieldHorizontalPadding = 6,
.textFieldNormalThickness = 1,
.textFieldCursorThickness = 3,
@@ -207,10 +232,13 @@ class BaseTheme {
const std::function<std::string(int index)>& buttonLabel,
const std::function<UIIcon(int index)>& rowIcon) const;
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const;
virtual void drawOptionPopup(const GfxRenderer& renderer, const char* title, const std::vector<std::string>& options,
int selectedIndex) 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,
std::string title, const int paddingBottom = 0, const int textYOffset = 0,
const bool fillMargin = true, const bool isPageBookmarked = false) const;
const bool fillMargin = true, const bool isPageBookmarked = false,
const bool pageCountEstimated = false) 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,
int contentStartX = 0, int contentWidth = 0) const;
@@ -72,7 +72,7 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
tileY + hPaddingInSelection + (Lyra3CoversMetrics::values.homeCoverHeight / 3),
tileWidth - 2 * hPaddingInSelection, 2 * Lyra3CoversMetrics::values.homeCoverHeight / 3,
true);
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32);
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32);
}
}
+3 -3
View File
@@ -288,7 +288,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
const uint8_t* iconBitmap = iconForName(icon, iconSize);
if (iconBitmap != nullptr) {
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
itemY + iconY, iconSize, iconSize);
itemY + iconY, iconSize);
}
}
@@ -454,7 +454,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
renderer.fillRect(tileX + hPaddingInSelection,
tileY + hPaddingInSelection + (LyraMetrics::values.homeCoverHeight / 3), coverWidth,
2 * LyraMetrics::values.homeCoverHeight / 3, true);
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32);
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32);
}
coverBufferStored = storeCoverBuffer();
@@ -534,7 +534,7 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount
UIIcon icon = rowIcon(i);
const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize);
if (iconBitmap != nullptr) {
renderer.drawIcon(iconBitmap, textX, textY + 3, mainMenuIconSize, mainMenuIconSize);
renderer.drawIcon(iconBitmap, textX, textY, mainMenuIconSize);
textX += mainMenuIconSize + hPaddingInSelection + 2;
}
}
+12
View File
@@ -65,6 +65,18 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
.popupProgressClampPercent = false,
.popupProgressFillInverted = false,
.popupProgressOutlineInverted = false,
.optionPopupItemSpacing = 8,
.optionPopupInnerPadding = 20,
.optionPopupSelectionHPadding = 16,
.optionPopupSelectionVPadding = 12,
.optionPopupTitleGap = 16,
.optionPopupUseSmallFont = true,
.optionPopupOptionFontBold = false,
.optionPopupSelectionRadius = 6,
.optionPopupSelectionLight = true,
.optionPopupDrawAllRows = false,
.optionPopupDialogSideMargin = 20,
.optionPopupTitleSeparator = true,
.textFieldHorizontalPadding = 6,
.textFieldNormalThickness = 1,
.textFieldCursorThickness = 3,
@@ -165,7 +165,7 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
// Render empty cover
renderer.fillRect(tileX + (tileWidth - coverWidth) / 2, imgY + (RoundedRaffMetrics::values.homeCoverHeight / 3),
coverWidth, 2 * RoundedRaffMetrics::values.homeCoverHeight / 3, true);
renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32, 32);
renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32);
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius,
Color::LightGray);
@@ -65,6 +65,18 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
.popupProgressClampPercent = true,
.popupProgressFillInverted = false,
.popupProgressOutlineInverted = false,
.optionPopupItemSpacing = 6,
.optionPopupInnerPadding = 24,
.optionPopupSelectionHPadding = 20,
.optionPopupSelectionVPadding = 10,
.optionPopupTitleGap = 16,
.optionPopupUseSmallFont = false,
.optionPopupOptionFontBold = true,
.optionPopupSelectionRadius = 30,
.optionPopupSelectionLight = false,
.optionPopupDrawAllRows = true,
.optionPopupDialogSideMargin = 20,
.optionPopupTitleSeparator = true,
.textFieldHorizontalPadding = 8,
.textFieldNormalThickness = 2,
.textFieldCursorThickness = 3,
+24 -3
View File
@@ -6,9 +6,12 @@
#include <HalStorage.h>
#include <Logging.h>
#include <WiFi.h>
#include <esp_efuse.h>
#include <esp_efuse_table.h>
#include <esp_task_wdt.h>
#include <algorithm>
#include <cctype>
#include "CrossPointSettings.h"
#include "FontInstaller.h"
@@ -368,9 +371,27 @@ void CrossPointWebServer::handleStatus() const {
doc["uptime"] = millis() / 1000;
doc["device"] = gpio.deviceIsX3() ? "X3" : "X4";
String json;
serializeJson(doc, json);
server->send(200, "application/json", json);
char snBuf[33] = {0};
bool valid = false;
if (esp_efuse_read_field_blob(ESP_EFUSE_USER_DATA, snBuf, 256) == ESP_OK) {
valid = snBuf[0] != '\0' && snBuf[0] != (char)0xFF;
for (int i = 0; i < 32 && snBuf[i] != '\0'; i++) {
if (!std::isprint(static_cast<unsigned char>(snBuf[i]))) {
valid = false;
break;
}
}
}
if (valid) {
doc["serial"] = snBuf;
} else {
doc["serial"] = "Not found";
}
String response;
serializeJson(doc, response);
server->send(200, "application/json", response);
}
void CrossPointWebServer::scanFiles(const char* path, const std::function<void(FileInfo)>& callback) const {
+5
View File
@@ -109,6 +109,10 @@
<div class="card">
<h2>Device Status</h2>
<div class="info-row">
<span class="label">Serial #</span>
<span class="value" id="serial"></span>
</div>
<div class="info-row">
<span class="label">Version</span>
<span class="value" id="version"></span>
@@ -141,6 +145,7 @@
}
const data = await response.json();
document.getElementById('version').textContent = data.version || 'N/A';
document.getElementById('serial').textContent = data.serial;
document.getElementById('ip-address').textContent = data.ip || 'N/A';
document.getElementById('free-heap').textContent = data.freeHeap
? data.freeHeap.toLocaleString() + ' bytes'
+85
View File
@@ -0,0 +1,85 @@
#include "NextBookFinder.h"
#include <FsHelpers.h>
#include <HalStorage.h>
#include <Logging.h>
#include <Memory.h>
#include <algorithm>
#include <string_view>
#include "CrossPointSettings.h"
namespace {
constexpr size_t NAME_BUFFER_SIZE = 500;
bool isSupportedBookFile(const std::string_view name) {
// Formats ReaderActivity can open (bmp is a viewer, not a book, so it is excluded)
return FsHelpers::hasEpubExtension(name) || FsHelpers::hasXtcExtension(name) || FsHelpers::hasTxtExtension(name) ||
FsHelpers::hasMarkdownExtension(name);
}
} // namespace
std::vector<std::string> NextBookFinder::findNextBooks(const std::string& currentBookPath, const size_t maxCount) {
std::vector<std::string> result;
if (maxCount == 0 || currentBookPath.empty()) {
return result;
}
const std::string folder = FsHelpers::extractFolderPath(currentBookPath);
const auto lastSlash = currentBookPath.find_last_of('/');
const std::string currentName =
lastSlash == std::string::npos ? currentBookPath : currentBookPath.substr(lastSlash + 1);
auto dir = Storage.open(folder.c_str());
if (!dir || !dir.isDirectory()) {
LOG_ERR("NBF", "Cannot open folder: %s", folder.c_str());
return result;
}
dir.rewindDirectory();
const auto nameBuffer = makeUniqueNoThrow<char[]>(NAME_BUFFER_SIZE);
if (!nameBuffer) {
LOG_ERR("NBF", "OOM: %d bytes", static_cast<int>(NAME_BUFFER_SIZE));
dir.close();
return result;
}
// Heap use is bounded: at most maxCount+1 short filename strings live at once (the
// file browser holds a whole folder in the same std::string form). A failed
// allocation here would abort like any STL growth in this codebase; the reserve
// below makes vector growth a single up-front allocation.
result.reserve(maxCount + 1);
const auto less = [](const std::string& a, const std::string& b) { return FsHelpers::naturalLess(a, b); };
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
if (file.isDirectory()) {
continue;
}
file.getName(nameBuffer.get(), NAME_BUFFER_SIZE);
if (!SETTINGS.showHiddenFiles && nameBuffer[0] == '.') {
continue;
}
if (!isSupportedBookFile(nameBuffer.get())) {
continue;
}
std::string name{nameBuffer.get()};
// Keep only files ordering strictly after the current one; equal names (the book
// itself, or a case-variant of it) compare "not less" both ways and drop out here.
if (!FsHelpers::naturalLess(currentName, name)) {
continue;
}
// Bounded insertion sort: keep the maxCount lowest-ordering candidates
if (result.size() >= maxCount && !less(name, result.back())) {
continue;
}
const auto pos = std::lower_bound(result.begin(), result.end(), name, less);
result.insert(pos, std::move(name));
if (result.size() > maxCount) {
result.pop_back();
}
}
dir.close();
return result;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <vector>
namespace NextBookFinder {
// Collects up to maxCount book files that order after currentBookPath's filename
// (natural sort, same ordering as the file browser) within the same folder.
// Returns bare filenames in sorted order; the current file itself is excluded.
// Single directory pass keeping only the maxCount best matches, so memory stays
// bounded regardless of folder size.
std::vector<std::string> findNextBooks(const std::string& currentBookPath, size_t maxCount);
} // namespace NextBookFinder
Binary file not shown.