Compare commits
81
Commits
feat-touch
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973a902332 | ||
|
|
cb9b971c26 | ||
|
|
f42fab1c66 | ||
|
|
c9188a7347 | ||
|
|
e03aa16330 | ||
|
|
3319aa1720 | ||
|
|
3dfbc0383f | ||
|
|
64364bec5d | ||
|
|
556b8aee5b | ||
|
|
b1d1d757ba | ||
|
|
5ba1d5747f | ||
|
|
9fe4dc5e38 | ||
|
|
b1d037569b | ||
|
|
9737cb335c | ||
|
|
fdffc2e5d9 | ||
|
|
a2db43d235 | ||
|
|
63093e606c | ||
|
|
a4ac3b2788 | ||
|
|
35a45f9c1e | ||
|
|
f3cad2f9e1 | ||
|
|
95d8cb712b | ||
|
|
b9867b0d09 | ||
|
|
3dac4446b7 | ||
|
|
f180069643 | ||
|
|
a0ec5812b1 | ||
|
|
e84840b473 | ||
|
|
94ae750e89 | ||
|
|
8c5cd67f38 | ||
|
|
c5787d1cc1 | ||
|
|
2a19eb9e0d | ||
|
|
fae3423dec | ||
|
|
932a472835 | ||
|
|
552b2683e6 | ||
|
|
bfa1d706b1 | ||
|
|
73ab9ab20d | ||
|
|
3a97f3e63e | ||
|
|
39ea4b045f | ||
|
|
444d87de82 | ||
|
|
859f6cb0d5 | ||
|
|
d8139e9605 | ||
|
|
b596f21e61 | ||
|
|
5fbc657aeb | ||
|
|
bb06219c9c | ||
|
|
5202bbf911 | ||
|
|
e0253a8664 | ||
|
|
e142e88ce7 | ||
|
|
6add0b91bc | ||
|
|
287457f7dd | ||
|
|
3e627112f6 | ||
|
|
1f5669a08a | ||
|
|
0dc92c142c | ||
|
|
4b34a576eb | ||
|
|
4fb843ef92 | ||
|
|
a43061305a | ||
|
|
7ec07b4d9d | ||
|
|
c2c1badc7e | ||
|
|
5776911af0 | ||
|
|
3c5c5e8aa7 | ||
|
|
6f5c5a0900 | ||
|
|
f2b00f6b93 | ||
|
|
44ff313740 | ||
|
|
ca1b833126 | ||
|
|
3e1dd31e53 | ||
|
|
685d4e88f9 | ||
|
|
79b4d63ee2 | ||
|
|
57447a56a6 | ||
|
|
5c86bfe1fd | ||
|
|
4cdefb137c | ||
|
|
e7f3406704 | ||
|
|
6f08a88b12 | ||
|
|
e4d3981469 | ||
|
|
c9d27826f9 | ||
|
|
13bd00bf9d | ||
|
|
fef39da23c | ||
|
|
fd43ca2fe1 | ||
|
|
28255061fb | ||
|
|
43ff1b6644 | ||
|
|
2754a5ff01 | ||
|
|
6edeed73c5 | ||
|
|
8ac302018c | ||
|
|
a11517a62d |
@@ -0,0 +1,13 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Scope or Roadmap Question (start here if unsure)
|
||||
url: https://github.com/crosspoint-reader/crosspoint-reader/discussions
|
||||
about: |
|
||||
Not sure if your idea fits CrossPoint's scope? Start a Discussion before filing an issue.
|
||||
See SCOPE.md and ROADMAP.md for what is in, out, and currently paused.
|
||||
- name: Read the Scope document
|
||||
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/SCOPE.md
|
||||
about: The authoritative list of what CrossPoint will and will not accept.
|
||||
- name: Read the Roadmap
|
||||
url: https://github.com/crosspoint-reader/crosspoint-reader/blob/master/ROADMAP.md
|
||||
about: Current phase, what is being closed out, and what comes next.
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Feature Request
|
||||
description: Propose a new feature, enhancement, or change to CrossPoint
|
||||
title: "Short, descriptive title of the request"
|
||||
labels: ["enhancement", "needs-scope-review"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to propose a change to CrossPoint!
|
||||
|
||||
**Before you continue, please read [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).**
|
||||
|
||||
CrossPoint is intentionally narrow. Most rejected proposals are rejected for scope reasons that are already
|
||||
documented. The checklist below exists to save both of us time.
|
||||
|
||||
If you are not sure whether your idea fits, open a [Discussion](../../discussions) first instead of filing
|
||||
this issue.
|
||||
|
||||
- type: checkboxes
|
||||
id: scope-check
|
||||
attributes:
|
||||
label: Scope Self-Check (required)
|
||||
description: Please confirm each of the following. If any are unchecked, your issue will likely be closed.
|
||||
options:
|
||||
- label: I have read SCOPE.md and ROADMAP.md.
|
||||
required: true
|
||||
- label: This is **not** a new theme or theming change (themes are temporarily closed pending the move to SD-loaded themes).
|
||||
required: true
|
||||
- label: This is **not** a new external network connector (sync engine, cloud storage, remote file access, OPDS extensions beyond what exists, or any new "talk to a server" feature).
|
||||
required: true
|
||||
- label: This is **not** an interactive app (game, calculator, notepad), writing/authoring tool, RSS/news/browser feature, media playback feature, or PDF rendering.
|
||||
required: true
|
||||
- label: The stock firmware does **not** already handle this well.
|
||||
required: true
|
||||
- label: No other popular CrossPoint fork already handles this well (or, if one does, I explain below why CrossPoint still needs it).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem this solves
|
||||
description: What user-facing problem or reading-experience gap does this address? Be concrete.
|
||||
placeholder: e.g., "When reading in landscape, paragraph breaks are inconsistent because..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: stock-gap
|
||||
attributes:
|
||||
label: Why the stock firmware (and other forks) do not already solve this
|
||||
description: Explain which existing solutions you checked and why they fall short. This is the core scope filter.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed change
|
||||
description: A short description of what you would build or change. Focus on user impact; implementation details can come later.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: tradeoffs
|
||||
attributes:
|
||||
label: Memory / flash / complexity cost
|
||||
description: |
|
||||
CrossPoint runs on 380KB of RAM. Roughly how much DRAM, flash, or code complexity does this add?
|
||||
"Don't know" is a valid answer, but please attempt an estimate.
|
||||
placeholder: e.g., "Adds ~2KB flash for the new font tables, no DRAM impact at runtime."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Anything else relevant (links, screenshots, related discussions).
|
||||
validations:
|
||||
required: false
|
||||
@@ -3,10 +3,28 @@
|
||||
* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.)
|
||||
* **What changes are included?**
|
||||
|
||||
## Scope Check
|
||||
|
||||
CrossPoint is intentionally narrow. See [SCOPE.md](../blob/master/SCOPE.md) and [ROADMAP.md](../blob/master/ROADMAP.md).
|
||||
Please confirm:
|
||||
|
||||
- [ ] I have read SCOPE.md and ROADMAP.md.
|
||||
- [ ] This PR is **not** a new built-in theme (themes are temporarily closed pending the move to SD-loaded themes).
|
||||
- [ ] This PR is **not** a new external network connector (sync engine, cloud storage, remote file access, etc.).
|
||||
- [ ] This PR is **not** an interactive app, writing tool, RSS/news/browser, media playback, or PDF feature.
|
||||
- [ ] The stock firmware does not already handle this well, **and** no other popular CrossPoint fork already does
|
||||
(or, if one does, I explain why CrossPoint still needs it below).
|
||||
- [ ] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader, OTA, or recovery code, I have coordinated with
|
||||
the relevant maintainer.
|
||||
|
||||
**If this PR was opened against the previous (broader) scope and was already in flight under Phase 0, link the
|
||||
relevant Discussion or issue so reviewers can see the history.**
|
||||
|
||||
## Additional Context
|
||||
|
||||
* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks,
|
||||
specific areas to focus on).
|
||||
* Memory / flash impact, if known.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -91,9 +91,13 @@ jobs:
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Build CrossPoint
|
||||
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
|
||||
# .pio/build tree (project-checksum mismatch after the S3 toolchain
|
||||
# installs into the restored cache), deleting the default firmware.bin
|
||||
# before the artifact upload.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pio run | tee pio.log
|
||||
pio run -e default -e sticky | tee pio.log
|
||||
|
||||
- name: Extract firmware stats
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
open-x4-sdk
|
||||
fs_
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nKeys.h
|
||||
lib/I18n/I18nStrings.h
|
||||
|
||||
@@ -8,9 +8,11 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
||||
|
||||

|
||||
|
||||
> If you're planning to buy an Xteink device, consider purchasing an **X3/X4 Developer Edition** through https://crosspointreader.com. CrossPoint receives a small share of each sale, helping fund development costs.
|
||||
|
||||
## What can CrossPoint do?
|
||||
|
||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||
- **Reader engine**: EPUB 2/3 rendering with embedded-style option, image handling, hyphenation, kerning, chapter navigation, footnotes, bookmarks, dictionary lookups ([StarDict](docs/dictionary.md)), go-to-percent, auto page turn, orientation control, focus reading, KOReader progress sync and more.
|
||||
|
||||
- **Various formats**: native handling for `.epub`, `.xtc/.xtch`, `.txt`, and `.bmp`.
|
||||
|
||||
@@ -40,8 +42,6 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
||||
|
||||
### Coming soon:
|
||||
|
||||
- Dictionary lookup — inline word lookup without leaving the reader.
|
||||
|
||||
- More themes.
|
||||
|
||||
- Much more! stay tuned.
|
||||
@@ -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
|
||||
|
||||
@@ -140,6 +136,7 @@ Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py`
|
||||
- [Web server endpoints](./docs/webserver-endpoints.md)
|
||||
- [Project scope](./SCOPE.md)
|
||||
- [Contributing docs](./docs/contributing/README.md)
|
||||
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - FreeInkUI components for new screens, the touch bridge for existing ones, and build envs for the non-Xteink touch devices
|
||||
|
||||
---
|
||||
|
||||
@@ -162,6 +159,24 @@ cd crosspoint-reader
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
### Nix/NixOS
|
||||
|
||||
Nix/NixOS users can enter the development shell with either `nix develop` (flakes) or `nix-shell`:
|
||||
|
||||
```bash
|
||||
nix develop -f nix
|
||||
# or
|
||||
nix-shell nix
|
||||
```
|
||||
|
||||
To flash a connected ESP32-C3 device, enable PlatformIO's udev rules in your NixOS configuration:
|
||||
|
||||
```nix
|
||||
services.udev.packages = with pkgs; [ platformio-core.udev ];
|
||||
```
|
||||
|
||||
After rebuilding the system configuration, reconnect the device or reload udev rules.
|
||||
|
||||
### Build / flash / monitor
|
||||
|
||||
```bash
|
||||
@@ -249,7 +264,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.
|
||||
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# CrossPoint Reader Roadmap
|
||||
|
||||
This roadmap describes how CrossPoint is moving through the tighter scope defined in [SCOPE.md](SCOPE.md). It is
|
||||
intentionally phased: Phase 0 closed out the commitments already in flight before locking down to the stricter
|
||||
"fill gaps the stock firmware leaves" delineator.
|
||||
|
||||
Phases are sequential. We do not start the next phase until the prior one is wrapped or explicitly carried over.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 - Close Out Legacy Scope Items — **COMPLETE**
|
||||
|
||||
**Goal:** Land the work that was already in motion under the prior, broader scope so contributors are not left
|
||||
hanging, and so we enter the stricter phases with a clean slate.
|
||||
|
||||
**Landed in Phase 0:**
|
||||
|
||||
* **RTL support PRs.** The in-flight right-to-left work was reviewed, iterated, and merged.
|
||||
* **Dictionary PR.** The offline dictionary lookup work was reviewed and merged.
|
||||
* **Bookmarks** feature. First-class navigation markers in EPUBs.
|
||||
* ~~**Transparent sleep screens.**~~ Shelved; not picked back up under the stricter phases.
|
||||
|
||||
Phase 0 is closed. The tighter scope in [SCOPE.md](SCOPE.md) is now fully enforced. "But it was on the old roadmap"
|
||||
is not a valid argument for accepting a PR.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 - Consolidation, Footprint, and Multi-Device Support — **IN PROGRESS**
|
||||
|
||||
**Goal:** Reduce memory and flash usage, clean up the codebase, and land the SDK / HAL generalization work so
|
||||
CrossPoint runs cleanly on ESP32-based e-reader hardware beyond Xteink (X3 / X4), including ESP32-S3 class devices.
|
||||
|
||||
**Focus areas:**
|
||||
|
||||
* DRAM and heap fragmentation reduction across the reader core.
|
||||
* Flash footprint reduction (dead code, redundant strings, oversized tables).
|
||||
* Refactors that tighten the HAL / SDK boundary.
|
||||
* Pluggable per-device SDK layers (display, input, storage, battery) and per-device build configuration without
|
||||
forking the reader core.
|
||||
* Documentation for adding a new ESP32 e-reader target.
|
||||
* E-ink driver refinement (ghosting, partial update behavior).
|
||||
|
||||
**Closed during this phase:** new themes built into firmware, new external network connectors (sync engines, cloud
|
||||
storage, remote file access).
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 - Languages, Fonts, and Themes
|
||||
|
||||
**Goal:** With the codebase smaller and portable, make reading great in every language: multi-language support,
|
||||
better font support with custom fonts, UI translations, and themes loaded from the SD card instead of consuming
|
||||
flash.
|
||||
|
||||
**Focus areas:**
|
||||
|
||||
* Multi-language reading support (underserved languages, complex script support where realistic on ESP32 hardware).
|
||||
* Better font support and custom fonts.
|
||||
* UI languages and localization.
|
||||
* Moving themes off-firmware to SD-loaded assets (see SCOPE.md Section 6).
|
||||
* **Moving hyphenation files off-firmware.** Hyphenation rules vary per language and the files are large (German
|
||||
alone is ~200KB). Today these eat flash budget that should be available for the reader core. The plan is to build
|
||||
a downloader analogous to the existing font downloader and store the dictionaries on SD / SPIFFS, loading on
|
||||
demand. This unlocks better hyphenation for long-word languages (German, Finnish, Norwegian, etc.) without paying
|
||||
the flash cost up front.
|
||||
|
||||
This phase depends on Phase 1 cleanup landing first; otherwise we generalize a moving target.
|
||||
|
||||
---
|
||||
|
||||
## Out of Roadmap
|
||||
|
||||
The following are explicitly *not* on the roadmap. They may live in other CrossPoint forks; they will not be picked
|
||||
up here:
|
||||
|
||||
* Interactive apps (games, calculators, notepads).
|
||||
* Writing / authoring tools.
|
||||
* Active connectivity features (RSS, news, browsers).
|
||||
* PDF rendering as a first-class format.
|
||||
|
||||
See [SCOPE.md](SCOPE.md) for the full rationale.
|
||||
|
||||
---
|
||||
|
||||
## How This Roadmap Changes
|
||||
|
||||
* Phase boundaries are decided by maintainers, not by individual PRs.
|
||||
* If a phase needs to be extended or an item carried over, that is documented here with a short note.
|
||||
* Proposals for new phases or reordering should go through a Discussion first.
|
||||
@@ -1,62 +1,100 @@
|
||||
# Project Vision & Scope: CrossPoint Reader
|
||||
|
||||
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for the Xteink X4. We believe a
|
||||
dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
||||
The goal of CrossPoint Reader is to create an efficient, open-source reading experience for ESP32-based e-reader devices. Xteink hardware (X3, X4) is where the project started and remains a primary target, but CrossPoint is explicitly broadening to support the wider ecosystem of small ESP32 e-ink readers. We believe a dedicated e-reader should do one thing exceptionally well: **facilitate focused reading.**
|
||||
|
||||
## 1. Core Mission
|
||||
|
||||
To provide a lightweight, high-performance firmware that maximizes the potential of the X4, prioritizing legibility and
|
||||
usability over "swiss-army-knife" functionality.
|
||||
To provide a lightweight, high-performance firmware that maximizes the potential of ESP32-based e-reader hardware, prioritizing legibility, performance, and usability over "swiss-army-knife" functionality.
|
||||
|
||||
## 2. Scope
|
||||
CrossPoint is **not** a kitchen-sink firmware, and it is **not** Xteink-only. We want clean, maintainable code that the community can build on, and that runs across the range of ESP32 e-reader devices (ESP32-C3, ESP32-S3, and adjacent variants). Every accepted change should make that goal easier, not harder. Device-specific code should live behind the HAL / SDK boundary so the reader core stays portable.
|
||||
|
||||
## 2. Guiding Principle: Fill Gaps the Stock Firmware Leaves
|
||||
|
||||
CrossPoint exists to do the things the stock firmware does poorly or not at all. New work is evaluated against that delineator:
|
||||
|
||||
* **Does the stock firmware already do this well?** We should hit that bar or surpass it
|
||||
* **Is another popular CrossPoint fork already solving this well?** If yes, we generally defer to that fork if it's not part of the core reading experience. e.g. stats
|
||||
* **Does this directly improve the reading experience or the firmware's long-term maintainability?** If no, it is out of scope.
|
||||
|
||||
## 3. Current Focus
|
||||
|
||||
We are intentionally narrowing scope to consolidate the codebase as we open it up to more ESP32 e-reader devices.
|
||||
|
||||
During this period, the priorities are:
|
||||
|
||||
* **Memory footprint:** Reducing DRAM usage and heap fragmentation. The ESP32-C3 is the tightest target and sets the ceiling, but the gains benefit every ESP32 variant we run on.
|
||||
* **Flash footprint:** Trimming binary size to leave room for additional device targets and features.
|
||||
* **Code cleanup:** Refactoring, removing dead code, tightening abstractions, and improving readability.
|
||||
* **Reading experience:** EPUB parsing and rendering, typography, hyphenation, line spacing, font handling, and legibility improvements.
|
||||
|
||||
### Temporarily Closed Areas
|
||||
|
||||
PRs in the following areas will be closed until this notice is lifted. Adding these now makes the cleanup and multi-device work materially harder:
|
||||
|
||||
* **New themes.** The existing theming surface is frozen.
|
||||
* **New external network connectors.** This includes sync engines, cloud storage clients, OPDS extensions beyond what exists, remote file access, and any new "talk to a server" feature. We now have our own CrossPoint KOSync server which gives us a way to sync to 3rd party systems like Hardcover at an API level instead of bloating the firmware. If you're interested in helping here, the sync server is also open source.
|
||||
|
||||
If you are unsure whether your idea falls into one of these categories, open a Discussion first.
|
||||
|
||||
## 4. Scope
|
||||
|
||||
### In-Scope
|
||||
|
||||
*These are features that directly improve the primary purpose of the device.*
|
||||
*Features that directly improve the core reading experience or the firmware's maintainability.*
|
||||
|
||||
* **User Experience:** E.g. User-friendly interfaces, and interactions, both inside the reader and navigating the
|
||||
firmware. This includes things like button mapping, book loading, and book navigation like bookmarks.
|
||||
* **Document Rendering:** E.g. Support for rendering documents (primarily EPUB) and improvements to the rendering
|
||||
engine.
|
||||
* **Format Optimization:** E.g. Efficiently parsing EPUB (CSS/Images) and other documents within the device's
|
||||
capabilities.
|
||||
* **Typography & Legibility:** E.g. Custom font support, hyphenation engines, and adjustable line spacing.
|
||||
* **E-Ink Driver Refinement:** E.g. Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books.
|
||||
* **Local Transfer:** E.g. Simple, "pull" based book loading via a basic web-server or public and widely-used standards.
|
||||
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
|
||||
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
|
||||
without breaking focus.
|
||||
* **Clock Display (device dependent):**
|
||||
|
||||
| Device | Scope |
|
||||
| -- | -- |
|
||||
| X3 | The X3 uses a dedicated DS3231 RTC, which maintains accurate time across sleep cycles and can be treated as a reliable wall clock. |
|
||||
| X4 | The X4 relies on the ESP32-C3's internal RTC, which drifts significantly during deep sleep. NTP sync could correct this, with an appropriate user experience around connecting to the internet on wake or on demand. This causes some tension with the **Active Connectivity** section below, so please open a discussion about this UX if it's a feature you would find useful. |
|
||||
* **EPUB Rendering & Optimization:** Improvements to the rendering engine, CSS/image handling, and parsing performance.
|
||||
* **Typography & Legibility:** Custom font support, hyphenation, line and paragraph spacing, margins.
|
||||
* **E-Ink Driver Refinement:** Reducing full-screen flashes (ghosting management) and improving general rendering.
|
||||
* **Reading UX:** Bookmarks, progress tracking, button mapping, page navigation, and other in-reader interactions.
|
||||
* **Library Management:** Simple, intuitive ways to organize and navigate a local book collection.
|
||||
* **Memory, Flash, and Code Quality:** Refactors and cleanups that reduce resource use or improve maintainability, even without a user-visible feature.
|
||||
|
||||
### Out-of-Scope
|
||||
|
||||
*These items are rejected because they compromise the device's stability or mission.*
|
||||
*Rejected because they compromise the device's stability, maintainability, or core mission.*
|
||||
|
||||
* **Interactive Apps:** No Notepads, Calculators, or Games. This is a reader, not a PDA.
|
||||
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery
|
||||
and complicate the single-core CPU's execution.
|
||||
* **Media Playback:** No Audio players or Audio-books.
|
||||
* **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
|
||||
capabilities and more powerful chips.
|
||||
* **Interactive Apps:** No notepads, calculators, or games. These belong in other forks and are not part of CrossPoint's focus.
|
||||
* **Writing / Authoring Tools:** No typed notes, journals, or editors. Input hardware and RAM are wrong for this, and other forks already explore this space.
|
||||
* **Active Connectivity:** No RSS readers, news aggregators, or web browsers. Background Wi-Fi drains the battery and complicates the single-core CPU.
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text, resulting in constant panning and zooming that makes for a poor reading experience on e-ink. Out of scope on the current hardware class.
|
||||
|
||||
### In-scope — Technically Unsupported
|
||||
## 5. Calls to Action
|
||||
|
||||
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
|
||||
These are the areas where contributor help is most valuable right now. If you want to take one of these on, open a Discussion or issue first so we can coordinate.
|
||||
|
||||
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
|
||||
### Theme System: Move Themes Off-Firmware
|
||||
|
||||
## 3. Idea Evaluation
|
||||
We want to abstract themes out of the firmware entirely so they no longer consume flash, and instead load from the SD card. This directly supports the current focus on flash footprint and code cleanup.
|
||||
|
||||
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
|
||||
a lightweight, reliable, and performant e-reader. Things which distract or compromise the device's core mission will not
|
||||
be accepted. As a guiding question, consider if your idea improve the "core reading experience" for the average user,
|
||||
and, critically, not distract from that reading experience.
|
||||
* **Status:** [@itsthisjustin](https://github.com/itsthisjustin) plans to take this on eventually but is very open to someone else claiming it sooner.
|
||||
* **Why it matters:** Every built-in theme costs flash that we would rather spend on rendering, fonts, or future device support. SD-loaded themes also let users customize without rebuilding firmware. It also leads to SD font loading for better language support in the UI.
|
||||
* **How to claim:** Comment on the relevant Discussion (or open one) before starting.
|
||||
|
||||
> **Note to Contributors:** If you are unsure if your idea fits the scope, please open a **Discussion** before you start
|
||||
> coding!
|
||||
### Identifying Other Stock-Firmware Gaps
|
||||
|
||||
We want help cataloguing things the stock firmware (and other popular CrossPoint forks) handle poorly or not at all, so future work has a clear target list. Particularly interested in:
|
||||
|
||||
* **RTL (right-to-left) text support:** Arabic, Hebrew, Persian, and similar scripts.
|
||||
* **Languages with poor stock and fork coverage:** Especially those that need shaping, complex layout, or non-Latin font work that nobody is handling well today.
|
||||
* **Other gaps:** Rendering edge cases, accessibility issues, input quirks, anything stock does badly and existing forks have not fixed.
|
||||
|
||||
If you can read or use the device in one of these languages, your feedback (even without code) is genuinely useful. Open a Discussion with concrete examples (screenshots, sample EPUBs, expected vs actual behavior) and we will prioritize from there.
|
||||
|
||||
## 6. Funding and Contributor Sustainability
|
||||
|
||||
CrossPoint uses [Royalty.dev](https://royalty.dev) (yes, a product built by [@itsthisjustin](https://github.com/itsthisjustin)) to fund contributors. There has been some tension in the community around this, so the intent is being clarified here directly.
|
||||
|
||||
**Why we do this:**
|
||||
|
||||
* To maintain long-term interest from contributors and maintainers, in direct response to substantial community requests for a way to give back.
|
||||
* To motivate contributors to invest in the *core* project rather than spinning up competing forks.
|
||||
* To help pay for new ESP32 devices so we can port CrossPoint to additional hardware.
|
||||
* To give the project a credible long-term path to sustainability.
|
||||
|
||||
**How it works:**
|
||||
|
||||
* Funds are distributed automatically to contributors based on impact to the codebase and tenure on the project.
|
||||
* Over **$600** was raised in the first few hours after opening up funding, which is a signal the demand is real.
|
||||
* The exact scoring methodology is published at <https://app.royalty.dev/transparency>.
|
||||
|
||||
**This is not fixed in stone.** The weighting, eligibility, and distribution rules can be tweaked as we learn what works for this project. If you have concerns or suggestions about how funds are allocated, open a Discussion. The goal is a system that fairly recognizes the people doing the work, not a perfect one on day one.
|
||||
|
||||
+54
-28
@@ -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)
|
||||
@@ -15,6 +16,10 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [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)
|
||||
- [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,7 +28,13 @@ 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: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default)
|
||||
- [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks)
|
||||
- [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose)
|
||||
- [Syncing While Reading](#syncing-while-reading)
|
||||
- [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)
|
||||
@@ -36,8 +47,8 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
|
||||
- [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)
|
||||
- [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap)
|
||||
- [7. Troubleshooting Issues \& Escaping Bootloop](#7-troubleshooting-issues--escaping-bootloop)
|
||||
|
||||
## 1. Hardware Overview
|
||||
|
||||
@@ -252,6 +263,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- "ON" - Vertical space will be added between paragraphs in Reading Mode
|
||||
- "OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
|
||||
|
||||
- **Dictionary**: Select the StarDict dictionary used for word lookups while reading, or "None" to disable lookups. *(Only shown when at least one dictionary folder exists under `/dictionaries/` on the SD card — see [docs/dictionary.md](docs/dictionary.md) for setup and usage.)*
|
||||
|
||||
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
|
||||
|
||||
- **Images**: Whether to display embedded images (JPG/PNG) found in EPUB files; options are "ON" (default) or "OFF".
|
||||
@@ -271,6 +284,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- **Long-press Menu**: Selects the function bound to holding the menu button (Confirm) while reading an EPUB. **Cycles through the available functions** each time the setting is selected — additional functions may be added in future releases, so this is not a binary on/off toggle. A short press of Confirm always opens the reader menu as normal:
|
||||
- "Bookmark" (default) - Hold Confirm (~0.4 second) to drop a bookmark at the current page.
|
||||
- "KOSync" - Hold Confirm (~1 second) to launch KOReader sync directly.
|
||||
- "Dictionary" - Hold Confirm (~0.4 second) to start dictionary word selection on the current page (see [docs/dictionary.md](docs/dictionary.md)).
|
||||
- "Disabled" - Long-press is ignored; only short-press opens the reader menu.
|
||||
|
||||
- **Short Power Button Click**: Controls the effect of a short click of the power button:
|
||||
@@ -288,7 +302,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
|
||||
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
|
||||
|
||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
|
||||
- **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation.
|
||||
|
||||
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
|
||||
|
||||
@@ -351,9 +365,37 @@ Behavior notes:
|
||||
CrossPoint can sync reading progress with KOReader-compatible sync servers.
|
||||
It also interoperates with KOReader apps/devices when they use the same server and credentials.
|
||||
|
||||
##### Option A: Free Public Server (`sync.koreader.rocks`)
|
||||
##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)
|
||||
|
||||
1. Register a user once (only if needed):
|
||||
When **Sync Server URL** is left empty, CrossPoint uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossPoint-to-CrossPoint sync.
|
||||
|
||||
1. On each CrossPoint device:
|
||||
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
|
||||
- Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`).
|
||||
|
||||
- On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**.
|
||||
|
||||
Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server.
|
||||
|
||||
##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)
|
||||
|
||||
Use this if you already sync KOReader devices against the official public server.
|
||||
|
||||
1. On each CrossPoint device:
|
||||
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
|
||||
- Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead).
|
||||
|
||||
- Set **Username** and **Password** to your existing KOReader Sync credentials.
|
||||
|
||||
- Run **Authenticate**.
|
||||
|
||||
2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl:
|
||||
|
||||
```bash
|
||||
USERNAME="user"
|
||||
@@ -366,27 +408,9 @@ curl -i "https://sync.koreader.rocks/users/create" \
|
||||
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
|
||||
```
|
||||
|
||||
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
|
||||
|
||||
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
|
||||
|
||||
2. On each CrossPoint device:
|
||||
|
||||
- Go to **Settings -> System -> KOReader Sync**.
|
||||
|
||||
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
|
||||
|
||||
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
|
||||
|
||||
- Run **Authenticate**.
|
||||
|
||||
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
|
||||
- Choose **Upload Local** to push current progress.
|
||||
|
||||
##### Option B: Self-Hosted Server (Docker Compose)
|
||||
##### Option C: Self-Hosted Server (Docker Compose)
|
||||
|
||||
1. Start a sync server:
|
||||
|
||||
@@ -459,11 +483,12 @@ If this returns `HTTP 402` with `{"code":2002,"message":"Username is already reg
|
||||
|
||||
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
|
||||
|
||||
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
|
||||
##### Syncing While Reading
|
||||
|
||||
- Choose **Apply Remote** to jump to remote progress.
|
||||
Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
|
||||
|
||||
- Choose **Upload Local** to push current progress.
|
||||
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
|
||||
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
|
||||
|
||||
### 3.7 Sleep Screen
|
||||
|
||||
@@ -561,7 +586,7 @@ If the device goes to sleep or you close the book while viewing a footnote, the
|
||||
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
|
||||
* **Reader Menu:** Press **Confirm** to open the **[Reader Menu](#5-reader-menu)**, which includes chapter navigation, reading options, and more.
|
||||
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Disabled" does nothing. A short press always opens the Reader Menu.
|
||||
* **Long-press Confirm (configurable):** Holding **Confirm** runs the function chosen by the **Long-press Menu** setting in **[Controls Settings](#363-controls)** — "Bookmark" (default) drops a bookmark, "KOSync" launches KOReader Sync, "Dictionary" starts a word lookup, "Disabled" does nothing. A short press always opens the Reader Menu.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
@@ -583,6 +608,7 @@ Available options include:
|
||||
|
||||
- **Select Chapter** – Open the table of contents to jump to a specific chapter (see [Chapter Selection](#51-chapter-selection) below).
|
||||
- **Footnotes** – Navigate to the footnotes for the current section *(only shown in books that contain footnotes)*.
|
||||
- **Look Up** – Select a word on the current page and show its dictionary definition (see [docs/dictionary.md](docs/dictionary.md)). Requires a dictionary to be selected in **Settings → Reader → Dictionary**.
|
||||
- **Reading Orientation** – Cycle through screen orientations without leaving the reader.
|
||||
- **Auto Turn (Pages Per Minute)** – Cycle through automatic page turn speed options for hands-free reading.
|
||||
- **Go to %** – Jump to a specific position in the book by percentage.
|
||||
|
||||
@@ -47,6 +47,7 @@ git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \
|
||||
| grep -v -E '^lib/EpdFont/builtinFonts/' \
|
||||
| grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \
|
||||
| grep -v -E '^lib/uzlib/' \
|
||||
| grep -v -E '^lib/miniz/third_party/' \
|
||||
| xargs -r "${CLANG_FORMAT_BIN}" -style=file -i
|
||||
# Restore strict pipeline failure handling for the rest of the script.
|
||||
set -o pipefail
|
||||
|
||||
@@ -7,5 +7,6 @@ It is written for software developers who may be new to embedded development.
|
||||
- [Architecture Overview](./architecture.md)
|
||||
- [Development Workflow](./development-workflow.md)
|
||||
- [Testing and Debugging](./testing-debugging.md)
|
||||
- [Touch and UI Development](./touch-and-ui.md)
|
||||
|
||||
If you are new, start with [Getting Started](./getting-started.md).
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Touch and UI Development
|
||||
|
||||
CrossPoint now runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. There are two supported ways to get there:
|
||||
|
||||
1. **New screens: build them with FreeInkUI components.** Touch hit-testing, tap highlighting, long-press, and button focus navigation come with the component; you never hand-roll coordinate math.
|
||||
2. **Existing screens and in-flight features: use the MappedInputManager touch bridge.** A small set of helpers adds tap/hold/swipe support to hand-rolled rendering without restructuring the activity.
|
||||
|
||||
If you are starting a new activity, use FreeInkUI. If you have a feature branch with a hand-rolled screen already working on buttons, use the bridge; do not rewrite mid-flight.
|
||||
|
||||
---
|
||||
|
||||
## Path 1: New screens with FreeInkUI
|
||||
|
||||
FreeInkUI (`freeink-sdk/libs/ui/FreeInkUI`, namespace `freeink::ui`) is an immediate-mode component library. The core idea:
|
||||
|
||||
- While a component renders, it registers its tappable areas ("hit rects") into the frame's interaction buffer via `Frame::hit(rect, action, value, inputMask, state)`.
|
||||
- Each loop you build an `InputSnapshot` from the input manager and route it against that buffer. If a tap (or mapped button press) lands in a registered rect, the routed `ActionEvent` tells you which action fired and with what value.
|
||||
- Components query `frame.stateFor(action, value)` while painting, so touch-down highlight and focus states render correctly without any per-activity code.
|
||||
|
||||
You get touch, hold highlighting, long-press, minimum touch-target sizing, and orientation-aware coordinates for free. The same action IDs fire from physical buttons, so one code path serves both input styles.
|
||||
|
||||
### Component inventory
|
||||
|
||||
All under `freeink-sdk/libs/ui/FreeInkUI/include/components/`:
|
||||
|
||||
| Category | Components |
|
||||
|---|---|
|
||||
| Controls | `button`, `checkbox`, `slider`, `progress-bar`, `header` |
|
||||
| Lists | `list` (virtualized), `table`, `dropdown`, `radio-group`, `setting-row`, `toggle-row`, `stepper-row` |
|
||||
| Keyboard | `keyboard` (QWERTY/AZERTY/QWERTZ/ES layouts), `key-grid` |
|
||||
| Overlays | `popup`, `option-dialog`, `context-menu`, `message-panel`, `toast` |
|
||||
| Bars | `status-bar`, `tab-bar`, `reader-chrome`, `battery-indicator`, `gesture-bar`, `tap-zones` |
|
||||
| Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` |
|
||||
| Text | `text-field`, `text-area` |
|
||||
|
||||
### Integration skeleton
|
||||
|
||||
The in-tree reference is [`src/activities/util/KeyboardEntryActivity.cpp`](../../src/activities/util/KeyboardEntryActivity.cpp); it drives the FreeInkUI keyboard component inside a normal Activity. The shape:
|
||||
|
||||
```cpp
|
||||
#include <FreeInkUIGfxRenderer.h>
|
||||
namespace fui = freeink::ui;
|
||||
|
||||
// Render step: draw the component and (re)register its hit rects.
|
||||
fui::GfxRendererTarget target(renderer); // adapts GfxRenderer to FreeInkUI's DrawTarget
|
||||
target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID);
|
||||
const fui::DeviceContext device = target.deviceContext(); // orientation, safe area, touch sizing
|
||||
|
||||
fui::Frame<48> frame(target, device, fui::InputSnapshot{}, interactions); // 48 = max hit rects
|
||||
fui::KeyboardProps props;
|
||||
props.layout = ¤tLayout();
|
||||
props.keyAction = ACTION_KEY; // one action id; the key is the event value
|
||||
props.inputMask = fui::InputTouch | fui::InputLongPress;
|
||||
fui::keyboard(frame, kbRect, props); // draws AND registers hit rects
|
||||
```
|
||||
|
||||
```cpp
|
||||
// Input step (each loop): feed touch state, act on routed events.
|
||||
int tx, ty, tapX, tapY, hx, hy;
|
||||
const bool pressedDown = mappedInput.wasScreenTouchDown(tx, ty);
|
||||
const bool tapped = mappedInput.wasScreenTapped(tapX, tapY);
|
||||
const bool inContact = mappedInput.isScreenTouchHeld(hx, hy);
|
||||
|
||||
const auto result = touchRouter.update(interactions, pressedDown, tx, ty,
|
||||
tapped, tapX, tapY, inContact, millis());
|
||||
if (result.event) {
|
||||
activateValue(result.event.value, result.event.longPress);
|
||||
requestUpdate();
|
||||
} else if (result.activeChanged) {
|
||||
requestUpdate(); // repaint touch-down highlight
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `Frame<N>` is templated on the max number of hit rects; the buffer is stack/static, no heap.
|
||||
- `fui::TouchHoldRouter` (see `components/keyboard/keyboard.h`) synthesizes long-press while the finger is still down and swallows the eventual release. Use it whenever a component distinguishes tap from long-press.
|
||||
- For simpler screens without long-press, `FreeInkApp` / `Screen<N>` (`FreeInkApp.h`) is the ergonomic layer: it owns the interaction buffer, offers a top-to-bottom builder (`takeTop`, footer helpers, etc.), and its `render(input)` call routes the snapshot and dispatches registered action callbacks in one step. `snapshotFrom(input, device)` in `FreeInkUIInputManager.h` builds the `InputSnapshot` with orientation-aware touch mapping.
|
||||
- Everything stays allocation-free and works on the button-only devices unchanged: physical buttons route through the same interaction table via each hit rect's `inputMask`.
|
||||
|
||||
---
|
||||
|
||||
## Path 2: Adding touch to existing hand-rolled screens
|
||||
|
||||
For activities that draw their own rows, menus, and buttons, `MappedInputManager` ([src/MappedInputManager.h](../../src/MappedInputManager.h)) exposes bridge helpers. They all return **logical screen coordinates** (orientation already applied via `GfxRenderer::tapToLogical`); never touch raw normalized panel coordinates or the SDK `InputManager` directly.
|
||||
|
||||
| Helper | Use for |
|
||||
|---|---|
|
||||
| `wasScreenTapped(x, y)` | A completed tap (press + release), with logical coords |
|
||||
| `wasScreenTouchDown(x, y)` | Touch-down (held > 90 ms, not yet released): draw selection highlight |
|
||||
| `isScreenTouchHeld(x, y)` | Live contact position while the finger is down (drag tracking) |
|
||||
| `wasTapInRect(x, y, w, h)` | One-off hit test on a rectangle (a single button, a banner) |
|
||||
| `wasListItemTapped(index, count, selected, listTop, listHeight, hasSubtitle)` | Taps on a standard UITheme list; does the row/paging math for you |
|
||||
| `wasListItemTouchedDown(...)` | Same geometry, touch-down phase (highlight before activate) |
|
||||
| `rowTouch(row, top, rowStep, rowCount, xStart, xEnd, rowHeight)` | Any custom band of equal-height rows; returns `RowTouch::None/Down/Tap` |
|
||||
| `colTouch(col, left, colStep, colCount, yStart, yEnd, colWidth)` | Horizontal button bands (dialogs, prompts) |
|
||||
| `wasSwipe()` | Raw swipe direction if you need one beyond the global gestures |
|
||||
| `hasTouch()` | True when the device has a touch panel (rarely needed; helpers simply never fire without one) |
|
||||
|
||||
### Pattern A: standard themed list
|
||||
|
||||
One call per loop; UITheme owns the row geometry ([EpubReaderBookmarksActivity.cpp:123](../../src/activities/reader/EpubReaderBookmarksActivity.cpp)):
|
||||
|
||||
```cpp
|
||||
int tapped = -1;
|
||||
if (mappedInput.wasListItemTapped(tapped, bookmarks.size(), selectorIndex, listY, listHeight, true)) {
|
||||
selectorIndex = tapped;
|
||||
openBookmark();
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern B: custom rows with hold highlight
|
||||
|
||||
For non-theme row layouts, use `rowTouch` and distinguish `Down` (highlight) from `Tap` (activate), as in [EpubReaderFootnotesActivity.cpp](../../src/activities/reader/EpubReaderFootnotesActivity.cpp):
|
||||
|
||||
```cpp
|
||||
int row = -1;
|
||||
const auto touch = mappedInput.rowTouch(row, listTop, lineHeight, visibleCount,
|
||||
contentX, contentX + contentWidth);
|
||||
if (touch != MappedInputManager::RowTouch::None) {
|
||||
const int touched = scrollOffset + row;
|
||||
if (touch == MappedInputManager::RowTouch::Down) {
|
||||
if (selectedIndex != touched) { selectedIndex = touched; requestUpdate(); }
|
||||
} else { // RowTouch::Tap
|
||||
selectedIndex = touched;
|
||||
activateSelection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
The `Down` state exists because e-ink repaints are slow: highlight on touch-down gives immediate feedback, activation happens on release.
|
||||
|
||||
### Global gestures: do not reimplement these
|
||||
|
||||
Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling:
|
||||
|
||||
| Gesture | Trigger | Where it is handled |
|
||||
|---|---|---|
|
||||
| Back | Right-swipe starting in the left 25% of the screen | Folded into `Button::Back`, so the existing `wasPressed(Button::Back)` in your activity already fires |
|
||||
| Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) |
|
||||
| Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) |
|
||||
|
||||
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. That is the bar to aim for: bridge helpers should make touch an additive layer over the button flow, not a second input state machine.
|
||||
|
||||
### Bridge rules
|
||||
|
||||
- Handle touch in `loop()` next to the existing button handling, one helper call per interaction zone, and `return` after consuming an event (mirrors the button pattern).
|
||||
- Never call the SDK `InputManager` or read GPIO directly; the HAL rule from the main guide applies to touch too.
|
||||
- Coordinates from the helpers are logical and orientation-correct on all four rotations; test at least Portrait and one Landscape mode before PR.
|
||||
- Nothing to clean up in `onExit()`; the helpers are stateless from the activity's point of view.
|
||||
|
||||
---
|
||||
|
||||
## Building and testing on non-Xteink devices
|
||||
|
||||
Each MCU family is its own binary: X3/X4 are ESP32-C3, Sticky and LilyGo T5 are ESP32-S3, M5Paper v1.1 is a classic ESP32. The Sticky env ships in `platformio.ini` (`pio run -e sticky`). Envs for other devices go in **`platformio.local.ini`**, a gitignored file that PlatformIO merges over `platformio.ini` (see `extra_configs`). Create it next to `platformio.ini`; personal envs, ports, and debug flags live there and never get committed.
|
||||
|
||||
Both envs below extend the repo's `[base]`, so they build against the `freeink-sdk` submodule with all the normal deps and scripts.
|
||||
|
||||
### M5Paper v1.1 (classic ESP32, IT8951 panel)
|
||||
|
||||
```ini
|
||||
[env:m5paper_v11]
|
||||
extends = base
|
||||
board = esp32dev
|
||||
board_build.mcu = esp32
|
||||
board_build.flash_mode = qio
|
||||
; CP2104 UART bridge: 921600 drops out on macOS after the stub baud switch
|
||||
upload_speed = 460800
|
||||
build_unflags =
|
||||
${base.build_unflags}
|
||||
; classic ESP32 has UART serial, not USB CDC; Logging.h keys off these
|
||||
-DARDUINO_USB_MODE=1
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_M5PAPER=1
|
||||
; the 63KB 540x960 framebuffer lives in PSRAM (FREEINK_FB_PSRAM auto-on)
|
||||
-DBOARD_HAS_PSRAM
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-m5paper\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2
|
||||
; touch-first device: hide front-button hint labels
|
||||
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
|
||||
; archive-scan-order workaround: without these a full relink drops Wire's i2c symbols
|
||||
-Wl,-u,i2cInit
|
||||
-Wl,-u,i2cSlaveInit
|
||||
```
|
||||
|
||||
### LilyGo T5 S3 (ESP32-S3, controller-less panel via LovyanGFX)
|
||||
|
||||
```ini
|
||||
[env:lilygo_t5s3]
|
||||
extends = base
|
||||
board = esp32-s3-devkitc1-n16r8
|
||||
board_build.mcu = esp32s3
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_LILYGO=1
|
||||
; board injects the parallel-bus pins + PMIC power hooks (BoardT5S3)
|
||||
-DFREEINK_LGFX_EPD_CONFIG=lilygoT5S3LgfxConfig
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-lilygo\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2
|
||||
-DCROSSPOINT_SHOW_BUTTON_HINTS=0
|
||||
lib_deps =
|
||||
${base.lib_deps}
|
||||
; LgfxEpdConfig for the T5 S3 (pins, PCA9535/TPS65185 power sequence)
|
||||
BoardT5S3=symlink://freeink-sdk/libs/hardware/BoardT5S3
|
||||
; LovyanGFX Panel_EPD drives the controller-less ED047TC1 panel
|
||||
m5stack/M5GFX @ 0.2.20
|
||||
```
|
||||
|
||||
Then `pio run -e m5paper_v11 -t upload` (or `-e lilygo_t5s3`). Gotchas worth knowing:
|
||||
|
||||
- **Flash mode matters.** The M5Paper is `qio`; the X4-family standalone envs need `dio`. A wrong flash-mode header boots into a `partition 0 invalid magic number 0xffff` loop even though esptool verified the write.
|
||||
- **One `FREEINK_DEVICE_*` flag per env** selects the board profile (pins, panel, touch controller) from the SDK's `BoardConfig`. See `freeink-sdk/platformio.sample.ini` for reference envs of every supported device.
|
||||
- **Serial logs:** `[base]` does not enable logging; without `-DENABLE_SERIAL_LOG` a non-default env prints nothing.
|
||||
- No touch hardware on your desk? The X4 build still exercises the same code paths through buttons; touch-specific behavior (tap zones, gestures) needs a real device.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Dictionary
|
||||
|
||||
Look up words while reading an EPUB using an offline StarDict dictionary stored on the SD card.
|
||||
|
||||
## Supported Format
|
||||
|
||||
The reader supports **StarDict** dictionaries. When searching for dictionaries online, look for "StarDict format" or files with `.dict`, `.idx`, and `.ifo` extensions.
|
||||
|
||||
A dictionary folder must contain:
|
||||
|
||||
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
|
||||
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
|
||||
- `.ifo` — metadata (optional)
|
||||
|
||||
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
|
||||
|
||||
## Setting Up a Dictionary
|
||||
|
||||
1. Copy your dictionary folder(s) to `/dictionaries/` on the SD card — one dictionary per folder, e.g. `/dictionaries/webster/webster.idx` + `webster.dict.dz`. A hidden `/.dictionaries/` folder (dot-prefixed) works the same way, for keeping it out of the file browser.
|
||||
2. Open **Settings → Reader → Dictionary** on the device.
|
||||
3. Select a dictionary from the list, or **None** to disable lookups.
|
||||
|
||||
The Dictionary setting only appears when at least one usable dictionary folder exists. Folders containing more than one dictionary (multiple `.idx` stems) are skipped as ambiguous.
|
||||
|
||||
## Looking Up a Word
|
||||
|
||||
Two ways to start a lookup while reading:
|
||||
|
||||
- Open the reader menu (**Confirm**) and choose **Look Up**.
|
||||
- Or set **Settings → Controls → Long-press Menu** to "Dictionary", then hold **Confirm** (~0.4s) on the reading page.
|
||||
|
||||
One word on the page becomes highlighted:
|
||||
|
||||
1. Use **Left/Right** to move between words in reading order, and the side **Up/Down** buttons to jump between lines.
|
||||
2. Press **Confirm** to look up the highlighted word.
|
||||
3. Press **Back** to return to the reader.
|
||||
|
||||
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
|
||||
|
||||
### How Lookup Works
|
||||
|
||||
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
|
||||
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
|
||||
3. **Not found** — a short popup appears and you return to word selection.
|
||||
|
||||
## The Definition Screen
|
||||
|
||||
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
|
||||
|
||||
- **Left/Right** or side **Up/Down** — previous / next page
|
||||
- **Back** — return to word selection
|
||||
+26
-8
@@ -90,13 +90,19 @@ if (parsedSize != fileSize) {
|
||||
|
||||
## `section.bin`
|
||||
|
||||
### Version 25
|
||||
### Version 30
|
||||
|
||||
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 30 is binary-identical to version 29. The version was bumped because
|
||||
Arabic contextual shaping changed text measurement (`getTextAdvanceX` now
|
||||
measures the shaped visual text), so word positions cached by v29 no longer
|
||||
match what `drawText` renders.
|
||||
|
||||
Version 28 introduced serialized word style bits for underline, strikethrough,
|
||||
superscript, and subscript. The format also includes:
|
||||
|
||||
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
|
||||
image rendering mode, and Focus Reading
|
||||
@@ -105,6 +111,12 @@ 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
|
||||
- flat TextBlock word storage (v29): per-word arrays plus one shared
|
||||
NUL-terminated text blob, replacing v28's length-prefixed word strings. The
|
||||
on-disk order mirrors the in-RAM arena so the firmware reads a whole block
|
||||
payload with a single allocation and a single SD read
|
||||
|
||||
ImHex pattern:
|
||||
|
||||
@@ -113,7 +125,7 @@ import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
#define EXPECTED_VERSION 25
|
||||
#define EXPECTED_VERSION 30
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
@@ -174,14 +186,20 @@ struct BlockStyle {
|
||||
|
||||
struct TextBlock {
|
||||
u16 wordCount;
|
||||
String words[wordCount];
|
||||
s16 wordXPos[wordCount];
|
||||
WordStyle wordStyle[wordCount];
|
||||
|
||||
u8 hasFocus;
|
||||
u16 textBytes [[comment("Total size of text[], including one NUL per word")]];
|
||||
|
||||
if (wordCount > 0) {
|
||||
u16 textOff[wordCount] [[comment("Byte offset of word i's text within text[]")]];
|
||||
s16 wordXPos[wordCount];
|
||||
if (hasFocus != 0) {
|
||||
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
||||
}
|
||||
WordStyle wordStyle[wordCount];
|
||||
if (hasFocus != 0) {
|
||||
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
|
||||
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
|
||||
}
|
||||
char text[textBytes] [[comment("All words back to back, each NUL-terminated")]];
|
||||
}
|
||||
|
||||
BlockStyle blockStyle;
|
||||
|
||||
+8
-3
@@ -32,9 +32,14 @@ networks or in hotspot mode when you control who is connected.
|
||||
## Join Network Mode
|
||||
|
||||
1. Select **Join Network**.
|
||||
2. Pick a 2.4 GHz Wi-Fi network from the scan results.
|
||||
3. Enter the password if prompted.
|
||||
4. Save credentials if you want the reader to reconnect automatically next time.
|
||||
2. If you have saved Wi-Fi credentials, CrossPoint first tries the last
|
||||
connected network, then other visible saved networks in signal-strength
|
||||
order. Press **Back** to cancel or **Confirm** to stop auto-connect and show
|
||||
the network list.
|
||||
3. If the network list is shown, pick a 2.4 GHz Wi-Fi network from the scan
|
||||
results.
|
||||
4. Enter the password if prompted.
|
||||
5. Save credentials if you want the reader to reconnect automatically next time.
|
||||
|
||||
After connection, the reader shows:
|
||||
|
||||
|
||||
+1
-1
Submodule freeink-sdk updated: 8be3236609...566fce3d4f
@@ -44,15 +44,16 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
continue;
|
||||
}
|
||||
|
||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
|
||||
const combiningMark::Anchor anchor = combiningMark::anchorFor(cp);
|
||||
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(anchor, glyph->top, glyph->height, lastBaseTop) : 0;
|
||||
|
||||
if (!isCombining && prevCp != 0) {
|
||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||
}
|
||||
|
||||
const int glyphBaseX =
|
||||
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
|
||||
const int glyphBaseX = isCombining ? combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||
glyph->left, glyph->width)
|
||||
: lastBaseX;
|
||||
const int glyphBaseY = startY - raiseBy;
|
||||
|
||||
|
||||
+76
-11
@@ -36,24 +36,71 @@ namespace combiningMark {
|
||||
|
||||
constexpr int MIN_GAP_PX = 1;
|
||||
|
||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||
/// is visually centered over the base glyph's bitmap.
|
||||
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
||||
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
|
||||
/// Placement of a mark relative to its base glyph. The default heuristic —
|
||||
/// centered over the base, raised clear of its top — suits Latin diacritics
|
||||
/// and Arabic harakat, but misplaces the Hebrew niqqud whose identity depends
|
||||
/// on position: dagesh sits inside the letter body, the shin/sin dots
|
||||
/// distinguish the letter by sitting over its right/left arm, and holam hangs
|
||||
/// over the left corner. "Native" anchors keep the glyph's font-designed
|
||||
/// height (which may overlap the base) instead of raising it.
|
||||
enum class Anchor : uint8_t {
|
||||
CenterRaised, ///< centered over the base, lifted above its top (default)
|
||||
CenterNative, ///< centered over the base at font-native height
|
||||
RightNative, ///< right edges aligned, font-native height
|
||||
LeftNative, ///< left edges aligned, font-native height
|
||||
};
|
||||
|
||||
constexpr Anchor anchorFor(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case 0x05BC: // dagesh / mapiq / shuruk dot: inside the letter body
|
||||
case 0x05BA: // holam haser for vav: straight above the vav stem
|
||||
return Anchor::CenterNative;
|
||||
case 0x05C1: // shin dot: over the letter's right arm
|
||||
return Anchor::RightNative;
|
||||
case 0x05B9: // holam: above the letter's left corner
|
||||
case 0x05C2: // sin dot: over the letter's left arm
|
||||
return Anchor::LeftNative;
|
||||
default:
|
||||
return Anchor::CenterRaised;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotated-90CW variant of centerOver. In the rotated coordinate system
|
||||
/// Horizontal offset from the base bitmap's left edge to the mark bitmap's
|
||||
/// left edge for a given anchor.
|
||||
constexpr int anchorShift(const Anchor anchor, const int baseWidth, const int markWidth) {
|
||||
switch (anchor) {
|
||||
case Anchor::LeftNative:
|
||||
return 0;
|
||||
case Anchor::RightNative:
|
||||
return baseWidth - markWidth;
|
||||
default:
|
||||
return baseWidth / 2 - markWidth / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the cursor-X at which to render a combining mark so its bitmap
|
||||
/// lands at its anchor position over the base glyph's bitmap.
|
||||
constexpr int anchorOver(const Anchor anchor, const int baseCursorPos, const int baseLeft, const int baseWidth,
|
||||
const int markLeft, const int markWidth) {
|
||||
return baseCursorPos + baseLeft + anchorShift(anchor, baseWidth, markWidth) - markLeft;
|
||||
}
|
||||
|
||||
/// Rotated-90CW variant of anchorOver. In the rotated coordinate system
|
||||
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
|
||||
/// every left/width term inverts sign.
|
||||
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
|
||||
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft;
|
||||
constexpr int anchorOverRotated90CW(const Anchor anchor, const int baseCursorPos, const int baseLeft,
|
||||
const int baseWidth, const int markLeft, const int markWidth) {
|
||||
return baseCursorPos - baseLeft - anchorShift(anchor, baseWidth, markWidth) + markLeft;
|
||||
}
|
||||
|
||||
/// For combining marks that sit entirely above the baseline, compute how many
|
||||
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
|
||||
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
|
||||
/// below the baseline (e.g. cedilla, dot-below, ogonek).
|
||||
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
||||
/// below the baseline (e.g. cedilla, dot-below, ogonek) and for anchors that
|
||||
/// keep the font-native height (dagesh must stay inside the letter, the
|
||||
/// shin/sin dots touch its arms).
|
||||
constexpr int raiseAboveBase(const Anchor anchor, const int markTop, const int markHeight, const int baseTop) {
|
||||
if (anchor != Anchor::CenterRaised) return 0;
|
||||
if (markTop - markHeight <= 0) return 0;
|
||||
const int gap = markTop - markHeight - baseTop;
|
||||
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
|
||||
@@ -61,6 +108,20 @@ constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
|
||||
|
||||
} // namespace combiningMark
|
||||
|
||||
/// GCC/Clang (the ESP32 firmware toolchain) pack structs with __attribute__((packed)).
|
||||
/// MSVC (host unit tests) has no equivalent attribute and instead needs a #pragma pack
|
||||
/// region achieving the same 1-byte alignment. These macros keep the on-disk font layout
|
||||
/// identical across both toolchains.
|
||||
#if defined(_MSC_VER)
|
||||
#define EPD_PACKED_BEGIN __pragma(pack(push, 1))
|
||||
#define EPD_PACKED_END __pragma(pack(pop))
|
||||
#define EPD_PACKED_ATTR
|
||||
#else
|
||||
#define EPD_PACKED_BEGIN
|
||||
#define EPD_PACKED_END
|
||||
#define EPD_PACKED_ATTR __attribute__((packed))
|
||||
#endif
|
||||
|
||||
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
|
||||
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
|
||||
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
|
||||
@@ -95,17 +156,21 @@ typedef struct {
|
||||
|
||||
/// Maps a codepoint to a kerning class ID, sorted by codepoint for binary search.
|
||||
/// Class IDs are 1-based; codepoints not in the table have implicit class 0 (no kerning).
|
||||
EPD_PACKED_BEGIN
|
||||
typedef struct {
|
||||
uint16_t codepoint; ///< Unicode codepoint
|
||||
uint8_t classId; ///< 1-based kerning class ID
|
||||
} __attribute__((packed)) EpdKernClassEntry;
|
||||
} EPD_PACKED_ATTR EpdKernClassEntry;
|
||||
EPD_PACKED_END
|
||||
|
||||
/// Ligature substitution for a specific glyph pair, sorted by `pair` for binary search.
|
||||
/// `pair` encodes (leftCodepoint << 16 | rightCodepoint) for single-key lookup.
|
||||
EPD_PACKED_BEGIN
|
||||
typedef struct {
|
||||
uint32_t pair; ///< Packed codepoint pair (left << 16 | right)
|
||||
uint32_t ligatureCp; ///< Codepoint of the replacement ligature glyph
|
||||
} __attribute__((packed)) EpdLigaturePair;
|
||||
} EPD_PACKED_ATTR EpdLigaturePair;
|
||||
EPD_PACKED_END
|
||||
|
||||
/// Data stored for FONT AS A WHOLE
|
||||
typedef struct {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() {
|
||||
}
|
||||
|
||||
void FontDecompressor::freeHotGroup() {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
free(hotGroup);
|
||||
hotGroup = nullptr;
|
||||
hotGroupCapacity = 0;
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
hotGlyphBuf.clear();
|
||||
hotGlyphBuf.shrink_to_fit();
|
||||
free(hotGlyphBuf);
|
||||
hotGlyphBuf = nullptr;
|
||||
hotGlyphBufCapacity = 0;
|
||||
}
|
||||
|
||||
bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) {
|
||||
if (capacity >= needed) return true;
|
||||
// Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the
|
||||
// old contents are dead -- freeing first gives the allocator its best shot on a tight heap.
|
||||
free(buf);
|
||||
buf = static_cast<uint8_t*>(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup()
|
||||
capacity = buf ? needed : 0;
|
||||
return buf != nullptr;
|
||||
}
|
||||
|
||||
uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) {
|
||||
@@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
||||
}
|
||||
|
||||
// Check if hot group already has this group decompressed — if not, decompress it
|
||||
if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) {
|
||||
stats.cacheMisses++;
|
||||
const EpdFontGroup& group = fontData->groups[groupIndex];
|
||||
|
||||
hotGroup.resize(group.uncompressedSize);
|
||||
if (hotGroup.empty()) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||
// ensureCapacity may free the buffer, so the cached-group identity dies with it either way.
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) {
|
||||
hotGroup.clear();
|
||||
hotGroup.shrink_to_fit();
|
||||
hotGroupFont = nullptr;
|
||||
hotGroupIndex = UINT16_MAX;
|
||||
if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) {
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep
|
||||
}
|
||||
|
||||
// Compact just the requested glyph from byte-aligned data into scratch buffer
|
||||
if (glyph->dataLength > hotGlyphBuf.size()) {
|
||||
hotGlyphBuf.resize(glyph->dataLength);
|
||||
}
|
||||
if (hotGlyphBuf.empty()) {
|
||||
if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) {
|
||||
LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height);
|
||||
compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height);
|
||||
stats.getBitmapTimeUs += micros() - tStart;
|
||||
return hotGlyphBuf.data();
|
||||
return hotGlyphBuf;
|
||||
}
|
||||
|
||||
// --- Prewarm: pre-decompress glyph bitmaps for a page of text ---
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <InflateReader.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class FontDecompressor {
|
||||
@@ -67,13 +65,22 @@ class FontDecompressor {
|
||||
|
||||
// Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path.
|
||||
// Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf.
|
||||
// Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path,
|
||||
// and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of
|
||||
// failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free).
|
||||
// ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully.
|
||||
const EpdFontData* hotGroupFont = nullptr;
|
||||
uint16_t hotGroupIndex = UINT16_MAX;
|
||||
std::vector<uint8_t> hotGroup;
|
||||
uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor
|
||||
uint32_t hotGroupCapacity = 0;
|
||||
|
||||
// Scratch buffer for compacting a single glyph from the hot group.
|
||||
// Valid until the next getBitmap() call.
|
||||
std::vector<uint8_t> hotGlyphBuf;
|
||||
// Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup.
|
||||
uint8_t* hotGlyphBuf = nullptr;
|
||||
uint32_t hotGlyphBufCapacity = 0;
|
||||
|
||||
// Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed.
|
||||
static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed);
|
||||
|
||||
void freePageBuffer();
|
||||
void freeHotGroup();
|
||||
|
||||
@@ -1195,7 +1195,8 @@ int SdCardFont::fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCoun
|
||||
}
|
||||
|
||||
template <typename Iter>
|
||||
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask) {
|
||||
int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||
const char* extraText) {
|
||||
if (!loaded_) return -1;
|
||||
styleMask = resolveStyleMask(styleMask);
|
||||
if (styleMask == 0) return 0;
|
||||
@@ -1215,6 +1216,9 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
||||
for (auto it = begin; it != end && !hitCap; ++it) {
|
||||
hitCap = collectUniqueCodepoints(asCStr(*it), codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||
}
|
||||
if (extraText && !hitCap) {
|
||||
hitCap = collectUniqueCodepoints(extraText, codepoints, cpCount, MAX_UNIQUE_CODEPOINTS);
|
||||
}
|
||||
|
||||
if (includeSpace && std::none_of(codepoints, codepoints + cpCount, [](uint32_t c) { return c == ' '; }))
|
||||
codepoints[cpCount++] = ' ';
|
||||
@@ -1232,12 +1236,13 @@ int SdCardFont::buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace,
|
||||
return totalMissed;
|
||||
}
|
||||
|
||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask) {
|
||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask);
|
||||
int SdCardFont::buildAdvanceTable(const char* utf8Text, uint8_t styleMask, const char* extraText) {
|
||||
return buildAdvanceTableRange(&utf8Text, &utf8Text + 1, false, false, styleMask, extraText);
|
||||
}
|
||||
|
||||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask) {
|
||||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask);
|
||||
int SdCardFont::buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask,
|
||||
const char* extraText) {
|
||||
return buildAdvanceTableRange(words.begin(), words.end(), words.size() > 1, includeHyphen, styleMask, extraText);
|
||||
}
|
||||
|
||||
// --- Stats ---
|
||||
|
||||
@@ -47,9 +47,12 @@ class SdCardFont {
|
||||
// Build a compact advance-only table for layout measurement.
|
||||
// Extracts ALL unique codepoints from words (no MAX_PAGE_GLYPHS cap),
|
||||
// batch-reads advanceX from SD, stores in a sorted per-style table.
|
||||
// extraText: optional additional codepoints to warm in the same SD pass
|
||||
// (e.g. shaped Arabic presentation forms the measurement path will look up).
|
||||
// Returns number of codepoints not found in font coverage.
|
||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F);
|
||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F);
|
||||
int buildAdvanceTable(const char* utf8Text, uint8_t styleMask = 0x0F, const char* extraText = nullptr);
|
||||
int buildAdvanceTable(const std::vector<std::string>& words, bool includeHyphen, uint8_t styleMask = 0x0F,
|
||||
const char* extraText = nullptr);
|
||||
|
||||
// Look up advanceX for a codepoint from the advance table.
|
||||
// Returns the 12.4 fixed-point advance, or 0 if not found.
|
||||
@@ -140,11 +143,13 @@ class SdCardFont {
|
||||
|
||||
// Full intervals loaded from file (kept in RAM for codepoint lookup)
|
||||
EpdUnicodeInterval* fullIntervals = nullptr;
|
||||
EPD_PACKED_BEGIN
|
||||
struct BmpInterval16 {
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
uint16_t offset;
|
||||
} __attribute__((packed));
|
||||
} EPD_PACKED_ATTR;
|
||||
EPD_PACKED_END
|
||||
static_assert(sizeof(BmpInterval16) == 6, "BmpInterval16 must remain compact");
|
||||
BmpInterval16* bmpIntervals = nullptr;
|
||||
bool intervalsAreBmp16 = false;
|
||||
@@ -249,7 +254,8 @@ class SdCardFont {
|
||||
int32_t findGlobalGlyphIndex(const PerStyle& s, uint32_t codepoint) const;
|
||||
int fetchAdvancesForCodepoints(uint32_t* codepoints, uint32_t cpCount, uint8_t styleMask);
|
||||
template <typename Iter>
|
||||
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask);
|
||||
int buildAdvanceTableRange(Iter begin, Iter end, bool includeSpace, bool includeHyphen, uint8_t styleMask,
|
||||
const char* extraText = nullptr);
|
||||
int prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint32_t cpCount, bool metadataOnly);
|
||||
|
||||
// Global helpers
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
||||
!NotoSerif/**
|
||||
!NotoSans/
|
||||
!NotoSans/**
|
||||
!NotoSansArabic/
|
||||
!NotoSansArabic/**
|
||||
!NotoSansHebrew/
|
||||
!NotoSansHebrew/**
|
||||
!OpenDyslexic/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
+1872
-1256
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2244
-1483
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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}...")
|
||||
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)
|
||||
except Exception as e:
|
||||
break
|
||||
except Exception as e: # noqa: BLE001 - reported via RuntimeError below
|
||||
last_err = e
|
||||
dest.unlink(missing_ok=True)
|
||||
raise RuntimeError(f"Failed to download {url}: {e}") from e
|
||||
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
|
||||
|
||||
@@ -31,19 +31,48 @@ done
|
||||
UI_FONT_SIZES=(10 12)
|
||||
UI_FONT_STYLES=("Regular" "Bold")
|
||||
|
||||
# Arabic glyphs for UI text (menus, file browser titles). The built-in fonts
|
||||
# must cover the *output* of MiniBidi's do_shape() — contextual presentation
|
||||
# forms — not base letters, or shaped UI text silently drops glyphs.
|
||||
# Curated for firmware-size budget: core Arabic (Presentation Forms-B,
|
||||
# incl. the Lam-Alef ligature forms) plus the Farsi/Urdu extra letters'
|
||||
# Presentation Forms-A blocks, the few characters shaping leaves at their
|
||||
# base codepoint, Arabic punctuation, and both digit sets. No harakat and
|
||||
# no Sindhi/Pashto/Kurdish forms — book text gets those from SD-card fonts.
|
||||
ARABIC_INTERVALS=(
|
||||
--additional-intervals 0x060C,0x060C # Arabic comma
|
||||
--additional-intervals 0x061B,0x061B # Arabic semicolon
|
||||
--additional-intervals 0x061F,0x061F # Arabic question mark
|
||||
--additional-intervals 0x0621,0x0621 # hamza (non-joining, never shaped)
|
||||
--additional-intervals 0x0640,0x0640 # tatweel
|
||||
--additional-intervals 0x0660,0x0669 # Arabic-Indic digits
|
||||
--additional-intervals 0x06BA,0x06BA # noon ghunna base (initial/medial keep base cp)
|
||||
--additional-intervals 0x06D4,0x06D4 # Urdu full stop
|
||||
--additional-intervals 0x06F0,0x06F9 # extended Arabic-Indic digits (Farsi/Urdu)
|
||||
--additional-intervals 0xFB56,0xFB59 # peh (Farsi)
|
||||
--additional-intervals 0xFB66,0xFB69 # tteh (Urdu)
|
||||
--additional-intervals 0xFB7A,0xFB7D # tcheh (Farsi)
|
||||
--additional-intervals 0xFB88,0xFB95 # ddal, jeh, rreh (Urdu), keheh, gaf (Farsi/Urdu)
|
||||
--additional-intervals 0xFB9E,0xFB9F # noon ghunna isolated/final (Urdu)
|
||||
--additional-intervals 0xFBA6,0xFBB1 # heh goal, heh doachashmee, yeh barree(+hamza) (Urdu)
|
||||
--additional-intervals 0xFBFC,0xFBFF # farsi yeh (Farsi/Urdu)
|
||||
--additional-intervals 0xFE80,0xFEFC # Presentation Forms-B: core Arabic + Lam-Alef
|
||||
)
|
||||
|
||||
for size in ${UI_FONT_SIZES[@]}; do
|
||||
for style in ${UI_FONT_STYLES[@]}; do
|
||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
||||
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
||||
arabic_path="../builtinFonts/source/NotoSansArabic/NotoSansArabic-${style}.ttf"
|
||||
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
||||
# (fontstack is ordered by descending priority).
|
||||
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
||||
--additional-intervals 0x05D0,0x05EA > $output_path
|
||||
python fontconvert.py $font_name $size $font_path $hebrew_path $arabic_path $viet_path \
|
||||
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > $output_path
|
||||
echo "Generated $output_path"
|
||||
done
|
||||
done
|
||||
@@ -51,7 +80,8 @@ done
|
||||
python fontconvert.py notosans_8_regular 8 \
|
||||
../builtinFonts/source/NotoSans/NotoSans-Regular.ttf \
|
||||
../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-Regular.ttf \
|
||||
--additional-intervals 0x05D0,0x05EA > ../builtinFonts/notosans_8_regular.h
|
||||
../builtinFonts/source/NotoSansArabic/NotoSansArabic-Regular.ttf \
|
||||
--additional-intervals 0x05D0,0x05EA "${ARABIC_INTERVALS[@]}" > ../builtinFonts/notosans_8_regular.h
|
||||
|
||||
echo ""
|
||||
echo "Running compression verification..."
|
||||
|
||||
@@ -133,6 +133,15 @@ families:
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
+19
-59
@@ -153,18 +153,11 @@ bool Epub::parseTocNcxFile() const {
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||
|
||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||
HalFile tempNcxFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
size_t ncxSize;
|
||||
if (!getItemSize(tocNcxItem, &ncxSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of toc ncx file");
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempNcxFile.close();
|
||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto ncxSize = tempNcxFile.size();
|
||||
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||
|
||||
@@ -173,29 +166,13 @@ bool Epub::parseTocNcxFile() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!ncxBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||
// Stream the decompressed NCX straight into the parser instead of round-tripping
|
||||
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read toc ncx file");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (tempNcxFile.available()) {
|
||||
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
||||
if (readSize == 0) break;
|
||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||
free(ncxBuffer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
free(ncxBuffer);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempNcxFile.close();
|
||||
Storage.remove(tmpNcxPath.c_str());
|
||||
|
||||
LOG_DBG("EBP", "Parsed TOC items");
|
||||
return true;
|
||||
}
|
||||
@@ -209,18 +186,11 @@ bool Epub::parseTocNavFile() const {
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||
|
||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||
HalFile tempNavFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
size_t navSize;
|
||||
if (!getItemSize(tocNavItem, &navSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of toc nav file");
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempNavFile.close();
|
||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto navSize = tempNavFile.size();
|
||||
|
||||
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||
// and the HTMLX nav file will have hrefs relative to itself
|
||||
@@ -232,28 +202,13 @@ bool Epub::parseTocNavFile() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!navBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||
// Stream the decompressed nav document straight into the parser instead of round-tripping
|
||||
// through a temp file on the SD card (decompress -> write -> reopen -> reread -> delete).
|
||||
if (!readItemContentsToStream(tocNavItem, navParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read toc nav file");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (tempNavFile.available()) {
|
||||
const auto readSize = tempNavFile.read(navBuffer, 1024);
|
||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||
free(navBuffer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
free(navBuffer);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempNavFile.close();
|
||||
Storage.remove(tmpNavPath.c_str());
|
||||
|
||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||
return true;
|
||||
}
|
||||
@@ -396,6 +351,11 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
Storage.removeDir((cachePath + "/sections").c_str());
|
||||
}
|
||||
}
|
||||
// Release the resolved CSS rule map: it is only needed transiently while building
|
||||
// section caches, and createSectionFile reloads it from cache on demand. Holding it
|
||||
// resident pins tens of KB for the whole reading session (more on warm resume into
|
||||
// an already-cached chapter, where createSectionFile never runs to clear it).
|
||||
cssParser->clear();
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "BookMetadataCache.h"
|
||||
|
||||
#include <BufferedFile.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
#include <Utf8.h>
|
||||
@@ -14,6 +15,52 @@ constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-com
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
// Buffer size for the buildBookBin streams. 3 buffers x 4KB, transient (freed on
|
||||
// return); 4KB = 8 SD sectors per transfer, enough to stop the sector-cache thrash.
|
||||
constexpr size_t BUILD_IO_BUFFER_SIZE = 4096;
|
||||
|
||||
// Entry (de)serializers, templated so they run over HalFile and the Buffered*
|
||||
// wrappers alike (two instantiations each -- a few hundred bytes of flash, in
|
||||
// exchange for the build path streaming at SD speed instead of per-pod).
|
||||
template <typename F>
|
||||
uint32_t writeSpineEntryTo(F& file, const BookMetadataCache::SpineEntry& entry) {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writePod(file, entry.cumulativeSize);
|
||||
serialization::writePod(file, entry.tocIndex);
|
||||
return pos;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
uint32_t writeTocEntryTo(F& file, const BookMetadataCache::TocEntry& entry) {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.title);
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writeString(file, entry.anchor);
|
||||
serialization::writePod(file, entry.level);
|
||||
serialization::writePod(file, entry.spineIndex);
|
||||
return pos;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
BookMetadataCache::SpineEntry readSpineEntryFrom(F& file) {
|
||||
BookMetadataCache::SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
return entry;
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
BookMetadataCache::TocEntry readTocEntryFrom(F& file) {
|
||||
BookMetadataCache::TocEntry entry;
|
||||
serialization::readString(file, entry.title);
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readString(file, entry.anchor);
|
||||
serialization::readPod(file, entry.level);
|
||||
serialization::readPod(file, entry.spineIndex);
|
||||
return entry;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||
@@ -30,13 +77,23 @@ bool BookMetadataCache::beginContentOpfPass() {
|
||||
LOG_DBG("BMC", "Beginning content opf pass");
|
||||
|
||||
// Open spine file for writing
|
||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
// Wrapper OOM is fine: createSpineEntry falls back to unbuffered writes.
|
||||
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endContentOpfPass() {
|
||||
const bool flushed = !passOut || passOut->flush();
|
||||
passOut.reset();
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
spineFile.close();
|
||||
return true;
|
||||
if (!flushed) {
|
||||
LOG_ERR("BMC", "Failed writing spine tmp file");
|
||||
}
|
||||
return flushed;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
@@ -74,10 +131,17 @@ bool BookMetadataCache::beginTocPass() {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
|
||||
// Wrapper OOM is fine: createTocEntry falls back to unbuffered writes.
|
||||
passOut = makeUniqueNoThrow<serialization::BufferedFileWriter>(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endTocPass() {
|
||||
const bool flushed = !passOut || passOut->flush();
|
||||
passOut.reset();
|
||||
if (!flushed) {
|
||||
LOG_ERR("BMC", "Failed writing toc tmp file");
|
||||
}
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
tocFile.close();
|
||||
spineFile.close();
|
||||
@@ -86,7 +150,7 @@ bool BookMetadataCache::endTocPass() {
|
||||
spineHrefIndex.shrink_to_fit();
|
||||
useSpineHrefIndex = false;
|
||||
|
||||
return true;
|
||||
return flushed;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endWrite() {
|
||||
@@ -119,6 +183,14 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
return false;
|
||||
}
|
||||
|
||||
// Buffered streams for the whole build: every access below is sequential per
|
||||
// file, but interleaved ACROSS files, which thrashes SdFat's single shared
|
||||
// sector cache when unbuffered (one 512B SD transaction per 4-byte pod --
|
||||
// measured 31s for a 1,732-spine omnibus). Three 4KB buffers, freed on return.
|
||||
serialization::BufferedFileWriter bookOut(bookFile, BUILD_IO_BUFFER_SIZE);
|
||||
serialization::BufferedFileReader spineIn(spineFile, BUILD_IO_BUFFER_SIZE);
|
||||
serialization::BufferedFileReader tocIn(tocFile, BUILD_IO_BUFFER_SIZE);
|
||||
|
||||
constexpr uint32_t headerASize =
|
||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||
@@ -128,31 +200,34 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
const uint32_t lutOffset = headerASize + metadataSize;
|
||||
|
||||
// Header A
|
||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookFile, lutOffset);
|
||||
serialization::writePod(bookFile, spineCount);
|
||||
serialization::writePod(bookFile, tocCount);
|
||||
serialization::writePod(bookOut, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookOut, lutOffset);
|
||||
serialization::writePod(bookOut, spineCount);
|
||||
serialization::writePod(bookOut, tocCount);
|
||||
// Metadata
|
||||
serialization::writeString(bookFile, metadata.title);
|
||||
serialization::writeString(bookFile, metadata.author);
|
||||
serialization::writeString(bookFile, metadata.language);
|
||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||
serialization::writeString(bookOut, metadata.title);
|
||||
serialization::writeString(bookOut, metadata.author);
|
||||
serialization::writeString(bookOut, metadata.language);
|
||||
serialization::writeString(bookOut, metadata.coverItemHref);
|
||||
serialization::writeString(bookOut, metadata.textReferenceHref);
|
||||
|
||||
// Loop through spine entries, writing LUT positions
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
uint32_t pos = spineFile.position();
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
||||
const uint32_t pos = spineIn.position();
|
||||
readSpineEntryFrom(spineIn);
|
||||
serialization::writePod(bookOut, pos + lutOffset + lutSize);
|
||||
}
|
||||
// Total size of the spine tmp file: entries land in book.bin after the toc LUT
|
||||
// and the full spine block, so toc LUT positions are offset by it.
|
||||
const auto spineBytes = static_cast<uint32_t>(spineIn.position());
|
||||
|
||||
// Loop through toc entries, writing LUT positions
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
uint32_t pos = tocFile.position();
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
||||
const uint32_t pos = tocIn.position();
|
||||
readTocEntryFrom(tocIn);
|
||||
serialization::writePod(bookOut, pos + lutOffset + lutSize + spineBytes);
|
||||
}
|
||||
|
||||
// LUTs complete
|
||||
@@ -160,9 +235,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
|
||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
auto tocEntry = readTocEntryFrom(tocIn);
|
||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||
@@ -197,9 +272,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
std::deque<ZipFile::SizeTarget> targets;
|
||||
targets.resize(spineCount);
|
||||
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
auto entry = readSpineEntryFrom(spineIn);
|
||||
std::string path = FsHelpers::normalisePath(entry.href);
|
||||
|
||||
ZipFile::SizeTarget t;
|
||||
@@ -224,10 +299,10 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
}
|
||||
|
||||
uint32_t cumSize = 0;
|
||||
spineFile.seek(0);
|
||||
spineIn.seek(0);
|
||||
int lastSpineTocIndex = -1;
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
auto spineEntry = readSpineEntryFrom(spineIn);
|
||||
|
||||
spineEntry.tocIndex = spineToTocIndex[i];
|
||||
|
||||
@@ -260,23 +335,33 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
writeSpineEntryTo(bookOut, spineEntry);
|
||||
}
|
||||
// Close opened zip file
|
||||
zip.close();
|
||||
|
||||
// Loop through toc entries from toc file writing to book.bin
|
||||
tocFile.seek(0);
|
||||
tocIn.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
writeTocEntry(bookFile, tocEntry);
|
||||
auto tocEntry = readTocEntryFrom(tocIn);
|
||||
writeTocEntryTo(bookOut, tocEntry);
|
||||
}
|
||||
|
||||
const bool written = bookOut.flush();
|
||||
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
|
||||
if (!written) {
|
||||
// A short write (card full/removed) would leave a truncated book.bin that
|
||||
// still passes the version check on load; remove it so the next open rebuilds.
|
||||
LOG_ERR("BMC", "Failed writing book.bin, removing truncated file");
|
||||
Storage.remove((cachePath + bookBinFile).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("BMC", "Successfully built book.bin");
|
||||
return true;
|
||||
}
|
||||
@@ -294,21 +379,11 @@ bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writePod(file, entry.cumulativeSize);
|
||||
serialization::writePod(file, entry.tocIndex);
|
||||
return pos;
|
||||
return writeSpineEntryTo(file, entry);
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.title);
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writeString(file, entry.anchor);
|
||||
serialization::writePod(file, entry.level);
|
||||
serialization::writePod(file, entry.spineIndex);
|
||||
return pos;
|
||||
return writeTocEntryTo(file, entry);
|
||||
}
|
||||
|
||||
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||
@@ -320,7 +395,11 @@ void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||
}
|
||||
|
||||
const SpineEntry entry(href, 0, -1);
|
||||
if (passOut) {
|
||||
writeSpineEntryTo(*passOut, entry);
|
||||
} else {
|
||||
writeSpineEntry(spineFile, entry);
|
||||
}
|
||||
spineCount++;
|
||||
}
|
||||
|
||||
@@ -368,7 +447,11 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
||||
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||
if (passOut) {
|
||||
writeTocEntryTo(*passOut, entry);
|
||||
} else {
|
||||
writeTocEntry(tocFile, entry);
|
||||
}
|
||||
tocCount++;
|
||||
}
|
||||
|
||||
@@ -442,19 +525,7 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||
SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
return entry;
|
||||
return readSpineEntryFrom(file);
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
||||
TocEntry entry;
|
||||
serialization::readString(file, entry.title);
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readString(file, entry.anchor);
|
||||
serialization::readPod(file, entry.level);
|
||||
serialization::readPod(file, entry.spineIndex);
|
||||
return entry;
|
||||
}
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const { return readTocEntryFrom(file); }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <BufferedFile.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class BookMetadataCache {
|
||||
@@ -54,6 +56,11 @@ class BookMetadataCache {
|
||||
// Temp file handles during build
|
||||
HalFile spineFile;
|
||||
HalFile tocFile;
|
||||
// Buffers the per-entry tmp-file writes during the OPF/TOC passes: those
|
||||
// writes interleave with zip-inflate SD reads, and unbuffered they thrash
|
||||
// SdFat's shared sector cache (one 512B transaction per 4-byte pod). One
|
||||
// wrapper serves whichever pass is active (spine, then toc).
|
||||
std::unique_ptr<serialization::BufferedFileWriter> passOut;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
|
||||
+32
-1
@@ -39,7 +39,17 @@ std::unique_ptr<PageLine> PageLine::deserialize(HalFile& file) {
|
||||
serialization::readPod(file, yPos);
|
||||
|
||||
auto tb = TextBlock::deserialize(file);
|
||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||
if (!tb) {
|
||||
LOG_ERR("PGE", "Deserialization failed: null TextBlock");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* line = new (std::nothrow) PageLine(std::move(tb), xPos, yPos);
|
||||
if (!line) {
|
||||
LOG_ERR("PGE", "Deserialization failed: could not allocate PageLine");
|
||||
return nullptr;
|
||||
}
|
||||
return std::unique_ptr<PageLine>(line);
|
||||
}
|
||||
|
||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
@@ -47,6 +57,10 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
|
||||
imageBlock->renderPlaceholder(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageImage::serialize(HalFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
@@ -115,6 +129,17 @@ void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffs
|
||||
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
void Page::renderWithImagePlaceholders(GfxRenderer& renderer, const int fontId, const int xOffset,
|
||||
const int yOffset) const {
|
||||
for (const auto& element : elements) {
|
||||
if (element->getTag() == TAG_PageImage) {
|
||||
static_cast<const PageImage&>(*element).renderPlaceholder(renderer, xOffset, yOffset);
|
||||
} else {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Page::serialize(HalFile& file) const {
|
||||
const uint16_t count = elements.size();
|
||||
serialization::writePod(file, count);
|
||||
@@ -155,9 +180,15 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
|
||||
|
||||
if (tag == TAG_PageLine) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
if (!pl) {
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else if (tag == TAG_PageImage) {
|
||||
auto pi = PageImage::deserialize(file);
|
||||
if (!pi) {
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(pi));
|
||||
} else if (tag == TAG_PageHorizontalRule) {
|
||||
auto rule = PageHorizontalRule::deserialize(file);
|
||||
|
||||
@@ -50,6 +50,7 @@ class PageImage final : public PageElement {
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
|
||||
bool serialize(HalFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
||||
@@ -89,6 +90,7 @@ class Page {
|
||||
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
void renderWithImagePlaceholders(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(HalFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(HalFile& file);
|
||||
|
||||
@@ -98,6 +100,13 @@ class Page {
|
||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
bool hasImagesNeedingDecode() const {
|
||||
return std::any_of(elements.begin(), elements.end(), [](const std::shared_ptr<PageElement>& element) {
|
||||
return element->getTag() == TAG_PageImage &&
|
||||
static_cast<const PageImage&>(*element).getImageBlock().needsDecode();
|
||||
});
|
||||
}
|
||||
|
||||
// Get bounding box of all images on the page (union of image rects)
|
||||
// Returns false if no images. Coordinates are relative to page origin.
|
||||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <BidiUtils.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -286,7 +287,30 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
effectiveNoSpaceBefore = true;
|
||||
}
|
||||
|
||||
const auto ensureTokenCapacity = [&](const size_t additionalTokens) {
|
||||
if (additionalTokens == 0) return;
|
||||
const size_t requiredSize = words.size() + additionalTokens;
|
||||
if (words.capacity() >= requiredSize) return;
|
||||
|
||||
size_t newCapacity = words.capacity();
|
||||
if (newCapacity < 16) {
|
||||
newCapacity = 16;
|
||||
}
|
||||
while (newCapacity < requiredSize) {
|
||||
newCapacity *= 2;
|
||||
}
|
||||
|
||||
words.reserve(newCapacity);
|
||||
wordStyles.reserve(newCapacity);
|
||||
wordContinues.reserve(newCapacity);
|
||||
wordNoSpaceBefore.reserve(newCapacity);
|
||||
wordIsFocusSuffix.reserve(newCapacity);
|
||||
};
|
||||
|
||||
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
|
||||
// CJK-heavy paragraphs can push hundreds of tiny tokens quickly when CSS toggles
|
||||
// inline styles. Reserve once up front to avoid repeated vector growth reallocations.
|
||||
ensureTokenCapacity(breakOffsets.size() + 1);
|
||||
bool firstToken = true;
|
||||
size_t tokenStart = 0;
|
||||
for (const size_t breakOffset : breakOffsets) {
|
||||
@@ -325,29 +349,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
|
||||
|
||||
// --- FOCUS READING LOGIC BELOW ---
|
||||
|
||||
// Pre-reserve capacity to prevent mid-word heap reallocations.
|
||||
size_t maxPossibleNewTokens = word.length();
|
||||
size_t requiredSize = words.size() + maxPossibleNewTokens;
|
||||
|
||||
if (words.capacity() < requiredSize) {
|
||||
// Emulate standard geometric growth (doubling) to ensure we don't reallocate on every word.
|
||||
size_t newCapacity = words.capacity() * 2;
|
||||
|
||||
// Ensure the doubled capacity is actually enough for this specific word
|
||||
if (newCapacity < requiredSize) {
|
||||
newCapacity = requiredSize;
|
||||
}
|
||||
// Set a sensible minimum starting size so the first few words don't trigger tiny reallocations
|
||||
if (newCapacity < 16) {
|
||||
newCapacity = 16;
|
||||
}
|
||||
|
||||
words.reserve(newCapacity);
|
||||
wordStyles.reserve(newCapacity);
|
||||
wordContinues.reserve(newCapacity);
|
||||
wordNoSpaceBefore.reserve(newCapacity);
|
||||
wordIsFocusSuffix.reserve(newCapacity);
|
||||
}
|
||||
// Worst case: a segment boundary on each byte (highly punctuated UTF-8 text).
|
||||
ensureTokenCapacity(word.length());
|
||||
|
||||
// Lambda helper to process and push individual sub-segments of the string
|
||||
// Use std::string_view to avoid heap allocations when slicing
|
||||
@@ -1133,8 +1136,14 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
|
||||
if (!lineHasFocusSplit) {
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
|
||||
std::vector<uint8_t>{}, std::vector<uint16_t>{}, blockStyle));
|
||||
// TextBlock flattens the vectors into its arena; they stay owned here and die at return.
|
||||
auto block = std::make_shared<TextBlock>(lineWords, lineXPos, lineWordStyles, std::vector<uint8_t>{},
|
||||
std::vector<uint16_t>{}, blockStyle);
|
||||
if (!block->valid()) {
|
||||
LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed");
|
||||
return;
|
||||
}
|
||||
processLine(std::move(block));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1179,6 +1188,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
}
|
||||
|
||||
processLine(std::make_shared<TextBlock>(std::move(outWords), std::move(outXPos), std::move(outStyles),
|
||||
std::move(outBoundaries), std::move(outSuffixX), blockStyle));
|
||||
auto block = std::make_shared<TextBlock>(outWords, outXPos, outStyles, outBoundaries, outSuffixX, blockStyle);
|
||||
if (!block->valid()) {
|
||||
LOG_ERR("PTX", "Dropping line: TextBlock arena allocation failed");
|
||||
return;
|
||||
}
|
||||
processLine(std::move(block));
|
||||
}
|
||||
|
||||
+500
-87
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "Epub/css/CssParser.h"
|
||||
@@ -10,34 +11,68 @@
|
||||
#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.
|
||||
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||
// v30: Arabic shaping changed both drawing and measurement (getTextAdvanceX now
|
||||
// measures the shaped visual text); cached word positions from v29 no longer
|
||||
// match what drawText renders.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 31;
|
||||
// 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.
|
||||
// MUST change in lockstep with SECTION_FILE_VERSION: the sentinel IS the partial's
|
||||
// format version, so a stale-format partial otherwise passes the header check and
|
||||
// only fails (noisily, via the block-decode error path) when a page is loaded.
|
||||
// Derived so the pairing can't be forgotten: 0xFE for v28, 0xFD for v29, ...
|
||||
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE - (SECTION_FILE_VERSION - 28);
|
||||
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 +91,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 +120,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 +166,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 +221,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,10 +265,23 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
Storage.mkdir(sectionsDir.c_str());
|
||||
}
|
||||
|
||||
// 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 success = false;
|
||||
bool streamed = false;
|
||||
uint32_t fileSize = 0;
|
||||
for (int attempt = 0; attempt < 3 && !success; attempt++) {
|
||||
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
|
||||
@@ -181,46 +296,72 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||
continue;
|
||||
}
|
||||
success = epub->readItemContentsToStream(localPath, tmpHtml, 1024);
|
||||
// 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 (!success && Storage.exists(tmpHtmlPath.c_str())) {
|
||||
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
if (!streamed) {
|
||||
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)) {
|
||||
// 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 (!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()) {
|
||||
ctx->cssParser = epub->getCssParser();
|
||||
if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
|
||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect TOC anchors for this spine so the parser can insert page breaks at chapter boundaries
|
||||
std::vector<std::string> tocAnchors;
|
||||
@@ -235,113 +376,376 @@ 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) {
|
||||
const auto& line = static_cast<const PageLine&>(*el);
|
||||
if (line.getBlock()) {
|
||||
const auto& words = line.getBlock()->getWords();
|
||||
for (const auto& w : words) {
|
||||
const auto& block = *line.getBlock();
|
||||
for (uint16_t i = 0; i < block.wordCount(); i++) {
|
||||
if (!fullText.empty()) fullText += " ";
|
||||
fullText += w;
|
||||
fullText += block.wordText(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,6 +765,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
@@ -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;
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@ struct BlockStyle {
|
||||
bool isRtl = false; // true if resolved direction is RTL
|
||||
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
||||
|
||||
// Set when this block was created by a <br> element. Used by startNewTextBlock to inject
|
||||
// a full line-height gap when the <br> block stays empty (section-break use case).
|
||||
// NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
|
||||
bool fromBrElement = false;
|
||||
|
||||
// Combined insets (margin + padding)
|
||||
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
|
||||
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
|
||||
@@ -92,6 +97,9 @@ struct BlockStyle {
|
||||
result.directionDefined = true;
|
||||
}
|
||||
|
||||
// fromBrElement is consumed by startNewTextBlock when an empty <br> block
|
||||
// is merged with the following paragraph; never propagate it further.
|
||||
result.fromBrElement = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Epub/converters/DirectPixelWriter.h"
|
||||
#include "Epub/converters/ImageDecoderFactory.h"
|
||||
|
||||
@@ -29,6 +31,54 @@ std::string getCachePath(const std::string& imagePath) {
|
||||
return imagePath + ".pxc";
|
||||
}
|
||||
|
||||
bool readValidCacheHeader(HalFile& cacheFile, const int expectedWidth, const int expectedHeight, uint16_t& cachedWidth,
|
||||
uint16_t& cachedHeight) {
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
const int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t bytesPerRow = (cachedWidth + 3) / 4;
|
||||
const size_t expectedSize = 4 + bytesPerRow * cachedHeight;
|
||||
return cacheFile.size() >= expectedSize;
|
||||
}
|
||||
|
||||
// Pages are deserialized afresh on each visit. Keep a bounded, allocation-free
|
||||
// record so an image that failed renders its placeholder directly for the rest
|
||||
// of the reader session instead of paying another placeholder refresh and
|
||||
// decode. The reader clears this on entry so transient memory/storage failures
|
||||
// are retried.
|
||||
constexpr size_t MAX_SESSION_IMAGE_FAILURES = 16;
|
||||
uint64_t failedImageHashes[MAX_SESSION_IMAGE_FAILURES];
|
||||
size_t failedImageCount = 0;
|
||||
|
||||
uint64_t imagePathHash(const std::string& path) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (const char c : path) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool imageFailedThisSession(const std::string& path) {
|
||||
const uint64_t hash = imagePathHash(path);
|
||||
for (size_t i = 0; i < failedImageCount; i++) {
|
||||
if (failedImageHashes[i] == hash) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rememberImageFailure(const std::string& path) {
|
||||
if (failedImageCount == MAX_SESSION_IMAGE_FAILURES || imageFailedThisSession(path)) return;
|
||||
failedImageHashes[failedImageCount++] = imagePathHash(path);
|
||||
}
|
||||
|
||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
HalFile cacheFile;
|
||||
@@ -37,16 +87,8 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||
expectedHeight);
|
||||
if (!readValidCacheHeader(cacheFile, expectedWidth, expectedHeight, cachedWidth, cachedHeight)) {
|
||||
LOG_ERR("IMG", "Invalid image cache: %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,6 +161,28 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ImageBlock::hasValidCache() const {
|
||||
const auto cachePath = getCachePath(imagePath);
|
||||
HalFile cacheFile;
|
||||
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
return readValidCacheHeader(cacheFile, width, height, cachedWidth, cachedHeight);
|
||||
}
|
||||
|
||||
bool ImageBlock::needsDecode() const { return !imageFailedThisSession(imagePath) && !hasValidCache(); }
|
||||
|
||||
void ImageBlock::clearSessionRenderFailures() { failedImageCount = 0; }
|
||||
|
||||
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
|
||||
renderer.fillRect(x, y, width, height, true);
|
||||
if (width > 2 && height > 2) {
|
||||
renderer.fillRect(x + 1, y + 1, width - 2, height - 2, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||
@@ -150,6 +214,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageFailedThisSession(imagePath)) {
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
@@ -161,6 +230,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
HalFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
@@ -168,6 +239,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
|
||||
if (fileSize == 0) {
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -187,6 +260,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,6 +270,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
rememberImageFailure(imagePath);
|
||||
renderPlaceholder(renderer, x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ class ImageBlock final : public Block {
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
bool imageExists() const;
|
||||
bool hasValidCache() const;
|
||||
bool needsDecode() const;
|
||||
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
|
||||
static void clearSessionRenderFailures();
|
||||
|
||||
BlockType getType() override { return IMAGE_BLOCK; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
@@ -3,30 +3,159 @@
|
||||
#include <BidiUtils.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||
size_t TextBlock::arenaSize(const uint16_t wordCount, const bool hasFocus, const uint16_t textBytes) {
|
||||
// Layout documented in TextBlock.h: 16-bit arrays first, then 8-bit arrays, then text.
|
||||
size_t size = static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(int16_t) + sizeof(uint8_t));
|
||||
if (hasFocus) {
|
||||
size += static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(uint8_t));
|
||||
}
|
||||
return size + textBytes;
|
||||
}
|
||||
|
||||
void TextBlock::bindArenaPointers() {
|
||||
uint8_t* base = arena.get();
|
||||
const size_t wc = numWords;
|
||||
textOffArr = reinterpret_cast<const uint16_t*>(base);
|
||||
xposArr = reinterpret_cast<const int16_t*>(base + wc * 2);
|
||||
size_t off = wc * 4;
|
||||
if (focusPresent) {
|
||||
focusSuffixXArr = reinterpret_cast<const uint16_t*>(base + off);
|
||||
off += wc * 2;
|
||||
}
|
||||
stylesArr = base + off;
|
||||
off += wc;
|
||||
if (focusPresent) {
|
||||
focusBoundaryArr = base + off;
|
||||
off += wc;
|
||||
}
|
||||
textArr = reinterpret_cast<const char*>(base + off);
|
||||
}
|
||||
|
||||
TextBlock::TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle)
|
||||
: blockStyle(blockStyle) {
|
||||
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
||||
// When present, they must be sized in lockstep with words[].
|
||||
const bool hasFocus = !wordFocusBoundary.empty();
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size(),
|
||||
(uint32_t)wordFocusBoundary.size(), (uint32_t)wordFocusSuffixX.size());
|
||||
const bool hasFocus = !focusBoundary.empty();
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() || words.size() > 10000 ||
|
||||
(hasFocus && (words.size() != focusBoundary.size() || words.size() != focusSuffixX.size()))) {
|
||||
LOG_ERR("TXB", "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)",
|
||||
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(focusBoundary.size()),
|
||||
static_cast<uint32_t>(focusSuffixX.size()));
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
numWords = static_cast<uint16_t>(words.size());
|
||||
focusPresent = hasFocus;
|
||||
if (numWords == 0) {
|
||||
return; // valid empty block, no arena
|
||||
}
|
||||
|
||||
// Pass 1: total text size, one NUL per word. A line is at most a physical
|
||||
// row of the page, so uint16_t offsets are ample; reject anything larger.
|
||||
size_t totalText = 0;
|
||||
for (const auto& w : words) totalText += w.size() + 1;
|
||||
if (totalText > UINT16_MAX) {
|
||||
LOG_ERR("TXB", "Construction failed: text size %u exceeds arena limit", static_cast<uint32_t>(totalText));
|
||||
numWords = 0;
|
||||
focusPresent = false;
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
textBytes = static_cast<uint16_t>(totalText);
|
||||
|
||||
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||
arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||
if (!arena) {
|
||||
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||
numWords = 0;
|
||||
textBytes = 0;
|
||||
focusPresent = false;
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
bindArenaPointers();
|
||||
|
||||
// Pass 2: fill. Mutable aliases of the const views bound above.
|
||||
auto* textOff = const_cast<uint16_t*>(textOffArr);
|
||||
auto* xpos = const_cast<int16_t*>(xposArr);
|
||||
auto* styles = const_cast<uint8_t*>(stylesArr);
|
||||
auto* text = const_cast<char*>(textArr);
|
||||
uint16_t off = 0;
|
||||
for (uint16_t i = 0; i < numWords; i++) {
|
||||
textOff[i] = off;
|
||||
xpos[i] = wordXpos[i];
|
||||
styles[i] = static_cast<uint8_t>(wordStyles[i]);
|
||||
memcpy(text + off, words[i].data(), words[i].size());
|
||||
off += static_cast<uint16_t>(words[i].size());
|
||||
text[off++] = '\0';
|
||||
}
|
||||
if (focusPresent) {
|
||||
auto* suffixX = const_cast<uint16_t*>(focusSuffixXArr);
|
||||
auto* boundary = const_cast<uint8_t*>(focusBoundaryArr);
|
||||
for (uint16_t i = 0; i < numWords; i++) {
|
||||
suffixX[i] = focusSuffixX[i];
|
||||
boundary[i] = focusBoundary[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||
if (!isValid) {
|
||||
LOG_ERR("TXB", "Render skipped: invalid block");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool scanning = renderer.isFontCacheScanning();
|
||||
const int ascender = renderer.getFontAscenderSize(fontId);
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
const int wordX = wordXpos[i] + x;
|
||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
||||
const auto baseDir = static_cast<BidiUtils::BidiBaseDir>(
|
||||
BidiUtils::detectParagraphLevel(words[i].c_str(), blockStyle.isRtl ? 1 : 0));
|
||||
const uint8_t boundary = hasFocus ? wordFocusBoundary[i] : 0;
|
||||
|
||||
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 (uint16_t i = 0; i < numWords; i++) {
|
||||
const char* word = wordText(i);
|
||||
const int wordX = xposArr[i] + x;
|
||||
const EpdFontFamily::Style currentStyle = wordStyle(i);
|
||||
const auto baseDir =
|
||||
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
|
||||
const uint8_t boundary = focusBoundary(i);
|
||||
|
||||
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
||||
// drawText, so these offsets are chosen relative to the full-size ascender:
|
||||
@@ -49,54 +178,81 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
||||
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
||||
const size_t boldLen = std::min<size_t>({static_cast<size_t>(boundary), words[i].size(), sizeof(boldBuf) - 1});
|
||||
memcpy(boldBuf, words[i].c_str(), boldLen);
|
||||
const size_t boldLen =
|
||||
std::min<size_t>({static_cast<size_t>(boundary), static_cast<size_t>(wordTextLen(i)), sizeof(boldBuf) - 1});
|
||||
memcpy(boldBuf, word, boldLen);
|
||||
boldBuf[boldLen] = '\0';
|
||||
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
||||
const int suffixX = wordX + wordFocusSuffixX[i];
|
||||
renderer.drawText(fontId, suffixX, wordY, words[i].c_str() + boldLen, true, currentStyle, baseDir);
|
||||
const int suffixX = wordX + focusSuffixXArr[i];
|
||||
renderer.drawText(fontId, suffixX, wordY, word + boldLen, true, currentStyle, baseDir);
|
||||
} else {
|
||||
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir);
|
||||
renderer.drawText(fontId, wordX, wordY, word, true, currentStyle, baseDir);
|
||||
}
|
||||
|
||||
if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = words[i];
|
||||
int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
|
||||
const int underlineY = wordY + ascender + 2;
|
||||
if (scanning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
|
||||
int lineStartX = wordX;
|
||||
int lineWidth = renderer.getTextWidth(fontId, word, 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 (wordTextLen(i) >= 3 && static_cast<uint8_t>(word[0]) == 0xE2 && static_cast<uint8_t>(word[1]) == 0x80 &&
|
||||
static_cast<uint8_t>(word[2]) == 0x83) {
|
||||
const char* visibleText = word + 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 {
|
||||
// Focus annotations are optional; vectors are either empty (no splits in this block)
|
||||
// or sized in lockstep with words[].
|
||||
const bool hasFocus = !wordFocusBoundary.empty();
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
||||
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(wordFocusBoundary.size()),
|
||||
static_cast<uint32_t>(wordFocusSuffixX.size()));
|
||||
if (!isValid) {
|
||||
LOG_ERR("TXB", "Serialization failed: invalid block");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Word data
|
||||
serialization::writePod(file, static_cast<uint16_t>(words.size()));
|
||||
for (const auto& w : words) serialization::writeString(file, w);
|
||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||
// Focus block: 1-byte presence flag, followed by per-word vectors only when present.
|
||||
// Saves 3 bytes/word when focus reading is disabled or no word on this line was split.
|
||||
serialization::writePod(file, static_cast<uint8_t>(hasFocus ? 1 : 0));
|
||||
if (hasFocus) {
|
||||
for (auto b : wordFocusBoundary) serialization::writePod(file, b);
|
||||
for (auto sx : wordFocusSuffixX) serialization::writePod(file, sx);
|
||||
// Word data: scalars, then the arena verbatim -- its in-memory layout is
|
||||
// exactly the on-disk layout (see TextBlock.h), so one write covers all
|
||||
// per-word arrays and the text blob.
|
||||
serialization::writePod(file, numWords);
|
||||
serialization::writePod(file, static_cast<uint8_t>(focusPresent ? 1 : 0));
|
||||
serialization::writePod(file, textBytes);
|
||||
if (numWords > 0) {
|
||||
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||
if (file.write(arena.get(), size) != size) {
|
||||
LOG_ERR("TXB", "Serialization failed: arena write (%u bytes)", static_cast<uint32_t>(size));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Style (alignment + margins/padding/indent)
|
||||
@@ -120,41 +276,64 @@ bool TextBlock::serialize(HalFile& file) const {
|
||||
|
||||
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
||||
uint16_t wc;
|
||||
std::vector<std::string> words;
|
||||
std::vector<int16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<uint8_t> wordFocusBoundary;
|
||||
std::vector<uint16_t> wordFocusSuffixX;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
uint8_t hasFocus;
|
||||
uint16_t textBytes;
|
||||
serialization::readPod(file, wc);
|
||||
serialization::readPod(file, hasFocus);
|
||||
serialization::readPod(file, textBytes);
|
||||
|
||||
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
|
||||
// Sanity checks: cap the arena allocation and reject impossible geometry
|
||||
// (every word carries at least its NUL terminator).
|
||||
if (wc > 10000) {
|
||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||
return nullptr;
|
||||
}
|
||||
if ((wc == 0 && textBytes != 0) || (wc > 0 && textBytes < wc)) {
|
||||
LOG_ERR("TXB", "Deserialization failed: bad text size %u for %u words", textBytes, wc);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Word data
|
||||
words.resize(wc);
|
||||
wordXpos.resize(wc);
|
||||
wordStyles.resize(wc);
|
||||
for (auto& w : words) serialization::readString(file, w);
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
// Focus block: presence flag, then vectors only if present. Empty vectors when absent
|
||||
// signal "no splits in this block" to render() (zero per-word RAM cost).
|
||||
uint8_t hasFocus;
|
||||
serialization::readPod(file, hasFocus);
|
||||
if (hasFocus) {
|
||||
wordFocusBoundary.resize(wc);
|
||||
wordFocusSuffixX.resize(wc);
|
||||
for (auto& b : wordFocusBoundary) serialization::readPod(file, b);
|
||||
for (auto& sx : wordFocusSuffixX) serialization::readPod(file, sx);
|
||||
std::unique_ptr<TextBlock> block(new (std::nothrow) TextBlock());
|
||||
if (!block) {
|
||||
LOG_ERR("TXB", "OOM: TextBlock");
|
||||
return nullptr;
|
||||
}
|
||||
block->numWords = wc;
|
||||
block->textBytes = textBytes;
|
||||
block->focusPresent = hasFocus != 0;
|
||||
|
||||
if (wc > 0) {
|
||||
const size_t size = arenaSize(wc, block->focusPresent, textBytes);
|
||||
block->arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||
if (!block->arena) {
|
||||
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||
return nullptr;
|
||||
}
|
||||
if (file.read(block->arena.get(), size) != size) {
|
||||
LOG_ERR("TXB", "Deserialization failed: arena read (%u bytes)", static_cast<uint32_t>(size));
|
||||
return nullptr;
|
||||
}
|
||||
block->bindArenaPointers();
|
||||
|
||||
// Validate offsets before anything dereferences wordText(): offset 0 first,
|
||||
// strictly increasing, in bounds, and every word NUL-terminated (word i ends
|
||||
// at the byte before offset i+1; the last word at the last text byte).
|
||||
const uint16_t* textOff = block->textOffArr;
|
||||
const char* text = block->textArr;
|
||||
if (textOff[0] != 0 || text[textBytes - 1] != '\0') {
|
||||
LOG_ERR("TXB", "Deserialization failed: corrupt text layout");
|
||||
return nullptr;
|
||||
}
|
||||
for (uint16_t i = 1; i < wc; i++) {
|
||||
if (textOff[i] <= textOff[i - 1] || textOff[i] >= textBytes || text[textOff[i] - 1] != '\0') {
|
||||
LOG_ERR("TXB", "Deserialization failed: corrupt word offset %u", i);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Style (alignment + margins/padding/indent)
|
||||
BlockStyle& blockStyle = block->blockStyle;
|
||||
serialization::readPod(file, blockStyle.alignment);
|
||||
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||
serialization::readPod(file, blockStyle.marginTop);
|
||||
@@ -170,7 +349,5 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
||||
serialization::readPod(file, blockStyle.isRtl);
|
||||
serialization::readPod(file, blockStyle.directionDefined);
|
||||
|
||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
|
||||
std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
|
||||
blockStyle));
|
||||
return block;
|
||||
}
|
||||
|
||||
@@ -9,42 +9,83 @@
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
|
||||
// Represents a line of text on a page
|
||||
// Represents a line of text on a page.
|
||||
//
|
||||
// All per-word data lives in ONE flat heap allocation (the arena) instead of
|
||||
// six parallel vectors: a resident page holds ~25-30 of these blocks, and the
|
||||
// vector-of-string layout cost ~250 throwing allocations per page load, which
|
||||
// was the primary driver of heap fragmentation on the ESP32-C3.
|
||||
//
|
||||
// Arena layout, in order (2-byte alignment holds by construction: all 16-bit
|
||||
// arrays come first and the arena base is allocator-aligned; RISC-V faults on
|
||||
// unaligned multi-byte access):
|
||||
// uint16_t textOff[wordCount] byte offset of word i's text in text[]
|
||||
// int16_t xpos[wordCount]
|
||||
// uint16_t focusSuffixX[wordCount] present only when focusPresent
|
||||
// uint8_t styles[wordCount]
|
||||
// uint8_t focusBoundary[wordCount] present only when focusPresent
|
||||
// char text[textBytes] all words back to back, NUL-terminated
|
||||
//
|
||||
// Each word is stored NUL-terminated so render() can hand `text + textOff[i]`
|
||||
// straight to C APIs (drawText) with no std::string materialization.
|
||||
//
|
||||
// Focus split semantics (unchanged from the vector layout): boundary N > 0
|
||||
// means the first N bytes of word i render bold, the remainder in the base
|
||||
// style. N is bounded to 9 codepoints (<= 36 UTF-8 bytes) by the clamp in
|
||||
// ParsedText::addWord. focusSuffixX is the pre-computed pixel offset from the
|
||||
// word start to the regular suffix. Both arrays are omitted from the arena
|
||||
// entirely when no word on the line has a split (zero per-word RAM cost when
|
||||
// focus reading is disabled).
|
||||
class TextBlock final : public Block {
|
||||
private:
|
||||
std::vector<std::string> words;
|
||||
std::vector<int16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
// Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
|
||||
// the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
|
||||
// N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
|
||||
// FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
|
||||
// Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
|
||||
// when focus reading is disabled, or on lines that happen to contain no splittable words).
|
||||
std::vector<uint8_t> wordFocusBoundary;
|
||||
// Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
|
||||
// Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
|
||||
// Empty in lockstep with wordFocusBoundary.
|
||||
std::vector<uint16_t> wordFocusSuffixX;
|
||||
BlockStyle blockStyle;
|
||||
uint16_t numWords = 0;
|
||||
uint16_t textBytes = 0; // total size of the text region, including NULs
|
||||
bool focusPresent = false;
|
||||
bool isValid = true;
|
||||
// The ONLY allocation: makeUniqueNoThrow, so OOM yields an invalid block
|
||||
// instead of abort() (bare new is not nothrow with -fno-exceptions).
|
||||
std::unique_ptr<uint8_t[]> arena;
|
||||
// Typed views into the arena, bound once after the arena is filled. All
|
||||
// 16-bit bases sit at even offsets, so direct dereference is alignment-safe.
|
||||
const uint16_t* textOffArr = nullptr;
|
||||
const int16_t* xposArr = nullptr;
|
||||
const uint16_t* focusSuffixXArr = nullptr; // null when !focusPresent
|
||||
const uint8_t* stylesArr = nullptr;
|
||||
const uint8_t* focusBoundaryArr = nullptr; // null when !focusPresent
|
||||
const char* textArr = nullptr;
|
||||
|
||||
TextBlock() = default; // deserialize() fills the fields directly
|
||||
static size_t arenaSize(uint16_t wordCount, bool hasFocus, uint16_t textBytes);
|
||||
void bindArenaPointers();
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
|
||||
std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
|
||||
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
wordFocusBoundary(std::move(focus_boundary)),
|
||||
wordFocusSuffixX(std::move(focus_suffix_x)),
|
||||
blockStyle(blockStyle) {}
|
||||
// Flatten-on-construct: copies the layout-time vectors into the arena; the
|
||||
// vectors die with the caller. On arena OOM the block is empty and valid()
|
||||
// is false -- callers must check and fail the line instead of using it.
|
||||
explicit TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle = BlockStyle());
|
||||
~TextBlock() override = default;
|
||||
TextBlock(const TextBlock&) = delete;
|
||||
TextBlock& operator=(const TextBlock&) = delete;
|
||||
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
const std::vector<std::string>& getWords() const { return words; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
size_t wordCount() const { return words.size(); }
|
||||
// given a renderer works out where to break the words into lines
|
||||
bool isEmpty() override { return numWords == 0; }
|
||||
bool valid() const { return isValid; }
|
||||
uint16_t wordCount() const { return numWords; }
|
||||
// NUL-terminated by construction; safe to pass to C APIs directly.
|
||||
const char* wordText(const uint16_t i) const { return textArr + textOffArr[i]; }
|
||||
uint16_t wordTextLen(const uint16_t i) const {
|
||||
const uint16_t end = (i + 1 < numWords) ? textOffArr[i + 1] : textBytes;
|
||||
return end - textOffArr[i] - 1; // exclude the NUL
|
||||
}
|
||||
int16_t wordXpos(const uint16_t i) const { return xposArr[i]; }
|
||||
EpdFontFamily::Style wordStyle(const uint16_t i) const { return static_cast<EpdFontFamily::Style>(stylesArr[i]); }
|
||||
uint8_t focusBoundary(const uint16_t i) const { return focusPresent ? focusBoundaryArr[i] : 0; }
|
||||
uint16_t focusSuffixX(const uint16_t i) const { return focusPresent ? focusSuffixXArr[i] : 0; }
|
||||
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
BlockType getType() override { return TEXT_BLOCK; }
|
||||
bool serialize(HalFile& file) const;
|
||||
|
||||
@@ -99,24 +99,58 @@ int bytesPerPixelFromType(int pixelType) {
|
||||
}
|
||||
}
|
||||
|
||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
|
||||
int packedRowBytes(int srcWidth, int bitsPerSample) { return (srcWidth * bitsPerSample + 7) / 8; }
|
||||
|
||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType, int bitsPerSample) {
|
||||
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||
if ((pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED) && bitsPerSample < 8) {
|
||||
pitch = packedRowBytes(srcWidth, bitsPerSample);
|
||||
}
|
||||
return ((pitch + 1) * 2) + 32;
|
||||
}
|
||||
|
||||
bool isSupportedBitDepth(int pixelType, int bitsPerSample) {
|
||||
if (bitsPerSample == 8) return true;
|
||||
if (bitsPerSample != 1 && bitsPerSample != 2 && bitsPerSample != 4) return false;
|
||||
return pixelType == PNG_PIXEL_GRAYSCALE || pixelType == PNG_PIXEL_INDEXED;
|
||||
}
|
||||
|
||||
uint8_t readPackedSample(const uint8_t* pixels, int x, int bitsPerSample) {
|
||||
if (bitsPerSample == 8) return pixels[x];
|
||||
|
||||
const int bitOffset = x * bitsPerSample;
|
||||
const int shift = 8 - bitsPerSample - (bitOffset & 7);
|
||||
const uint8_t mask = (1U << bitsPerSample) - 1;
|
||||
return (pixels[bitOffset >> 3] >> shift) & mask;
|
||||
}
|
||||
|
||||
uint8_t expandSampleToByte(uint8_t sample, int bitsPerSample) {
|
||||
if (bitsPerSample == 8) return sample;
|
||||
const uint8_t maxSample = (1U << bitsPerSample) - 1;
|
||||
return static_cast<uint8_t>((sample * 255U) / maxSample);
|
||||
}
|
||||
|
||||
// Convert entire source line to grayscale with alpha blending to white background.
|
||||
// Low-bit-depth grayscale/indexed scanlines are packed most-significant sample first.
|
||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||
void convertLineToGray(const uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, int bitsPerSample,
|
||||
uint8_t* palette, int hasAlpha) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
if (bitsPerSample == 8) {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 3];
|
||||
const uint8_t* p = &pPixels[x * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
break;
|
||||
@@ -125,7 +159,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = pPixels[x];
|
||||
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
@@ -133,12 +167,15 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &palette[pPixels[x] * 3];
|
||||
uint8_t idx = readPackedSample(pPixels, x, bitsPerSample);
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
for (int x = 0; x < width; x++) {
|
||||
grayLine[x] = expandSampleToByte(readPackedSample(pPixels, x, bitsPerSample), bitsPerSample);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -152,7 +189,7 @@ void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixel
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 4];
|
||||
const uint8_t* p = &pPixels[x * 4];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
@@ -172,39 +209,47 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * ctx->scale);
|
||||
// Map source rows with the exact output-height ratio. During downscaling,
|
||||
// multiple source rows can select the same output row; during upscaling, one
|
||||
// source row must be repeated across every output row in its range. Emitting
|
||||
// only the first row of an upscale leaves zero-filled (black) gaps in the
|
||||
// streamed pixel cache.
|
||||
int firstDstY = (srcY * ctx->dstHeight) / ctx->srcHeight;
|
||||
int endDstY = firstDstY + 1;
|
||||
if (ctx->dstHeight > ctx->srcHeight) {
|
||||
endDstY = ((srcY + 1) * ctx->dstHeight) / ctx->srcHeight;
|
||||
}
|
||||
|
||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||
if (dstY == ctx->lastDstY) return 1;
|
||||
ctx->lastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= ctx->dstHeight) return 1;
|
||||
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) return 1;
|
||||
if (firstDstY <= ctx->lastDstY) firstDstY = ctx->lastDstY + 1;
|
||||
if (firstDstY >= endDstY || firstDstY >= ctx->dstHeight) return 1;
|
||||
if (endDstY > ctx->dstHeight) endDstY = ctx->dstHeight;
|
||||
|
||||
// Convert entire source line to grayscale (improves cache locality)
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->iBpp, pDraw->pPalette,
|
||||
pDraw->iHasAlpha);
|
||||
|
||||
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||
// Render scaled rows using Bresenham-style integer stepping (no floating-point division)
|
||||
int dstWidth = ctx->dstWidth;
|
||||
int outXBase = ctx->config->x;
|
||||
int screenWidth = ctx->screenWidth;
|
||||
bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
|
||||
// Pre-compute orientation and render-mode state once per row
|
||||
// Pre-compute orientation and render-mode state once per callback.
|
||||
DirectPixelWriter pw;
|
||||
pw.init(*ctx->renderer);
|
||||
|
||||
for (int dstY = firstDstY; dstY < endDstY; dstY++) {
|
||||
ctx->lastDstY = dstY;
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) continue;
|
||||
|
||||
pw.beginRow(outY);
|
||||
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
bool caching = ctx->caching;
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
if (!ctx->cache.advanceTo(dstY)) {
|
||||
@@ -242,6 +287,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
srcX++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -332,30 +378,44 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||
ctx.scale, png->getBpp());
|
||||
|
||||
const int pixelType = png->getPixelType();
|
||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
|
||||
const int bitsPerSample = png->getBpp();
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), type: %d, bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth,
|
||||
ctx.dstHeight, ctx.scale, pixelType, bitsPerSample);
|
||||
|
||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType, bitsPerSample);
|
||||
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||
LOG_ERR("PNG",
|
||||
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
||||
LOG_ERR(
|
||||
"PNG",
|
||||
"PNG row buffer too small: need %d bytes for width=%d type=%d bpp=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||
requiredInternal, ctx.srcWidth, pixelType, bitsPerSample, PNG_MAX_BUFFERED_PIXELS);
|
||||
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (png->getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||
if (!isSupportedBitDepth(pixelType, bitsPerSample)) {
|
||||
warnUnsupportedFeature(
|
||||
"bit depth (" + std::to_string(bitsPerSample) + "bpp) for pixel type " + std::to_string(pixelType), imagePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
// The converter expands each source row to 8-bit grayscale before dithering,
|
||||
// so this scratch buffer is sized by source pixels even when PNGdec reads a
|
||||
// packed 1/2/4-bit row internally.
|
||||
constexpr size_t MAX_GRAY_LINE_BUFFER_BYTES = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
const size_t grayBufSize = static_cast<size_t>(ctx.srcWidth);
|
||||
if (grayBufSize > MAX_GRAY_LINE_BUFFER_BYTES) {
|
||||
LOG_ERR("PNG", "Expanded gray row too wide: need %u bytes for width=%d, max=%u", static_cast<unsigned>(grayBufSize),
|
||||
ctx.srcWidth, static_cast<unsigned>(MAX_GRAY_LINE_BUFFER_BYTES));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto grayLineBuffer = makeUniqueNoThrow<uint8_t[]>(grayBufSize);
|
||||
if (!grayLineBuffer) {
|
||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||
return false;
|
||||
}
|
||||
ctx.grayLineBuffer = grayLineBuffer.get();
|
||||
|
||||
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||
@@ -375,7 +435,6 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
free(ctx.grayLineBuffer);
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
|
||||
@@ -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;
|
||||
// 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 CssTextDecoration::None;
|
||||
});
|
||||
return explicitNone ? CssTextDecoration::None : result;
|
||||
}
|
||||
|
||||
CssLength CssParser::interpretLength(std::string_view val) {
|
||||
@@ -429,6 +431,11 @@ CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
|
||||
// Rule processing
|
||||
|
||||
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
|
||||
// Skip rules that don't define any supported properties to save RAM.
|
||||
if (!style.defined.anySet()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've reached the rule limit before processing
|
||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||
@@ -801,6 +808,9 @@ bool CssParser::loadFromCache() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Size the bucket array up front to avoid incremental rehashes while loading rules.
|
||||
rulesBySelector_.reserve(ruleCount);
|
||||
|
||||
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||
};
|
||||
@@ -868,7 +878,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();
|
||||
|
||||
@@ -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 = 8;
|
||||
|
||||
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||
~CssParser() = default;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -24,7 +24,7 @@ struct Iso639Mapping {
|
||||
};
|
||||
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
||||
{"ukr", "uk"}, {"swe", "sv"}};
|
||||
{"ukr", "uk"}, {"swe", "sv"}, {"fin", "fi"}};
|
||||
|
||||
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fi.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-it.trie.h"
|
||||
#include "generated/hyph-pl.trie.h"
|
||||
@@ -26,8 +27,9 @@ LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator finnishHyphenator(fi_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 9>;
|
||||
using EntryArray = std::array<LanguageEntry, 10>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
@@ -38,7 +40,8 @@ const EntryArray& entries() {
|
||||
{"italian", "it", &italianHyphenator},
|
||||
{"polish", "pl", &polishHyphenator},
|
||||
{"swedish", "sv", &swedishHyphenator},
|
||||
{"ukrainian", "uk", &ukrainianHyphenator}}};
|
||||
{"ukrainian", "uk", &ukrainianHyphenator},
|
||||
{"finnish", "fi", &finnishHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t fi_trie_data[] = {
|
||||
0x01, 0x01, 0x16, 0x0B, 0x0C, 0x17, 0x0C, 0x15, 0x0C, 0x0B, 0x16, 0x29, 0x2B, 0x20, 0x1F, 0x0C,
|
||||
0x33, 0x02, 0x0B, 0x02, 0x0B, 0x0C, 0x01, 0x0C, 0x34, 0x0B, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x20,
|
||||
0x0B, 0x21, 0xA0, 0x00, 0x41, 0xA0, 0x02, 0x51, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00,
|
||||
0x41, 0x62, 0xFD, 0xA0, 0x01, 0xA2, 0xA1, 0x00, 0x81, 0x6F, 0xFD, 0xA3, 0x00, 0x81, 0x69, 0x6F,
|
||||
0x75, 0xF8, 0xF8, 0xF8, 0x28, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72, 0xDE, 0xDE, 0xEA,
|
||||
0xDE, 0xDE, 0xDE, 0xF2, 0xF7, 0x22, 0xA4, 0xB6, 0xCD, 0xCD, 0xA1, 0x00, 0x81, 0x61, 0xD9, 0x28,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x72, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xF6, 0xFB,
|
||||
0xA2, 0x00, 0x81, 0x61, 0x65, 0xC3, 0xC3, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6C, 0x72,
|
||||
0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xAB, 0xFF, 0xE3, 0xFF, 0xF9,
|
||||
0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92,
|
||||
0xFF, 0x92, 0xFF, 0x92, 0xFF, 0x92, 0xFF, 0xC5, 0xFF, 0xA6, 0xFF, 0xCA, 0x47, 0x61, 0x65, 0x69,
|
||||
0x6F, 0x75, 0x79, 0xC3, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76, 0xFF, 0x76,
|
||||
0xFF, 0xA9, 0xA0, 0x00, 0xF1, 0x21, 0x73, 0xFD, 0xA1, 0x00, 0x41, 0x75, 0xFD, 0xA0, 0x00, 0x81,
|
||||
0x43, 0x61, 0x65, 0x69, 0xFF, 0x63, 0xFF, 0x63, 0xFF, 0x63, 0xC1, 0x01, 0xA2, 0x61, 0xFF, 0x59,
|
||||
0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0x76, 0xFF, 0x42, 0xFF, 0xE8, 0xFF,
|
||||
0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x42, 0xFF, 0x75, 0xFF, 0xED, 0xFF, 0xF0, 0xFF, 0xFA, 0xA1,
|
||||
0x00, 0x41, 0x73, 0xCE, 0x47, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xFB, 0xFF, 0x1E,
|
||||
0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x1E, 0xFF, 0x51, 0xA0, 0x01, 0x52, 0x21, 0x6F, 0xFD,
|
||||
0x22, 0x74, 0x6E, 0xFD, 0xFD, 0xA0, 0x01, 0x73, 0x21, 0x6E, 0xFD, 0x22, 0x61, 0x6F, 0xFD, 0xFA,
|
||||
0x21, 0x61, 0xEA, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xF2, 0xA4, 0x00, 0x41, 0x6E, 0x6A, 0x69, 0x6C,
|
||||
0xE7, 0xF2, 0xFA, 0xFD, 0x21, 0x73, 0xE1, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41, 0x64, 0xFD, 0x21,
|
||||
0x74, 0xD6, 0x21, 0x73, 0xFD, 0x22, 0x65, 0x69, 0xFA, 0xFD, 0x21, 0x61, 0xCB, 0x21, 0x6E, 0xBD,
|
||||
0x22, 0x74, 0x6F, 0xBD, 0xFD, 0x21, 0x69, 0xC0, 0x21, 0x61, 0xFD, 0xA4, 0x00, 0x41, 0x70, 0x73,
|
||||
0x74, 0x6D, 0xEA, 0xEF, 0xF5, 0xFD, 0x21, 0x69, 0xD9, 0xA1, 0x00, 0x41, 0x6C, 0xFD, 0x47, 0x61,
|
||||
0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0xFF, 0xBB, 0xFF, 0xCC, 0xFE, 0xA4, 0xFF, 0xED, 0xFE, 0xA4,
|
||||
0xFF, 0xFB, 0xFE, 0xD7, 0xA0, 0x01, 0x41, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x00, 0x41,
|
||||
0x72, 0xFD, 0x49, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x6C, 0x72, 0xFE, 0x80, 0xFF, 0xFB,
|
||||
0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0x80, 0xFE, 0xB3, 0xFF, 0x2B, 0xFE, 0x94, 0x21, 0x61,
|
||||
0xDC, 0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0xFF, 0x3E, 0xFE, 0x61, 0xFE, 0x61,
|
||||
0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x61, 0xFE, 0x94, 0xFF, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0x80, 0xFF,
|
||||
0x40, 0x42, 0x6F, 0x65, 0xFF, 0x84, 0xFF, 0x47, 0x41, 0x75, 0xFF, 0x32, 0x21, 0x74, 0xFC, 0x42,
|
||||
0x61, 0x6F, 0xFF, 0xFD, 0xFF, 0x36, 0xA4, 0x00, 0x41, 0x73, 0x6C, 0x6A, 0x70, 0xE4, 0xEB, 0xF9,
|
||||
0xF2, 0x41, 0x79, 0xFF, 0x24, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x41, 0x73, 0xFD,
|
||||
0x42, 0x2E, 0x6E, 0xFF, 0x15, 0xFF, 0x15, 0x21, 0x61, 0xF9, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x41,
|
||||
0x64, 0xFD, 0x41, 0x65, 0xFE, 0xF8, 0x21, 0x6A, 0xFC, 0x42, 0x6B, 0x74, 0xFE, 0xFC, 0xFE, 0xFC,
|
||||
0x21, 0x73, 0xF9, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0x41, 0x68, 0x70, 0x73, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||
0xFF, 0x24, 0x41, 0x74, 0xFF, 0x23, 0xC2, 0x00, 0x41, 0x72, 0x68, 0xFF, 0x30, 0xFF, 0xFC, 0xA0,
|
||||
0x00, 0x52, 0x21, 0x72, 0xFD, 0x21, 0x69, 0xFA, 0x21, 0x6C, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x68,
|
||||
0xFD, 0x4A, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x74, 0x70, 0x63, 0xFF, 0x95, 0xFF, 0xAA,
|
||||
0xFF, 0xBC, 0xFF, 0xD5, 0xFD, 0xC1, 0xFF, 0xE5, 0xFD, 0xF4, 0xFF, 0xF1, 0xFF, 0xF7, 0xFF, 0xFD,
|
||||
0x48, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xC3, 0x73, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD,
|
||||
0xA2, 0xFD, 0xA2, 0xFD, 0xA2, 0xFD, 0xD5, 0xFF, 0xDE, 0xA0, 0x00, 0x92, 0xA0, 0x00, 0xB2, 0xA0,
|
||||
0x01, 0x01, 0xC3, 0x00, 0x61, 0x69, 0x65, 0x79, 0xFE, 0x20, 0xFE, 0x20, 0xFF, 0xFD, 0x22, 0xA4,
|
||||
0xB6, 0xF4, 0xAD, 0x25, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0xA8, 0xE6, 0xE6, 0xE9, 0xFB, 0x42, 0xB6,
|
||||
0xA4, 0xFF, 0x9D, 0xFF, 0x9D, 0x46, 0x79, 0x61, 0x6F, 0x75, 0xC3, 0x65, 0xFF, 0x96, 0xFF, 0xD4,
|
||||
0xFF, 0xD4, 0xFF, 0xD7, 0xFF, 0xF9, 0xFF, 0xD7, 0x22, 0xA4, 0xB6, 0xDB, 0xED, 0xA0, 0x00, 0x72,
|
||||
0xA0, 0x00, 0x71, 0x21, 0xA4, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xA4, 0xFD, 0x21, 0x69, 0xF4, 0xA0,
|
||||
0x01, 0x22, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x26, 0x61, 0x6F, 0x75, 0xC3,
|
||||
0x65, 0x6C, 0xE2, 0xE2, 0xE2, 0xEE, 0xF1, 0xFD, 0x22, 0xA4, 0xB6, 0xD8, 0xD8, 0x21, 0x61, 0xD3,
|
||||
0xA0, 0x00, 0xB1, 0x24, 0x75, 0x69, 0x65, 0x6F, 0xCD, 0xCD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F,
|
||||
0x75, 0xF4, 0xF4, 0xF4, 0xF4, 0x25, 0x79, 0xC3, 0x61, 0x75, 0x69, 0xBB, 0xE3, 0xE8, 0xEE, 0xF7,
|
||||
0xA0, 0x00, 0xD2, 0x22, 0xA4, 0xB6, 0xFD, 0xFD, 0x44, 0x61, 0x65, 0x6F, 0x69, 0xFF, 0x64, 0xFF,
|
||||
0x64, 0xFF, 0x64, 0xFF, 0x64, 0x42, 0x65, 0x61, 0xFF, 0x9B, 0xFF, 0xCB, 0x21, 0x65, 0xC4, 0x22,
|
||||
0x61, 0x75, 0xC1, 0xC1, 0xA0, 0x02, 0x32, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x49, 0x79, 0xC3,
|
||||
0x75, 0x61, 0x65, 0x69, 0x6F, 0x73, 0x6C, 0xFF, 0x80, 0xFF, 0xD6, 0xFF, 0xDB, 0xFF, 0xB0, 0xFF,
|
||||
0xE8, 0xFF, 0xEF, 0xFF, 0xF2, 0xFD, 0x70, 0xFF, 0xFD, 0x44, 0x69, 0x65, 0x6F, 0x75, 0xFF, 0x23,
|
||||
0xFF, 0x23, 0xFF, 0x23, 0xFF, 0x23, 0x43, 0x75, 0x61, 0x65, 0xFF, 0x5A, 0xFF, 0x8A, 0xFF, 0x8A,
|
||||
0x41, 0x76, 0xFF, 0x5F, 0x21, 0x61, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x43, 0x69, 0x6F, 0x6B, 0xFF, 0xF1, 0xFD, 0xF7, 0xFF, 0xFD, 0xA0, 0x01, 0xA4,
|
||||
0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x43, 0x6E, 0x74, 0x6B, 0xFC, 0x7D, 0xFC, 0x7D, 0xFF, 0xFD,
|
||||
0x41, 0x69, 0xFC, 0x73, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x48, 0xC3, 0x61, 0x75,
|
||||
0x65, 0x6F, 0x69, 0x6C, 0x73, 0xFF, 0x3C, 0xFF, 0xAD, 0xFF, 0xBA, 0xFF, 0x20, 0xFF, 0x20, 0xFF,
|
||||
0x50, 0xFF, 0xD7, 0xFF, 0xFD, 0x44, 0x61, 0x69, 0x75, 0x79, 0xFE, 0xB7, 0xFE, 0xB7, 0xFE, 0xB7,
|
||||
0xFE, 0xB7, 0x42, 0x61, 0x69, 0xFE, 0xEE, 0xFE, 0xEE, 0x42, 0x75, 0x61, 0xFE, 0xE7, 0xFF, 0x17,
|
||||
0x42, 0xA4, 0xB6, 0xFE, 0xE6, 0xFF, 0x30, 0x24, 0x65, 0x61, 0x75, 0xC3, 0xDE, 0xEB, 0xF2, 0xF9,
|
||||
0x43, 0x61, 0x65, 0x6F, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0x42, 0x61, 0x75, 0xFE, 0xC6, 0xFE,
|
||||
0xC6, 0x44, 0x75, 0x61, 0x65, 0x6F, 0xFE, 0xBF, 0xFE, 0xEF, 0xFE, 0xEF, 0xFE, 0xEF, 0x41, 0xB6,
|
||||
0xFE, 0xB2, 0x21, 0xC3, 0xFC, 0x42, 0xA4, 0xB6, 0xFE, 0xB1, 0xFF, 0xFD, 0x43, 0x61, 0x6F, 0x79,
|
||||
0xFE, 0xD4, 0xFE, 0xD4, 0xFE, 0xD4, 0x42, 0x61, 0x65, 0xFE, 0x56, 0xFE, 0x56, 0x26, 0x69, 0x61,
|
||||
0x75, 0xC3, 0x65, 0x6F, 0xC3, 0xCD, 0xD4, 0xE8, 0xEF, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0xA4, 0xFD,
|
||||
0xA0, 0x01, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x02, 0x03, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x75, 0xFD, 0x23, 0xC3, 0x79, 0x73, 0xE2,
|
||||
0xEE, 0xFD, 0x41, 0x72, 0xFD, 0xD9, 0x42, 0x6C, 0x68, 0xFC, 0x47, 0xFF, 0xFC, 0xC1, 0x00, 0x81,
|
||||
0x69, 0xFB, 0xA6, 0x21, 0x76, 0xFA, 0x59, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0xC3, 0x79, 0x6F, 0x75, 0x61, 0x65, 0x69, 0x2E, 0x63, 0x71,
|
||||
0xFB, 0xAE, 0xFB, 0xC9, 0xFB, 0xE1, 0xFB, 0xFA, 0xFC, 0x16, 0xFC, 0x16, 0xFC, 0x4A, 0xFC, 0x6E,
|
||||
0xFC, 0x16, 0xFC, 0xE8, 0xFD, 0x0C, 0xFD, 0x2B, 0xFD, 0xCB, 0xFD, 0xEA, 0xFC, 0x16, 0xFE, 0x42,
|
||||
0xFE, 0x65, 0xFE, 0x8F, 0xFE, 0xC7, 0xFF, 0x36, 0xFF, 0x71, 0xFF, 0xB7, 0xFF, 0xE5, 0xFF, 0xF0,
|
||||
0xFF, 0xFD,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns fi_patterns = {
|
||||
0x496u,
|
||||
fi_trie_data,
|
||||
sizeof(fi_trie_data),
|
||||
};
|
||||
@@ -22,6 +22,17 @@
|
||||
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
|
||||
constexpr size_t PARSE_BUFFER_SIZE = 1024;
|
||||
|
||||
// This number comes from PR #73
|
||||
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
||||
// memory.
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS = 750;
|
||||
|
||||
// When CSS is enabled, flush earlier to save RAM. 320 is still more than enough to build a CJK
|
||||
// page at font size 14
|
||||
constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS = 320;
|
||||
|
||||
// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation
|
||||
// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per
|
||||
// chapter. A runaway count usually means a converter injected machine-generated IDs on
|
||||
@@ -34,7 +45,8 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
|
||||
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
|
||||
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||
constexpr const char* IMAGE_TAGS[] = {"img"};
|
||||
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
|
||||
constexpr const char* IMAGE_TAGS[] = {"img", "image"};
|
||||
constexpr const char* SKIP_TAGS[] = {"head"};
|
||||
|
||||
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
||||
@@ -89,13 +101,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 +158,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 +215,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 +224,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) {
|
||||
@@ -188,6 +236,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues);
|
||||
partWordBufferIndex = 0;
|
||||
nextWordContinues = false;
|
||||
listItemBulletOnly = false;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
@@ -202,8 +251,28 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
// open. Merge those into the new style so the first child in a container inherits
|
||||
// the container's vertical spacing.
|
||||
const auto style = currentTextBlock->getBlockStyle();
|
||||
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
||||
BlockStyle incoming = blockStyle;
|
||||
if (style.fromBrElement) {
|
||||
// The empty block was created by a <br> section separator. Inject a full line of
|
||||
// blank space before the following paragraph so the scene/section break is visible.
|
||||
// This only fires when the <br> block stayed empty (i.e. no inline text was added).
|
||||
const int16_t lineHeight = static_cast<int16_t>(renderer.getLineHeight(fontId) * lineCompression + 0.5f);
|
||||
incoming.marginTop = static_cast<int16_t>(incoming.marginTop + lineHeight);
|
||||
}
|
||||
|
||||
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(incoming, BlockStyle::CombineAxis::Vertical));
|
||||
|
||||
flushPendingAnchor();
|
||||
return;
|
||||
}
|
||||
|
||||
// <li> added a bullet as the first word, making the block non-empty. When a nested
|
||||
// block-level child (<p>, <div>, etc.) opens, reuse the block instead of flushing
|
||||
// the bullet to its own line. The bullet stays inline with the child's text.
|
||||
if (listItemBulletOnly) {
|
||||
const auto style = currentTextBlock->getBlockStyle();
|
||||
currentTextBlock->setBlockStyle(style.getCombinedBlockStyle(blockStyle, BlockStyle::CombineAxis::Vertical));
|
||||
listItemBulletOnly = false;
|
||||
flushPendingAnchor();
|
||||
return;
|
||||
}
|
||||
@@ -215,6 +284,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
flushPendingAnchor();
|
||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
||||
wordsExtractedInBlock = 0;
|
||||
listItemBulletOnly = false;
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::emitHorizontalRule(const BlockStyle& blockStyle) {
|
||||
@@ -420,14 +490,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();
|
||||
@@ -448,11 +519,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "src") == 0) {
|
||||
src = atts[i + 1];
|
||||
} else if (src.empty() && (strcmp(atts[i], "href") == 0 || strcmp(atts[i], "xlink:href") == 0)) {
|
||||
src = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "alt") == 0) {
|
||||
alt = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
const size_t fragmentPos = src.find('#');
|
||||
if (fragmentPos != std::string::npos) {
|
||||
src.resize(fragmentPos);
|
||||
}
|
||||
|
||||
// imageRendering: 0=display, 1=placeholder (alt text only), 2=suppress entirely
|
||||
if (self->imageRendering == 2) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
@@ -460,19 +538,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip image if CSS display:none
|
||||
if (self->cssParser) {
|
||||
CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr);
|
||||
if (!styleAttr.empty()) {
|
||||
imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
}
|
||||
if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) {
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!src.empty() && self->imageRendering != 1) {
|
||||
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||
|
||||
@@ -496,24 +561,28 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||
cachedImageFile.flush();
|
||||
cachedImageFile.close();
|
||||
delay(50); // Give SD card time to sync
|
||||
}
|
||||
|
||||
if (extractSuccess) {
|
||||
// Get image dimensions
|
||||
// Get image dimensions, retrying to absorb SD-card sync latency on slow
|
||||
// cards. Replaces a blanket delay(50) that cost ~50ms on every image, and
|
||||
// closes the silent-drop bug where a single getDimensions failure was fatal.
|
||||
ImageDimensions dims = {0, 0};
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
||||
bool gotDimensions = false;
|
||||
for (int attempt = 0; attempt < 3 && !gotDimensions; attempt++) {
|
||||
if (attempt > 0) {
|
||||
delay(50); // Give a slow SD card time to finish syncing before retrying
|
||||
}
|
||||
gotDimensions = decoder && decoder->getDimensions(cachedImagePath, dims);
|
||||
}
|
||||
if (gotDimensions) {
|
||||
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||
|
||||
int displayWidth = 0;
|
||||
int displayHeight = 0;
|
||||
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
|
||||
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
|
||||
// Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules
|
||||
if (!styleAttr.empty()) {
|
||||
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
|
||||
}
|
||||
const CssStyle& imgStyle = cssStyle;
|
||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||
|
||||
@@ -760,12 +829,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();
|
||||
@@ -818,7 +886,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
self->startNewTextBlock(self->blockStyleStack.back().withoutBottom());
|
||||
// Tag the new block so startNewTextBlock can inject a full line-height gap if
|
||||
// the block remains empty (i.e. <br> is a section separator between paragraphs).
|
||||
// If the block gets text added before the next block opens it becomes non-empty,
|
||||
// goes through makePages() normally, and the flag has no effect (inline <br> case).
|
||||
BlockStyle brStyle =
|
||||
self->currentTextBlock ? self->currentTextBlock->getBlockStyle() : self->blockStyleStack.back();
|
||||
brStyle.fromBrElement = true;
|
||||
self->startNewTextBlock(brStyle);
|
||||
} else {
|
||||
self->currentCssStyle = cssStyle;
|
||||
const auto accumulated = self->blockStyleStack.back().getCombinedBlockStyle(userAlignmentBlockStyle,
|
||||
@@ -829,6 +904,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
self->listItemBulletOnly = true;
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS))) {
|
||||
@@ -837,23 +913,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 +937,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 +957,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 +996,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) {
|
||||
@@ -1096,24 +1154,28 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
}
|
||||
self->partWordBufferIndex = safeLen;
|
||||
self->flushPartWordBuffer();
|
||||
self->nextWordContinues = true;
|
||||
for (int j = 0; j < overflow; j++) {
|
||||
self->partWordBuffer[j] = saved[j];
|
||||
}
|
||||
self->partWordBufferIndex = overflow;
|
||||
} else {
|
||||
self->flushPartWordBuffer();
|
||||
self->nextWordContinues = true;
|
||||
}
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
}
|
||||
|
||||
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
||||
// memory.
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
if (self->currentTextBlock->size() > 750) {
|
||||
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
|
||||
// Keep token growth bounded: CSS-heavy spans can fragment text into many tiny
|
||||
// words, so flush earlier when embedded CSS is active. We still keep the
|
||||
// "exclude last line" behavior to preserve paragraph flow across chunks.
|
||||
const size_t blockWordCount = self->currentTextBlock->size();
|
||||
const size_t softFlushThreshold =
|
||||
self->embeddedStyle ? TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS : TEXT_BLOCK_SOFT_FLUSH_WORDS;
|
||||
if (blockWordCount > softFlushThreshold) {
|
||||
LOG_DBG("EHP", "Text block soft flush (%u words)", static_cast<unsigned>(blockWordCount));
|
||||
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
|
||||
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
|
||||
@@ -1150,9 +1212,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 +1232,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 +1292,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) {
|
||||
@@ -1258,10 +1315,19 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
}
|
||||
self->blockStyleStack.pop_back();
|
||||
}
|
||||
|
||||
// </li> closes: if the bullet never got inline text (empty <li> or <li> with only
|
||||
// block children that were flushed), clear the flag so the next sibling doesn't
|
||||
// merge into this block.
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->listItemBulletOnly = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +1345,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 ) 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);
|
||||
parseStartTime_ = millis();
|
||||
return true;
|
||||
}
|
||||
|
||||
ChapterHtmlSlimParser::ParseStatus ChapterHtmlSlimParser::parseStep() {
|
||||
void* const buf = XML_GetBuffer(xmlParser_, PARSE_BUFFER_SIZE);
|
||||
if (!buf) {
|
||||
LOG_ERR("EHP", "Couldn't allocate memory for buffer");
|
||||
destroyXmlParser(parser);
|
||||
file.close();
|
||||
return false;
|
||||
return ParseStatus::Error;
|
||||
}
|
||||
|
||||
const size_t len = file.read(buf, PARSE_BUFFER_SIZE);
|
||||
const size_t len = parseFile_.read(buf, PARSE_BUFFER_SIZE);
|
||||
|
||||
if (len == 0 && file.available() > 0) {
|
||||
if (len == 0 && parseFile_.available() > 0) {
|
||||
LOG_ERR("EHP", "File read error");
|
||||
destroyXmlParser(parser);
|
||||
file.close();
|
||||
return false;
|
||||
return ParseStatus::Error;
|
||||
}
|
||||
|
||||
done = file.available() == 0;
|
||||
const int done = parseFile_.available() == 0;
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
} while (!done);
|
||||
LOG_DBG("EHP", "Time to parse and build pages: %lu ms", millis() - chapterStartTime);
|
||||
|
||||
destroyXmlParser(parser);
|
||||
file.close();
|
||||
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 +1434,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -79,6 +80,7 @@ class ChapterHtmlSlimParser {
|
||||
int tableDepth = 0;
|
||||
int tableRowIndex = 0;
|
||||
int tableColIndex = 0;
|
||||
bool listItemBulletOnly = false; // true when currentTextBlock has only the <li> bullet
|
||||
|
||||
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
||||
int completedPageCount = 0;
|
||||
@@ -96,12 +98,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 +156,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; }
|
||||
};
|
||||
|
||||
@@ -137,8 +137,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||
}
|
||||
|
||||
// Sort item index for binary search if we have enough items
|
||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
||||
// Sort the (unconditionally-built) item index so every idref lookup uses binary
|
||||
// search. Without this, small/medium manifests fell back to an O(spine × manifest)
|
||||
// linear rescan of .items.bin per itemref (up to ~200ms/item at large scale).
|
||||
if (!self->itemIndex.empty()) {
|
||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
@@ -284,9 +286,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
// Fallback linear scan, only reached when the index is empty (no manifest
|
||||
// items). The fast binary-search path above is used for all real manifests.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
while (self->tempItemStore.available()) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class ContentOpfParser final : public Print {
|
||||
HalFile tempItemStore;
|
||||
std::string coverItemId;
|
||||
|
||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
||||
// Index for fast idref→href lookup (binary search over .items.bin)
|
||||
struct ItemIndexEntry {
|
||||
uint32_t idHash; // FNV-1a hash of itemId
|
||||
uint16_t idLen; // length for collision reduction
|
||||
@@ -41,8 +41,6 @@ class ContentOpfParser final : public Print {
|
||||
std::deque<ItemIndexEntry> itemIndex;
|
||||
bool useItemIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a hash function
|
||||
static uint32_t fnvHash(const std::string& s) {
|
||||
uint32_t hash = 2166136261u;
|
||||
|
||||
+21
-13
@@ -79,29 +79,27 @@ std::string normalisePath(const std::string& path) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void sortFileList(std::vector<std::string>& strs) {
|
||||
std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) {
|
||||
// Directories first
|
||||
bool isDir1 = str1.back() == '/';
|
||||
bool isDir2 = str2.back() == '/';
|
||||
if (isDir1 != isDir2) return isDir1;
|
||||
|
||||
// Start naive natural sort
|
||||
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)) {
|
||||
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++;
|
||||
while (isDigit(s1[len1])) len1++;
|
||||
while (isDigit(s2[len2])) len2++;
|
||||
|
||||
// Different length so return smaller integer value
|
||||
if (len1 != len2) return len1 < len2;
|
||||
@@ -116,8 +114,8 @@ void sortFileList(std::vector<std::string>& strs) {
|
||||
s2 += len2;
|
||||
} else {
|
||||
// Regular case-insensitive character comparison
|
||||
char c1 = tolower(*s1);
|
||||
char c2 = tolower(*s2);
|
||||
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++;
|
||||
@@ -126,6 +124,16 @@ void sortFileList(std::vector<std::string>& strs) {
|
||||
|
||||
// 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
|
||||
bool isDir1 = str1.back() == '/';
|
||||
bool isDir2 = str2.back() == '/';
|
||||
if (isDir1 != isDir2) return isDir1;
|
||||
|
||||
return naturalLess(str1, str2);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
+272
-23
@@ -1,6 +1,7 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <BidiUtils.h>
|
||||
#include <BuildScratch.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
@@ -24,6 +25,36 @@ uint8_t resolveSdCardStyle(const SdCardFont& font, const EpdFontFamily::Style st
|
||||
|
||||
namespace {
|
||||
const char* resolveVisualText(const char* text, std::string& visualBuffer, BidiUtils::BidiBaseDir baseDir);
|
||||
|
||||
// Appends the shaped visual form of every RTL token in `text` to `shapedOut`.
|
||||
// getTextAdvanceX() measures the bidi-reordered, Arabic-shaped codepoint stream,
|
||||
// so the SD advance table must be warmed with the presentation forms as well as
|
||||
// the logical codepoints — otherwise every RTL word measurement misses the fast
|
||||
// path and falls through to onGlyphMiss(), which opens the .cpfont and reads
|
||||
// glyph metadata + bitmap into the 8-slot overflow ring, once per glyph.
|
||||
// Tokens without RTL lead bytes (0xD6-0xDB) are skipped with a byte scan, so
|
||||
// pure-LTR text pays almost nothing.
|
||||
void appendShapedRtlTokens(const char* text, std::string& shapedOut) {
|
||||
const auto isBreak = [](const char c) { return c == ' ' || c == '\n' || c == '\r' || c == '\t'; };
|
||||
std::string token;
|
||||
std::string visual;
|
||||
const char* p = text;
|
||||
while (*p) {
|
||||
while (*p && isBreak(*p)) ++p;
|
||||
const char* start = p;
|
||||
bool hasRtlBytes = false;
|
||||
while (*p && !isBreak(*p)) {
|
||||
const auto b = static_cast<unsigned char>(*p);
|
||||
hasRtlBytes = hasRtlBytes || (b >= 0xD6 && b <= 0xDB);
|
||||
++p;
|
||||
}
|
||||
if (!hasRtlBytes) continue;
|
||||
token.assign(start, p - start);
|
||||
if (BidiUtils::applyBidiVisual(token.c_str(), visual, static_cast<int>(BidiUtils::BidiBaseDir::AUTO))) {
|
||||
shapedOut += visual;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const {
|
||||
@@ -57,7 +88,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
||||
void GfxRenderer::ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask) const {
|
||||
auto it = sdCardFonts_.find(fontId);
|
||||
if (it != sdCardFonts_.end()) {
|
||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask);
|
||||
std::string shaped;
|
||||
appendShapedRtlTokens(utf8Text, shaped);
|
||||
int missed = it->second->buildAdvanceTable(utf8Text, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||
if (missed > 0) {
|
||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||
}
|
||||
@@ -71,7 +104,12 @@ void GfxRenderer::ensureSdCardFontReady(int fontId, const std::vector<std::strin
|
||||
// Augment the persistent advance-only table for layout measurement.
|
||||
// The table survives across paragraphs/sections (capped per font), so
|
||||
// repeated indexing of the same SD font amortizes glyph-metric SD reads.
|
||||
int missed = it->second->buildAdvanceTable(words, includeHyphen, styleMask);
|
||||
std::string shaped;
|
||||
for (const auto& w : words) {
|
||||
appendShapedRtlTokens(w.c_str(), shaped);
|
||||
}
|
||||
int missed =
|
||||
it->second->buildAdvanceTable(words, includeHyphen, styleMask, shaped.empty() ? nullptr : shaped.c_str());
|
||||
if (missed > 0) {
|
||||
LOG_DBG("GFX", "ensureSdCardFontReady: %d glyph(s) not found", missed);
|
||||
}
|
||||
@@ -91,6 +129,47 @@ void GfxRenderer::begin() {
|
||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||
}
|
||||
|
||||
void GfxRenderer::releaseFrameBufferForBuild() {
|
||||
// Lend the framebuffer's bytes IN PLACE: the allocation is never freed, so
|
||||
// it cannot move and repeated loans cannot fragment the heap (the previous
|
||||
// free+realloc model measurably decayed the max contiguous block over a
|
||||
// session). The bytes are deposited in the build-scratch registry so
|
||||
// memory-hungry build phases (e.g. InflateStream's tinfl state + window)
|
||||
// can claim them instead of allocating.
|
||||
uint32_t size = 0;
|
||||
uint8_t* scratch = display.lendFrameBufferStorage(&size);
|
||||
frameBuffer = nullptr;
|
||||
if (scratch) {
|
||||
buildscratch::lend(scratch, size);
|
||||
}
|
||||
}
|
||||
|
||||
bool GfxRenderer::restoreFrameBufferAfterBuild() {
|
||||
buildscratch::reclaim();
|
||||
display.returnFrameBufferStorage(); // cannot fail: the allocation was never freed
|
||||
frameBuffer = display.getFrameBuffer();
|
||||
return frameBuffer != nullptr;
|
||||
}
|
||||
|
||||
GfxRenderer::FrameBufferLoan::FrameBufferLoan(GfxRenderer& renderer) : renderer_(renderer) {
|
||||
// Nesting guard: if the framebuffer is already lent out (an outer loan),
|
||||
// stay inert so this end() cannot return storage the outer loan still owns.
|
||||
if (!renderer_.hasFrameBuffer()) return;
|
||||
renderer_.releaseFrameBufferForBuild();
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void GfxRenderer::FrameBufferLoan::end() {
|
||||
if (!active_) return;
|
||||
active_ = false;
|
||||
if (!renderer_.restoreFrameBufferAfterBuild()) {
|
||||
// Only reachable if the framebuffer never existed, which begin() already
|
||||
// asserts against; kept as a backstop since running blind helps nobody.
|
||||
LOG_ERR("GFX", "Framebuffer restore failed - restarting");
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
bool GfxRenderer::isFontCacheScanning() const { return fontCacheManager_ && fontCacheManager_->isScanning(); }
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||
@@ -134,6 +213,61 @@ static inline void rotateCoordinates(const GfxRenderer::Orientation orientation,
|
||||
}
|
||||
}
|
||||
|
||||
// Output of screenRectToAlignedMemRect: a rectangle in panel-memory
|
||||
// coordinates whose x and width are guaranteed to be multiples of 8 (the
|
||||
// SDK's EInkDisplay::displayWindow alignment requirement). `valid == false`
|
||||
// means the input was empty or fully outside the panel.
|
||||
struct AlignedMemRect {
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
uint16_t w = 0;
|
||||
uint16_t h = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
// Translate a screen-coordinate rectangle (the coordinate system used by
|
||||
// fillRect / drawText / the rest of the renderer's public API) into a
|
||||
// panel-memory rectangle suitable for direct framebuffer indexing. Rotates
|
||||
// the rectangle's two opposite corners with rotateCoordinates(), takes the
|
||||
// bounding box (which naturally swaps width/height in Portrait /
|
||||
// PortraitInverted), then snaps the x extent outward to multiples of 8 and
|
||||
// clamps to panel bounds. Precondition: panel dims are multiples of 8 (true
|
||||
// for the 800x480 panel), so clamping cannot re-break alignment.
|
||||
static AlignedMemRect screenRectToAlignedMemRect(GfxRenderer::Orientation orientation, int sx, int sy, int sw, int sh,
|
||||
uint16_t panelWidth, uint16_t panelHeight) {
|
||||
AlignedMemRect out;
|
||||
if (sw <= 0 || sh <= 0) return out;
|
||||
|
||||
int x0, y0, x1, y1;
|
||||
rotateCoordinates(orientation, sx, sy, &x0, &y0, panelWidth, panelHeight);
|
||||
rotateCoordinates(orientation, sx + sw - 1, sy + sh - 1, &x1, &y1, panelWidth, panelHeight);
|
||||
|
||||
const int memXLo = std::min(x0, x1);
|
||||
const int memYLo = std::min(y0, y1);
|
||||
const int memXHi = std::max(x0, x1) + 1; // exclusive upper bound
|
||||
const int memYHi = std::max(y0, y1) + 1;
|
||||
|
||||
// Snap x outward to multiples of 8.
|
||||
int alignedXLo = memXLo & ~0x7; // round down
|
||||
int alignedXHi = (memXHi + 7) & ~0x7; // round up
|
||||
|
||||
if (alignedXLo < 0) alignedXLo = 0;
|
||||
if (alignedXHi > panelWidth) alignedXHi = panelWidth;
|
||||
int clampedYLo = memYLo;
|
||||
int clampedYHi = memYHi;
|
||||
if (clampedYLo < 0) clampedYLo = 0;
|
||||
if (clampedYHi > panelHeight) clampedYHi = panelHeight;
|
||||
|
||||
if (alignedXHi <= alignedXLo || clampedYHi <= clampedYLo) return out;
|
||||
|
||||
out.x = static_cast<uint16_t>(alignedXLo);
|
||||
out.y = static_cast<uint16_t>(clampedYLo);
|
||||
out.w = static_cast<uint16_t>(alignedXHi - alignedXLo);
|
||||
out.h = static_cast<uint16_t>(clampedYHi - clampedYLo);
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
enum class TextRotation { None, Rotated90CW };
|
||||
|
||||
// Shared glyph rendering logic for normal and rotated text.
|
||||
@@ -418,18 +552,21 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&textCursor)))) {
|
||||
// Skip Hebrew Niqqud (vowel marks)
|
||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||
// marks stay centered, raised above the base or (kasra) at their
|
||||
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||
// the getGlyph lookup and skip them, as before.
|
||||
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||
if (!combiningGlyph) continue;
|
||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||
const int combiningX = combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, combiningGlyph->left,
|
||||
combiningGlyph->width);
|
||||
const auto anchor = combiningMark::anchorFor(cp);
|
||||
const int raiseBy =
|
||||
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||
const int combiningX = combiningMark::anchorOver(anchor, lastBaseX, lastBaseLeft, lastBaseWidth,
|
||||
combiningGlyph->left, combiningGlyph->width);
|
||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, combiningX, yPos - raiseBy, black, style);
|
||||
continue;
|
||||
}
|
||||
@@ -1054,8 +1191,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,
|
||||
@@ -1353,6 +1506,53 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
}
|
||||
|
||||
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
|
||||
// The async path has no turn-off-screen hook, which the sunlight fading fix
|
||||
// relies on; keep those users on the blocking path.
|
||||
if (fadingFix) {
|
||||
display.displayBuffer(refreshMode, fadingFix);
|
||||
return;
|
||||
}
|
||||
display.displayBufferAsync(refreshMode);
|
||||
}
|
||||
|
||||
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
|
||||
|
||||
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
|
||||
|
||||
size_t GfxRenderer::readFramebufferRegion(int x, int y, int w, int h, uint8_t* dst, size_t dstCapacity) const {
|
||||
if (dst == nullptr || w <= 0 || h <= 0) return 0;
|
||||
|
||||
const AlignedMemRect mem = screenRectToAlignedMemRect(orientation, x, y, w, h, panelWidth, panelHeight);
|
||||
if (!mem.valid) return 0;
|
||||
|
||||
const size_t rowBytes = mem.w / 8; // exact: mem.w is a multiple of 8
|
||||
const size_t needed = rowBytes * mem.h;
|
||||
if (needed > dstCapacity) return 0;
|
||||
|
||||
for (uint16_t row = 0; row < mem.h; ++row) {
|
||||
const uint8_t* srcRow = frameBuffer + (static_cast<uint32_t>(mem.y + row) * panelWidthBytes) + (mem.x / 8);
|
||||
uint8_t* dstRow = dst + (static_cast<size_t>(row) * rowBytes);
|
||||
memcpy(dstRow, srcRow, rowBytes);
|
||||
}
|
||||
return needed;
|
||||
}
|
||||
|
||||
void GfxRenderer::writeFramebufferRegion(int x, int y, int w, int h, const uint8_t* src) {
|
||||
if (src == nullptr || w <= 0 || h <= 0) return;
|
||||
|
||||
const AlignedMemRect mem = screenRectToAlignedMemRect(orientation, x, y, w, h, panelWidth, panelHeight);
|
||||
if (!mem.valid) return;
|
||||
|
||||
const size_t rowBytes = mem.w / 8; // exact: mem.w is a multiple of 8
|
||||
|
||||
for (uint16_t row = 0; row < mem.h; ++row) {
|
||||
const uint8_t* srcRow = src + (static_cast<size_t>(row) * rowBytes);
|
||||
uint8_t* dstRow = frameBuffer + (static_cast<uint32_t>(mem.y + row) * panelWidthBytes) + (mem.x / 8);
|
||||
memcpy(dstRow, srcRow, rowBytes);
|
||||
}
|
||||
}
|
||||
|
||||
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
||||
const EpdFontFamily::Style style) const {
|
||||
if (!text || maxWidth <= 0) return "";
|
||||
@@ -1466,6 +1666,35 @@ int GfxRenderer::getScreenHeight() const {
|
||||
return panelWidth;
|
||||
}
|
||||
|
||||
void GfxRenderer::tapToLogical(float nx, float ny, int& outX, int& outY) const {
|
||||
int phyX = static_cast<int>(nx * panelWidth);
|
||||
int phyY = static_cast<int>(ny * panelHeight);
|
||||
if (phyX < 0) phyX = 0;
|
||||
if (phyX > panelWidth - 1) phyX = panelWidth - 1;
|
||||
if (phyY < 0) phyY = 0;
|
||||
if (phyY > panelHeight - 1) phyY = panelHeight - 1;
|
||||
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
outX = panelHeight - 1 - phyY;
|
||||
outY = phyX;
|
||||
break;
|
||||
case PortraitInverted:
|
||||
outX = phyY;
|
||||
outY = panelWidth - 1 - phyX;
|
||||
break;
|
||||
case LandscapeClockwise:
|
||||
outX = panelWidth - 1 - phyX;
|
||||
outY = panelHeight - 1 - phyY;
|
||||
break;
|
||||
case LandscapeCounterClockwise:
|
||||
default:
|
||||
outX = phyX;
|
||||
outY = phyY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Translate a logical rect through rotateCoordinates and take the bounding
|
||||
// box of its four corners on the physical panel. Output coords are inclusive
|
||||
// and clamped. Returns false if the rect ends up fully off-panel.
|
||||
@@ -1599,6 +1828,15 @@ int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint3
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFamily::Style style) const {
|
||||
// Measure the exact codepoint stream drawText renders: bidi-reordered and
|
||||
// Arabic-shaped (contextual presentation forms, Lam-Alef collapse).
|
||||
// Measuring the raw logical text counts the Alef a ligature absorbs and
|
||||
// uses base-letter advances instead of presentation-form advances, so RTL
|
||||
// lines come out wider than they draw — uneven word gaps and a ragged
|
||||
// right margin.
|
||||
std::string visual;
|
||||
text = resolveVisualText(text, visual, BidiUtils::BidiBaseDir::AUTO);
|
||||
|
||||
// Advance table fast-path for SD card fonts during layout.
|
||||
// No kerning/ligature lookup — consistent with previous metadataOnly behavior
|
||||
// where kern/lig data was not loaded.
|
||||
@@ -1614,6 +1852,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
}
|
||||
const auto& font = fontIt->second;
|
||||
while (uint32_t cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text))) {
|
||||
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||
if (BidiUtils::isTransparentMark(cp)) {
|
||||
continue;
|
||||
}
|
||||
int32_t advFP = sdIt->second->getAdvance(cp, styleIdx);
|
||||
if (advFP == 0 && !utf8IsCombiningMark(cp)) {
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
@@ -1636,6 +1878,10 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
const auto& font = fontIt->second;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
// RTL vowel marks (niqqud/harakat) are zero-advance overlays in drawText — no width.
|
||||
if (BidiUtils::isTransparentMark(cp)) {
|
||||
continue;
|
||||
}
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1712,18 +1958,21 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
// Skip Hebrew Niqqud (vowel marks)
|
||||
// Temporary: avoid adding Niqqud to built-in fonts. Remove when custom fonts are supported.
|
||||
if (cp >= 0x0591 && cp <= 0x05C7) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
// RTL vowel marks (Hebrew niqqud, Arabic harakat) ride the combining-mark
|
||||
// path: zero-advance overlays on the preceding base glyph (applyBidiVisual
|
||||
// emits base-then-marks per UAX#9 L3). anchorFor pins position-sensitive
|
||||
// niqqud (dagesh, shin/sin dots, holam) to their spot on the base; other
|
||||
// marks stay centered, raised above the base or (kasra) at their
|
||||
// font-native position. Fonts without their glyphs — the built-ins — miss
|
||||
// the getGlyph lookup and skip them, as before.
|
||||
if (utf8IsCombiningMark(cp) || BidiUtils::isTransparentMark(cp)) {
|
||||
const EpdGlyph* combiningGlyph = font.getGlyph(cp, style);
|
||||
if (!combiningGlyph) continue;
|
||||
const int raiseBy = combiningMark::raiseAboveBase(combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||
const auto anchor = combiningMark::anchorFor(cp);
|
||||
const int raiseBy =
|
||||
combiningMark::raiseAboveBase(anchor, combiningGlyph->top, combiningGlyph->height, lastBaseTop);
|
||||
const int combiningX = x - raiseBy;
|
||||
const int combiningY = combiningMark::centerOverRotated90CW(lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||
const int combiningY = combiningMark::anchorOverRotated90CW(anchor, lastBaseY, lastBaseLeft, lastBaseWidth,
|
||||
combiningGlyph->left, combiningGlyph->width);
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, combiningX, combiningY, black, style);
|
||||
continue;
|
||||
|
||||
@@ -134,7 +134,19 @@ class GfxRenderer {
|
||||
// Screen ops
|
||||
int getScreenWidth() const;
|
||||
int getScreenHeight() const;
|
||||
void tapToLogical(float nx, float ny, int& outX, int& outY) const;
|
||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
|
||||
// grayscale strip rendering) can overlap the panel's refresh time. The
|
||||
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
|
||||
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
|
||||
// support. See HalDisplay::displayBufferAsync for the baseline contract.
|
||||
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
void waitRefreshComplete() const;
|
||||
// True when displayBufferAsync() genuinely overlaps: panel defers and
|
||||
// fadingFix isn't forcing the blocking path. Callers can skip overlap
|
||||
// scaffolding (e.g. whole-plane grayscale buffers) when false.
|
||||
bool supportsAsyncRefresh() const;
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||
// void displayWindow(int x, int y, int width, int height) const;
|
||||
void invertScreen() const;
|
||||
@@ -183,12 +195,21 @@ 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;
|
||||
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||
|
||||
// Snapshot / restore a screen-coordinate framebuffer region (byte-aligned in
|
||||
// panel memory). readFramebufferRegion returns the bytes written to dst, or
|
||||
// 0 when the region is empty, offscreen, or exceeds dstCapacity. Pass the
|
||||
// same rectangle to writeFramebufferRegion to restore the saved pixels.
|
||||
// Enables partial-repaint patterns (e.g. moving a selection highlight)
|
||||
// without re-rendering the whole page.
|
||||
size_t readFramebufferRegion(int x, int y, int w, int h, uint8_t* dst, size_t dstCapacity) const;
|
||||
void writeFramebufferRegion(int x, int y, int w, int h, const uint8_t* src);
|
||||
|
||||
// Text
|
||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
||||
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
|
||||
@@ -250,6 +271,34 @@ class GfxRenderer {
|
||||
// Font helpers
|
||||
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
||||
|
||||
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
|
||||
// builds) WITHOUT freeing the allocation, so it never moves and repeated
|
||||
// loans cannot fragment the heap. Between release and restore NOTHING may
|
||||
// draw or display — the panel keeps showing its last refreshed image. The
|
||||
// lent bytes are published via buildscratch::claim() for consumers like
|
||||
// InflateStream. restore returns the buffer white, so the caller must
|
||||
// redraw the full screen; it cannot fail (no allocation involved).
|
||||
void releaseFrameBufferForBuild();
|
||||
bool restoreFrameBufferAfterBuild();
|
||||
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
|
||||
|
||||
// RAII form of the loan above, for blocking build regions with early-return
|
||||
// error paths: restores on scope exit (or explicitly via end()). Display the
|
||||
// popup/screen the panel should hold BEFORE constructing one. Constructing
|
||||
// while the framebuffer is already lent yields an inert loan (nesting-safe).
|
||||
class FrameBufferLoan {
|
||||
public:
|
||||
explicit FrameBufferLoan(GfxRenderer& renderer);
|
||||
~FrameBufferLoan() { end(); }
|
||||
void end();
|
||||
FrameBufferLoan(const FrameBufferLoan&) = delete;
|
||||
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
|
||||
|
||||
private:
|
||||
GfxRenderer& renderer_;
|
||||
bool active_ = false;
|
||||
};
|
||||
|
||||
// Low level functions
|
||||
uint8_t* getFrameBuffer() const;
|
||||
size_t getBufferSize() const;
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
_language_name: "العربية"
|
||||
_language_code: "AR"
|
||||
_order: "28"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "جاري بدء التشغيل"
|
||||
STR_SLEEPING: "وضع السكون"
|
||||
STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون"
|
||||
STR_BROWSE_FILES: "تصفح الملفات"
|
||||
STR_FILE_TRANSFER: "نقل الملفات"
|
||||
STR_SETTINGS_TITLE: "الإعدادات"
|
||||
STR_CONTINUE_READING: "متابعة القراءة"
|
||||
STR_NO_OPEN_BOOK: "لا يوجد كتاب مفتوح"
|
||||
STR_START_READING: "ابدأ القراءة أدناه"
|
||||
STR_NO_FILES_FOUND: "لم يتم العثور على ملفات"
|
||||
STR_SELECT_CHAPTER: "اختر الفصل"
|
||||
STR_NO_CHAPTERS: "لا توجد فصول"
|
||||
STR_END_OF_BOOK: "نهاية الكتاب"
|
||||
STR_EMPTY_CHAPTER: "فصل فارغ"
|
||||
STR_INDEXING: "جاري الفهرسة"
|
||||
STR_INDEX_FAILED: "فشلت الفهرسة - كتاب غير صالح"
|
||||
STR_MEMORY_ERROR: "خطأ في الذاكرة"
|
||||
STR_PAGE_LOAD_ERROR: "خطأ في تحميل الصفحة"
|
||||
STR_EMPTY_FILE: "ملف فارغ"
|
||||
STR_OUT_OF_BOUNDS: "خارج النطاق"
|
||||
STR_LOADING: "جاري التحميل..."
|
||||
STR_LOADING_POPUP: "جاري التحميل"
|
||||
STR_WIFI_NETWORKS: "شبكات Wi-Fi"
|
||||
STR_NO_NETWORKS: "لم يتم العثور على شبكات"
|
||||
STR_NETWORKS_FOUND: "تم العثور على %zu شبكة"
|
||||
STR_SCANNING: "جاري البحث..."
|
||||
STR_FINDING_SAVED_WIFI: "جاري البحث عن شبكة Wi-Fi محفوظة..."
|
||||
STR_CONNECTING: "جاري الاتصال..."
|
||||
STR_CONNECTING_SAVED_WIFI: "جاري الاتصال بشبكة Wi-Fi محفوظة..."
|
||||
STR_SHOW_NETWORKS: "عرض"
|
||||
STR_CONNECTED: "تم الاتصال!"
|
||||
STR_CONNECTION_FAILED: "فشل الاتصال"
|
||||
STR_FORGET_NETWORK: "نسيان هذه الشبكة؟"
|
||||
STR_SAVE_PASSWORD: "حفظ كلمة المرور للمرة القادمة؟"
|
||||
STR_PRESS_OK_SCAN: "اضغط موافق لإعادة البحث"
|
||||
STR_JOIN_NETWORK: "الانضمام إلى شبكة"
|
||||
STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال"
|
||||
STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة"
|
||||
STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها"
|
||||
STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..."
|
||||
STR_HOTSPOT_MODE: "وضع نقطة الاتصال"
|
||||
STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه"
|
||||
STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح"
|
||||
STR_OR_HTTP_PREFIX: "أو http://"
|
||||
STR_SCAN_QR_HINT: "أو امسح رمز QR بهاتفك:"
|
||||
STR_CALIBRE_WIRELESS: "اتصال Calibre اللاسلكي"
|
||||
STR_NETWORK_LEGEND: "* = مشفرة | + = محفوظة"
|
||||
STR_MAC_ADDRESS: "عنوان MAC:"
|
||||
STR_CHECKING_WIFI: "جاري فحص Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "أدخل كلمة مرور الشبكة"
|
||||
STR_ADD_HIDDEN_NETWORK: "إضافة شبكة مخفية..."
|
||||
STR_ENTER_WIFI_SSID: "أدخل اسم الشبكة (SSID)"
|
||||
STR_TO_PREFIX: "إلى "
|
||||
STR_CALIBRE_RECEIVING: "جاري الاستلام: "
|
||||
STR_CALIBRE_RECEIVED: "تم الاستلام: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) قم بتثبيت إضافة CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) اتصل بنفس شبكة Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) في Calibre: اختر \"إرسال إلى الجهاز\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "اترك هذه الشاشة مفتوحة أثناء الإرسال"
|
||||
STR_CAT_DISPLAY: "الشاشة"
|
||||
STR_CAT_READER: "القراءة"
|
||||
STR_CAT_CONTROLS: "الأزرار"
|
||||
STR_CAT_SYSTEM: "النظام"
|
||||
STR_SLEEP_SCREEN: "شاشة السكون"
|
||||
STR_QUICK_RESUME_TIMEOUT: "استئناف سريع بعد المهلة"
|
||||
STR_SLEEP_COVER_MODE: "عرض الغلاف في وضع السكون"
|
||||
STR_HIDE_BATTERY: "إخفاء نسبة البطارية"
|
||||
STR_EXTRA_SPACING: "تباعد إضافي بين الفقرات"
|
||||
STR_TEXT_AA: "تنعيم حواف النص"
|
||||
STR_IMAGES: "الصور"
|
||||
STR_IMAGES_DISPLAY: "عرض"
|
||||
STR_IMAGES_PLACEHOLDER: "عنصر نائب"
|
||||
STR_IMAGES_SUPPRESS: "إخفاء"
|
||||
STR_EOB_HOME: "الرئيسية"
|
||||
STR_EOB_CONTINUE_WITH: "المتابعة إلى"
|
||||
STR_SHORT_PWR_BTN: "ضغطة قصيرة على زر التشغيل"
|
||||
STR_ORIENTATION: "اتجاه القراءة"
|
||||
STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "تدوير الأزرار الأمامية مع الشاشة"
|
||||
STR_LONG_PRESS_BEHAVIOR: "سلوك الضغطة الطويلة"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "إيقاف"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "تخطي الفصل"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "تغيير الاتجاه"
|
||||
STR_LONG_PRESS_MENU: "قائمة الضغطة الطويلة"
|
||||
STR_FONT_PREVIEW_TEXT: "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"
|
||||
STR_FONT_FAMILY: "خط القراءة"
|
||||
STR_FONT_SIZE: "حجم الخط"
|
||||
STR_LINE_SPACING: "تباعد الأسطر"
|
||||
STR_SCREEN_MARGIN: "هوامش شاشة القراءة"
|
||||
STR_PARA_ALIGNMENT: "محاذاة الفقرات"
|
||||
STR_HYPHENATION: "تقسيم الكلمات"
|
||||
STR_TIME_TO_SLEEP: "مهلة الدخول في السكون"
|
||||
STR_SHOW_HIDDEN_FILES: "عرض الملفات المخفية"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "إزالة الكتب المقروءة من قائمة الكتب الأخيرة"
|
||||
STR_MOVE_FINISHED_TO_READ: "نقل الكتب المنتهية إلى مجلد المقروءة"
|
||||
STR_REFRESH_FREQ: "معدل تحديث الشاشة"
|
||||
STR_KOREADER_SYNC: "مزامنة KOReader"
|
||||
STR_CHECK_UPDATES: "التحقق من التحديثات"
|
||||
STR_LANGUAGE: "اللغة"
|
||||
STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة"
|
||||
STR_USERNAME: "اسم المستخدم"
|
||||
STR_PASSWORD: "كلمة المرور"
|
||||
STR_SYNC_SERVER_URL: "عنوان خادم المزامنة"
|
||||
STR_DOCUMENT_MATCHING: "مطابقة المستندات"
|
||||
STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند"
|
||||
STR_AUTHENTICATE: "تسجيل الدخول"
|
||||
STR_KOREADER_USERNAME: "اسم مستخدم KOReader"
|
||||
STR_KOREADER_PASSWORD: "كلمة مرور KOReader"
|
||||
STR_FILENAME: "اسم الملف"
|
||||
STR_BINARY: "ثنائي"
|
||||
STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا"
|
||||
STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة"
|
||||
STR_AUTHENTICATING: "جاري التحقق..."
|
||||
STR_AUTH_SUCCESS: "تم تسجيل الدخول بنجاح!"
|
||||
STR_KOREADER_AUTH: "تسجيل الدخول إلى KOReader"
|
||||
STR_SYNC_READY: "مزامنة KOReader جاهزة للاستخدام"
|
||||
STR_AUTH_FAILED: "فشل تسجيل الدخول"
|
||||
STR_DONE: "تم"
|
||||
STR_CLEAR_CACHE_WARNING_1: "سيؤدي هذا إلى مسح جميع بيانات الكتب المخزنة."
|
||||
STR_CLEAR_CACHE_WARNING_2: "سيتم فقدان كل تقدم القراءة!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "ستحتاج الكتب إلى إعادة فهرسة"
|
||||
STR_CLEAR_CACHE_WARNING_4: "عند فتحها مرة أخرى."
|
||||
STR_CLEARING_CACHE: "جاري مسح الذاكرة المؤقتة..."
|
||||
STR_CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
|
||||
STR_ITEMS_REMOVED: "عناصر تمت إزالتها"
|
||||
STR_FAILED_LOWER: "فشل"
|
||||
STR_CLEAR_CACHE_FAILED: "فشل مسح الذاكرة المؤقتة"
|
||||
STR_CHECK_SERIAL_OUTPUT: "راجع مخرجات المنفذ التسلسلي للتفاصيل"
|
||||
STR_DARK: "داكن"
|
||||
STR_LIGHT: "فاتح"
|
||||
STR_CUSTOM: "مخصص"
|
||||
STR_COVER: "الغلاف"
|
||||
STR_NONE_OPT: "بدون"
|
||||
STR_FIT: "ملاءمة"
|
||||
STR_CROP: "قص"
|
||||
STR_NEVER: "أبدا"
|
||||
STR_IN_READER: "أثناء القراءة"
|
||||
STR_ALWAYS: "دائما"
|
||||
STR_IGNORE: "تجاهل"
|
||||
STR_SLEEP: "سكون"
|
||||
STR_PAGE_TURN: "تقليب الصفحة"
|
||||
STR_FORCE_REFRESH: "تحديث الشاشة"
|
||||
STR_PORTRAIT: "عمودي"
|
||||
STR_LANDSCAPE_CW: "أفقي (يمين)"
|
||||
STR_INVERTED: "عكس الألوان"
|
||||
STR_ORIENTATION_INVERTED: "عمودي 180°"
|
||||
STR_LANDSCAPE_CCW: "أفقي (يسار)"
|
||||
STR_PREV_NEXT: "السابق/التالي"
|
||||
STR_NEXT_PREV: "التالي/السابق"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "إشارة مرجعية"
|
||||
STR_DISABLED: "معطل"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "صغير"
|
||||
STR_MEDIUM: "متوسط"
|
||||
STR_LARGE: "كبير"
|
||||
STR_X_LARGE: "كبير جدا"
|
||||
STR_TIGHT: "ضيق"
|
||||
STR_NORMAL: "عادي"
|
||||
STR_WIDE: "واسع"
|
||||
STR_JUSTIFY: "ضبط الطرفين"
|
||||
STR_ALIGN_LEFT: "يسار"
|
||||
STR_CENTER: "وسط"
|
||||
STR_ALIGN_RIGHT: "يمين"
|
||||
STR_PAGES_1: "صفحة واحدة"
|
||||
STR_PAGES_5: "5 صفحات"
|
||||
STR_PAGES_10: "10 صفحات"
|
||||
STR_PAGES_15: "15 صفحة"
|
||||
STR_PAGES_30: "30 صفحة"
|
||||
STR_UPDATE: "تحديث"
|
||||
STR_CHECKING_UPDATE: "جاري التحقق من التحديثات..."
|
||||
STR_NEW_UPDATE: "يتوفر تحديث جديد!"
|
||||
STR_CURRENT_VERSION: "الإصدار الحالي: "
|
||||
STR_NEW_VERSION: "الإصدار الجديد: "
|
||||
STR_UPDATING: "جاري التحديث..."
|
||||
STR_NO_UPDATE: "لا يتوفر تحديث"
|
||||
STR_UPDATE_FAILED: "فشل التحديث"
|
||||
STR_UPDATE_COMPLETE: "اكتمل التحديث"
|
||||
STR_POWER_ON_HINT: "اضغط مطولا على زر التشغيل لتشغيل الجهاز من جديد"
|
||||
STR_RESTARTING_HINT: "جاري إعادة التشغيل... إذا لم يعمل الجهاز، اضغط مطولا على زر التشغيل لعدة ثوان."
|
||||
STR_NO_ENTRIES: "لم يتم العثور على عناصر"
|
||||
STR_DOWNLOADING: "جاري التنزيل..."
|
||||
STR_DOWNLOAD_FAILED: "فشل التنزيل"
|
||||
STR_ERROR_MSG: "خطأ:"
|
||||
STR_UNNAMED: "بدون اسم"
|
||||
STR_HOLD_OPEN_TO_DELETE: "اضغط مطولا على فتح للحذف"
|
||||
STR_NO_SERVER_URL: "لم يتم تحديد عنوان الخادم"
|
||||
STR_FETCH_FEED_FAILED: "فشل جلب القائمة"
|
||||
STR_PARSE_FEED_FAILED: "فشل تحليل القائمة"
|
||||
STR_NEXT_PAGE: "الصفحة التالية"
|
||||
STR_PREV_PAGE: "الصفحة السابقة"
|
||||
STR_NETWORK_PREFIX: "الشبكة: "
|
||||
STR_IP_ADDRESS_PREFIX: "عنوان IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "خطأ: فشل عام"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "خطأ: الشبكة غير موجودة"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "خطأ: انتهت مهلة الاتصال"
|
||||
STR_SD_CARD: "بطاقة SD"
|
||||
STR_BACK: "رجوع »"
|
||||
STR_EXIT: "خروج »"
|
||||
STR_HOME: "الرئيسية »"
|
||||
STR_SELECT: "اختيار"
|
||||
STR_SELECTED: "محدد"
|
||||
STR_TOGGLE: "تبديل"
|
||||
STR_TOGGLE_BOOKMARK: "إضافة/إزالة إشارة مرجعية"
|
||||
STR_CONFIRM: "تأكيد"
|
||||
STR_CANCEL: "إلغاء"
|
||||
STR_CONNECT: "اتصال"
|
||||
STR_OPEN: "فتح"
|
||||
STR_DOWNLOAD: "تنزيل"
|
||||
STR_RETRY: "إعادة المحاولة"
|
||||
STR_YES: "نعم"
|
||||
STR_NO: "لا"
|
||||
STR_SHOW: "عرض"
|
||||
STR_HIDE: "إخفاء"
|
||||
STR_STATE_ON: "تشغيل"
|
||||
STR_STATE_OFF: "إيقاف"
|
||||
STR_NOT_SET: "غير محدد"
|
||||
STR_DIR_LEFT: "يسار"
|
||||
STR_DIR_RIGHT: "يمين"
|
||||
STR_DIR_UP: "أعلى"
|
||||
STR_DIR_DOWN: "أسفل"
|
||||
STR_OK_BUTTON: "موافق"
|
||||
STR_SLEEP_COVER_FILTER: "مرشح غلاف شاشة السكون"
|
||||
STR_FILTER_CONTRAST: "التباين"
|
||||
STR_CUSTOMISE_STATUS_BAR: "تخصيص شريط الحالة"
|
||||
STR_CHAPTER_PAGE_COUNT: "عدد صفحات الفصل"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "نسبة التقدم في الكتاب"
|
||||
STR_PROGRESS_BAR: "شريط التقدم"
|
||||
STR_PROGRESS_BAR_THICKNESS: "سمك شريط التقدم"
|
||||
STR_PROGRESS_BAR_THIN: "رفيع"
|
||||
STR_PROGRESS_BAR_MEDIUM: "متوسط"
|
||||
STR_PROGRESS_BAR_THICK: "سميك"
|
||||
STR_BOOK: "الكتاب"
|
||||
STR_CHAPTER: "الفصل"
|
||||
STR_EXAMPLE_CHAPTER: "الفصل 21"
|
||||
STR_EXAMPLE_BOOK: "عنوان الكتاب"
|
||||
STR_PREVIEW: "معاينة"
|
||||
STR_TITLE: "العنوان"
|
||||
STR_BATTERY: "البطارية"
|
||||
STR_XTC_STATUS_BAR: "شريط حالة XTC"
|
||||
STR_BOTTOM: "أسفل"
|
||||
STR_TOP: "أعلى"
|
||||
STR_CLOCK: "الساعة"
|
||||
STR_CLOCK_UTC_OFFSET: "فرق التوقيت عن UTC"
|
||||
STR_CLOCK_FORMAT: "تنسيق الساعة"
|
||||
STR_CLOCK_FORMAT_24H: "24 ساعة"
|
||||
STR_CLOCK_FORMAT_12H: "12 ساعة"
|
||||
STR_CURRENT_TIME: "الوقت الحالي:"
|
||||
STR_NEXT_FIELD: "التالي"
|
||||
STR_CLOCK_SYNC: "مزامنة الساعة"
|
||||
STR_CLOCK_SYNC_NOW: "مزامنة الساعة الآن"
|
||||
STR_CLOCK_SYNCING: "جاري المزامنة عبر NTP..."
|
||||
STR_CLOCK_SYNC_OK: "تمت مزامنة الساعة"
|
||||
STR_CLOCK_SYNC_FAIL: "فشلت المزامنة"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "لا يوجد اتصال Wi-Fi"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "اتصل بشبكة Wi-Fi أولا، ثم حاول مرة أخرى."
|
||||
STR_CLOCK_SYNCED: "تمت مزامنة الساعة"
|
||||
STR_UI_THEME: "مظهر الواجهة"
|
||||
STR_THEME_CLASSIC: "كلاسيكي"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra موسع"
|
||||
STR_SUNLIGHT_FADING_FIX: "إصلاح بهتان الشاشة في الشمس"
|
||||
STR_REMAP_FRONT_BUTTONS: "تغيير وظائف الأزرار الأمامية"
|
||||
STR_BOOKMARKS: "الإشارات المرجعية"
|
||||
STR_BOOKMARK_ADDED: "تمت إضافة الإشارة المرجعية."
|
||||
STR_BOOKMARK_REMOVED: "تمت إزالة الإشارة المرجعية."
|
||||
STR_OPDS_BROWSER: "متصفح OPDS"
|
||||
STR_SEARCH: "بحث"
|
||||
STR_COVER_CUSTOM: "الغلاف + مخصص"
|
||||
STR_QUICK_RESUME: "استئناف سريع"
|
||||
STR_MENU_RECENT_BOOKS: "الكتب الأخيرة"
|
||||
STR_REMOVE_FROM_RECENTS: "إزالة من الكتب الأخيرة؟"
|
||||
STR_NO_RECENT_BOOKS: "لا توجد كتب أخيرة"
|
||||
STR_CALIBRE_DESC: "استخدم النقل اللاسلكي من Calibre"
|
||||
STR_FORGET_AND_REMOVE: "نسيان الشبكة وحذف كلمة المرور المحفوظة؟"
|
||||
STR_FORGET_BUTTON: "نسيان"
|
||||
STR_CALIBRE_STARTING: "جاري تشغيل Calibre..."
|
||||
STR_CALIBRE_SETUP: "الإعداد"
|
||||
STR_CALIBRE_STATUS: "الحالة"
|
||||
STR_CLEAR_BUTTON: "مسح"
|
||||
STR_DEFAULT_VALUE: "افتراضي"
|
||||
STR_REMAP_PROMPT: "اضغط زرا أماميا لكل وظيفة"
|
||||
STR_UNASSIGNED: "غير معين"
|
||||
STR_ALREADY_ASSIGNED: "معين مسبقا"
|
||||
STR_REMAP_RESET_HINT: "الزر الجانبي العلوي: استعادة التخطيط الافتراضي"
|
||||
STR_REMAP_CANCEL_HINT: "الزر الجانبي السفلي: إلغاء التغيير"
|
||||
STR_HW_BACK_LABEL: "رجوع (الزر 1)"
|
||||
STR_HW_CONFIRM_LABEL: "تأكيد (الزر 2)"
|
||||
STR_HW_LEFT_LABEL: "يسار (الزر 3)"
|
||||
STR_HW_RIGHT_LABEL: "يمين (الزر 4)"
|
||||
STR_GO_TO_PERCENT: "الانتقال إلى نسبة %"
|
||||
STR_GO_HOME_BUTTON: "العودة إلى الرئيسية"
|
||||
STR_SYNC_PROGRESS: "مزامنة التقدم"
|
||||
STR_DELETE_CACHE: "حذف ذاكرة الكتاب المؤقتة"
|
||||
STR_DELETE: "حذف"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "حذف هذه الإشارة المرجعية؟"
|
||||
STR_DISPLAY_QR: "عرض الصفحة كرمز QR"
|
||||
STR_CHAPTER_PREFIX: "الفصل: "
|
||||
STR_PAGES_SEPARATOR: " صفحات | "
|
||||
STR_BOOK_PREFIX: "الكتاب: "
|
||||
STR_CALIBRE_URL_HINT: "في Calibre، أضف /opds إلى العنوان"
|
||||
STR_SYNCING_TIME: "جاري مزامنة الوقت..."
|
||||
STR_CALC_HASH: "جاري حساب بصمة المستند..."
|
||||
STR_HASH_FAILED: "فشل حساب بصمة المستند"
|
||||
STR_FETCH_PROGRESS: "جاري جلب التقدم من الخادم..."
|
||||
STR_UPLOAD_PROGRESS: "جاري رفع التقدم..."
|
||||
STR_NO_CREDENTIALS_MSG: "لم يتم إعداد بيانات الاعتماد"
|
||||
STR_KOREADER_SETUP_HINT: "قم بإعداد حساب KOReader في الإعدادات"
|
||||
STR_PROGRESS_FOUND: "تم العثور على تقدم سابق!"
|
||||
STR_REMOTE_LABEL: "الخادم:"
|
||||
STR_LOCAL_LABEL: "الجهاز:"
|
||||
STR_PAGE_OVERALL_FORMAT: "صفحة %d، %.2f%% إجمالا"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "صفحة %d من %d، %.2f%% إجمالا"
|
||||
STR_DEVICE_FROM_FORMAT: " من: %s"
|
||||
STR_APPLY_REMOTE: "استخدام تقدم الخادم"
|
||||
STR_UPLOAD_LOCAL: "رفع التقدم المحلي"
|
||||
STR_NO_REMOTE_MSG: "لا يوجد تقدم على الخادم"
|
||||
STR_UPLOAD_PROMPT: "رفع الموضع الحالي؟"
|
||||
STR_UPLOAD_SUCCESS: "تم رفع التقدم!"
|
||||
STR_SYNC_FAILED_MSG: "فشلت المزامنة"
|
||||
STR_SAVE_PROGRESS_FAILED: "تعذر حفظ التقدم"
|
||||
STR_SECTION_PREFIX: "القسم "
|
||||
STR_UPLOAD: "رفع"
|
||||
STR_BOOK_S_STYLE: "تنسيق الكتاب الأصلي"
|
||||
STR_EMBEDDED_STYLE: "التنسيق المضمن"
|
||||
STR_FOCUS_READING: "قراءة مركزة"
|
||||
STR_OPDS_SERVER_URL: "عنوان خادم OPDS"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي"
|
||||
STR_SET_SLEEP_COVER: "تعيين الغلاف"
|
||||
STR_FOOTNOTES: "الحواشي"
|
||||
STR_NO_FOOTNOTES: "لا توجد حواش في هذه الصفحة"
|
||||
STR_LINK: "[رابط]"
|
||||
STR_SCREENSHOT_BUTTON: "التقاط لقطة شاشة"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u دقيقة"
|
||||
STR_SLEEP_NEVER: "أبدا"
|
||||
STR_STEP_HINT_FRONT: "الأزرار الأمامية:"
|
||||
STR_STEP_HINT_SIDE: "الأزرار الجانبية:"
|
||||
STR_ADD_SERVER: "إضافة خادم"
|
||||
STR_SERVER_NAME: "اسم الخادم"
|
||||
STR_NO_SERVERS: "لم يتم إعداد خوادم OPDS"
|
||||
STR_DELETE_SERVER: "حذف الخادم"
|
||||
STR_OPDS_SERVERS: "خوادم OPDS"
|
||||
STR_AUTO_TURN_ENABLED: "التقليب التلقائي مفعل: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "التقليب التلقائي (صفحات في الدقيقة)"
|
||||
STR_MANAGE_FONTS: "إدارة الخطوط"
|
||||
STR_FONT_BROWSER: "متصفح الخطوط"
|
||||
STR_LOADING_FONT_LIST: "جاري تحميل قائمة الخطوط..."
|
||||
STR_NO_FONTS_AVAILABLE: "لا تتوفر خطوط"
|
||||
STR_FONT_INSTALLED: "تم تثبيت الخط!"
|
||||
STR_FONT_INSTALL_FAILED: "فشل تثبيت الخط"
|
||||
STR_INSTALLED: "مثبت"
|
||||
STR_DOWNLOAD_ALL: "تنزيل الكل"
|
||||
STR_UPDATE_ALL: "تحديث الكل"
|
||||
STR_UPDATE_AVAILABLE: "تحديث"
|
||||
STR_CRASH_TITLE: "تعطل النظام"
|
||||
STR_CRASH_DESCRIPTION: "تم حفظ تقرير مفصل في الملف crash_report.txt. يرجى إرفاق هذا الملف عند الإبلاغ عن المشكلة."
|
||||
STR_CRASH_REASON: "سبب التعطل:"
|
||||
STR_CRASH_NO_REASON: "(لم يتم تسجيل سبب)"
|
||||
STR_TILT_PAGE_TURN: "تقليب الصفحة بالإمالة"
|
||||
STR_KB_HINT_MOVE_CURSOR: "اضغط زر اليمين أو اليسار لتحريك المؤشر"
|
||||
STR_KB_HINT_RETURN_CURSOR: "اضغط زر اليسار للعودة إلى موضع المؤشر"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [***] لإخفاء كلمة المرور"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [abc] لإظهار كلمة المرور"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "اضغط [***] لإخفاء كلمة المرور"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "اضغط [abc] لإظهار كلمة المرور"
|
||||
STR_KB_HINT_EDIT_ENTRY: "اضغط مطولا على زر الأعلى للتعديل"
|
||||
STR_KB_TIPS: "نصائح:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "اضغط زر الأسفل للعودة إلى لوحة المفاتيح"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "اضغط ABC للخروج من وضع URL"
|
||||
STR_KB_HINT_CLEAR_TEXT: "اضغط مطولا على DEL لمسح كل النص"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "اضغط مطولا على زر الاختيار للحرف الثانوي"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي"
|
||||
STR_KB_HINT_URL_SNIPPETS: "اضغط URL للاختصارات"
|
||||
STR_SD_FIRMWARE_UPDATE: "تحديث البرنامج الثابت من بطاقة SD"
|
||||
STR_SELECT_FIRMWARE_FILE: "اختر ملف البرنامج الثابت (.bin)"
|
||||
STR_NO_BIN_FILES: "لم يتم العثور على ملفات .bin"
|
||||
STR_VALIDATING_FIRMWARE: "جاري التحقق من البرنامج الثابت..."
|
||||
STR_INVALID_FIRMWARE: "ملف برنامج ثابت غير صالح"
|
||||
STR_FIRMWARE_TOO_LARGE: "البرنامج الثابت أكبر من القسم المخصص له"
|
||||
STR_FIRMWARE_TOO_SMALL: "ملف البرنامج الثابت صغير جدا"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "تحديث البرنامج الثابت؟"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "تعذر فتح الملف"
|
||||
STR_FIRMWARE_WRITE_FAILED: "فشلت كتابة البرنامج الثابت"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "لا تطفئ الجهاز!"
|
||||
STR_RECOVERY_MODE: "وضع الاستعادة"
|
||||
STR_RECOVERY_MODE_HINT: "ضع الملف firmware.bin في المجلد الرئيسي لبطاقة SD ثم اختره"
|
||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Згладжванне тэксту"
|
||||
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
|
||||
STR_ORIENTATION: "Арыентацыя чытання"
|
||||
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
|
||||
STR_TOUCH_READER_CONTROLS: "Сэнсарнае кіраванне чытаннем"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
|
||||
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
|
||||
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
|
||||
@@ -87,6 +88,7 @@ STR_USERNAME: "Імя карыстальніка"
|
||||
STR_PASSWORD: "Пароль"
|
||||
STR_SYNC_SERVER_URL: "URL сервера сінхранізацыі"
|
||||
STR_DOCUMENT_MATCHING: "Супастаўленне дакументаў"
|
||||
STR_SEND_METADATA: "Адпраўляць метаданыя дакумента"
|
||||
STR_AUTHENTICATE: "Аўтарызацыя"
|
||||
STR_KOREADER_USERNAME: "Імя карыстальніка KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
@@ -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,15 @@ 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_OPDS_DOWNLOAD_FOLDER: "Папка для спампоўкі"
|
||||
STR_OPDS_FILENAME_FORMAT: "Фармат назвы файла"
|
||||
STR_FMT_AUTHOR_TITLE: "Аўтар - Назва"
|
||||
STR_FMT_TITLE_AUTHOR: "Назва - Аўтар"
|
||||
STR_FMT_TITLE: "Назва"
|
||||
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
|
||||
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
|
||||
STR_ADD_HIDDEN_NETWORK: "Дадаць схаваную сетку..."
|
||||
STR_ENTER_WIFI_SSID: "Увядзіце назву сеткі (SSID)"
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
_language_name: "Bosanski"
|
||||
_language_code: "BS"
|
||||
_order: "29"
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "POKRETANJE"
|
||||
STR_SLEEPING: "REŽIM MIROVANJA"
|
||||
STR_ENTERING_SLEEP: "Prelazak u režim mirovanja"
|
||||
STR_BROWSE_FILES: "Pregled datoteka"
|
||||
STR_FILE_TRANSFER: "Prijenos datoteka"
|
||||
STR_SETTINGS_TITLE: "Postavke"
|
||||
STR_CONTINUE_READING: "Nastavi čitanje"
|
||||
STR_NO_OPEN_BOOK: "Nema otvorene knjige"
|
||||
STR_START_READING: "Počni čitati ispod"
|
||||
STR_NO_FILES_FOUND: "Nema pronađenih datoteka"
|
||||
STR_SELECT_CHAPTER: "Odaberi poglavlje"
|
||||
STR_NO_CHAPTERS: "Nema poglavlja"
|
||||
STR_END_OF_BOOK: "Kraj knjige"
|
||||
STR_EMPTY_CHAPTER: "Prazno poglavlje"
|
||||
STR_INDEXING: "Indeksiranje"
|
||||
STR_INDEX_FAILED: "Indeksiranje nije uspjelo - nevažeća knjiga"
|
||||
STR_MEMORY_ERROR: "Greška memorije"
|
||||
STR_PAGE_LOAD_ERROR: "Greška pri učitavanju stranice"
|
||||
STR_EMPTY_FILE: "Prazna datoteka"
|
||||
STR_OUT_OF_BOUNDS: "Izvan granica"
|
||||
STR_LOADING: "Učitavanje..."
|
||||
STR_LOADING_POPUP: "Učitavanje"
|
||||
STR_WIFI_NETWORKS: "Wi-Fi mreže"
|
||||
STR_NO_NETWORKS: "Nema pronađenih mreža"
|
||||
STR_NETWORKS_FOUND: "%zu pronađenih mreža"
|
||||
STR_SCANNING: "Skeniranje..."
|
||||
STR_FINDING_SAVED_WIFI: "Traženje sačuvanog Wi-Fi-a..."
|
||||
STR_CONNECTING: "Povezivanje..."
|
||||
STR_CONNECTING_SAVED_WIFI: "Povezivanje na sačuvani Wi-Fi..."
|
||||
STR_SHOW_NETWORKS: "Prikaži"
|
||||
STR_CONNECTED: "Povezano!"
|
||||
STR_CONNECTION_FAILED: "Povezivanje nije uspjelo"
|
||||
STR_FORGET_NETWORK: "Zaboraviti mrežu?"
|
||||
STR_SAVE_PASSWORD: "Sačuvati lozinku za sljedeći put?"
|
||||
STR_PRESS_OK_SCAN: "Pritisni OK za ponovno skeniranje"
|
||||
STR_JOIN_NETWORK: "Pridruži se mreži"
|
||||
STR_CREATE_HOTSPOT: "Kreiraj hotspot"
|
||||
STR_JOIN_DESC: "Poveži se na postojeću Wi-Fi mrežu"
|
||||
STR_HOTSPOT_DESC: "Kreiraj Wi-Fi mrežu kojoj se drugi mogu pridružiti"
|
||||
STR_STARTING_HOTSPOT: "Pokretanje hotspota..."
|
||||
STR_HOTSPOT_MODE: "Hotspot način rada"
|
||||
STR_CONNECT_WIFI_HINT: "Poveži svoj uređaj na ovu Wi-Fi mrežu"
|
||||
STR_OPEN_URL_HINT: "Otvori ovaj URL u svom pregledniku"
|
||||
STR_OR_HTTP_PREFIX: "ili http://"
|
||||
STR_SCAN_QR_HINT: "ili skeniraj QR kod svojim telefonom:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre bežično"
|
||||
STR_NETWORK_LEGEND: "* = Šifrovano | + = Sačuvano"
|
||||
STR_MAC_ADDRESS: "MAC adresa:"
|
||||
STR_CHECKING_WIFI: "Provjera Wi-Fi-a..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Unesi Wi-Fi lozinku"
|
||||
STR_ADD_HIDDEN_NETWORK: "Dodaj skrivenu mrežu..."
|
||||
STR_ENTER_WIFI_SSID: "Unesi naziv mreže (SSID)"
|
||||
STR_TO_PREFIX: "na "
|
||||
STR_CALIBRE_RECEIVING: "Primanje: "
|
||||
STR_CALIBRE_RECEIVED: "Primljeno: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instaliraj CrossPoint Reader dodatak"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Budi na istoj Wi-Fi mreži"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) U Calibreu: \"Pošalji na uređaj\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Drži ovaj ekran otvorenim tokom slanja\""
|
||||
STR_CAT_DISPLAY: "Ekran"
|
||||
STR_CAT_READER: "Čitač"
|
||||
STR_CAT_CONTROLS: "Kontrole"
|
||||
STR_CAT_SYSTEM: "Sistem"
|
||||
STR_SLEEP_SCREEN: "Ekran mirovanja"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Brzi nastavak nakon isteka vremena"
|
||||
STR_SLEEP_COVER_MODE: "Način prikaza korica na ekranu mirovanja"
|
||||
STR_HIDE_BATTERY: "Sakrij % baterije"
|
||||
STR_EXTRA_SPACING: "Dodatni razmak između paragrafa"
|
||||
STR_TEXT_AA: "Zaglađivanje teksta"
|
||||
STR_IMAGES: "Slike"
|
||||
STR_IMAGES_DISPLAY: "Prikaz"
|
||||
STR_IMAGES_PLACEHOLDER: "Rezervisano mjesto"
|
||||
STR_IMAGES_SUPPRESS: "Sakrij"
|
||||
STR_EOB_HOME: "Početna"
|
||||
STR_EOB_CONTINUE_WITH: "Nastavi sa"
|
||||
STR_SHORT_PWR_BTN: "Kratak klik dugmeta napajanja"
|
||||
STR_ORIENTATION: "Orijentacija čitanja"
|
||||
STR_SIDE_BTN_LAYOUT: "Raspored bočnih dugmadi (čitač)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orijentiši prednja dugmad"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Ponašanje dugog pritiska dugmeta"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "ISKLJUČENO"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskakanje poglavlja"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Promjena orijentacije"
|
||||
STR_LONG_PRESS_MENU: "Meni dugog pritiska"
|
||||
STR_FONT_PREVIEW_TEXT: "Brza smeđa lisica preskače lijenog psa"
|
||||
STR_FONT_FAMILY: "Porodica fonta čitača"
|
||||
STR_FONT_SIZE: "Veličina fonta čitača"
|
||||
STR_LINE_SPACING: "Razmak između redova čitača"
|
||||
STR_SCREEN_MARGIN: "Margina ekrana čitača"
|
||||
STR_PARA_ALIGNMENT: "Poravnanje paragrafa čitača"
|
||||
STR_HYPHENATION: "Rastavljanje riječi"
|
||||
STR_TIME_TO_SLEEP: "Vrijeme do mirovanja"
|
||||
STR_SHOW_HIDDEN_FILES: "Prikaži skrivene datoteke"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "Ukloni pročitane knjige sa liste nedavnih"
|
||||
STR_MOVE_FINISHED_TO_READ: "Premjesti završene knjige u fasciklu Read"
|
||||
STR_REFRESH_FREQ: "Učestalost osvježavanja"
|
||||
STR_KOREADER_SYNC: "KOReader sinhronizacija"
|
||||
STR_CHECK_UPDATES: "Provjeri ažuriranja"
|
||||
STR_LANGUAGE: "Jezik"
|
||||
STR_CLEAR_READING_CACHE: "Očisti keš čitanja"
|
||||
STR_USERNAME: "Korisničko ime"
|
||||
STR_PASSWORD: "Lozinka"
|
||||
STR_SYNC_SERVER_URL: "URL servera za sinhronizaciju"
|
||||
STR_DOCUMENT_MATCHING: "Podudaranje dokumenata"
|
||||
STR_SEND_METADATA: "Pošalji metapodatke dokumenta"
|
||||
STR_AUTHENTICATE: "Autentifikuj"
|
||||
STR_KOREADER_USERNAME: "KOReader korisničko ime"
|
||||
STR_KOREADER_PASSWORD: "KOReader lozinka"
|
||||
STR_FILENAME: "Naziv datoteke"
|
||||
STR_BINARY: "Binarno"
|
||||
STR_SET_CREDENTIALS_FIRST: "Prvo postavi vjerodajnice"
|
||||
STR_WIFI_CONN_FAILED: "Wi-Fi povezivanje nije uspjelo"
|
||||
STR_AUTHENTICATING: "Autentifikacija..."
|
||||
STR_AUTH_SUCCESS: "Uspješno autentifikovano!"
|
||||
STR_KOREADER_AUTH: "KOReader autentifikacija"
|
||||
STR_SYNC_READY: "KOReader sinhronizacija je spremna za upotrebu"
|
||||
STR_AUTH_FAILED: "Autentifikacija nije uspjela"
|
||||
STR_DONE: "Gotovo"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Ovo će očistiti sve keširane podatke knjiga."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Sav napredak čitanja će biti izgubljen!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Knjige će morati biti ponovo indeksirane"
|
||||
STR_CLEAR_CACHE_WARNING_4: "kada se ponovo otvore."
|
||||
STR_CLEARING_CACHE: "Čišćenje keša..."
|
||||
STR_CACHE_CLEARED: "Keš očišćen"
|
||||
STR_ITEMS_REMOVED: "stavki uklonjeno"
|
||||
STR_FAILED_LOWER: "nije uspjelo"
|
||||
STR_CLEAR_CACHE_FAILED: "Čišćenje keša nije uspjelo"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Provjeri serijski izlaz za detalje"
|
||||
STR_DARK: "Tamno"
|
||||
STR_LIGHT: "Svijetlo"
|
||||
STR_CUSTOM: "Prilagođeno"
|
||||
STR_COVER: "Korice"
|
||||
STR_NONE_OPT: "Ništa"
|
||||
STR_FIT: "Prilagodi"
|
||||
STR_CROP: "Isjeci"
|
||||
STR_NEVER: "Nikad"
|
||||
STR_IN_READER: "U čitaču"
|
||||
STR_ALWAYS: "Uvijek"
|
||||
STR_IGNORE: "Ignoriši"
|
||||
STR_SLEEP: "Mirovanje"
|
||||
STR_PAGE_TURN: "Okretanje stranice"
|
||||
STR_FORCE_REFRESH: "Osvježi ekran"
|
||||
STR_PORTRAIT: "Uspravno"
|
||||
STR_LANDSCAPE_CW: "Položeno (u smjeru kazaljke na satu)"
|
||||
STR_INVERTED: "Obrnuto"
|
||||
STR_ORIENTATION_INVERTED: "Uspravno 180°"
|
||||
STR_LANDSCAPE_CCW: "Položeno (suprotno smjeru kazaljke na satu)"
|
||||
STR_PREV_NEXT: "Prethodno/Sljedeće"
|
||||
STR_NEXT_PREV: "Sljedeće/Prethodno"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Obilježivač"
|
||||
STR_DISABLED: "Onemogućeno"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Malo"
|
||||
STR_MEDIUM: "Srednje"
|
||||
STR_LARGE: "Veliko"
|
||||
STR_X_LARGE: "Vrlo veliko"
|
||||
STR_TIGHT: "Usko"
|
||||
STR_NORMAL: "Normalno"
|
||||
STR_WIDE: "Široko"
|
||||
STR_JUSTIFY: "Poravnaj obostrano"
|
||||
STR_ALIGN_LEFT: "Lijevo"
|
||||
STR_CENTER: "Centrirano"
|
||||
STR_ALIGN_RIGHT: "Desno"
|
||||
STR_PAGES_1: "1 stranica"
|
||||
STR_PAGES_5: "5 stranica"
|
||||
STR_PAGES_10: "10 stranica"
|
||||
STR_PAGES_15: "15 stranica"
|
||||
STR_PAGES_30: "30 stranica"
|
||||
STR_UPDATE: "Ažuriraj"
|
||||
STR_CHECKING_UPDATE: "Provjera ažuriranja..."
|
||||
STR_NEW_UPDATE: "Dostupno je novo ažuriranje!"
|
||||
STR_CURRENT_VERSION: "Trenutna verzija: "
|
||||
STR_NEW_VERSION: "Nova verzija: "
|
||||
STR_UPDATING: "Ažuriranje..."
|
||||
STR_NO_UPDATE: "Nema dostupnih ažuriranja"
|
||||
STR_UPDATE_FAILED: "Ažuriranje nije uspjelo"
|
||||
STR_UPDATE_COMPLETE: "Ažuriranje završeno"
|
||||
STR_POWER_ON_HINT: "Pritisni i drži dugme napajanja da ponovo uključiš"
|
||||
STR_RESTARTING_HINT: "Ponovno pokretanje... Ako se uređaj ne pokrene ponovo, drži dugme napajanja nekoliko sekundi."
|
||||
STR_NO_ENTRIES: "Nema pronađenih unosa"
|
||||
STR_DOWNLOADING: "Preuzimanje..."
|
||||
STR_DOWNLOAD_FAILED: "Preuzimanje nije uspjelo"
|
||||
STR_ERROR_MSG: "Greška:"
|
||||
STR_UNNAMED: "Neimenovano"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Drži Otvori za brisanje"
|
||||
STR_NO_SERVER_URL: "Nije podešen URL servera"
|
||||
STR_FETCH_FEED_FAILED: "Dohvatanje feeda nije uspjelo"
|
||||
STR_PARSE_FEED_FAILED: "Obrada feeda nije uspjela"
|
||||
STR_NEXT_PAGE: "Sljedeća stranica »"
|
||||
STR_PREV_PAGE: "« Prethodna stranica"
|
||||
STR_NETWORK_PREFIX: "Mreža: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP adresa: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Greška: Opšta greška"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Greška: Mreža nije pronađena"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Greška: Isteklo vrijeme veze"
|
||||
STR_SD_CARD: "SD kartica"
|
||||
STR_BACK: "« Nazad"
|
||||
STR_EXIT: "« Izlaz"
|
||||
STR_HOME: "« Početna"
|
||||
STR_SELECT: "Odaberi"
|
||||
STR_SELECTED: "Odabrano"
|
||||
STR_TOGGLE: "Promijeni"
|
||||
STR_TOGGLE_BOOKMARK: "Promijeni obilježivač"
|
||||
STR_CONFIRM: "Potvrdi"
|
||||
STR_CANCEL: "Otkaži"
|
||||
STR_CONNECT: "Poveži"
|
||||
STR_OPEN: "Otvori"
|
||||
STR_DOWNLOAD: "Preuzmi"
|
||||
STR_RETRY: "Pokušaj ponovo"
|
||||
STR_YES: "Da"
|
||||
STR_NO: "Ne"
|
||||
STR_SHOW: "Prikaži"
|
||||
STR_HIDE: "Sakrij"
|
||||
STR_STATE_ON: "UKLJUČENO"
|
||||
STR_STATE_OFF: "ISKLJUČENO"
|
||||
STR_NOT_SET: "Nije postavljeno"
|
||||
STR_DIR_LEFT: "Lijevo"
|
||||
STR_DIR_RIGHT: "Desno"
|
||||
STR_DIR_UP: "Gore"
|
||||
STR_DIR_DOWN: "Dolje"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filter korica ekrana mirovanja"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Prilagodi statusnu traku"
|
||||
STR_CHAPTER_PAGE_COUNT: "Broj stranica poglavlja"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Procenat napretka knjige"
|
||||
STR_PROGRESS_BAR: "Traka napretka"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Debljina trake napretka"
|
||||
STR_PROGRESS_BAR_THIN: "Tanka"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Srednja"
|
||||
STR_PROGRESS_BAR_THICK: "Debela"
|
||||
STR_BOOK: "Knjiga"
|
||||
STR_CHAPTER: "Poglavlje"
|
||||
STR_EXAMPLE_CHAPTER: "Poglavlje 21"
|
||||
STR_EXAMPLE_BOOK: "Naslov knjige"
|
||||
STR_PREVIEW: "Pregled"
|
||||
STR_TITLE: "Naslov"
|
||||
STR_BATTERY: "Baterija"
|
||||
STR_XTC_STATUS_BAR: "XTC statusna traka"
|
||||
STR_BOTTOM: "Dolje"
|
||||
STR_TOP: "Gore"
|
||||
STR_CLOCK: "Sat"
|
||||
STR_CLOCK_UTC_OFFSET: "UTC odstupanje sata"
|
||||
STR_CLOCK_FORMAT: "Format sata"
|
||||
STR_CLOCK_FORMAT_24H: "24-časovni"
|
||||
STR_CLOCK_FORMAT_12H: "12-časovni"
|
||||
STR_CURRENT_TIME: "Trenutno vrijeme:"
|
||||
STR_NEXT_FIELD: "Sljedeće"
|
||||
STR_CLOCK_SYNC: "Sinhronizuj sat"
|
||||
STR_CLOCK_SYNC_NOW: "Sinhronizuj sat sada"
|
||||
STR_CLOCK_SYNCING: "Sinhronizacija sa NTP-a..."
|
||||
STR_CLOCK_SYNC_OK: "Sat sinhronizovan"
|
||||
STR_CLOCK_SYNC_FAIL: "Sinhronizacija nije uspjela"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nije povezan"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Prvo se poveži na Wi-Fi, pa pokušaj ponovo."
|
||||
STR_CLOCK_SYNCED: "Sat sinhronizovan"
|
||||
STR_UI_THEME: "Tema korisničkog interfejsa"
|
||||
STR_THEME_CLASSIC: "Klasična"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra proširena"
|
||||
STR_SUNLIGHT_FADING_FIX: "Ispravka blijeđenja na suncu"
|
||||
STR_REMAP_FRONT_BUTTONS: "Premapiraj prednja dugmad"
|
||||
STR_BOOKMARKS: "Obilježivači"
|
||||
STR_BOOKMARK_ADDED: "Obilježivač dodan."
|
||||
STR_BOOKMARK_REMOVED: "Obilježivač uklonjen."
|
||||
STR_OPDS_BROWSER: "OPDS pregledač"
|
||||
STR_SEARCH: "Pretraga"
|
||||
STR_COVER_CUSTOM: "Korice + Prilagođeno"
|
||||
STR_QUICK_RESUME: "Brzi nastavak"
|
||||
STR_MENU_RECENT_BOOKS: "Nedavne knjige"
|
||||
STR_REMOVE_FROM_RECENTS: "Ukloniti iz nedavnih knjiga?"
|
||||
STR_NO_RECENT_BOOKS: "Nema nedavnih knjiga"
|
||||
STR_CALIBRE_DESC: "Koristi Calibre bežični prijenos uređaja"
|
||||
STR_FORGET_AND_REMOVE: "Zaboraviti mrežu i ukloniti sačuvanu lozinku?"
|
||||
STR_FORGET_BUTTON: "Zaboravi"
|
||||
STR_CALIBRE_STARTING: "Pokretanje Calibrea..."
|
||||
STR_CALIBRE_SETUP: "Podešavanje"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Očisti"
|
||||
STR_DEFAULT_VALUE: "Zadano"
|
||||
STR_REMAP_PROMPT: "Pritisni prednje dugme za svaku ulogu"
|
||||
STR_UNASSIGNED: "Nedodijeljeno"
|
||||
STR_ALREADY_ASSIGNED: "Već dodijeljeno"
|
||||
STR_REMAP_RESET_HINT: "Bočno dugme Gore: Vrati na zadani raspored"
|
||||
STR_REMAP_CANCEL_HINT: "Bočno dugme Dolje: Otkaži premapiranje"
|
||||
STR_HW_BACK_LABEL: "Nazad (1. dugme)"
|
||||
STR_HW_CONFIRM_LABEL: "Potvrdi (2. dugme)"
|
||||
STR_HW_LEFT_LABEL: "Lijevo (3. dugme)"
|
||||
STR_HW_RIGHT_LABEL: "Desno (4. dugme)"
|
||||
STR_GO_TO_PERCENT: "Idi na %"
|
||||
STR_GO_HOME_BUTTON: "Idi na početnu"
|
||||
STR_SYNC_PROGRESS: "Sinhronizuj napredak"
|
||||
STR_DELETE_CACHE: "Obriši keš knjige"
|
||||
STR_DELETE: "Obriši"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Obrisati ovaj obilježivač?"
|
||||
STR_DISPLAY_QR: "Prikaži stranicu kao QR"
|
||||
STR_CHAPTER_PREFIX: "Poglavlje: "
|
||||
STR_PAGES_SEPARATOR: " stranica | "
|
||||
STR_BOOK_PREFIX: "Knjiga: "
|
||||
STR_CALIBRE_URL_HINT: "Za Calibre, dodaj /opds na svoj URL"
|
||||
STR_SYNCING_TIME: "Sinhronizacija vremena..."
|
||||
STR_CALC_HASH: "Izračunavanje heša dokumenta..."
|
||||
STR_HASH_FAILED: "Izračunavanje heša dokumenta nije uspjelo"
|
||||
STR_FETCH_PROGRESS: "Dohvatanje udaljenog napretka..."
|
||||
STR_UPLOAD_PROGRESS: "Slanje napretka..."
|
||||
STR_NO_CREDENTIALS_MSG: "Nisu podešene vjerodajnice"
|
||||
STR_KOREADER_SETUP_HINT: "Podesi KOReader nalog u Postavkama"
|
||||
STR_PROGRESS_FOUND: "Napredak pronađen!"
|
||||
STR_REMOTE_LABEL: "Udaljeno:"
|
||||
STR_LOCAL_LABEL: "Lokalno:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Stranica %d, %.2f%% ukupno"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stranica %d/%d, %.2f%% ukupno"
|
||||
STR_DEVICE_FROM_FORMAT: " Sa: %s"
|
||||
STR_APPLY_REMOTE: "Primijeni udaljeni napredak"
|
||||
STR_UPLOAD_LOCAL: "Pošalji lokalni napredak"
|
||||
STR_NO_REMOTE_MSG: "Nije pronađen udaljeni napredak"
|
||||
STR_UPLOAD_PROMPT: "Poslati trenutnu poziciju?"
|
||||
STR_UPLOAD_SUCCESS: "Napredak poslan!"
|
||||
STR_SYNC_FAILED_MSG: "Sinhronizacija nije uspjela"
|
||||
STR_SAVE_PROGRESS_FAILED: "Napredak nije mogao biti sačuvan"
|
||||
STR_SECTION_PREFIX: "Sekcija "
|
||||
STR_UPLOAD: "Pošalji"
|
||||
STR_BOOK_S_STYLE: "Stil knjige"
|
||||
STR_EMBEDDED_STYLE: "Ugrađeni stil"
|
||||
STR_FOCUS_READING: "Fokusirano čitanje"
|
||||
STR_OPDS_SERVER_URL: "URL OPDS servera"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Brzi povratak iz fusnota"
|
||||
STR_SET_SLEEP_COVER: "Postavi korice"
|
||||
STR_FOOTNOTES: "Fusnote"
|
||||
STR_NO_FOOTNOTES: "Nema fusnota na ovoj stranici"
|
||||
STR_LINK: "[link]"
|
||||
STR_SCREENSHOT_BUTTON: "Snimi ekran"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||
STR_SLEEP_NEVER: "Nikad"
|
||||
STR_STEP_HINT_FRONT: "Prednja dugmad:"
|
||||
STR_STEP_HINT_SIDE: "Bočna dugmad:"
|
||||
STR_ADD_SERVER: "Dodaj server"
|
||||
STR_SERVER_NAME: "Naziv servera"
|
||||
STR_NO_SERVERS: "Nema podešenih OPDS servera"
|
||||
STR_DELETE_SERVER: "Obriši server"
|
||||
STR_OPDS_SERVERS: "OPDS serveri"
|
||||
STR_AUTO_TURN_ENABLED: "Automatsko okretanje omogućeno: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatsko okretanje (stranica po minuti)"
|
||||
STR_MANAGE_FONTS: "Upravljaj fontovima"
|
||||
STR_FONT_BROWSER: "Pregledač fontova"
|
||||
STR_LOADING_FONT_LIST: "Učitavanje liste fontova..."
|
||||
STR_NO_FONTS_AVAILABLE: "Nema dostupnih fontova"
|
||||
STR_FONT_INSTALLED: "Font instaliran!"
|
||||
STR_FONT_INSTALL_FAILED: "Instalacija fonta nije uspjela"
|
||||
STR_INSTALLED: "Instalirano"
|
||||
STR_DOWNLOAD_ALL: "Preuzmi sve"
|
||||
STR_UPDATE_ALL: "Ažuriraj sve"
|
||||
STR_UPDATE_AVAILABLE: "Ažuriranje"
|
||||
STR_CRASH_TITLE: "Sistemski pad"
|
||||
STR_CRASH_DESCRIPTION: "Detaljan izvještaj je sačuvan u crash_report.txt. Molimo priloži ovu datoteku u svoj izvještaj o grešci."
|
||||
STR_CRASH_REASON: "Razlog pada:"
|
||||
STR_CRASH_NO_REASON: "(Razlog nije zabilježen)"
|
||||
STR_TILT_PAGE_TURN: "Okretanje stranice naginjanjem"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Pritisni LIJEVO ili DESNO za pomjeranje kursora"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Pritisni LIJEVO za povratak na poziciju kursora"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Drži DESNO pa pritisni [***] za skrivanje lozinke"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Drži DESNO pa pritisni [abc] za prikaz lozinke"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pritisni [***] za skrivanje lozinke"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pritisni [abc] za prikaz lozinke"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Drži GORE za uređivanje unosa"
|
||||
STR_KB_TIPS: "Savjeti:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Pritisni DOLJE za povratak na tastaturu"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Pritisni ABC za izlazak iz URL načina"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Drži DEL za brisanje cijelog teksta"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Drži SELECT za sekundarni znak"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Drži SELECT za VELIKA SLOVA ili sekundarni znak"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Drži SELECT za mala slova ili sekundarni znak"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Pritisni URL za isječke"
|
||||
STR_SD_FIRMWARE_UPDATE: "Ažuriranje firmvera sa SD kartice"
|
||||
STR_SELECT_FIRMWARE_FILE: "Odaberi datoteku firmvera (.bin)"
|
||||
STR_NO_BIN_FILES: "Nema pronađenih .bin datoteka"
|
||||
STR_VALIDATING_FIRMWARE: "Provjera firmvera..."
|
||||
STR_INVALID_FIRMWARE: "Nevažeća datoteka firmvera"
|
||||
STR_FIRMWARE_TOO_LARGE: "Firmver je prevelik za particiju"
|
||||
STR_FIRMWARE_TOO_SMALL: "Datoteka firmvera je premala"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Ažurirati firmver?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Datoteka se ne može otvoriti"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Upisivanje firmvera nije uspjelo"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne isključuj napajanje!"
|
||||
STR_RECOVERY_MODE: "Način oporavka"
|
||||
STR_RECOVERY_MODE_HINT: "Stavi firmware.bin u korijenski direktorij SD kartice i odaberi ga"
|
||||
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
|
||||
STR_SLEEPING: "ENTRANT EN REPÒS"
|
||||
STR_ENTERING_SLEEP: "Entrant en repòs"
|
||||
STR_BROWSE_FILES: "Explora fitxers"
|
||||
STR_FILE_TRANSFER: "Transferència"
|
||||
STR_FILE_TRANSFER: "Transferència de fitxers"
|
||||
STR_SETTINGS_TITLE: "Configuració"
|
||||
STR_CONTINUE_READING: "Continua llegint"
|
||||
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
|
||||
STR_END_OF_BOOK: "Final del llibre"
|
||||
STR_EMPTY_CHAPTER: "Capítol buit"
|
||||
STR_INDEXING: "S'està indexant"
|
||||
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
|
||||
STR_MEMORY_ERROR: "Error de memòria"
|
||||
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||
STR_EMPTY_FILE: "Fitxer buit"
|
||||
@@ -28,58 +29,64 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
||||
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
||||
STR_SCANNING: "S'està escanejant..."
|
||||
STR_FINDING_SAVED_WIFI: "S'estan cercant xarxes Wi-Fi desades..."
|
||||
STR_CONNECTING: "S'està connectant..."
|
||||
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi desada..."
|
||||
STR_SHOW_NETWORKS: "Mostra"
|
||||
STR_CONNECTED: "S'ha connectat!"
|
||||
STR_CONNECTION_FAILED: "Error de connexió"
|
||||
STR_FORGET_NETWORK: "Voleu oblidar aquesta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Voleu desar la contrasenya per a la propera vegada?"
|
||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
||||
STR_FORGET_NETWORK: "Vols oblidar aquesta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Vols desar la contrasenya per a la propera vegada?"
|
||||
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
||||
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
||||
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
||||
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
||||
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a aquesta xarxa Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Obriu aquest URL al navegador"
|
||||
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a aquesta xarxa Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Obre aquest URL al navegador"
|
||||
STR_OR_HTTP_PREFIX: "o http://"
|
||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
||||
STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
||||
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
|
||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi"
|
||||
STR_ENTER_WIFI_PASSWORD: "Introdueix la contrasenya Wi-Fi"
|
||||
STR_TO_PREFIX: "a "
|
||||
STR_CALIBRE_RECEIVING: "S'està rebent: "
|
||||
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu aquesta pantalla oberta mentre s'envia\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantén aquesta pantalla oberta mentre s'envia\""
|
||||
STR_CAT_DISPLAY: "Visualització"
|
||||
STR_CAT_READER: "Lector"
|
||||
STR_CAT_CONTROLS: "Controls"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
||||
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
|
||||
STR_SLEEP_COVER_MODE: "Ajust de la portada en 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_IMAGES: "Imatges"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
||||
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||
STR_IMAGES_SUPPRESS: "Suprimir"
|
||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
||||
STR_EOB_HOME: "Inici"
|
||||
STR_EOB_CONTINUE_WITH: "Continua amb"
|
||||
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
|
||||
STR_ORIENTATION: "Orientació de lectura"
|
||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||
STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Acció en mantenir premut un botó"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||
STR_FONT_FAMILY: "Tipus de lletra"
|
||||
STR_FONT_SIZE: "Mida de la lletra (UI)"
|
||||
STR_FONT_FAMILY: "Font del lector"
|
||||
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||
STR_LINE_SPACING: "Interlineat del lector"
|
||||
STR_SCREEN_MARGIN: "Marge de pantalla del lector"
|
||||
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
||||
@@ -88,15 +95,16 @@ STR_TIME_TO_SLEEP: "Temps per entrar en repòs"
|
||||
STR_SHOW_HIDDEN_FILES: "Mostra fitxers 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"
|
||||
STR_REFRESH_FREQ: "Freqüència de refresc"
|
||||
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
||||
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
|
||||
STR_LANGUAGE: "Idioma"
|
||||
STR_LANGUAGE: "Llengua"
|
||||
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
||||
STR_USERNAME: "Nom d'usuari"
|
||||
STR_PASSWORD: "Contrasenya"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||
STR_SEND_METADATA: "Envia metadades del document"
|
||||
STR_AUTHENTICATE: "Autentica"
|
||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||
@@ -142,7 +150,7 @@ STR_PREV_NEXT: "Anterior/Següent"
|
||||
STR_NEXT_PREV: "Següent/Anterior"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||
STR_DISABLED: "Desactivats"
|
||||
STR_DISABLED: "Sense funció"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Petita"
|
||||
@@ -170,16 +178,16 @@ STR_UPDATING: "S'està actualitzant..."
|
||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
||||
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
||||
STR_UPDATE_COMPLETE: "Actualització completada"
|
||||
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar"
|
||||
STR_POWER_ON_HINT: "Prem i mantén premut el botó d'encesa per tornar a engegar"
|
||||
STR_NO_ENTRIES: "No s'ha trobat cap entrada"
|
||||
STR_DOWNLOADING: "S'està baixant..."
|
||||
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
||||
STR_ERROR_MSG: "Error:"
|
||||
STR_UNNAMED: "Sense nom"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Mantén premut Obre per esborrar"
|
||||
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||
STR_NETWORK_PREFIX: "Xarxa: "
|
||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||
@@ -192,7 +200,7 @@ STR_HOME: "« Inici"
|
||||
STR_SELECT: "Selecciona"
|
||||
STR_SELECTED: "Seleccionat"
|
||||
STR_TOGGLE: "Canvia"
|
||||
STR_TOGGLE_BOOKMARK: "Commuta punt de llibre"
|
||||
STR_TOGGLE_BOOKMARK: "Afegeix o elimina el punt de llibre"
|
||||
STR_CONFIRM: "Confirma"
|
||||
STR_CANCEL: "Cancel·la"
|
||||
STR_CONNECT: "Connecta"
|
||||
@@ -211,7 +219,7 @@ STR_DIR_RIGHT: "Dreta"
|
||||
STR_DIR_UP: "Amunt"
|
||||
STR_DIR_DOWN: "Avall"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
|
||||
STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
|
||||
STR_FILTER_CONTRAST: "Contrast"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||
@@ -233,7 +241,7 @@ STR_THEME_CLASSIC: "Clàssic"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
||||
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||
STR_BOOKMARKS: "Punts de llibre"
|
||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||
@@ -242,17 +250,17 @@ STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||
STR_QUICK_RESUME: "Represa ràpida"
|
||||
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||
STR_REMOVE_FROM_RECENTS: "Voleu suprimir-lo de Llibres recents?"
|
||||
STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?"
|
||||
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
||||
STR_CALIBRE_DESC: "Usa les transferències sense fils de Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Voleu suprimir la contrasenya desada?"
|
||||
STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i suprimir la contrasenya desada?"
|
||||
STR_FORGET_BUTTON: "Oblida"
|
||||
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configura"
|
||||
STR_CALIBRE_STATUS: "Estat"
|
||||
STR_CLEAR_BUTTON: "Esborra"
|
||||
STR_DEFAULT_VALUE: "Per defecte"
|
||||
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol"
|
||||
STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
|
||||
STR_UNASSIGNED: "No assignat"
|
||||
STR_ALREADY_ASSIGNED: "Ja assignat"
|
||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
||||
@@ -266,20 +274,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
|
||||
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
||||
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
||||
STR_DELETE: "Esborra"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar aquest punt de llibre?"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar aquest punt de llibre?"
|
||||
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
||||
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_CALIBRE_URL_HINT: "Per al Calibre, afegeix /opds a la URL"
|
||||
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"
|
||||
STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
|
||||
STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
|
||||
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
||||
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
||||
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader a la configuració"
|
||||
STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader a la configuració"
|
||||
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
||||
STR_REMOTE_LABEL: "Remot:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
@@ -289,7 +296,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
||||
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?"
|
||||
STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
|
||||
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
||||
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés"
|
||||
@@ -305,13 +312,14 @@ 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)"
|
||||
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
|
||||
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
||||
STR_FORCE_REFRESH: "Refresca la pantalla"
|
||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons."
|
||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantén premut el botó d'encesa durant uns segons."
|
||||
STR_NEXT_PAGE: "Pàgina següent »"
|
||||
STR_PREV_PAGE: "« Pàgina anterior"
|
||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||
@@ -340,20 +348,25 @@ STR_SERVER_NAME: "Nom del servidor"
|
||||
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
|
||||
STR_DELETE_SERVER: "Suprimeix el servidor"
|
||||
STR_OPDS_SERVERS: "Servidors OPDS"
|
||||
STR_MANAGE_FONTS: "Gestiona els tipus de lletra"
|
||||
STR_FONT_BROWSER: "Navegador de tipus de lletra"
|
||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de tipus de lletra..."
|
||||
STR_NO_FONTS_AVAILABLE: "No hi ha tipus de lletra disponibles"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
|
||||
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
|
||||
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
|
||||
STR_FMT_TITLE: "Títol"
|
||||
STR_MANAGE_FONTS: "Gestiona les fonts"
|
||||
STR_FONT_BROWSER: "Navegador de fonts"
|
||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
|
||||
STR_NO_FONTS_AVAILABLE: "No hi ha fonts disponibles"
|
||||
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
||||
STR_FONT_INSTALLED: "Tipus de lletra instal·lat!"
|
||||
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació del tipus de lletra"
|
||||
STR_FONT_INSTALLED: "Font instal·lada!"
|
||||
STR_FONT_INSTALL_FAILED: "Ha fallat la instal·lació de la font"
|
||||
STR_INSTALLED: "Instal·lat"
|
||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||
STR_CRASH_TITLE: "Bloqueig del sistema"
|
||||
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Incloeu aquest fitxer a l'informe d'errors."
|
||||
STR_CRASH_REASON: "Motiu del bloqueig:"
|
||||
STR_CRASH_TITLE: "Fallada del sistema"
|
||||
STR_CRASH_DESCRIPTION: "S'ha desat un informe detallat a crash_report.txt. Inclou aquest fitxer a l'informe d'errors."
|
||||
STR_CRASH_REASON: "Motiu de la fallada"
|
||||
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
||||
@@ -366,20 +379,22 @@ STR_KB_TIPS: "Consells:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per sortir del mode URL"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT: caràcter secundari"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT: majúscules o caràcter secundari"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT: minúscules o caràcter secundari"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
||||
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un fitxer de firmware (.bin)"
|
||||
STR_SELECT_FIRMWARE_FILE: "Selecciona un fitxer de firmware (.bin)"
|
||||
STR_NO_BIN_FILES: "No s'han trobat fitxers .bin"
|
||||
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apaguis el dispositiu!"
|
||||
STR_RECOVERY_MODE: "Mode de recuperació"
|
||||
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo"
|
||||
STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
|
||||
STR_ADD_HIDDEN_NETWORK: "Afegeix una xarxa oculta..."
|
||||
STR_ENTER_WIFI_SSID: "Introdueix el nom de la xarxa (SSID)"
|
||||
|
||||
@@ -49,7 +49,7 @@ STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
||||
STR_MAC_ADDRESS: "MAC adresa:"
|
||||
STR_CHECKING_WIFI: "Kontrola Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo Wi-Fi"
|
||||
STR_TO_PREFIX: "pro"
|
||||
STR_TO_PREFIX: "k "
|
||||
STR_CALIBRE_RECEIVING: "Příjem:"
|
||||
STR_CALIBRE_RECEIVED: "Přijato:"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
|
||||
@@ -58,7 +58,7 @@ 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"
|
||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Vyhlazování textu"
|
||||
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
|
||||
STR_ORIENTATION: "Orientace čtení"
|
||||
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
|
||||
STR_TOUCH_READER_CONTROLS: "Dotykové ovládání čtečky"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientovat přední tlačítka"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
||||
@@ -92,6 +93,7 @@ STR_USERNAME: "Uživatelské jméno"
|
||||
STR_PASSWORD: "Heslo"
|
||||
STR_SYNC_SERVER_URL: "URL synch. serveru"
|
||||
STR_DOCUMENT_MATCHING: "Párování dokumentů"
|
||||
STR_SEND_METADATA: "Odesílat metadata dokumentu"
|
||||
STR_AUTHENTICATE: "Ověření"
|
||||
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
|
||||
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
|
||||
@@ -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"
|
||||
@@ -270,8 +271,16 @@ STR_BOOK_S_STYLE: "Styl knihy"
|
||||
STR_EMBEDDED_STYLE: "Vložený styl"
|
||||
STR_FOCUS_READING: "Soustředěné čtení"
|
||||
STR_OPDS_SERVER_URL: "URL serveru OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Složka pro stahování"
|
||||
STR_OPDS_FILENAME_FORMAT: "Formát názvu souboru"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Název"
|
||||
STR_FMT_TITLE_AUTHOR: "Název - Autor"
|
||||
STR_FMT_TITLE: "Název"
|
||||
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"
|
||||
STR_ADD_HIDDEN_NETWORK: "Přidat skrytou síť..."
|
||||
STR_ENTER_WIFI_SSID: "Zadejte název sítě (SSID)"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Skjul"
|
||||
STR_SHORT_PWR_BTN: "Kort tryk på tænd/sluk-knap"
|
||||
STR_ORIENTATION: "Læseretning"
|
||||
STR_SIDE_BTN_LAYOUT: "Knaplayout på siden (læser)"
|
||||
STR_TOUCH_READER_CONTROLS: "Touchkontroller i læser"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientér forreste knapper"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportamiento al mantener pulsado el botón"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivado"
|
||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Brugernavn"
|
||||
STR_PASSWORD: "Adgangskode"
|
||||
STR_SYNC_SERVER_URL: "Synkroniseringsserver-URL"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentsammenkobling"
|
||||
STR_SEND_METADATA: "Send dokumentmetadata"
|
||||
STR_AUTHENTICATE: "Godkend"
|
||||
STR_KOREADER_USERNAME: "KOReader brugernavn"
|
||||
STR_KOREADER_PASSWORD: "KOReader adgangskode"
|
||||
@@ -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,8 +299,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Downloadmappe"
|
||||
STR_OPDS_FILENAME_FORMAT: "Filnavnsformat"
|
||||
STR_FMT_AUTHOR_TITLE: "Forfatter - Titel"
|
||||
STR_FMT_TITLE_AUTHOR: "Titel - Forfatter"
|
||||
STR_FMT_TITLE: "Titel"
|
||||
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
|
||||
STR_TILT_PAGE_TURN: "Vip for at vende side"
|
||||
STR_ADD_HIDDEN_NETWORK: "Tilføj skjult netværk..."
|
||||
STR_ENTER_WIFI_SSID: "Indtast netværksnavn (SSID)"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Verbergen"
|
||||
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
|
||||
STR_ORIENTATION: "Leesstand"
|
||||
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
|
||||
STR_TOUCH_READER_CONTROLS: "Aanraakbediening lezer"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Richt voorste knoppen"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Gebruikersnaam"
|
||||
STR_PASSWORD: "Wachtwoord"
|
||||
STR_SYNC_SERVER_URL: "Sync-server URL"
|
||||
STR_DOCUMENT_MATCHING: "Documentkoppeling"
|
||||
STR_SEND_METADATA: "Documentmetadata versturen"
|
||||
STR_AUTHENTICATE: "Authenticatie"
|
||||
STR_KOREADER_USERNAME: "KOReader gebruikersnaam"
|
||||
STR_KOREADER_PASSWORD: "KOReader wachtwoord"
|
||||
@@ -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,8 +299,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Downloadmap"
|
||||
STR_OPDS_FILENAME_FORMAT: "Bestandsnaamformaat"
|
||||
STR_FMT_AUTHOR_TITLE: "Auteur - Titel"
|
||||
STR_FMT_TITLE_AUTHOR: "Titel - Auteur"
|
||||
STR_FMT_TITLE: "Titel"
|
||||
STR_SCREENSHOT_BUTTON: "Screenshot maken"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
|
||||
STR_TILT_PAGE_TURN: "Kantel om te bladeren"
|
||||
STR_ADD_HIDDEN_NETWORK: "Verborgen netwerk toevoegen..."
|
||||
STR_ENTER_WIFI_SSID: "Voer netwerknaam in (SSID)"
|
||||
|
||||
@@ -10,6 +10,7 @@ STR_BROWSE_FILES: "Browse Files"
|
||||
STR_FILE_TRANSFER: "File Transfer"
|
||||
STR_SETTINGS_TITLE: "Settings"
|
||||
STR_CONTINUE_READING: "Continue Reading"
|
||||
STR_RESUME: "Resume"
|
||||
STR_NO_OPEN_BOOK: "No open book"
|
||||
STR_START_READING: "Start reading below"
|
||||
STR_NO_FILES_FOUND: "No files found"
|
||||
@@ -18,17 +19,27 @@ STR_NO_CHAPTERS: "No chapters"
|
||||
STR_END_OF_BOOK: "End of book"
|
||||
STR_EMPTY_CHAPTER: "Empty chapter"
|
||||
STR_INDEXING: "Indexing"
|
||||
STR_INDEX_FAILED: "Failed to index - invalid book"
|
||||
STR_MEMORY_ERROR: "Memory error"
|
||||
STR_PAGE_LOAD_ERROR: "Page load error"
|
||||
STR_EMPTY_FILE: "Empty file"
|
||||
STR_OUT_OF_BOUNDS: "Out of bounds"
|
||||
STR_LOADING: "Loading..."
|
||||
STR_LOADING_POPUP: "Loading"
|
||||
STR_LOOKUP: "Look Up"
|
||||
STR_DICT_LOOKING_UP: "Looking up..."
|
||||
STR_DICT_INDEXING: "Indexing dictionary..."
|
||||
STR_DICT_NOT_FOUND: "Not found"
|
||||
STR_DICT_NO_DICT_SET: "No dictionary set"
|
||||
STR_DICT_ERROR: "Dictionary error"
|
||||
STR_WIFI_NETWORKS: "Wi-Fi Networks"
|
||||
STR_NO_NETWORKS: "No networks found"
|
||||
STR_NETWORKS_FOUND: "%zu networks found"
|
||||
STR_SCANNING: "Scanning..."
|
||||
STR_FINDING_SAVED_WIFI: "Finding saved Wi-Fi..."
|
||||
STR_CONNECTING: "Connecting..."
|
||||
STR_CONNECTING_SAVED_WIFI: "Connecting to saved Wi-Fi..."
|
||||
STR_SHOW_NETWORKS: "Show"
|
||||
STR_CONNECTED: "Connected!"
|
||||
STR_CONNECTION_FAILED: "Connection Failed"
|
||||
STR_FORGET_NETWORK: "Forget Network?"
|
||||
@@ -49,6 +60,8 @@ STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||
STR_MAC_ADDRESS: "MAC address:"
|
||||
STR_CHECKING_WIFI: "Checking Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Enter Wi-Fi password"
|
||||
STR_ADD_HIDDEN_NETWORK: "Add hidden network..."
|
||||
STR_ENTER_WIFI_SSID: "Enter network name (SSID)"
|
||||
STR_TO_PREFIX: "to "
|
||||
STR_CALIBRE_RECEIVING: "Receiving: "
|
||||
STR_CALIBRE_RECEIVED: "Received: "
|
||||
@@ -70,9 +83,12 @@ 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)"
|
||||
STR_TOUCH_READER_CONTROLS: "Touch Reader Controls"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orient front buttons"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
@@ -99,7 +115,16 @@ STR_USERNAME: "Username"
|
||||
STR_PASSWORD: "Password"
|
||||
STR_SYNC_SERVER_URL: "Sync Server URL"
|
||||
STR_DOCUMENT_MATCHING: "Document Matching"
|
||||
STR_SEND_METADATA: "Send Document Metadata"
|
||||
STR_SYNC_BEHAVIOR: "Sync Behavior"
|
||||
STR_ASK_EVERY_TIME: "Ask every time"
|
||||
STR_SMART_SYNC: "Smart sync"
|
||||
STR_AUTHENTICATE: "Authenticate"
|
||||
STR_SIGN_UP: "Sign Up"
|
||||
STR_CREATING_ACCOUNT: "Creating account..."
|
||||
STR_ACCOUNT_CREATED: "Account created"
|
||||
STR_SIGNUP_FAILED: "Sign up failed"
|
||||
STR_USERNAME_TAKEN: "Username is already registered"
|
||||
STR_KOREADER_USERNAME: "KOReader Username"
|
||||
STR_KOREADER_PASSWORD: "KOReader Password"
|
||||
STR_FILENAME: "Filename"
|
||||
@@ -145,6 +170,7 @@ STR_PREV_NEXT: "Prev/Next"
|
||||
STR_NEXT_PREV: "Next/Prev"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Bookmark"
|
||||
STR_DICTIONARY: "Dictionary"
|
||||
STR_DISABLED: "Disabled"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
@@ -205,6 +231,7 @@ STR_CONNECT: "Connect"
|
||||
STR_OPEN: "Open"
|
||||
STR_DOWNLOAD: "Download"
|
||||
STR_RETRY: "Retry"
|
||||
STR_TAP_TO_RETRY: "Tap to retry"
|
||||
STR_YES: "Yes"
|
||||
STR_NO: "No"
|
||||
STR_SHOW: "Show"
|
||||
@@ -217,6 +244,9 @@ STR_DIR_RIGHT: "Right"
|
||||
STR_DIR_UP: "Up"
|
||||
STR_DIR_DOWN: "Down"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_KEY_SHIFT: "Shift"
|
||||
STR_KEY_MODE_SYMBOLS: "?123"
|
||||
STR_KEY_MODE_ABC: "abc"
|
||||
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
|
||||
STR_FILTER_CONTRAST: "Contrast"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
|
||||
@@ -297,7 +327,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"
|
||||
@@ -316,6 +345,7 @@ STR_UPLOAD_LOCAL: "Upload local progress"
|
||||
STR_NO_REMOTE_MSG: "No remote progress found"
|
||||
STR_UPLOAD_PROMPT: "Upload current position?"
|
||||
STR_UPLOAD_SUCCESS: "Progress uploaded!"
|
||||
STR_ALREADY_SYNCED: "Already synced"
|
||||
STR_SYNC_FAILED_MSG: "Sync failed"
|
||||
STR_SAVE_PROGRESS_FAILED: "Could not save progress"
|
||||
STR_SECTION_PREFIX: "Section "
|
||||
@@ -325,6 +355,7 @@ STR_EMBEDDED_STYLE: "Embedded Style"
|
||||
STR_FOCUS_READING: "Focus Reading"
|
||||
STR_OPDS_SERVER_URL: "OPDS Server URL"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Quick-return from footnotes"
|
||||
STR_BACK_SHORT_TO_FILE_BROWSER: "Short Back to File Browser"
|
||||
STR_SET_SLEEP_COVER: "Set Cover"
|
||||
STR_FOOTNOTES: "Footnotes"
|
||||
STR_NO_FOOTNOTES: "No footnotes on this page"
|
||||
@@ -332,12 +363,19 @@ 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"
|
||||
STR_DELETE_SERVER: "Delete Server"
|
||||
STR_OPDS_SERVERS: "OPDS Servers"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Download folder"
|
||||
STR_OPDS_FILENAME_FORMAT: "Filename format"
|
||||
STR_FMT_AUTHOR_TITLE: "Author - Title"
|
||||
STR_FMT_TITLE_AUTHOR: "Title - Author"
|
||||
STR_FMT_TITLE: "Title"
|
||||
STR_OPDS_SD_ROOT: "SD root"
|
||||
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
|
||||
STR_MANAGE_FONTS: "Manage Fonts"
|
||||
|
||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Tekstin reunanpehmennys"
|
||||
STR_SHORT_PWR_BTN: "Lyhyt virtapainikkeen painallus"
|
||||
STR_ORIENTATION: "Lukusuunta"
|
||||
STR_SIDE_BTN_LAYOUT: "Sivupainikkeiden asettelu (lukija)"
|
||||
STR_TOUCH_READER_CONTROLS: "Lukijan kosketusohjaus"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Suuntaa etupainikkeet"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
@@ -92,6 +93,7 @@ STR_USERNAME: "Käyttäjänimi"
|
||||
STR_PASSWORD: "Salasana"
|
||||
STR_SYNC_SERVER_URL: "Synkronointipalvelimen osoite"
|
||||
STR_DOCUMENT_MATCHING: "Dokumenttien tunnistus"
|
||||
STR_SEND_METADATA: "Lähetä asiakirjan metatiedot"
|
||||
STR_AUTHENTICATE: "Tunnistaudu"
|
||||
STR_KOREADER_USERNAME: "KOReader-käyttäjänimi"
|
||||
STR_KOREADER_PASSWORD: "KOReader-salasana"
|
||||
@@ -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"
|
||||
@@ -268,8 +269,16 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
|
||||
STR_EMBEDDED_STYLE: "Upotettu tyyli"
|
||||
STR_FOCUS_READING: "Keskittynyt lukeminen"
|
||||
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Latauskansio"
|
||||
STR_OPDS_FILENAME_FORMAT: "Tiedostonimen muoto"
|
||||
STR_FMT_AUTHOR_TITLE: "Tekijä - Nimi"
|
||||
STR_FMT_TITLE_AUTHOR: "Nimi - Tekijä"
|
||||
STR_FMT_TITLE: "Nimi"
|
||||
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"
|
||||
STR_ADD_HIDDEN_NETWORK: "Lisää piilotettu verkko..."
|
||||
STR_ENTER_WIFI_SSID: "Anna verkon nimi (SSID)"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Masquer"
|
||||
STR_SHORT_PWR_BTN: "Appui court alim."
|
||||
STR_ORIENTATION: "Orientation de lecture"
|
||||
STR_SIDE_BTN_LAYOUT: "Boutons latéraux"
|
||||
STR_TOUCH_READER_CONTROLS: "Commandes tactiles lecteur"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter boutons avant"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportement lors d'un appui long"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Désactivé"
|
||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Nom d’utilisateur"
|
||||
STR_PASSWORD: "Mot de passe"
|
||||
STR_SYNC_SERVER_URL: "URL du serveur"
|
||||
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||
STR_SEND_METADATA: "Envoyer métadonnées"
|
||||
STR_AUTHENTICATE: "Connexion"
|
||||
STR_KOREADER_USERNAME: "Utilisateur"
|
||||
STR_KOREADER_PASSWORD: "Mot de passe"
|
||||
@@ -267,7 +269,6 @@ STR_CHAPTER_PREFIX: "Chapitre : "
|
||||
STR_PAGES_SEPARATOR: " pages | "
|
||||
STR_BOOK_PREFIX: "Livre : "
|
||||
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à l’URL"
|
||||
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
|
||||
STR_SYNCING_TIME: "Synchro de l’heure…"
|
||||
STR_CALC_HASH: "Calcul hash doc…"
|
||||
STR_HASH_FAILED: "Échec calcul hash"
|
||||
@@ -299,8 +300,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Dossier de téléchargement"
|
||||
STR_OPDS_FILENAME_FORMAT: "Format du nom de fichier"
|
||||
STR_FMT_AUTHOR_TITLE: "Auteur - Titre"
|
||||
STR_FMT_TITLE_AUTHOR: "Titre - Auteur"
|
||||
STR_FMT_TITLE: "Titre"
|
||||
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)"
|
||||
STR_TILT_PAGE_TURN: "Tourner par inclinaison"
|
||||
STR_ADD_HIDDEN_NETWORK: "Ajouter un réseau masqué..."
|
||||
STR_ENTER_WIFI_SSID: "Saisir le nom du réseau (SSID)"
|
||||
|
||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Schriftglättung"
|
||||
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
|
||||
STR_ORIENTATION: "Leseausrichtung"
|
||||
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
|
||||
STR_TOUCH_READER_CONTROLS: "Touch-Steuerung beim Lesen"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Vordere Tasten ausrichten"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Verhalten bei langem Tastendruck"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Aus"
|
||||
@@ -91,6 +92,7 @@ STR_USERNAME: "Benutzername"
|
||||
STR_PASSWORD: "Passwort"
|
||||
STR_SYNC_SERVER_URL: "Sync-Server-URL"
|
||||
STR_DOCUMENT_MATCHING: "Dateizuordnung"
|
||||
STR_SEND_METADATA: "Metadaten senden"
|
||||
STR_AUTHENTICATE: "Authentifizieren"
|
||||
STR_KOREADER_USERNAME: "KOReader-Benutzername"
|
||||
STR_KOREADER_PASSWORD: "KOReader-Passwort"
|
||||
@@ -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"
|
||||
@@ -315,6 +316,7 @@ STR_BOOK_S_STYLE: "Buch-Stil"
|
||||
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
|
||||
STR_FOCUS_READING: "Fokus-Lesen"
|
||||
STR_OPDS_SERVER_URL: "OPDS-Server-URL"
|
||||
STR_BACK_SHORT_TO_FILE_BROWSER: "Kurz zurück drücken zum Datei-Browser"
|
||||
STR_SET_SLEEP_COVER: "Wähle Cover"
|
||||
STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
|
||||
STR_FOOTNOTES: "Fußnoten"
|
||||
@@ -322,12 +324,18 @@ 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"
|
||||
STR_DELETE_SERVER: "Server entfernen"
|
||||
STR_OPDS_SERVERS: "OPDS-Server"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Download-Ordner"
|
||||
STR_OPDS_FILENAME_FORMAT: "Dateinamenformat"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Titel"
|
||||
STR_FMT_TITLE_AUTHOR: "Titel - Autor"
|
||||
STR_FMT_TITLE: "Titel"
|
||||
STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)"
|
||||
STR_IMAGES: "Bilder"
|
||||
@@ -380,3 +388,5 @@ STR_FIRMWARE_WRITE_FAILED: "Schreiben der Firmware-Datei ist fehlgeschlagen"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nicht ausschalten!"
|
||||
STR_RECOVERY_MODE: "Wiederherstellungsmodus"
|
||||
STR_RECOVERY_MODE_HINT: "Lege firmware.bin im SD-Kartenwurzelverzeichnis ab und wähle es aus"
|
||||
STR_ADD_HIDDEN_NETWORK: "Verstecktes Netzwerk hinzufügen..."
|
||||
STR_ENTER_WIFI_SSID: "Netzwerknamen eingeben (SSID)"
|
||||
|
||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "הסתר תמונות"
|
||||
STR_SHORT_PWR_BTN: "לחיצה קצרה על כפתור ההפעלה"
|
||||
STR_ORIENTATION: "כיוון קריאה (מסך)"
|
||||
STR_SIDE_BTN_LAYOUT: "פריסת כפתורי צד (בקריאה)"
|
||||
STR_TOUCH_READER_CONTROLS: "שליטה במגע (קורא)"
|
||||
STR_LONG_PRESS_BEHAVIOR: "פעולת לחיצה ארוכה"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "כבוי"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "דלג פרק"
|
||||
@@ -95,6 +96,7 @@ STR_USERNAME: "שם משתמש"
|
||||
STR_PASSWORD: "סיסמה"
|
||||
STR_SYNC_SERVER_URL: "כתובת שרת סנכרון"
|
||||
STR_DOCUMENT_MATCHING: "התאמת מסמכים"
|
||||
STR_SEND_METADATA: "שלח מטא-נתוני מסמך"
|
||||
STR_AUTHENTICATE: "התחבר"
|
||||
STR_KOREADER_USERNAME: "שם משתמש ב-KOReader"
|
||||
STR_KOREADER_PASSWORD: "סיסמת KOReader"
|
||||
@@ -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: "חישוב האש נכשל"
|
||||
@@ -306,6 +307,11 @@ STR_NO_SERVERS: "לא הוגדרו שרתי OPDS"
|
||||
STR_DELETE_SERVER: "מחק שרת"
|
||||
STR_DELETE_CONFIRM: "למחוק שרת זה?"
|
||||
STR_OPDS_SERVERS: "שרתי OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "תיקיית הורדות"
|
||||
STR_OPDS_FILENAME_FORMAT: "תבנית שם קובץ"
|
||||
STR_FMT_AUTHOR_TITLE: "מחבר - כותרת"
|
||||
STR_FMT_TITLE_AUTHOR: "כותרת - מחבר"
|
||||
STR_FMT_TITLE: "כותרת"
|
||||
STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)"
|
||||
STR_MANAGE_FONTS: "ניהול גופנים"
|
||||
@@ -390,4 +396,7 @@ 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: "לחצני צד:"
|
||||
STR_ADD_HIDDEN_NETWORK: "הוסף רשת מוסתרת..."
|
||||
STR_ENTER_WIFI_SSID: "הזן שם רשת (SSID)"
|
||||
|
||||
@@ -49,6 +49,8 @@ STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
|
||||
STR_MAC_ADDRESS: "MAC-cím:"
|
||||
STR_CHECKING_WIFI: "Wi-Fi ellenőrzése..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Add meg a Wi-Fi jelszót"
|
||||
STR_ADD_HIDDEN_NETWORK: "Rejtett hálózat hozzáadása..."
|
||||
STR_ENTER_WIFI_SSID: "Add meg a hálózat nevét (SSID)"
|
||||
STR_TO_PREFIX: "- "
|
||||
STR_CALIBRE_RECEIVING: "Fogadás: "
|
||||
STR_CALIBRE_RECEIVED: "Fogadva: "
|
||||
@@ -72,6 +74,7 @@ STR_IMAGES_SUPPRESS: "Elnyomás"
|
||||
STR_SHORT_PWR_BTN: "Rövid bekapcsológomb nyomás"
|
||||
STR_ORIENTATION: "Olvasási irány"
|
||||
STR_SIDE_BTN_LAYOUT: "Oldalsó gomb elrendezés (olvasó)"
|
||||
STR_TOUCH_READER_CONTROLS: "Érintős olvasóvezérlés"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Elülső gombok tájolása"
|
||||
STR_LONG_PRESS_SKIP: "Hosszú nyomás - fejezet ugrás"
|
||||
STR_FONT_PREVIEW_TEXT: "Egy hűtlen vejét fülöncsípő, dühös mexikói úr Wesselényinél mázol Quitóban"
|
||||
@@ -94,6 +97,7 @@ STR_USERNAME: "Felhasználónév"
|
||||
STR_PASSWORD: "Jelszó"
|
||||
STR_SYNC_SERVER_URL: "Szinkronizálás szerver URL"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentum egyeztetés"
|
||||
STR_SEND_METADATA: "Dokumentum metaadatok küldése"
|
||||
STR_AUTHENTICATE: "Hitelesítés"
|
||||
STR_KOREADER_USERNAME: "KOReader felhasználónév"
|
||||
STR_KOREADER_PASSWORD: "KOReader jelszó"
|
||||
@@ -263,7 +267,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,8 +298,104 @@ 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_OPDS_DOWNLOAD_FOLDER: "Letöltési mappa"
|
||||
STR_OPDS_FILENAME_FORMAT: "Fájlnév formátuma"
|
||||
STR_FMT_AUTHOR_TITLE: "Szerző - Cím"
|
||||
STR_FMT_TITLE_AUTHOR: "Cím - Szerző"
|
||||
STR_FMT_TITLE: "Cím"
|
||||
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)"
|
||||
STR_TILT_PAGE_TURN: "Döntéses lapozás"
|
||||
STR_ADD_SERVER: "Szerver hozzáadása"
|
||||
STR_BOOKMARKS: "Könyvjelzők"
|
||||
STR_BOOKMARK_ADDED: "Könyvjelző hozzáadva."
|
||||
STR_BOOKMARK_OPTION: "Könyvjelző"
|
||||
STR_BOTTOM: "Alul"
|
||||
STR_CLOCK: "Óra"
|
||||
STR_CLOCK_FORMAT: "Óraformátum"
|
||||
STR_CLOCK_FORMAT_12H: "12 órás"
|
||||
STR_CLOCK_FORMAT_24H: "24 órás"
|
||||
STR_CLOCK_SYNC: "Óra szinkronizálása"
|
||||
STR_CLOCK_SYNCED: "Óra szinkronizálva"
|
||||
STR_CLOCK_SYNCING: "Szinkronizálás NTP-ről..."
|
||||
STR_CLOCK_SYNC_FAIL: "Szinkronizálás sikertelen"
|
||||
STR_CLOCK_SYNC_NOW: "Óra szinkronizálása most"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nincs csatlakoztatva"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Először csatlakozz Wi-Fi-hez, majd próbáld újra."
|
||||
STR_CLOCK_SYNC_OK: "Óra szinkronizálva"
|
||||
STR_CLOCK_UTC_OFFSET: "Óra UTC eltolás"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Törlöd ezt a könyvjelzőt?"
|
||||
STR_CONNECTING_SAVED_WIFI: "Csatlakozás mentett Wi-Fi-hez..."
|
||||
STR_CRASH_DESCRIPTION: "A részletes jelentés a crash_report.txt fájlba lett mentve. Kérjük, csatold ezt a fájlt a hibajelentéshez."
|
||||
STR_CRASH_NO_REASON: "(Nem került rögzítésre ok)"
|
||||
STR_CRASH_REASON: "Összeomlás oka:"
|
||||
STR_CRASH_TITLE: "Rendszerösszeomlás"
|
||||
STR_CURRENT_TIME: "Aktuális idő:"
|
||||
STR_DELETE_SERVER: "Szerver törlése"
|
||||
STR_DISABLED: "Letiltva"
|
||||
STR_DOWNLOAD_ALL: "Összes letöltése"
|
||||
STR_EOB_CONTINUE_WITH: "Folytatás ezzel"
|
||||
STR_EOB_HOME: "Kezdőképernyő"
|
||||
STR_FINDING_SAVED_WIFI: "Mentett Wi-Fi keresése..."
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "A fájl nem nyitható meg"
|
||||
STR_FIRMWARE_TOO_LARGE: "A firmware túl nagy a partícióhoz"
|
||||
STR_FIRMWARE_TOO_SMALL: "A firmware fájl túl kicsi"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne kapcsold ki!"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Frissíted a firmware-t?"
|
||||
STR_FIRMWARE_WRITE_FAILED: "A firmware írása sikertelen"
|
||||
STR_FONT_BROWSER: "Betűtípus-böngésző"
|
||||
STR_FONT_INSTALLED: "Betűtípus telepítve!"
|
||||
STR_FONT_INSTALL_FAILED: "A betűtípus telepítése sikertelen"
|
||||
STR_FORCE_REFRESH: "Képernyő frissítése"
|
||||
STR_INDEX_FAILED: "Indexelés sikertelen – érvénytelen könyv"
|
||||
STR_INSTALLED: "Telepítve"
|
||||
STR_INVALID_FIRMWARE: "Érvénytelen firmware fájl"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Tartsd nyomva a DEL-t az összes szöveg törléséhez"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Tartsd nyomva a FEL-t a bejegyzés szerkesztéséhez"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Nyomd meg az ABC-t az URL mód elhagyásához"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg a [***]-ot a jelszó elrejtéséhez"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Tartsd nyomva a SELECT-et kisbetűért vagy másodlagos karakterért"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Nyomd meg a BALRA vagy JOBBRA gombot a kurzor mozgatásához"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Nyomd meg a BALRA-t a kurzorpozícióhoz való visszatéréshez"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Nyomd meg a LE-t a billentyűzethez való visszatéréshez"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Tartsd nyomva a SELECT-et másodlagos karakterért"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Tartsd nyomva a JOBBRA-t, majd nyomd meg az [abc]-t a jelszó megjelenítéséhez"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Nyomd meg a [***]-ot a jelszó elrejtéséhez"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Nyomd meg az [abc]-t a jelszó megjelenítéséhez"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Tartsd nyomva a SELECT-et NAGYBETŰÉRT vagy másodlagos karakterért"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Nyomd meg az URL-t az URL-részletekért"
|
||||
STR_KB_TIPS: "Tippek:"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_LOADING_FONT_LIST: "Betűtípuslista betöltése..."
|
||||
STR_LONG_PRESS_BEHAVIOR: "Hosszú gombnyomás viselkedése"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "KI"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Tájolásváltás"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Fejezetugrás"
|
||||
STR_LONG_PRESS_MENU: "Hosszú nyomás – Menü"
|
||||
STR_MANAGE_FONTS: "Betűtípusok kezelése"
|
||||
STR_NEXT_FIELD: "Következő"
|
||||
STR_NEXT_PAGE: "Következő oldal »"
|
||||
STR_NO_BIN_FILES: "Nincs .bin fájl"
|
||||
STR_NO_FONTS_AVAILABLE: "Nincs elérhető betűtípus"
|
||||
STR_NO_SERVERS: "Nincs beállított OPDS szerver"
|
||||
STR_OPDS_SERVERS: "OPDS szerverek"
|
||||
STR_PREV_PAGE: "« Előző oldal"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Gyors visszatérés lábjegyzetből"
|
||||
STR_RECOVERY_MODE: "Helyreállítási mód"
|
||||
STR_RECOVERY_MODE_HINT: "Helyezd a firmware.bin fájlt az SD-kártya gyökerébe, majd válaszd ki"
|
||||
STR_RESTARTING_HINT: "Újraindítás... Ha az eszköz nem indul újra, tartsd nyomva a bekapcsológombot néhány másodpercig."
|
||||
STR_SD_FIRMWARE_UPDATE: "Firmware frissítés SD-kártyáról"
|
||||
STR_SEARCH: "Keresés"
|
||||
STR_SELECT_FIRMWARE_FILE: "Válassz firmware fájlt (.bin)"
|
||||
STR_SERVER_NAME: "Szerver neve"
|
||||
STR_SET_SLEEP_COVER: "Borító beállítása"
|
||||
STR_SHOW_NETWORKS: "Megjelenítés"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_TOP: "Felül"
|
||||
STR_UPDATE_ALL: "Összes frissítése"
|
||||
STR_UPDATE_AVAILABLE: "Frissítés"
|
||||
STR_VALIDATING_FIRMWARE: "Firmware ellenőrzése..."
|
||||
STR_XTC_STATUS_BAR: "XTC állapotsáv"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Nascondi"
|
||||
STR_SHORT_PWR_BTN: "Press. breve pul. accensione"
|
||||
STR_ORIENTATION: "Orientamento lettura"
|
||||
STR_SIDE_BTN_LAYOUT: "Pul. laterali (lettore)"
|
||||
STR_TOUCH_READER_CONTROLS: "Controlli touch lettore"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienta pul. frontali"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Press. lunga pul. laterali"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
@@ -98,6 +99,7 @@ STR_USERNAME: "Nome utente"
|
||||
STR_PASSWORD: "Password"
|
||||
STR_SYNC_SERVER_URL: "URL server di sincronizzazione"
|
||||
STR_DOCUMENT_MATCHING: "Corrispondenza documenti"
|
||||
STR_SEND_METADATA: "Invia metadati documento"
|
||||
STR_AUTHENTICATE: "Autentica"
|
||||
STR_KOREADER_USERNAME: "Nome utente KOReader"
|
||||
STR_KOREADER_PASSWORD: "Password KOReader"
|
||||
@@ -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,13 +308,19 @@ 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"
|
||||
STR_NO_SERVERS: "Nessun server OPDS configurato"
|
||||
STR_DELETE_SERVER: "Elimina server"
|
||||
STR_OPDS_SERVERS: "Server OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Cartella download"
|
||||
STR_OPDS_FILENAME_FORMAT: "Formato nome file"
|
||||
STR_FMT_AUTHOR_TITLE: "Autore - Titolo"
|
||||
STR_FMT_TITLE_AUTHOR: "Titolo - Autore"
|
||||
STR_FMT_TITLE: "Titolo"
|
||||
STR_AUTO_TURN_ENABLED: "Volta pagina automatico: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)"
|
||||
STR_MANAGE_FONTS: "Gestisci font"
|
||||
@@ -383,3 +390,8 @@ STR_DISABLED: "Disattivato"
|
||||
STR_BOOKMARK_OPTION: "Segnalibro"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Rientro rapido dalle note"
|
||||
STR_EOB_CONTINUE_WITH: "Continua con"
|
||||
STR_EOB_HOME: "Home"
|
||||
STR_INDEX_FAILED: "Indicizzazione fallita - libro non valido"
|
||||
STR_ADD_HIDDEN_NETWORK: "Aggiungi rete nascosta..."
|
||||
STR_ENTER_WIFI_SSID: "Inserisci il nome della rete (SSID)"
|
||||
|
||||
@@ -67,6 +67,7 @@ STR_TEXT_AA: "Мәтін сырғытпасы"
|
||||
STR_SHORT_PWR_BTN: "Қуат түймесін қысқа басу"
|
||||
STR_ORIENTATION: "Оқу бағдары"
|
||||
STR_SIDE_BTN_LAYOUT: "Бүйірлік түймелер орналасуы (оқырман)"
|
||||
STR_TOUCH_READER_CONTROLS: "Оқырманның сенсорлық басқаруы"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Алдыңғы түймелерді бағдарлау"
|
||||
STR_LONG_PRESS_SKIP: "Ұзақ басу арқылы тарау өткізу"
|
||||
STR_FONT_PREVIEW_TEXT: "Канагаттандырылмагандыктарыныздан"
|
||||
@@ -88,6 +89,7 @@ STR_USERNAME: "Пайдаланушы аты"
|
||||
STR_PASSWORD: "Құпия сөз"
|
||||
STR_SYNC_SERVER_URL: "Синхрондау сервері URL"
|
||||
STR_DOCUMENT_MATCHING: "Құжат сәйкестендіру"
|
||||
STR_SEND_METADATA: "Құжат метадеректерін жіберу"
|
||||
STR_AUTHENTICATE: "Аутентификация"
|
||||
STR_KOREADER_USERNAME: "KOReader пайдаланушы аты"
|
||||
STR_KOREADER_PASSWORD: "KOReader құпия сөзі"
|
||||
@@ -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,8 +295,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Жүктеп алу қалтасы"
|
||||
STR_OPDS_FILENAME_FORMAT: "Файл атауының пішімі"
|
||||
STR_FMT_AUTHOR_TITLE: "Автор - Атауы"
|
||||
STR_FMT_TITLE_AUTHOR: "Атауы - Автор"
|
||||
STR_FMT_TITLE: "Атауы"
|
||||
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
|
||||
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
|
||||
STR_TILT_PAGE_TURN: "Еңкейту арқылы бет аудару"
|
||||
STR_ADD_HIDDEN_NETWORK: "Жасырын желіні қосу..."
|
||||
STR_ENTER_WIFI_SSID: "Желі атауын енгізіңіз (SSID)"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Slėpti"
|
||||
STR_SHORT_PWR_BTN: "Trumpas įjungimo pasp."
|
||||
STR_ORIENTATION: "Orientacija"
|
||||
STR_SIDE_BTN_LAYOUT: "Šoniniai mygtukai"
|
||||
STR_TOUCH_READER_CONTROLS: "Lietimo valdikliai skaityklėje"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuoti priekinius mygtukus"
|
||||
STR_LONG_PRESS_SKIP: "Praleisti skyrių (ilgai)"
|
||||
STR_FONT_PREVIEW_TEXT: "Įlinkdama fechtuotojo špaga sublykčiojusi pragręžė apvalų arbūzą"
|
||||
@@ -94,6 +95,7 @@ STR_USERNAME: "Vartotojas"
|
||||
STR_PASSWORD: "Slaptažodis"
|
||||
STR_SYNC_SERVER_URL: "Serverio URL"
|
||||
STR_DOCUMENT_MATCHING: "Atpažinimas"
|
||||
STR_SEND_METADATA: "Siųsti dokumento metaduomenis"
|
||||
STR_AUTHENTICATE: "Prisijungti"
|
||||
STR_KOREADER_USERNAME: "KOReader vartotojas"
|
||||
STR_KOREADER_PASSWORD: "KOReader slaptažodis"
|
||||
@@ -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,8 +296,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Atsisiuntimų aplankas"
|
||||
STR_OPDS_FILENAME_FORMAT: "Failo pavadinimo formatas"
|
||||
STR_FMT_AUTHOR_TITLE: "Autorius - Pavadinimas"
|
||||
STR_FMT_TITLE_AUTHOR: "Pavadinimas - Autorius"
|
||||
STR_FMT_TITLE: "Pavadinimas"
|
||||
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
|
||||
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
|
||||
STR_TILT_PAGE_TURN: "Puslapio vertimas pakreipiant"
|
||||
STR_ADD_HIDDEN_NETWORK: "Pridėti paslėptą tinklą..."
|
||||
STR_ENTER_WIFI_SSID: "Įveskite tinklo pavadinimą (SSID)"
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
_language_name: "Norsk bokmål"
|
||||
_language_code: "NB"
|
||||
_order: "26"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "STARTER"
|
||||
STR_SLEEPING: "HVILER"
|
||||
STR_ENTERING_SLEEP: "Går i hvilemodus"
|
||||
STR_BROWSE_FILES: "Bla i filer"
|
||||
STR_FILE_TRANSFER: "Filoverføring"
|
||||
STR_SETTINGS_TITLE: "Innstillinger"
|
||||
STR_CONTINUE_READING: "Fortsett å lese"
|
||||
STR_NO_OPEN_BOOK: "Ingen åpen bok"
|
||||
STR_START_READING: "Start å lese nedenfor"
|
||||
STR_NO_FILES_FOUND: "Ingen filer funnet"
|
||||
STR_SELECT_CHAPTER: "Velg kapittel"
|
||||
STR_NO_CHAPTERS: "Ingen kapitler"
|
||||
STR_END_OF_BOOK: "Slutten av boken"
|
||||
STR_EMPTY_CHAPTER: "Tomt kapittel"
|
||||
STR_INDEXING: "Indekserer"
|
||||
STR_MEMORY_ERROR: "Minnefeil"
|
||||
STR_PAGE_LOAD_ERROR: "Feil ved sidelasting"
|
||||
STR_EMPTY_FILE: "Tom fil"
|
||||
STR_OUT_OF_BOUNDS: "Utenfor område"
|
||||
STR_LOADING: "Laster..."
|
||||
STR_LOADING_POPUP: "Laster"
|
||||
STR_WIFI_NETWORKS: "WiFi-nettverk"
|
||||
STR_NO_NETWORKS: "Ingen nettverk funnet"
|
||||
STR_NETWORKS_FOUND: "%zu nettverk funnet"
|
||||
STR_SCANNING: "Skanner..."
|
||||
STR_CONNECTING: "Kobler til..."
|
||||
STR_CONNECTED: "Tilkoblet!"
|
||||
STR_CONNECTION_FAILED: "Tilkobling mislyktes"
|
||||
STR_FORGET_NETWORK: "Glem nettverk?"
|
||||
STR_SAVE_PASSWORD: "Lagre passord til neste gang?"
|
||||
STR_PRESS_OK_SCAN: "Trykk OK for å skanne på nytt"
|
||||
STR_JOIN_NETWORK: "Koble til et nettverk"
|
||||
STR_CREATE_HOTSPOT: "Opprett hotspot"
|
||||
STR_JOIN_DESC: "Koble til et eksisterende WiFi-nettverk"
|
||||
STR_HOTSPOT_DESC: "Opprett et WiFi-nettverk andre kan koble seg til"
|
||||
STR_STARTING_HOTSPOT: "Starter hotspot..."
|
||||
STR_HOTSPOT_MODE: "Hotspot-modus"
|
||||
STR_CONNECT_WIFI_HINT: "Koble enheten din til dette WiFi-nettverket"
|
||||
STR_OPEN_URL_HINT: "Åpne denne URL-en i nettleseren din"
|
||||
STR_OR_HTTP_PREFIX: "eller http://"
|
||||
STR_SCAN_QR_HINT: "eller skann QR-kode med telefonen din:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_NETWORK_LEGEND: "* = Kryptert | + = Lagret"
|
||||
STR_MAC_ADDRESS: "MAC-adresse:"
|
||||
STR_CHECKING_WIFI: "Sjekker WiFi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Skriv inn WiFi-passord"
|
||||
STR_TO_PREFIX: "til "
|
||||
STR_CALIBRE_RECEIVING: "Mottar: "
|
||||
STR_CALIBRE_RECEIVED: "Mottatt: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Vær på samme WiFi-nettverk"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhet\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skjermen åpen mens du sender\""
|
||||
STR_CAT_DISPLAY: "Skjerm"
|
||||
STR_CAT_READER: "Leser"
|
||||
STR_CAT_CONTROLS: "Betjening"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
STR_SLEEP_SCREEN: "Hvileskjerm"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Hurtig gjenopptak ved tidsavbrudd"
|
||||
STR_SLEEP_COVER_MODE: "Omslagsmodus for hvileskjerm"
|
||||
STR_HIDE_BATTERY: "Skjul batteri %"
|
||||
STR_EXTRA_SPACING: "Ekstra avsnittsavstand"
|
||||
STR_TEXT_AA: "Tekstutjevning"
|
||||
STR_IMAGES: "Bilder"
|
||||
STR_IMAGES_DISPLAY: "Vis"
|
||||
STR_IMAGES_PLACEHOLDER: "Plassholder"
|
||||
STR_IMAGES_SUPPRESS: "Skjul"
|
||||
STR_SHORT_PWR_BTN: "Kort trykk på av/på-knapp"
|
||||
STR_ORIENTATION: "Leseretning"
|
||||
STR_SIDE_BTN_LAYOUT: "Sideknapp-oppsett (leser)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter frontknapper"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Atferd ved langt trykk"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapittelhopp"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Endre orientering"
|
||||
STR_LONG_PRESS_MENU: "Langt trykk på Meny"
|
||||
STR_FONT_PREVIEW_TEXT: "Høvdingens kjære squaw får litt pizza i Mexico by"
|
||||
STR_FONT_FAMILY: "Skrifttype i leser"
|
||||
STR_FONT_SIZE: "Skriftstørrelse i leser"
|
||||
STR_LINE_SPACING: "Linjeavstand i leser"
|
||||
STR_SCREEN_MARGIN: "Skjermmarg i leser"
|
||||
STR_PARA_ALIGNMENT: "Avsnittsjustering i leser"
|
||||
STR_HYPHENATION: "Orddeling"
|
||||
STR_TIME_TO_SLEEP: "Tid før hvile"
|
||||
STR_SHOW_HIDDEN_FILES: "Vis skjulte filer"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "Fjern leste bøker fra Nylig-listen"
|
||||
STR_MOVE_FINISHED_TO_READ: "Flytt fullførte bøker til Read-mappen"
|
||||
STR_REFRESH_FREQ: "Oppdateringsfrekvens"
|
||||
STR_KOREADER_SYNC: "KOReader Sync"
|
||||
STR_CHECK_UPDATES: "Se etter oppdateringer"
|
||||
STR_LANGUAGE: "Språk"
|
||||
STR_CLEAR_READING_CACHE: "Tøm lesebuffer"
|
||||
STR_USERNAME: "Brukernavn"
|
||||
STR_PASSWORD: "Passord"
|
||||
STR_SYNC_SERVER_URL: "URL til synk-server"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentgjenkjenning"
|
||||
STR_AUTHENTICATE: "Godkjenn"
|
||||
STR_KOREADER_USERNAME: "KOReader-brukernavn"
|
||||
STR_KOREADER_PASSWORD: "KOReader-passord"
|
||||
STR_FILENAME: "Filnavn"
|
||||
STR_BINARY: "Binær"
|
||||
STR_SET_CREDENTIALS_FIRST: "Angi påloggingsinfo først"
|
||||
STR_WIFI_CONN_FAILED: "WiFi-tilkobling mislyktes"
|
||||
STR_AUTHENTICATING: "Godkjenner..."
|
||||
STR_AUTH_SUCCESS: "Godkjenning vellykket!"
|
||||
STR_KOREADER_AUTH: "KOReader-godkjenning"
|
||||
STR_SYNC_READY: "KOReader-synk er klar til bruk"
|
||||
STR_AUTH_FAILED: "Godkjenning mislyktes"
|
||||
STR_DONE: "Ferdig"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Dette tømmer alle bufrede bokdata."
|
||||
STR_CLEAR_CACHE_WARNING_2: "All lesefremdrift går tapt!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Bøker må indekseres på nytt"
|
||||
STR_CLEAR_CACHE_WARNING_4: "når de åpnes igjen."
|
||||
STR_CLEARING_CACHE: "Tømmer buffer..."
|
||||
STR_CACHE_CLEARED: "Buffer tømt"
|
||||
STR_ITEMS_REMOVED: "elementer fjernet"
|
||||
STR_FAILED_LOWER: "mislyktes"
|
||||
STR_CLEAR_CACHE_FAILED: "Kunne ikke tømme buffer"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Sjekk seriell utdata for detaljer"
|
||||
STR_DARK: "Mørk"
|
||||
STR_LIGHT: "Lys"
|
||||
STR_CUSTOM: "Egendefinert"
|
||||
STR_COVER: "Omslag"
|
||||
STR_NONE_OPT: "Ingen"
|
||||
STR_FIT: "Tilpass"
|
||||
STR_CROP: "Beskjær"
|
||||
STR_NEVER: "Aldri"
|
||||
STR_IN_READER: "I leseren"
|
||||
STR_ALWAYS: "Alltid"
|
||||
STR_IGNORE: "Ignorer"
|
||||
STR_SLEEP: "Hvile"
|
||||
STR_PAGE_TURN: "Bla om"
|
||||
STR_FORCE_REFRESH: "Oppdater skjerm"
|
||||
STR_PORTRAIT: "Stående"
|
||||
STR_LANDSCAPE_CW: "Liggende med klokken"
|
||||
STR_INVERTED: "Opp ned"
|
||||
STR_ORIENTATION_INVERTED: "Portrett 180°"
|
||||
STR_LANDSCAPE_CCW: "Liggende mot klokken"
|
||||
STR_PREV_NEXT: "Forrige/Neste"
|
||||
STR_NEXT_PREV: "Neste/Forrige"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Bokmerke"
|
||||
STR_DISABLED: "Deaktivert"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Liten"
|
||||
STR_MEDIUM: "Middels"
|
||||
STR_LARGE: "Stor"
|
||||
STR_X_LARGE: "Ekstra stor"
|
||||
STR_TIGHT: "Tett"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Bred"
|
||||
STR_JUSTIFY: "Blokkjustert"
|
||||
STR_ALIGN_LEFT: "Venstre"
|
||||
STR_CENTER: "Midtstilt"
|
||||
STR_ALIGN_RIGHT: "Høyre"
|
||||
STR_PAGES_1: "1 side"
|
||||
STR_PAGES_5: "5 sider"
|
||||
STR_PAGES_10: "10 sider"
|
||||
STR_PAGES_15: "15 sider"
|
||||
STR_PAGES_30: "30 sider"
|
||||
STR_UPDATE: "Oppdater"
|
||||
STR_CHECKING_UPDATE: "Ser etter oppdatering..."
|
||||
STR_NEW_UPDATE: "Ny oppdatering tilgjengelig!"
|
||||
STR_CURRENT_VERSION: "Nåværende versjon: "
|
||||
STR_NEW_VERSION: "Ny versjon: "
|
||||
STR_UPDATING: "Oppdaterer..."
|
||||
STR_NO_UPDATE: "Ingen oppdatering tilgjengelig"
|
||||
STR_UPDATE_FAILED: "Oppdatering mislyktes"
|
||||
STR_UPDATE_COMPLETE: "Oppdatering fullført"
|
||||
STR_POWER_ON_HINT: "Trykk og hold av/på-knappen for å slå på igjen"
|
||||
STR_RESTARTING_HINT: "Starter på nytt... Hvis enheten ikke starter, hold av/på-knappen i noen sekunder."
|
||||
STR_NO_ENTRIES: "Ingen oppføringer funnet"
|
||||
STR_DOWNLOADING: "Laster ned..."
|
||||
STR_DOWNLOAD_FAILED: "Nedlasting mislyktes"
|
||||
STR_ERROR_MSG: "Feil:"
|
||||
STR_UNNAMED: "Uten navn"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Hold Åpne for å slette"
|
||||
STR_NO_SERVER_URL: "Ingen server-URL konfigurert"
|
||||
STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
|
||||
STR_PARSE_FEED_FAILED: "Kunne ikke tolke feed"
|
||||
STR_NEXT_PAGE: "Neste side »"
|
||||
STR_PREV_PAGE: "« Forrige side"
|
||||
STR_NETWORK_PREFIX: "Nettverk: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP-adresse: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Feil: Generell feil"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Feil: Nettverk ikke funnet"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Feil: Tidsavbrudd for tilkobling"
|
||||
STR_SD_CARD: "SD-kort"
|
||||
STR_BACK: "« Tilbake"
|
||||
STR_EXIT: "« Avslutt"
|
||||
STR_HOME: "« Hjem"
|
||||
STR_SELECT: "Velg"
|
||||
STR_SELECTED: "Valgt"
|
||||
STR_TOGGLE: "Veksle"
|
||||
STR_TOGGLE_BOOKMARK: "Veksle bokmerke"
|
||||
STR_CONFIRM: "Bekreft"
|
||||
STR_CANCEL: "Avbryt"
|
||||
STR_CONNECT: "Koble til"
|
||||
STR_OPEN: "Åpne"
|
||||
STR_DOWNLOAD: "Last ned"
|
||||
STR_RETRY: "Prøv igjen"
|
||||
STR_YES: "Ja"
|
||||
STR_NO: "Nei"
|
||||
STR_SHOW: "Vis"
|
||||
STR_HIDE: "Skjul"
|
||||
STR_STATE_ON: "PÅ"
|
||||
STR_STATE_OFF: "AV"
|
||||
STR_NOT_SET: "Ikke angitt"
|
||||
STR_DIR_LEFT: "Venstre"
|
||||
STR_DIR_RIGHT: "Høyre"
|
||||
STR_DIR_UP: "Opp"
|
||||
STR_DIR_DOWN: "Ned"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Omslagsfilter for hvileskjerm"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Tilpass statuslinje"
|
||||
STR_CHAPTER_PAGE_COUNT: "Sideantall i kapittel"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Bokfremdrift i prosent"
|
||||
STR_PROGRESS_BAR: "Fremdriftslinje"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Tykkelse på fremdriftslinje"
|
||||
STR_PROGRESS_BAR_THIN: "Tynn"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Middels"
|
||||
STR_PROGRESS_BAR_THICK: "Tykk"
|
||||
STR_BOOK: "Bok"
|
||||
STR_CHAPTER: "Kapittel"
|
||||
STR_EXAMPLE_CHAPTER: "Kapittel 21"
|
||||
STR_EXAMPLE_BOOK: "Boktittel"
|
||||
STR_PREVIEW: "Forhåndsvisning"
|
||||
STR_TITLE: "Tittel"
|
||||
STR_BATTERY: "Batteri"
|
||||
STR_XTC_STATUS_BAR: "XTC-statuslinje"
|
||||
STR_BOTTOM: "Nederst"
|
||||
STR_TOP: "Øverst"
|
||||
STR_CLOCK: "Klokke"
|
||||
STR_CLOCK_UTC_OFFSET: "UTC-forskyvning for klokke"
|
||||
STR_CLOCK_FORMAT: "Klokkeformat"
|
||||
STR_CLOCK_FORMAT_24H: "24-timers"
|
||||
STR_CLOCK_FORMAT_12H: "12-timers"
|
||||
STR_CURRENT_TIME: "Nåværende tid:"
|
||||
STR_NEXT_FIELD: "Neste"
|
||||
STR_CLOCK_SYNC: "Synkroniser klokke"
|
||||
STR_CLOCK_SYNC_NOW: "Synkroniser klokke nå"
|
||||
STR_CLOCK_SYNCING: "Synkroniserer fra NTP..."
|
||||
STR_CLOCK_SYNC_OK: "Klokke synkronisert"
|
||||
STR_CLOCK_SYNC_FAIL: "Synkronisering mislyktes"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "WiFi ikke tilkoblet"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Koble til WiFi først, og prøv igjen."
|
||||
STR_CLOCK_SYNCED: "Klokke synkronisert"
|
||||
STR_UI_THEME: "Grensesnittstema"
|
||||
STR_THEME_CLASSIC: "Klassisk"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Retting for soluttoning"
|
||||
STR_REMAP_FRONT_BUTTONS: "Tilordne frontknapper på nytt"
|
||||
STR_BOOKMARKS: "Bokmerker"
|
||||
STR_BOOKMARK_ADDED: "Bokmerke lagt til."
|
||||
STR_BOOKMARK_REMOVED: "Bokmerke fjernet."
|
||||
STR_OPDS_BROWSER: "OPDS-leser"
|
||||
STR_SEARCH: "Søk"
|
||||
STR_COVER_CUSTOM: "Omslag + Egendefinert"
|
||||
STR_QUICK_RESUME: "Hurtig gjenopptak"
|
||||
STR_MENU_RECENT_BOOKS: "Nylige bøker"
|
||||
STR_REMOVE_FROM_RECENTS: "Fjern fra Nylige bøker?"
|
||||
STR_NO_RECENT_BOOKS: "Ingen nylige bøker"
|
||||
STR_CALIBRE_DESC: "Bruk trådløs enhetsoverføring med Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Glem nettverk og fjern lagret passord?"
|
||||
STR_FORGET_BUTTON: "Glem"
|
||||
STR_CALIBRE_STARTING: "Starter Calibre..."
|
||||
STR_CALIBRE_SETUP: "Oppsett"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Tøm"
|
||||
STR_DEFAULT_VALUE: "Standard"
|
||||
STR_REMAP_PROMPT: "Trykk en frontknapp for hver rolle"
|
||||
STR_UNASSIGNED: "Ikke tilordnet"
|
||||
STR_ALREADY_ASSIGNED: "Allerede tilordnet"
|
||||
STR_REMAP_RESET_HINT: "Sideknapp opp: Nullstill til standardoppsett"
|
||||
STR_REMAP_CANCEL_HINT: "Sideknapp ned: Avbryt omtilordning"
|
||||
STR_HW_BACK_LABEL: "Tilbake (1. knapp)"
|
||||
STR_HW_CONFIRM_LABEL: "Bekreft (2. knapp)"
|
||||
STR_HW_LEFT_LABEL: "Venstre (3. knapp)"
|
||||
STR_HW_RIGHT_LABEL: "Høyre (4. knapp)"
|
||||
STR_GO_TO_PERCENT: "Gå til %"
|
||||
STR_GO_HOME_BUTTON: "Hjem"
|
||||
STR_SYNC_PROGRESS: "Synkroniser fremdrift"
|
||||
STR_DELETE_CACHE: "Slett bokbuffer"
|
||||
STR_DELETE: "Slett"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Slette dette bokmerket?"
|
||||
STR_DISPLAY_QR: "Vis side som QR"
|
||||
STR_CHAPTER_PREFIX: "Kapittel: "
|
||||
STR_PAGES_SEPARATOR: " sider | "
|
||||
STR_BOOK_PREFIX: "Bok: "
|
||||
STR_CALIBRE_URL_HINT: "For Calibre, legg til /opds i URL-en din"
|
||||
STR_PERCENT_STEP_HINT: "Venstre/Høyre: 1% Opp/Ned: 10%"
|
||||
STR_SYNCING_TIME: "Synkroniserer tid..."
|
||||
STR_CALC_HASH: "Beregner dokument-hash..."
|
||||
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
||||
STR_FETCH_PROGRESS: "Henter ekstern fremdrift..."
|
||||
STR_UPLOAD_PROGRESS: "Laster opp fremdrift..."
|
||||
STR_NO_CREDENTIALS_MSG: "Ingen påloggingsinfo angitt"
|
||||
STR_KOREADER_SETUP_HINT: "Sett opp KOReader-konto i Innstillinger"
|
||||
STR_PROGRESS_FOUND: "Fremdrift funnet!"
|
||||
STR_REMOTE_LABEL: "Ekstern:"
|
||||
STR_LOCAL_LABEL: "Lokal:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Side %d, %.2f%% totalt"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Side %d/%d, %.2f%% totalt"
|
||||
STR_DEVICE_FROM_FORMAT: " Fra: %s"
|
||||
STR_APPLY_REMOTE: "Bruk ekstern fremdrift"
|
||||
STR_UPLOAD_LOCAL: "Last opp lokal fremdrift"
|
||||
STR_NO_REMOTE_MSG: "Ingen ekstern fremdrift funnet"
|
||||
STR_UPLOAD_PROMPT: "Last opp nåværende posisjon?"
|
||||
STR_UPLOAD_SUCCESS: "Fremdrift lastet opp!"
|
||||
STR_SYNC_FAILED_MSG: "Synkronisering mislyktes"
|
||||
STR_SAVE_PROGRESS_FAILED: "Kunne ikke lagre fremdrift"
|
||||
STR_SECTION_PREFIX: "Del "
|
||||
STR_UPLOAD: "Last opp"
|
||||
STR_BOOK_S_STYLE: "Bokens stil"
|
||||
STR_EMBEDDED_STYLE: "Innebygd stil"
|
||||
STR_FOCUS_READING: "Fokuslesing"
|
||||
STR_OPDS_SERVER_URL: "OPDS-server-URL"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Hurtig retur fra fotnoter"
|
||||
STR_SET_SLEEP_COVER: "Velg omslag"
|
||||
STR_FOOTNOTES: "Fotnoter"
|
||||
STR_NO_FOOTNOTES: "Ingen fotnoter på denne siden"
|
||||
STR_LINK: "[lenke]"
|
||||
STR_SCREENSHOT_BUTTON: "Ta skjermbilde"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||
STR_SLEEP_NEVER: "Aldri"
|
||||
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Høyre: 1 min Opp/Ned: 5 min"
|
||||
STR_ADD_SERVER: "Legg til server"
|
||||
STR_SERVER_NAME: "Servernavn"
|
||||
STR_NO_SERVERS: "Ingen OPDS-servere konfigurert"
|
||||
STR_DELETE_SERVER: "Slett server"
|
||||
STR_OPDS_SERVERS: "OPDS-servere"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisk bla aktivert: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk bla (sider per minutt)"
|
||||
STR_MANAGE_FONTS: "Administrer skrifttyper"
|
||||
STR_FONT_BROWSER: "Bla i skrifttyper"
|
||||
STR_LOADING_FONT_LIST: "Laster skrifttypeliste..."
|
||||
STR_NO_FONTS_AVAILABLE: "Ingen skrifttyper tilgjengelig"
|
||||
STR_FONT_INSTALLED: "Skrifttype installert!"
|
||||
STR_FONT_INSTALL_FAILED: "Installasjon av skrifttype mislyktes"
|
||||
STR_INSTALLED: "Installert"
|
||||
STR_DOWNLOAD_ALL: "Last ned alle"
|
||||
STR_UPDATE_ALL: "Oppdater alle"
|
||||
STR_UPDATE_AVAILABLE: "Oppdater"
|
||||
STR_CRASH_TITLE: "Systemkrasj"
|
||||
STR_CRASH_DESCRIPTION: "En detaljert rapport ble lagret i crash_report.txt. Legg ved denne filen i feilrapporten din."
|
||||
STR_CRASH_REASON: "Krasjårsak:"
|
||||
STR_CRASH_NO_REASON: "(Ingen årsak ble registrert)"
|
||||
STR_TILT_PAGE_TURN: "Vipp for å bla"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Trykk VENSTRE eller HØYRE for å flytte markøren"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Trykk VENSTRE for å gå tilbake til markørposisjonen"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Hold HØYRE og trykk [***] for å skjule passord"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Hold HØYRE og trykk [abc] for å vise passord"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Trykk [***] for å skjule passord"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Trykk [abc] for å vise passord"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Hold OPP for å redigere oppføring"
|
||||
STR_KB_TIPS: "Tips:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Trykk NED for å gå tilbake til tastaturet"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Trykk ABC for å avslutte URL-modus"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Hold DEL for å slette all tekst"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Hold VELG for sekundærtegn"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Hold VELG for STORE BOKSTAVER eller sekundærtegn"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Hold VELG for små bokstaver eller sekundærtegn"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Trykk URL for tekstbiter"
|
||||
STR_SD_FIRMWARE_UPDATE: "Fastvareoppdatering fra SD-kort"
|
||||
STR_SELECT_FIRMWARE_FILE: "Velg fastvarefil (.bin)"
|
||||
STR_NO_BIN_FILES: "Ingen .bin-filer funnet"
|
||||
STR_VALIDATING_FIRMWARE: "Validerer fastvare..."
|
||||
STR_INVALID_FIRMWARE: "Ugyldig fastvarefil"
|
||||
STR_FIRMWARE_TOO_LARGE: "Fastvaren er for stor for partisjonen"
|
||||
STR_FIRMWARE_TOO_SMALL: "Fastvarefilen er for liten"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Oppdatere fastvare?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Kan ikke åpne filen"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Skriving av fastvare mislyktes"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ikke slå av!"
|
||||
STR_RECOVERY_MODE: "Gjenopprettingsmodus"
|
||||
STR_RECOVERY_MODE_HINT: "Legg firmware.bin i roten av SD-kortet og velg den"
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Pomijaj"
|
||||
STR_SHORT_PWR_BTN: "Krótkie naciśnięcie zasilania"
|
||||
STR_ORIENTATION: "Układ czytania"
|
||||
STR_SIDE_BTN_LAYOUT: "Układ przycisków bocznych"
|
||||
STR_TOUCH_READER_CONTROLS: "Sterowanie dotykowe czytnika"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientuj przednie przyciski"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Funkcja długiego przyciśnięcia"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Wył."
|
||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Użytkownik"
|
||||
STR_PASSWORD: "Hasło"
|
||||
STR_SYNC_SERVER_URL: "Serwer URL synchronizacji"
|
||||
STR_DOCUMENT_MATCHING: "Dopasowanie dokumentów"
|
||||
STR_SEND_METADATA: "Wyślij metadane dokumentu"
|
||||
STR_AUTHENTICATE: "Uwierzytelnianie"
|
||||
STR_KOREADER_USERNAME: "Użytkownik KOReader"
|
||||
STR_KOREADER_PASSWORD: "Hasło KOReader"
|
||||
@@ -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,13 +309,19 @@ 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"
|
||||
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
|
||||
STR_DELETE_SERVER: "Usuń serwer"
|
||||
STR_OPDS_SERVERS: "Serwery OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Folder pobierania"
|
||||
STR_OPDS_FILENAME_FORMAT: "Format nazwy pliku"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Tytuł"
|
||||
STR_FMT_TITLE_AUTHOR: "Tytuł - Autor"
|
||||
STR_FMT_TITLE: "Tytuł"
|
||||
STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
|
||||
STR_DOWNLOAD_FONTS: "Pobierz czcionki"
|
||||
@@ -360,3 +367,5 @@ STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!"
|
||||
STR_RECOVERY_MODE: "Tryb przywracania"
|
||||
STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go"
|
||||
STR_ADD_HIDDEN_NETWORK: "Dodaj ukrytą sieć..."
|
||||
STR_ENTER_WIFI_SSID: "Wprowadź nazwę sieci (SSID)"
|
||||
|
||||
@@ -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,37 @@ 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_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_TOUCH_READER_CONTROLS: "Controles táteis do leitor"
|
||||
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 +88,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"
|
||||
@@ -92,6 +100,7 @@ STR_USERNAME: "Nome de usuário"
|
||||
STR_PASSWORD: "Senha"
|
||||
STR_SYNC_SERVER_URL: "URL servidor sincronização"
|
||||
STR_DOCUMENT_MATCHING: "Documento correspondente"
|
||||
STR_SEND_METADATA: "Enviar metadados do doc."
|
||||
STR_AUTHENTICATE: "Autenticar"
|
||||
STR_KOREADER_USERNAME: "Usuário do KOReader"
|
||||
STR_KOREADER_PASSWORD: "Senha do KOReader"
|
||||
@@ -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,76 @@ 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_OPDS_DOWNLOAD_FOLDER: "Pasta de downloads"
|
||||
STR_OPDS_FILENAME_FORMAT: "Formato do nome do arquivo"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Título"
|
||||
STR_FMT_TITLE_AUTHOR: "Título - Autor"
|
||||
STR_FMT_TITLE: "Título"
|
||||
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"
|
||||
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
|
||||
STR_ENTER_WIFI_SSID: "Insira o nome da rede (SSID)"
|
||||
@@ -0,0 +1,393 @@
|
||||
_language_name: "Português (Portugal)"
|
||||
_language_code: "P2"
|
||||
_order: "27"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "A INICIAR"
|
||||
STR_SLEEPING: "EM REPOUSO"
|
||||
STR_ENTERING_SLEEP: "A entrar em repouso"
|
||||
STR_BROWSE_FILES: "Explorar ficheiros"
|
||||
STR_FILE_TRANSFER: "Transferência de ficheiros"
|
||||
STR_SETTINGS_TITLE: "Definições"
|
||||
STR_CONTINUE_READING: "Continuar a ler"
|
||||
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
|
||||
STR_START_READING: "Comece a ler abaixo"
|
||||
STR_NO_FILES_FOUND: "Nenhum ficheiro encontrado"
|
||||
STR_SELECT_CHAPTER: "Selecionar capítulo"
|
||||
STR_NO_CHAPTERS: "Sem capítulos"
|
||||
STR_END_OF_BOOK: "Fim do livro"
|
||||
STR_EMPTY_CHAPTER: "Capítulo vazio"
|
||||
STR_INDEXING: "A indexar"
|
||||
STR_MEMORY_ERROR: "Erro de memória"
|
||||
STR_PAGE_LOAD_ERROR: "Erro ao carregar a página"
|
||||
STR_EMPTY_FILE: "Ficheiro vazio"
|
||||
STR_OUT_OF_BOUNDS: "Fora dos limites"
|
||||
STR_LOADING: "A carregar..."
|
||||
STR_LOADING_POPUP: "A carregar"
|
||||
STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||
STR_NO_NETWORKS: "Nenhuma rede encontrada"
|
||||
STR_NETWORKS_FOUND: "%zu redes encontradas"
|
||||
STR_SCANNING: "A procurar..."
|
||||
STR_CONNECTING: "A ligar..."
|
||||
STR_CONNECTED: "Ligado!"
|
||||
STR_CONNECTION_FAILED: "Falha na ligação"
|
||||
STR_FORGET_NETWORK: "Esquecer rede?"
|
||||
STR_SAVE_PASSWORD: "Guardar palavra-passe para a próxima vez?"
|
||||
STR_PRESS_OK_SCAN: "Prima OK para procurar novamente"
|
||||
STR_JOIN_NETWORK: "Aderir a uma rede"
|
||||
STR_CREATE_HOTSPOT: "Criar ponto de acesso"
|
||||
STR_JOIN_DESC: "Ligue-se a uma rede Wi-Fi existente"
|
||||
STR_HOTSPOT_DESC: "Crie uma rede Wi-Fi para outras pessoas se ligarem"
|
||||
STR_STARTING_HOTSPOT: "A iniciar ponto de acesso..."
|
||||
STR_HOTSPOT_MODE: "Modo de ponto de acesso"
|
||||
STR_CONNECT_WIFI_HINT: "Ligue o seu dispositivo a esta rede Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Abra este URL no seu navegador"
|
||||
STR_OR_HTTP_PREFIX: "ou http://"
|
||||
STR_SCAN_QR_HINT: "ou leia o código QR com o seu telemóvel:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre sem fios"
|
||||
STR_NETWORK_LEGEND: "* = Encriptada | + = Guardada"
|
||||
STR_MAC_ADDRESS: "Endereço MAC:"
|
||||
STR_CHECKING_WIFI: "A verificar o Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Introduza a palavra-passe do Wi-Fi"
|
||||
STR_TO_PREFIX: "para "
|
||||
STR_CALIBRE_RECEIVING: "A receber: "
|
||||
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 para o dispositivo\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantenha este ecrã aberto durante o envio\""
|
||||
STR_CAT_DISPLAY: "Ecrã"
|
||||
STR_CAT_READER: "Leitor"
|
||||
STR_CAT_CONTROLS: "Controlos"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
STR_SLEEP_SCREEN: "Ecrã de repouso"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Retoma rápida após tempo limite"
|
||||
STR_SLEEP_COVER_MODE: "Modo de capa no ecrã de repouso"
|
||||
STR_HIDE_BATTERY: "Ocultar % da bateria"
|
||||
STR_EXTRA_SPACING: "Espaçamento extra entre parágrafos"
|
||||
STR_TEXT_AA: "Suavização do texto"
|
||||
STR_IMAGES: "Imagens"
|
||||
STR_IMAGES_DISPLAY: "Exibição"
|
||||
STR_IMAGES_PLACEHOLDER: "Espaço reservado"
|
||||
STR_IMAGES_SUPPRESS: "Suprimir"
|
||||
STR_SHORT_PWR_BTN: "Clique curto no botão de energia"
|
||||
STR_ORIENTATION: "Orientação de leitura"
|
||||
STR_SIDE_BTN_LAYOUT: "Disposição dos botões laterais (leitor)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botões frontais"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportamento de pressão longa"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "DESL."
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítulo"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Alterar orientação"
|
||||
STR_LONG_PRESS_MENU: "Pressão longa no Menu"
|
||||
STR_FONT_PREVIEW_TEXT: "A rápida raposa castanha salta por cima do cão preguiçoso"
|
||||
STR_FONT_FAMILY: "Tipo de letra do leitor"
|
||||
STR_FONT_SIZE: "Tamanho do tipo de letra"
|
||||
STR_LINE_SPACING: "Espaçamento entre linhas"
|
||||
STR_SCREEN_MARGIN: "Margens do ecrã"
|
||||
STR_PARA_ALIGNMENT: "Alinhamento dos parágrafos"
|
||||
STR_HYPHENATION: "Hifenização"
|
||||
STR_TIME_TO_SLEEP: "Tempo até repousar"
|
||||
STR_SHOW_HIDDEN_FILES: "Mostrar ficheiros ocultos"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "Limpar livros lidos da lista de recentes"
|
||||
STR_MOVE_FINISHED_TO_READ: "Mover livros concluídos para a pasta Read"
|
||||
STR_REFRESH_FREQ: "Frequência de atualização"
|
||||
STR_KOREADER_SYNC: "Sincronização KOReader"
|
||||
STR_CHECK_UPDATES: "Procurar atualizações"
|
||||
STR_LANGUAGE: "Idioma"
|
||||
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
|
||||
STR_USERNAME: "Nome de utilizador"
|
||||
STR_PASSWORD: "Palavra-passe"
|
||||
STR_SYNC_SERVER_URL: "URL do servidor de sincronização"
|
||||
STR_DOCUMENT_MATCHING: "Correspondência de documentos"
|
||||
STR_SEND_METADATA: "Enviar metadados do documento"
|
||||
STR_AUTHENTICATE: "Autenticar"
|
||||
STR_KOREADER_USERNAME: "Utilizador do KOReader"
|
||||
STR_KOREADER_PASSWORD: "Palavra-passe do KOReader"
|
||||
STR_FILENAME: "Nome do ficheiro"
|
||||
STR_BINARY: "Binário"
|
||||
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
|
||||
STR_WIFI_CONN_FAILED: "Falha na ligação Wi-Fi"
|
||||
STR_AUTHENTICATING: "A autenticar..."
|
||||
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
|
||||
STR_KOREADER_AUTH: "Autenticação KOReader"
|
||||
STR_SYNC_READY: "A sincronização KOReader está pronta a usar"
|
||||
STR_AUTH_FAILED: "Falha na autenticação"
|
||||
STR_DONE: "Concluído"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Isto irá limpar todos os dados de livros em cache."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Os livros terão de ser reindexados"
|
||||
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
|
||||
STR_CLEARING_CACHE: "A limpar a cache..."
|
||||
STR_CACHE_CLEARED: "Cache limpa"
|
||||
STR_ITEMS_REMOVED: "itens removidos"
|
||||
STR_FAILED_LOWER: "falhou"
|
||||
STR_CLEAR_CACHE_FAILED: "Falha ao limpar a cache"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Verifique a saída de série para obter detalhes"
|
||||
STR_DARK: "Escuro"
|
||||
STR_LIGHT: "Claro"
|
||||
STR_CUSTOM: "Personalizado"
|
||||
STR_COVER: "Capa"
|
||||
STR_NONE_OPT: "Nenhum"
|
||||
STR_FIT: "Ajustar"
|
||||
STR_CROP: "Recortar"
|
||||
STR_NEVER: "Nunca"
|
||||
STR_IN_READER: "No leitor"
|
||||
STR_ALWAYS: "Sempre"
|
||||
STR_IGNORE: "Ignorar"
|
||||
STR_SLEEP: "Repouso"
|
||||
STR_PAGE_TURN: "Virar página"
|
||||
STR_FORCE_REFRESH: "Atualizar ecrã"
|
||||
STR_PORTRAIT: "Retrato"
|
||||
STR_LANDSCAPE_CW: "Paisagem (Dir.)"
|
||||
STR_INVERTED: "Invertido"
|
||||
STR_ORIENTATION_INVERTED: "Retrato 180°"
|
||||
STR_LANDSCAPE_CCW: "Paisagem (Esq.)"
|
||||
STR_PREV_NEXT: "Ant/Seg"
|
||||
STR_NEXT_PREV: "Seg/Ant"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Marcador"
|
||||
STR_DISABLED: "Desativado"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Pequeno"
|
||||
STR_MEDIUM: "Médio"
|
||||
STR_LARGE: "Grande"
|
||||
STR_X_LARGE: "Extra grande"
|
||||
STR_TIGHT: "Apertado"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Largo"
|
||||
STR_JUSTIFY: "Justificar"
|
||||
STR_ALIGN_LEFT: "Esquerda"
|
||||
STR_CENTER: "Centrar"
|
||||
STR_ALIGN_RIGHT: "Direita"
|
||||
STR_PAGES_1: "1 página"
|
||||
STR_PAGES_5: "5 páginas"
|
||||
STR_PAGES_10: "10 páginas"
|
||||
STR_PAGES_15: "15 páginas"
|
||||
STR_PAGES_30: "30 páginas"
|
||||
STR_UPDATE: "Atualizar"
|
||||
STR_CHECKING_UPDATE: "A procurar atualização..."
|
||||
STR_NEW_UPDATE: "Nova atualização disponível!"
|
||||
STR_CURRENT_VERSION: "Versão atual: "
|
||||
STR_NEW_VERSION: "Nova versão: "
|
||||
STR_UPDATING: "A atualizar..."
|
||||
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: "Prima sem soltar o botão de energia para voltar a ligar"
|
||||
STR_RESTARTING_HINT: "A reiniciar... Se o dispositivo não reiniciar, mantenha premido o botão de energia durante alguns segundos."
|
||||
STR_NO_ENTRIES: "Nenhum registo encontrado"
|
||||
STR_DOWNLOADING: "A transferir..."
|
||||
STR_DOWNLOAD_FAILED: "Falha na transferência"
|
||||
STR_ERROR_MSG: "Erro:"
|
||||
STR_UNNAMED: "Sem nome"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Mantenha premido Abrir para eliminar"
|
||||
STR_NO_SERVER_URL: "Nenhum URL de servidor configurado"
|
||||
STR_FETCH_FEED_FAILED: "Falha ao obter o feed"
|
||||
STR_PARSE_FEED_FAILED: "Falha ao analisar o feed"
|
||||
STR_NEXT_PAGE: "Página seguinte »"
|
||||
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 de ligação esgotado"
|
||||
STR_SD_CARD: "Cartão SD"
|
||||
STR_BACK: "« Voltar"
|
||||
STR_EXIT: "« Sair"
|
||||
STR_HOME: "« Início"
|
||||
STR_SELECT: "Selecionar"
|
||||
STR_SELECTED: "Selecionado"
|
||||
STR_TOGGLE: "Alternar"
|
||||
STR_TOGGLE_BOOKMARK: "Alternar marcador"
|
||||
STR_CONFIRM: "Confirmar"
|
||||
STR_CANCEL: "Cancelar"
|
||||
STR_CONNECT: "Ligar"
|
||||
STR_OPEN: "Abrir"
|
||||
STR_DOWNLOAD: "Transferir"
|
||||
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"
|
||||
STR_DIR_LEFT: "Esquerda"
|
||||
STR_DIR_RIGHT: "Direita"
|
||||
STR_DIR_UP: "Cima"
|
||||
STR_DIR_DOWN: "Baixo"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filtro da capa de ecrã de repouso"
|
||||
STR_FILTER_CONTRAST: "Contraste"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Personalizar barra de estado"
|
||||
STR_CHAPTER_PAGE_COUNT: "Contagem de páginas do capítulo"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Percentagem de progresso do livro"
|
||||
STR_PROGRESS_BAR: "Barra de progresso"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Espessura da barra de progresso"
|
||||
STR_PROGRESS_BAR_THIN: "Fina"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Média"
|
||||
STR_PROGRESS_BAR_THICK: "Grossa"
|
||||
STR_BOOK: "Livro"
|
||||
STR_CHAPTER: "Capítulo"
|
||||
STR_EXAMPLE_CHAPTER: "Capítulo 21"
|
||||
STR_EXAMPLE_BOOK: "Título do livro"
|
||||
STR_PREVIEW: "Pré-visualização"
|
||||
STR_TITLE: "Título"
|
||||
STR_BATTERY: "Bateria"
|
||||
STR_XTC_STATUS_BAR: "Barra de estado XTC"
|
||||
STR_BOTTOM: "Inferior"
|
||||
STR_TOP: "Superior"
|
||||
STR_CLOCK: "Relógio"
|
||||
STR_CLOCK_UTC_OFFSET: "Deslocamento 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: "Seguinte"
|
||||
STR_CLOCK_SYNC: "Sincronizar relógio"
|
||||
STR_CLOCK_SYNC_NOW: "Sincronizar relógio agora"
|
||||
STR_CLOCK_SYNCING: "A sincronizar 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 ligado"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Ligue-se primeiro ao Wi-Fi e, em seguida, tente novamente."
|
||||
STR_CLOCK_SYNCED: "Relógio sincronizado"
|
||||
STR_UI_THEME: "Tema da interface"
|
||||
STR_THEME_CLASSIC: "Clássico"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Ajuste de desbotamento ao sol"
|
||||
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: "Pesquisar"
|
||||
STR_COVER_CUSTOM: "Capa + Personalizado"
|
||||
STR_QUICK_RESUME: "Retoma rápida"
|
||||
STR_MENU_RECENT_BOOKS: "Livros recentes"
|
||||
STR_REMOVE_FROM_RECENTS: "Remover dos Livros recentes?"
|
||||
STR_NO_RECENT_BOOKS: "Nenhum livro recente"
|
||||
STR_CALIBRE_DESC: "Usar transferências sem fios de dispositivos do Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Esquecer rede e remover palavra-passe guardada?"
|
||||
STR_FORGET_BUTTON: "Esquecer"
|
||||
STR_CALIBRE_STARTING: "A iniciar o Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configuração"
|
||||
STR_CALIBRE_STATUS: "Estado"
|
||||
STR_CLEAR_BUTTON: "Limpar"
|
||||
STR_DEFAULT_VALUE: "Predefinição"
|
||||
STR_REMAP_PROMPT: "Prima 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: Repor disposição predefinida"
|
||||
STR_REMAP_CANCEL_HINT: "Botão lateral 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)"
|
||||
STR_HW_RIGHT_LABEL: "Direita (4º botão)"
|
||||
STR_GO_TO_PERCENT: "Ir para %"
|
||||
STR_GO_HOME_BUTTON: "Ir para o Início"
|
||||
STR_SYNC_PROGRESS: "Sincronizar progresso"
|
||||
STR_DELETE_CACHE: "Eliminar cache do livro"
|
||||
STR_DELETE: "Eliminar"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Eliminar 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_SYNCING_TIME: "A sincronizar a hora..."
|
||||
STR_CALC_HASH: "A calcular o hash do documento..."
|
||||
STR_HASH_FAILED: "Falha ao calcular o hash do documento"
|
||||
STR_FETCH_PROGRESS: "A procurar progresso remoto..."
|
||||
STR_UPLOAD_PROGRESS: "A enviar progresso..."
|
||||
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
|
||||
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader nas Definições"
|
||||
STR_PROGRESS_FOUND: "Progresso encontrado!"
|
||||
STR_REMOTE_LABEL: "Remoto:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% total"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d/%d, %.2f%% total"
|
||||
STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||
STR_APPLY_REMOTE: "Aplicar progresso remoto"
|
||||
STR_UPLOAD_LOCAL: "Enviar progresso local"
|
||||
STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado"
|
||||
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 guardar o progresso"
|
||||
STR_SECTION_PREFIX: "Secçã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 das 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: "[ligação]"
|
||||
STR_SCREENSHOT_BUTTON: "Captura de ecrã"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||
STR_SLEEP_NEVER: "Nunca"
|
||||
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: "Eliminar servidor"
|
||||
STR_OPDS_SERVERS: "Servidores OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Pasta de downloads"
|
||||
STR_OPDS_FILENAME_FORMAT: "Formato do nome do ficheiro"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Título"
|
||||
STR_FMT_TITLE: "Título"
|
||||
STR_FMT_TITLE_AUTHOR: "Título - Autor"
|
||||
STR_AUTO_TURN_ENABLED: "Virar página automático ativado: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Virar página automático (Páginas por minuto)"
|
||||
STR_MANAGE_FONTS: "Gerir tipos de letra"
|
||||
STR_FONT_BROWSER: "Navegador de tipos de letra"
|
||||
STR_LOADING_FONT_LIST: "A carregar a lista de tipos de letra..."
|
||||
STR_NO_FONTS_AVAILABLE: "Nenhum tipo de letra disponível"
|
||||
STR_FONT_INSTALLED: "Tipo de letra instalado!"
|
||||
STR_FONT_INSTALL_FAILED: "Falha na instalação do tipo de letra"
|
||||
STR_INSTALLED: "Instalado"
|
||||
STR_DOWNLOAD_ALL: "Transferir tudo"
|
||||
STR_UPDATE_ALL: "Atualizar tudo"
|
||||
STR_UPDATE_AVAILABLE: "Atualizar"
|
||||
STR_CRASH_TITLE: "Falha do sistema"
|
||||
STR_CRASH_DESCRIPTION: "Um relatório detalhado foi guardado em crash_report.txt. Por favor, inclua este ficheiro no seu relatório de erro."
|
||||
STR_CRASH_REASON: "Motivo da falha:"
|
||||
STR_CRASH_NO_REASON: "(Nenhum motivo foi registado)"
|
||||
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Prima ESQUERDA ou DIREITA para mover o cursor"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Prima ESQUERDA para regressar à posição do cursor"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Mantenha premido DIREITA e depois prima [***] para ocultar a palavra-passe"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Mantenha premido DIREITA e depois prima [abc] para mostrar a palavra-passe"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prima [***] para ocultar a palavra-passe"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prima [abc] para mostrar a palavra-passe"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Mantenha premido CIMA para editar a entrada"
|
||||
STR_KB_TIPS: "Dicas:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Prima BAIXO para regressar ao teclado"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Prima ABC para sair do modo URL"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Mantenha premido DEL para limpar todo o texto"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Mantenha premido SELECT para o carácter secundário"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Mantenha premido SELECT para MAIÚSCULAS ou carácter secundário"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Mantenha premido SELECT para minúsculas ou carácter secundário"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Prima URL para atalhos"
|
||||
STR_SD_FIRMWARE_UPDATE: "Atualização de firmware via cartão SD"
|
||||
STR_SELECT_FIRMWARE_FILE: "Selecione o ficheiro de firmware (.bin)"
|
||||
STR_NO_BIN_FILES: "Nenhum ficheiro .bin encontrado"
|
||||
STR_VALIDATING_FIRMWARE: "A validar o firmware..."
|
||||
STR_INVALID_FIRMWARE: "Ficheiro de firmware inválido"
|
||||
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande para a partição"
|
||||
STR_FIRMWARE_TOO_SMALL: "Ficheiro de firmware demasiado pequeno"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Atualizar firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Não foi possível abrir o ficheiro"
|
||||
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 ficheiro firmware.bin na raiz do cartão SD e selecione-o"
|
||||
STR_ADD_HIDDEN_NETWORK: "Adicionar rede oculta..."
|
||||
STR_ENTER_WIFI_SSID: "Introduza o nome da rede (SSID)"
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Suprimare"
|
||||
STR_SHORT_PWR_BTN: "Apăsare scurtă întrerupător"
|
||||
STR_ORIENTATION: "Orientare lectură"
|
||||
STR_SIDE_BTN_LAYOUT: "Aspect butoane laterale (lectură)"
|
||||
STR_TOUCH_READER_CONTROLS: "Comenzi tactile cititor"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientare butoane frontale"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportament buton apăsat lung"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Dezactivat"
|
||||
@@ -97,6 +98,7 @@ STR_USERNAME: "Utilizator"
|
||||
STR_PASSWORD: "Parolă"
|
||||
STR_SYNC_SERVER_URL: "URL server sincronizare"
|
||||
STR_DOCUMENT_MATCHING: "Corespondenţă document"
|
||||
STR_SEND_METADATA: "Trimite metadate document"
|
||||
STR_AUTHENTICATE: "Autentificare"
|
||||
STR_KOREADER_USERNAME: "Nume utilizator KOReader"
|
||||
STR_KOREADER_PASSWORD: "Parolă KOReader"
|
||||
@@ -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,8 +299,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Dosar descărcări"
|
||||
STR_OPDS_FILENAME_FORMAT: "Format nume fișier"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Titlu"
|
||||
STR_FMT_TITLE_AUTHOR: "Titlu - Autor"
|
||||
STR_FMT_TITLE: "Titlu"
|
||||
STR_SCREENSHOT_BUTTON: "Captură ecran"
|
||||
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
|
||||
STR_TILT_PAGE_TURN: "Întoarcere pagină prin înclinare"
|
||||
STR_ADD_HIDDEN_NETWORK: "Adaugă rețea ascunsă..."
|
||||
STR_ENTER_WIFI_SSID: "Introduceți numele rețelei (SSID)"
|
||||
|
||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Скрыть"
|
||||
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
|
||||
STR_ORIENTATION: "Ориентация чтения"
|
||||
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
|
||||
STR_TOUCH_READER_CONTROLS: "Сенсорное управление чтением"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентировать передние кнопки"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Долгое нажатие"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Ничего"
|
||||
@@ -99,6 +100,7 @@ STR_USERNAME: "Имя пользователя"
|
||||
STR_PASSWORD: "Пароль"
|
||||
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
|
||||
STR_DOCUMENT_MATCHING: "Сопоставление документов"
|
||||
STR_SEND_METADATA: "Отправлять метаданные"
|
||||
STR_AUTHENTICATE: "Авторизация"
|
||||
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
@@ -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,12 +333,18 @@ 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"
|
||||
STR_DELETE_SERVER: "Удалить сервер"
|
||||
STR_OPDS_SERVERS: "Серверы OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Папка загрузок"
|
||||
STR_OPDS_FILENAME_FORMAT: "Формат имени файла"
|
||||
STR_FMT_AUTHOR_TITLE: "Автор - Название"
|
||||
STR_FMT_TITLE_AUTHOR: "Название - Автор"
|
||||
STR_FMT_TITLE: "Название"
|
||||
STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)"
|
||||
STR_MANAGE_FONTS: "Управление шрифтами"
|
||||
@@ -383,3 +390,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ошибка записи прошивки"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не выключайте питание!"
|
||||
STR_RECOVERY_MODE: "Режим восстановления"
|
||||
STR_RECOVERY_MODE_HINT: "Поместите firmware.bin в корень SD-карты и выберите его"
|
||||
STR_ADD_HIDDEN_NETWORK: "Добавить скрытую сеть..."
|
||||
STR_ENTER_WIFI_SSID: "Введите имя сети (SSID)"
|
||||
|
||||
@@ -73,6 +73,7 @@ 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_TOUCH_READER_CONTROLS: "Dotykové ovládanie čítačky"
|
||||
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"
|
||||
@@ -98,6 +99,7 @@ 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_SEND_METADATA: "Odosielať metadáta dokumentu"
|
||||
STR_AUTHENTICATE: "Overiť"
|
||||
STR_KOREADER_USERNAME: "Používateľské meno KOReader"
|
||||
STR_KOREADER_PASSWORD: "Heslo KOReader"
|
||||
@@ -294,7 +296,6 @@ 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"
|
||||
@@ -328,12 +329,18 @@ 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_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_OPDS_DOWNLOAD_FOLDER: "Priečinok sťahovania"
|
||||
STR_OPDS_FILENAME_FORMAT: "Formát názvu súboru"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Názov"
|
||||
STR_FMT_TITLE_AUTHOR: "Názov - Autor"
|
||||
STR_FMT_TITLE: "Názov"
|
||||
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"
|
||||
@@ -379,3 +386,5 @@ 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_ADD_HIDDEN_NETWORK: "Pridať skrytú sieť..."
|
||||
STR_ENTER_WIFI_SSID: "Zadajte názov siete (SSID)"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Zatdi"
|
||||
STR_SHORT_PWR_BTN: "Kratek pritisk na gumb za vklop"
|
||||
STR_ORIENTATION: "Orientacija branja"
|
||||
STR_SIDE_BTN_LAYOUT: "Razpored stranskih gumbov"
|
||||
STR_TOUCH_READER_CONTROLS: "Upravljanje bralnika na dotik"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Usmeri sprednje gumbe"
|
||||
STR_LONG_PRESS_SKIP: "Dolgi pritisk za preskok poglavja"
|
||||
STR_FONT_PREVIEW_TEXT: "V kožuščku hudobnega fanta stopiclja mizar"
|
||||
@@ -94,6 +95,7 @@ STR_USERNAME: "Uporabniško ime"
|
||||
STR_PASSWORD: "Geslo"
|
||||
STR_SYNC_SERVER_URL: "URL strežnika za sinhronizacijo"
|
||||
STR_DOCUMENT_MATCHING: "Ujemanje dokumentov"
|
||||
STR_SEND_METADATA: "Pošlji metapodatke dokumenta"
|
||||
STR_AUTHENTICATE: "Avtentikacija"
|
||||
STR_KOREADER_USERNAME: "KOReader uporabnik"
|
||||
STR_KOREADER_PASSWORD: "KOReader geslo"
|
||||
@@ -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,8 +296,16 @@ 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_OPDS_DOWNLOAD_FOLDER: "Mapa za prenose"
|
||||
STR_OPDS_FILENAME_FORMAT: "Oblika imena datoteke"
|
||||
STR_FMT_AUTHOR_TITLE: "Avtor - Naslov"
|
||||
STR_FMT_TITLE_AUTHOR: "Naslov - Avtor"
|
||||
STR_FMT_TITLE: "Naslov"
|
||||
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)"
|
||||
STR_TILT_PAGE_TURN: "Obračanje s priklonom"
|
||||
STR_ADD_HIDDEN_NETWORK: "Dodaj skrito omrežje..."
|
||||
STR_ENTER_WIFI_SSID: "Vnesite ime omrežja (SSID)"
|
||||
|
||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sin capítulos"
|
||||
STR_END_OF_BOOK: "Fin del libro"
|
||||
STR_EMPTY_CHAPTER: "Capítulo vacío"
|
||||
STR_INDEXING: "Indexando"
|
||||
STR_INDEX_FAILED: "No se pudo indexar: libro no válido"
|
||||
STR_MEMORY_ERROR: "Error de memoria"
|
||||
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
|
||||
STR_EMPTY_FILE: "Archivo vacío"
|
||||
@@ -28,7 +29,10 @@ STR_WIFI_NETWORKS: "Redes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No hay redes disponibles"
|
||||
STR_NETWORKS_FOUND: "%zu red(es) encontrada(s)"
|
||||
STR_SCANNING: "Buscando..."
|
||||
STR_FINDING_SAVED_WIFI: "Buscando redes Wi-Fi guardadas..."
|
||||
STR_CONNECTING: "Conectando..."
|
||||
STR_CONNECTING_SAVED_WIFI: "Conectando a una red Wi-Fi guardada..."
|
||||
STR_SHOW_NETWORKS: "Mostrar"
|
||||
STR_CONNECTED: "¡Conectado!"
|
||||
STR_CONNECTION_FAILED: "Error de conexión"
|
||||
STR_FORGET_NETWORK: "¿Olvidar la red?"
|
||||
@@ -69,9 +73,12 @@ STR_IMAGES: "Imágenes"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Reemplazar"
|
||||
STR_IMAGES_SUPPRESS: "Ocultar"
|
||||
STR_EOB_HOME: "Inicio"
|
||||
STR_EOB_CONTINUE_WITH: "Continuar con"
|
||||
STR_SHORT_PWR_BTN: "Toque corto del encendido"
|
||||
STR_ORIENTATION: "Orientación"
|
||||
STR_SIDE_BTN_LAYOUT: "Función botones laterales (lector)"
|
||||
STR_TOUCH_READER_CONTROLS: "Controles táctiles del lector"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botones frontales"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Al mantener pulsado un botón"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "No hacer nada"
|
||||
@@ -98,6 +105,7 @@ STR_USERNAME: "Usuario"
|
||||
STR_PASSWORD: "Contraseña"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de sinc."
|
||||
STR_DOCUMENT_MATCHING: "Coincidencia de doc."
|
||||
STR_SEND_METADATA: "Enviar metadatos doc."
|
||||
STR_AUTHENTICATE: "Autenticar"
|
||||
STR_KOREADER_USERNAME: "Usuario de KOReader"
|
||||
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
|
||||
@@ -297,7 +305,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,13 +338,19 @@ 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"
|
||||
STR_NO_SERVERS: "No se configuraron servidores OPDS"
|
||||
STR_DELETE_SERVER: "Borrar servidor"
|
||||
STR_OPDS_SERVERS: "Servidores OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de descargas"
|
||||
STR_OPDS_FILENAME_FORMAT: "Formato del nombre de archivo"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Título"
|
||||
STR_FMT_TITLE_AUTHOR: "Título - Autor"
|
||||
STR_FMT_TITLE: "Título"
|
||||
STR_AUTO_TURN_ENABLED: "Avance activado: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)"
|
||||
STR_MANAGE_FONTS: "Gestionar tipografías"
|
||||
@@ -383,3 +396,5 @@ STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "¡No apague el dispositivo!"
|
||||
STR_RECOVERY_MODE: "Modo de recuperación"
|
||||
STR_RECOVERY_MODE_HINT: "Ponga firmware.bin en la raíz de la tarj. SD y selecciónelo"
|
||||
STR_ADD_HIDDEN_NETWORK: "Añadir red oculta..."
|
||||
STR_ENTER_WIFI_SSID: "Introduce el nombre de la red (SSID)"
|
||||
|
||||
@@ -10,6 +10,7 @@ STR_BROWSE_FILES: "Bläddra filer…"
|
||||
STR_FILE_TRANSFER: "Filöverföring"
|
||||
STR_SETTINGS_TITLE: "Inställningar"
|
||||
STR_CONTINUE_READING: "Fortsätt läsa"
|
||||
STR_RESUME: "Återuppta"
|
||||
STR_NO_OPEN_BOOK: "Ingen öppen bok"
|
||||
STR_START_READING: "Börja läsa nedan"
|
||||
STR_NO_FILES_FOUND: "Inga filer hittades"
|
||||
@@ -18,17 +19,27 @@ STR_NO_CHAPTERS: "Inga kapitel"
|
||||
STR_END_OF_BOOK: "Slutet på boken"
|
||||
STR_EMPTY_CHAPTER: "Tomt kapitel"
|
||||
STR_INDEXING: "Indexerar"
|
||||
STR_INDEX_FAILED: "Misslyckades att indexera - ogiltig bok"
|
||||
STR_MEMORY_ERROR: "Minnesfel"
|
||||
STR_PAGE_LOAD_ERROR: "Sidladdningsfel"
|
||||
STR_EMPTY_FILE: "Tom fil"
|
||||
STR_OUT_OF_BOUNDS: "Utanför gränserna"
|
||||
STR_LOADING: "Laddar…"
|
||||
STR_LOADING_POPUP: "Laddar"
|
||||
STR_LOOKUP: "Slå upp"
|
||||
STR_DICT_LOOKING_UP: "Slår upp..."
|
||||
STR_DICT_INDEXING: "Indexerar ordbok..."
|
||||
STR_DICT_NOT_FOUND: "Hittades inte"
|
||||
STR_DICT_NO_DICT_SET: "Ingen ordbok angiven"
|
||||
STR_DICT_ERROR: "Ordboksfel"
|
||||
STR_WIFI_NETWORKS: "Trådlösa nätverk"
|
||||
STR_NO_NETWORKS: "Inga nätverk funna"
|
||||
STR_NETWORKS_FOUND: "%zu nätverk funna"
|
||||
STR_SCANNING: "Scannar…"
|
||||
STR_FINDING_SAVED_WIFI: "Hittar sparade Wi-Fi..."
|
||||
STR_CONNECTING: "Ansluter…"
|
||||
STR_CONNECTING_SAVED_WIFI: "Ansluter till sparat Wi-Fi..."
|
||||
STR_SHOW_NETWORKS: "Visa"
|
||||
STR_CONNECTED: "Ansluten!"
|
||||
STR_CONNECTION_FAILED: "Anslutning misslyckades"
|
||||
STR_FORGET_NETWORK: "Glöm nätverk?"
|
||||
@@ -49,6 +60,8 @@ STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
|
||||
STR_MAC_ADDRESS: "MAC-adress:"
|
||||
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
|
||||
STR_ENTER_WIFI_PASSWORD: "Skriv in Wi-Fi-lösenord"
|
||||
STR_ADD_HIDDEN_NETWORK: "Lägg till dolt nätverk..."
|
||||
STR_ENTER_WIFI_SSID: "Ange nätverksnamn (SSID)"
|
||||
STR_TO_PREFIX: "till "
|
||||
STR_CALIBRE_RECEIVING: "Tar emot:"
|
||||
STR_CALIBRE_RECEIVED: "Mottaget:"
|
||||
@@ -61,7 +74,7 @@ STR_CAT_READER: "Läsare"
|
||||
STR_CAT_CONTROLS: "Kontroller"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
STR_SLEEP_SCREEN: "Viloskärm"
|
||||
STR_SEAMLESS_SLEEP: "Sida som viloskärm"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
||||
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
|
||||
STR_HIDE_BATTERY: "Dölj batteriprocent"
|
||||
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
|
||||
@@ -70,14 +83,18 @@ STR_IMAGES: "Bilder"
|
||||
STR_IMAGES_DISPLAY: "Visa"
|
||||
STR_IMAGES_PLACEHOLDER: "Platshållare"
|
||||
STR_IMAGES_SUPPRESS: "Dölj"
|
||||
STR_EOB_HOME: "Hem"
|
||||
STR_EOB_CONTINUE_WITH: "Fortsätt med"
|
||||
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
|
||||
STR_ORIENTATION: "Läsrikting"
|
||||
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
|
||||
STR_TOUCH_READER_CONTROLS: "Pekkontroller i läsaren"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Rikta främre knappar"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Beteende vid lång knapptryckning"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Hoppa över kapitel"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ändra orientering"
|
||||
STR_LONG_PRESS_MENU: "Långtrycksmeny"
|
||||
STR_FONT_PREVIEW_TEXT: "Flygande bäckasiner söka hwila på mjuka tuvor"
|
||||
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
|
||||
STR_FONT_SIZE: "Eboksläsarens typsnittsstorlek"
|
||||
@@ -98,7 +115,16 @@ STR_USERNAME: "Användarnamn"
|
||||
STR_PASSWORD: "Lösenord"
|
||||
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
|
||||
STR_SEND_METADATA: "Skicka dokumentmetadata"
|
||||
STR_SYNC_BEHAVIOR: "Synkroniseringsbeteende"
|
||||
STR_ASK_EVERY_TIME: "Fråga varje gång"
|
||||
STR_SMART_SYNC: "Smart synkronisering"
|
||||
STR_AUTHENTICATE: "Autentisera "
|
||||
STR_SIGN_UP: "Registrera dig"
|
||||
STR_CREATING_ACCOUNT: "Skapar konto..."
|
||||
STR_ACCOUNT_CREATED: "Konto skapat"
|
||||
STR_SIGNUP_FAILED: "Registreringen misslyckades"
|
||||
STR_USERNAME_TAKEN: "Användarnamnet är redan registrerat"
|
||||
STR_KOREADER_USERNAME: "KOReader användarnamn"
|
||||
STR_KOREADER_PASSWORD: "KOReader lösenord"
|
||||
STR_FILENAME: "Filnamn"
|
||||
@@ -142,6 +168,9 @@ STR_ORIENTATION_INVERTED: "Porträtt 180°"
|
||||
STR_LANDSCAPE_CCW: "Landskap moturs"
|
||||
STR_PREV_NEXT: "Förra/Nästa"
|
||||
STR_NEXT_PREV: "Nästa/Förra"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Bokmärke"
|
||||
STR_DICTIONARY: "Ordbok"
|
||||
STR_DISABLED: "Inaktiverad"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
@@ -255,7 +284,6 @@ STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra utökad"
|
||||
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Snabb återupptagning efter timeout"
|
||||
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
|
||||
STR_BOOKMARKS: "Bokmärken"
|
||||
STR_BOOKMARK_ADDED: "Bokmärke tillagt."
|
||||
@@ -295,7 +323,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"
|
||||
@@ -314,6 +341,7 @@ STR_UPLOAD_LOCAL: "Ladda upp lokala framsteg"
|
||||
STR_NO_REMOTE_MSG: "Inga fjärrframsteg funna"
|
||||
STR_UPLOAD_PROMPT: "Ladda upp nuvarande position?"
|
||||
STR_UPLOAD_SUCCESS: "Framsteg uppladdade!"
|
||||
STR_ALREADY_SYNCED: "Redan synkroniserad"
|
||||
STR_SYNC_FAILED_MSG: "Synkronisering misslyckades"
|
||||
STR_SAVE_PROGRESS_FAILED: "Kunde inte spara framsteg"
|
||||
STR_SECTION_PREFIX: "Sektion"
|
||||
@@ -322,19 +350,28 @@ STR_BOOK_S_STYLE: "Bokstil"
|
||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||
STR_FOCUS_READING: "Fokusläsning"
|
||||
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Snabbåtergång från fotnoter"
|
||||
STR_BACK_SHORT_TO_FILE_BROWSER: "Snabb tillbaka till filläsaren"
|
||||
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
||||
STR_FOOTNOTES: "Fotnoter"
|
||||
STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan"
|
||||
STR_LINK: "[länk]"
|
||||
STR_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
||||
STR_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_SCREENSHOT_BUTTON: "Ta en skärmdump"
|
||||
STR_STEP_HINT_FRONT: "Framknappar:"
|
||||
STR_STEP_HINT_SIDE: "Sidoknappar:"
|
||||
STR_ADD_SERVER: "Lägg till server"
|
||||
STR_SERVER_NAME: "Servernamn"
|
||||
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
|
||||
STR_DELETE_SERVER: "Ta bort server"
|
||||
STR_OPDS_SERVERS: "OPDS-servrar"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Hämtningsmapp"
|
||||
STR_OPDS_FILENAME_FORMAT: "Filnamnsformat"
|
||||
STR_FMT_AUTHOR_TITLE: "Författare - Titel"
|
||||
STR_FMT_TITLE_AUTHOR: "Titel - Författare"
|
||||
STR_FMT_TITLE: "Titel"
|
||||
STR_OPDS_SD_ROOT: "SD-rot"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
|
||||
STR_MANAGE_FONTS: "Hantera teckensnitt"
|
||||
|
||||
@@ -67,11 +67,12 @@ STR_TEXT_AA: "Metin Yumuşatma (AA)"
|
||||
STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
|
||||
STR_ORIENTATION: "Okuma Yönü"
|
||||
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
|
||||
STR_TOUCH_READER_CONTROLS: "Dokunmatik okuyucu kontrolleri"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Uzun basma tuş davranışı"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "KAPALI"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Bölüm atlama"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Yön değiştirme"
|
||||
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
|
||||
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
|
||||
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
|
||||
@@ -92,6 +93,7 @@ STR_USERNAME: "Kullanıcı Adı"
|
||||
STR_PASSWORD: "Şifre"
|
||||
STR_SYNC_SERVER_URL: "Senkronizasyon Sunucu Adresi"
|
||||
STR_DOCUMENT_MATCHING: "Belge Eşleştirme"
|
||||
STR_SEND_METADATA: "Belge üst verisi gönder"
|
||||
STR_AUTHENTICATE: "Kimlik Doğrula"
|
||||
STR_KOREADER_USERNAME: "KOReader Kullanıcı Adı"
|
||||
STR_KOREADER_PASSWORD: "KOReader Şifresi"
|
||||
@@ -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,13 @@ 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_OPDS_DOWNLOAD_FOLDER: "İndirme klasörü"
|
||||
STR_OPDS_FILENAME_FORMAT: "Dosya adı biçimi"
|
||||
STR_FMT_AUTHOR_TITLE: "Yazar - Başlık"
|
||||
STR_FMT_TITLE_AUTHOR: "Başlık - Yazar"
|
||||
STR_FMT_TITLE: "Başlık"
|
||||
STR_NO_FILES_FOUND: "Dosya bulunamadı"
|
||||
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
|
||||
STR_PREVIEW: "Önizleme"
|
||||
@@ -303,3 +310,91 @@ STR_SELECTED: "Seçili"
|
||||
STR_SHOW: "Göster"
|
||||
STR_TITLE: "Başlık"
|
||||
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
||||
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
|
||||
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
|
||||
STR_ADD_SERVER: "Sunucu Ekle"
|
||||
STR_BOOKMARKS: "Yer İmleri"
|
||||
STR_BOOKMARK_ADDED: "Yer imi eklendi."
|
||||
STR_BOOKMARK_OPTION: "Yer İmi"
|
||||
STR_BOTTOM: "Alt"
|
||||
STR_CLOCK: "Saat"
|
||||
STR_CLOCK_FORMAT: "Saat Biçimi"
|
||||
STR_CLOCK_FORMAT_12H: "12 saat"
|
||||
STR_CLOCK_FORMAT_24H: "24 saat"
|
||||
STR_CLOCK_SYNC: "Saati Eşitle"
|
||||
STR_CLOCK_SYNCED: "Saat Eşitlendi"
|
||||
STR_CLOCK_SYNCING: "NTP'den eşitleniyor..."
|
||||
STR_CLOCK_SYNC_FAIL: "Eşitleme başarısız"
|
||||
STR_CLOCK_SYNC_NOW: "Saati şimdi eşitle"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi bağlı değil"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Önce Wi-Fi'ye bağlanın, sonra tekrar deneyin."
|
||||
STR_CLOCK_SYNC_OK: "Saat eşitlendi"
|
||||
STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Bu yer imi silinsin mi?"
|
||||
STR_CONNECTING_SAVED_WIFI: "Kayıtlı Wi-Fi'ye bağlanılıyor..."
|
||||
STR_CRASH_DESCRIPTION: "Ayrıntılı rapor crash_report.txt dosyasına kaydedildi. Lütfen hata bildiriminize bu dosyayı ekleyin."
|
||||
STR_CRASH_NO_REASON: "(Neden kaydedilmedi)"
|
||||
STR_CRASH_REASON: "Çökme nedeni:"
|
||||
STR_CRASH_TITLE: "Sistem Çökmesi"
|
||||
STR_CURRENT_TIME: "Şu anki saat:"
|
||||
STR_DELETE_SERVER: "Sunucuyu Sil"
|
||||
STR_DISABLED: "Devre dışı"
|
||||
STR_DOWNLOAD_ALL: "Tümünü İndir"
|
||||
STR_EOB_CONTINUE_WITH: "Şununla devam et"
|
||||
STR_EOB_HOME: "Ana Ekran"
|
||||
STR_FINDING_SAVED_WIFI: "Kayıtlı Wi-Fi aranıyor..."
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Dosya açılamıyor"
|
||||
STR_FIRMWARE_TOO_LARGE: "Firmware, bölümlemeye sığmıyor"
|
||||
STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
|
||||
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
|
||||
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
|
||||
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
|
||||
STR_FORCE_REFRESH: "Ekranı Yenile"
|
||||
STR_INDEX_FAILED: "Endeksleme başarısız - geçersiz kitap"
|
||||
STR_INSTALLED: "Yüklü"
|
||||
STR_INVALID_FIRMWARE: "Geçersiz firmware dosyası"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Tüm metni silmek için DEL tuşunu basılı tutun"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Girdiyi düzenlemek için UP tuşunu basılı tutun"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "URL modundan çıkmak için ABC'ye basın"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Şifreyi gizlemek için RIGHT'ı basılı tutup [***] tuşuna basın"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT'i basılı tutun"
|
||||
STR_KB_HINT_MOVE_CURSOR: "İmleci taşımak için LEFT veya RIGHT'a basın"
|
||||
STR_KB_HINT_RETURN_CURSOR: "İmleç konumuna dönmek için LEFT'e basın"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Klavyeye dönmek için DOWN'a basın"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT'i basılı tutun"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Şifreyi göstermek için RIGHT'ı basılı tutup [abc] tuşuna basın"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Şifreyi gizlemek için [***] tuşuna basın"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Şifreyi göstermek için [abc] tuşuna basın"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK harf veya ikincil karakter için SELECT'i basılı tutun"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Hazır kalıplar için URL'ye basın"
|
||||
STR_KB_TIPS: "İpuçları:"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_LOADING_FONT_LIST: "Yazı tipi listesi yükleniyor..."
|
||||
STR_LONG_PRESS_MENU: "Uzun Basma Menüsü"
|
||||
STR_MANAGE_FONTS: "Yazı Tiplerini Yönet"
|
||||
STR_NEXT_FIELD: "Sonraki"
|
||||
STR_NEXT_PAGE: "Sonraki Sayfa »"
|
||||
STR_NO_BIN_FILES: ".bin dosyası bulunamadı"
|
||||
STR_NO_FONTS_AVAILABLE: "Kullanılabilir yazı tipi yok"
|
||||
STR_NO_SERVERS: "Yapılandırılmış OPDS sunucusu yok"
|
||||
STR_OPDS_SERVERS: "OPDS Sunucuları"
|
||||
STR_PREV_PAGE: "« Önceki Sayfa"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Dipnottan hızlı dönüş"
|
||||
STR_RECOVERY_MODE: "Kurtarma Modu"
|
||||
STR_RECOVERY_MODE_HINT: "firmware.bin dosyasını SD kartın köküne koyup seçin"
|
||||
STR_RESTARTING_HINT: "Yeniden başlatılıyor... Cihaz yeniden başlamazsa güç düğmesini birkaç saniye basılı tutun."
|
||||
STR_SD_FIRMWARE_UPDATE: "SD Karttan Firmware Güncelleme"
|
||||
STR_SEARCH: "Ara"
|
||||
STR_SELECT_FIRMWARE_FILE: "Firmware dosyası seçin (.bin)"
|
||||
STR_SERVER_NAME: "Sunucu Adı"
|
||||
STR_SET_SLEEP_COVER: "Kapak Ayarla"
|
||||
STR_SHOW_NETWORKS: "Göster"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_TOP: "Üst"
|
||||
STR_UPDATE_ALL: "Tümünü Güncelle"
|
||||
STR_UPDATE_AVAILABLE: "Güncelle"
|
||||
STR_VALIDATING_FIRMWARE: "Firmware doğrulanıyor..."
|
||||
STR_XTC_STATUS_BAR: "XTC Durum Çubuğu"
|
||||
|
||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Приховати"
|
||||
STR_SHORT_PWR_BTN: "Короткий натиск кн. живл."
|
||||
STR_ORIENTATION: "Орієнтація читання"
|
||||
STR_SIDE_BTN_LAYOUT: "Схема бічних кнопок"
|
||||
STR_TOUCH_READER_CONTROLS: "Сенсорне керування читанням"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Орієнтувати передні кнопки"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Поведінка при довгому настику"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Немає"
|
||||
@@ -98,6 +99,7 @@ STR_USERNAME: "Ім'я користувача"
|
||||
STR_PASSWORD: "Пароль"
|
||||
STR_SYNC_SERVER_URL: "URL для синхронізації"
|
||||
STR_DOCUMENT_MATCHING: "Порівняння документів"
|
||||
STR_SEND_METADATA: "Надсилати метадані"
|
||||
STR_AUTHENTICATE: "Автентифікувати"
|
||||
STR_KOREADER_USERNAME: "Ім'я користувача KOReader"
|
||||
STR_KOREADER_PASSWORD: "Пароль KOReader"
|
||||
@@ -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,12 +330,18 @@ 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"
|
||||
STR_DELETE_SERVER: "Видалити сервер"
|
||||
STR_OPDS_SERVERS: "Сервери OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Папка завантажень"
|
||||
STR_OPDS_FILENAME_FORMAT: "Формат імені файлу"
|
||||
STR_FMT_AUTHOR_TITLE: "Автор - Назва"
|
||||
STR_FMT_TITLE_AUTHOR: "Назва - Автор"
|
||||
STR_FMT_TITLE: "Назва"
|
||||
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
|
||||
STR_MANAGE_FONTS: "Керування шрифтами"
|
||||
@@ -380,3 +387,5 @@ STR_FIRMWARE_WRITE_FAILED: "Помилка запису прошивки"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не вимикайте пристрій!"
|
||||
STR_RECOVERY_MODE: "Режим відновлення"
|
||||
STR_RECOVERY_MODE_HINT: "Помістіть firmware.bin у корінь SD-карти та виберіть його"
|
||||
STR_ADD_HIDDEN_NETWORK: "Додати приховану мережу..."
|
||||
STR_ENTER_WIFI_SSID: "Введіть назву мережі (SSID)"
|
||||
|
||||
@@ -7,7 +7,7 @@ STR_BOOTING: "ARRENCANT"
|
||||
STR_SLEEPING: "ENTRANT EN REPÒS"
|
||||
STR_ENTERING_SLEEP: "Entrant en repòs"
|
||||
STR_BROWSE_FILES: "Explora arxius"
|
||||
STR_FILE_TRANSFER: "Transferència"
|
||||
STR_FILE_TRANSFER: "Transferència d'arxius"
|
||||
STR_SETTINGS_TITLE: "Configuració"
|
||||
STR_CONTINUE_READING: "Continua llegint"
|
||||
STR_NO_OPEN_BOOK: "Cap llibre obert"
|
||||
@@ -18,6 +18,7 @@ STR_NO_CHAPTERS: "Sense capítols"
|
||||
STR_END_OF_BOOK: "Final del llibre"
|
||||
STR_EMPTY_CHAPTER: "Capítol buit"
|
||||
STR_INDEXING: "S'està indexant"
|
||||
STR_INDEX_FAILED: "No s'ha pogut indexar: llibre no vàlid"
|
||||
STR_MEMORY_ERROR: "Error de memòria"
|
||||
STR_PAGE_LOAD_ERROR: "Error en carregar la pàgina"
|
||||
STR_EMPTY_FILE: "Arxiu buit"
|
||||
@@ -28,60 +29,66 @@ STR_WIFI_NETWORKS: "Xarxes Wi-Fi"
|
||||
STR_NO_NETWORKS: "No s'han trobat xarxes"
|
||||
STR_NETWORKS_FOUND: "%zu xarxes trobades"
|
||||
STR_SCANNING: "S'està escanejant..."
|
||||
STR_FINDING_SAVED_WIFI: "S'estan buscant xarxes Wi-Fi guardades..."
|
||||
STR_CONNECTING: "S'està connectant..."
|
||||
STR_CONNECTING_SAVED_WIFI: "S'està connectant a una xarxa Wi-Fi guardada..."
|
||||
STR_SHOW_NETWORKS: "Mostra"
|
||||
STR_CONNECTED: "S'ha connectat!"
|
||||
STR_CONNECTION_FAILED: "Error de connexió"
|
||||
STR_FORGET_NETWORK: "Voleu oblidar esta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Voleu guardar la contrasenya per a la pròxima vegada?"
|
||||
STR_PRESS_OK_SCAN: "Premeu OK per tornar a escanejar"
|
||||
STR_JOIN_NETWORK: "Uneix-te a una xarxa"
|
||||
STR_FORGET_NETWORK: "Vols oblidar esta xarxa?"
|
||||
STR_SAVE_PASSWORD: "Vols guardar la contrasenya per a la pròxima vegada?"
|
||||
STR_PRESS_OK_SCAN: "Prem OK per tornar a escanejar"
|
||||
STR_JOIN_NETWORK: "Unix-te a una xarxa"
|
||||
STR_CREATE_HOTSPOT: "Crea un punt d'accés"
|
||||
STR_JOIN_DESC: "Connecta't a una xarxa Wi-Fi existent"
|
||||
STR_HOTSPOT_DESC: "Crea una xarxa Wi-Fi per unir-s'hi"
|
||||
STR_STARTING_HOTSPOT: "S'està iniciant el punt d'accés..."
|
||||
STR_HOTSPOT_MODE: "Mode de punt d'accés"
|
||||
STR_CONNECT_WIFI_HINT: "Connecteu el dispositiu a esta xarxa Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Obriu este URL al navegador"
|
||||
STR_CONNECT_WIFI_HINT: "Connecta el dispositiu a esta xarxa Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Obri este URL al navegador"
|
||||
STR_OR_HTTP_PREFIX: "o http://"
|
||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
||||
STR_SCAN_QR_HINT: "o escaneja el codi QR amb el telèfon:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
||||
STR_NETWORK_LEGEND: "* = Encriptat | + = Guardat"
|
||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||
STR_CHECKING_WIFI: "S'està comprovant el Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Introduïu la contrasenya Wi-Fi"
|
||||
STR_ENTER_WIFI_PASSWORD: "Introduïx la contrasenya Wi-Fi"
|
||||
STR_TO_PREFIX: "a "
|
||||
STR_CALIBRE_RECEIVING: "S'està rebent: "
|
||||
STR_CALIBRE_RECEIVED: "S'ha rebut: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·leu el connector CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigueu a la mateixa xarxa Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Instal·la el connector CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Estigues a la mateixa xarxa Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) A Calibre: \"Envia a un dispositiu\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantingueu esta pantalla oberta mentre s'envia\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Mantín esta pantalla oberta mentre s'envia\""
|
||||
STR_CAT_DISPLAY: "Visualització"
|
||||
STR_CAT_READER: "Lector"
|
||||
STR_CAT_CONTROLS: "Controls"
|
||||
STR_CAT_SYSTEM: "Sistema"
|
||||
STR_SLEEP_SCREEN: "Pantalla de repòs"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida després del temps"
|
||||
STR_SLEEP_COVER_MODE: "Mode de pantalla de repòs"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||
STR_SLEEP_COVER_MODE: "Ajust de la portada en 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_IMAGES: "Imatges"
|
||||
STR_IMAGES_DISPLAY: "Mostrar"
|
||||
STR_IMAGES_PLACEHOLDER: "Text de mostra"
|
||||
STR_IMAGES_PLACEHOLDER: "Text alternatiu"
|
||||
STR_IMAGES_SUPPRESS: "Eliminar"
|
||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
||||
STR_EOB_HOME: "Inici"
|
||||
STR_EOB_CONTINUE_WITH: "Continua amb"
|
||||
STR_SHORT_PWR_BTN: "Pulsació curta del botó d'engegada"
|
||||
STR_ORIENTATION: "Orientació de lectura"
|
||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||
STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Acció en mantindre premut un botó"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Saltar capítols"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Canvi d'orientació"
|
||||
STR_LONG_PRESS_MENU: "Funció de pulsació llarga"
|
||||
STR_FONT_PREVIEW_TEXT: "Jove xef, porti whisky amb quinze glaçons d'hidrogen, coi!"
|
||||
STR_FONT_FAMILY: "Família de fonts"
|
||||
STR_FONT_SIZE: "Grandària de la lletra (UI)"
|
||||
STR_FONT_SIZE: "Cos de lletra del lector"
|
||||
STR_LINE_SPACING: "Interlineat del lector"
|
||||
STR_SCREEN_MARGIN: "Marge de pantalla del lector"
|
||||
STR_PARA_ALIGNMENT: "Alineació de paràgrafs del lector"
|
||||
@@ -89,25 +96,27 @@ 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"
|
||||
STR_REFRESH_FREQ: "Freqüència de refresc"
|
||||
STR_REFRESH_FREQ: "Freqüència d'actualització"
|
||||
STR_KOREADER_SYNC: "Sincronització del KOReader"
|
||||
STR_CHECK_UPDATES: "Comprova si hi ha actualitzacions"
|
||||
STR_LANGUAGE: "Idioma"
|
||||
STR_LANGUAGE: "Llengua"
|
||||
STR_CLEAR_READING_CACHE: "Esborra la memòria cau de lectura"
|
||||
STR_USERNAME: "Nom d'usuari"
|
||||
STR_PASSWORD: "Contrasenya"
|
||||
STR_SYNC_SERVER_URL: "URL del servidor de sincronització"
|
||||
STR_DOCUMENT_MATCHING: "Coincidència de documents"
|
||||
STR_SEND_METADATA: "Envia metadades del document"
|
||||
STR_AUTHENTICATE: "Autentica"
|
||||
STR_KOREADER_USERNAME: "Nom d'usuari del KOReader"
|
||||
STR_KOREADER_PASSWORD: "Contrasenya del KOReader"
|
||||
STR_FILENAME: "Nom d'arxiu"
|
||||
STR_BINARY: "Binari"
|
||||
STR_SET_CREDENTIALS_FIRST: "Estableix les credencials primer"
|
||||
STR_SET_CREDENTIALS_FIRST: "Establix les credencials primer"
|
||||
STR_WIFI_CONN_FAILED: "Connexió Wi-Fi fallida"
|
||||
STR_AUTHENTICATING: "S'està autenticant..."
|
||||
STR_AUTH_SUCCESS: "Autenticació correcta!"
|
||||
@@ -147,7 +156,7 @@ STR_PREV_NEXT: "Anterior/Següent"
|
||||
STR_NEXT_PREV: "Següent/Anterior"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Punt de llibre"
|
||||
STR_DISABLED: "Desactivats"
|
||||
STR_DISABLED: "Sense funció"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Petita"
|
||||
@@ -175,16 +184,16 @@ STR_UPDATING: "S'està actualitzant..."
|
||||
STR_NO_UPDATE: "No hi ha actualitzacions disponibles"
|
||||
STR_UPDATE_FAILED: "Ha fallat l'actualització"
|
||||
STR_UPDATE_COMPLETE: "Actualització completada"
|
||||
STR_POWER_ON_HINT: "Premeu i manteniu premut el botó d'encesa per tornar a engegar"
|
||||
STR_POWER_ON_HINT: "Prem i mantín premut el botó d'encesa per tornar a engegar"
|
||||
STR_NO_ENTRIES: "No s'ha trobat cap entrada"
|
||||
STR_DOWNLOADING: "S'està baixant..."
|
||||
STR_DOWNLOAD_FAILED: "Ha fallat la baixada"
|
||||
STR_ERROR_MSG: "Error:"
|
||||
STR_UNNAMED: "Sense nom"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Manteniu premut Obre per esborrar"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Mantín premut Obri per esborrar"
|
||||
STR_NO_SERVER_URL: "No s'ha configurat cap URL de servidor"
|
||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del feed"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del feed"
|
||||
STR_FETCH_FEED_FAILED: "Ha fallat l'obtenció del canal de continguts"
|
||||
STR_PARSE_FEED_FAILED: "Ha fallat l'anàlisi del canal de continguts"
|
||||
STR_NETWORK_PREFIX: "Xarxa: "
|
||||
STR_IP_ADDRESS_PREFIX: "Adreça IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: Fallada general"
|
||||
@@ -197,11 +206,11 @@ STR_HOME: "« Inici"
|
||||
STR_SELECT: "Selecciona"
|
||||
STR_SELECTED: "Seleccionat"
|
||||
STR_TOGGLE: "Canvia"
|
||||
STR_TOGGLE_BOOKMARK: "Canvia punt de llibre"
|
||||
STR_TOGGLE_BOOKMARK: "Afig o elimina el punt de llibre"
|
||||
STR_CONFIRM: "Confirma"
|
||||
STR_CANCEL: "Cancel·la"
|
||||
STR_CONNECT: "Connecta"
|
||||
STR_OPEN: "Obre"
|
||||
STR_OPEN: "Obri"
|
||||
STR_DOWNLOAD: "Descarrega"
|
||||
STR_RETRY: "Reintenta"
|
||||
STR_YES: "Sí"
|
||||
@@ -216,7 +225,7 @@ STR_DIR_RIGHT: "Dreta"
|
||||
STR_DIR_UP: "Amunt"
|
||||
STR_DIR_DOWN: "Avall"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Filtre de pantalla de repòs"
|
||||
STR_SLEEP_COVER_FILTER: "Filtre de la portada en repòs"
|
||||
STR_FILTER_CONTRAST: "Contrast"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Personalitza la barra d'estat"
|
||||
STR_CHAPTER_PAGE_COUNT: "Comptador de pàgines del capítol"
|
||||
@@ -246,20 +255,20 @@ STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||
STR_QUICK_RESUME: "Represa ràpida"
|
||||
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||
STR_REMOVE_FROM_RECENTS: "Voleu eliminar-lo de Llibres recents?"
|
||||
STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?"
|
||||
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
||||
STR_CALIBRE_DESC: "Utilitza les transferències sense fils de Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Voleu eliminar la contrasenya guardada?"
|
||||
STR_FORGET_AND_REMOVE: "Vols oblidar la xarxa i eliminar la contrasenya guardada?"
|
||||
STR_FORGET_BUTTON: "Oblida"
|
||||
STR_CALIBRE_STARTING: "S'està iniciant el Calibre..."
|
||||
STR_CALIBRE_SETUP: "Configura"
|
||||
STR_CALIBRE_STATUS: "Estat"
|
||||
STR_CLEAR_BUTTON: "Esborra"
|
||||
STR_DEFAULT_VALUE: "Per defecte"
|
||||
STR_REMAP_PROMPT: "Premeu un botó frontal per a cada rol"
|
||||
STR_REMAP_PROMPT: "Prem un botó frontal per a cada rol"
|
||||
STR_UNASSIGNED: "No assignat"
|
||||
STR_ALREADY_ASSIGNED: "Ja assignat"
|
||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restableix la disposició per defecte"
|
||||
STR_REMAP_RESET_HINT: "Botó lateral Amunt: Restablix la disposició per defecte"
|
||||
STR_REMAP_CANCEL_HINT: "Botó lateral Avall: Cancel·la la reassignació"
|
||||
STR_HW_BACK_LABEL: "Arrere (1r botó)"
|
||||
STR_HW_CONFIRM_LABEL: "Confirma (2n botó)"
|
||||
@@ -270,20 +279,19 @@ STR_GO_HOME_BUTTON: "Ves a l'inici"
|
||||
STR_SYNC_PROGRESS: "Sincronitza el progrés"
|
||||
STR_DELETE_CACHE: "Esborra la memòria cau del llibre"
|
||||
STR_DELETE: "Esborra"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Voleu esborrar este punt de llibre?"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Vols esborrar este punt de llibre?"
|
||||
STR_DISPLAY_QR: "Mostra la pàgina com a QR"
|
||||
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_CALIBRE_URL_HINT: "Per al Calibre, afig /opds a la URL"
|
||||
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"
|
||||
STR_CALC_HASH: "S'està calculant l'empremta electrònica del document..."
|
||||
STR_HASH_FAILED: "No s'ha pogut calcular l'empremta electrònica del document"
|
||||
STR_FETCH_PROGRESS: "S'està obtenint el progrés remot..."
|
||||
STR_UPLOAD_PROGRESS: "S'està pujant el progrés..."
|
||||
STR_NO_CREDENTIALS_MSG: "No s'han configurat credencials"
|
||||
STR_KOREADER_SETUP_HINT: "Configureu el compte de KOReader en Configuració"
|
||||
STR_KOREADER_SETUP_HINT: "Configura el compte de KOReader en Configuració"
|
||||
STR_PROGRESS_FOUND: "S'ha trobat progrés!"
|
||||
STR_REMOTE_LABEL: "Remot:"
|
||||
STR_LOCAL_LABEL: "Local:"
|
||||
@@ -293,7 +301,7 @@ STR_DEVICE_FROM_FORMAT: " De: %s"
|
||||
STR_APPLY_REMOTE: "Aplica el progrés remot"
|
||||
STR_UPLOAD_LOCAL: "Puja el progrés local"
|
||||
STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot"
|
||||
STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?"
|
||||
STR_UPLOAD_PROMPT: "Vols pujar la posició actual?"
|
||||
STR_UPLOAD_SUCCESS: "Progrés pujat!"
|
||||
STR_SYNC_FAILED_MSG: "Sincronització fallida"
|
||||
STR_SAVE_PROGRESS_FAILED: "No s'ha pogut guardar el progrés"
|
||||
@@ -308,11 +316,11 @@ STR_FOOTNOTES: "Notes al peu"
|
||||
STR_NO_FOOTNOTES: "No hi ha notes al peu en esta pàgina"
|
||||
STR_LINK: "[enllaç]"
|
||||
STR_SCREENSHOT_BUTTON: "Fes una captura de pantalla"
|
||||
STR_AUTO_TURN_ENABLED: "Passar automàtic activat: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Passar automàtic (pàgines per minut)"
|
||||
STR_AUTO_TURN_ENABLED: "Pas automàtic de pàgina activat"
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Pas automàtic de pàgina (pàg./min)"
|
||||
STR_TILT_PAGE_TURN: "Pas de pàgina per inclinació"
|
||||
STR_FORCE_REFRESH: "Refresca la pantalla"
|
||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, manteniu premut el botó d'encesa durant uns segons."
|
||||
STR_RESTARTING_HINT: "S'està reiniciant... Si el dispositiu no es reinicia, mantín premut el botó d'encesa durant uns segons."
|
||||
STR_NEXT_PAGE: "Pàgina següent »"
|
||||
STR_PREV_PAGE: "« Pàgina anterior"
|
||||
STR_XTC_STATUS_BAR: "Barra d'estat XTC"
|
||||
@@ -341,6 +349,11 @@ STR_SERVER_NAME: "Nom del servidor"
|
||||
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
|
||||
STR_DELETE_SERVER: "Elimina el servidor"
|
||||
STR_OPDS_SERVERS: "Servidors OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
|
||||
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
|
||||
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
|
||||
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
|
||||
STR_FMT_TITLE: "Títol"
|
||||
STR_MANAGE_FONTS: "Gestiona les fonts"
|
||||
STR_FONT_BROWSER: "Navegador de fonts"
|
||||
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
|
||||
@@ -351,35 +364,37 @@ STR_INSTALLED: "Instal·lat"
|
||||
STR_DOWNLOAD_ALL: "Descarrega-ho tot"
|
||||
STR_UPDATE_ALL: "Actualitza-ho tot"
|
||||
STR_UPDATE_AVAILABLE: "Actualitza"
|
||||
STR_CRASH_TITLE: "Bloqueig del sistema"
|
||||
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Incloeu este arxiu en l'informe d'errors."
|
||||
STR_CRASH_REASON: "Motiu del bloqueig:"
|
||||
STR_CRASH_TITLE: "Fallada del sistema"
|
||||
STR_CRASH_DESCRIPTION: "S'ha guardat un informe detallat a crash_report.txt. Inclou este arxiu en l'informe d'errors."
|
||||
STR_CRASH_REASON: "Motiu de la fallada"
|
||||
STR_CRASH_NO_REASON: "(No s'ha registrat cap motiu)"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Prem Esquerra o Dreta per moure el cursor"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Prem Esquerra per tornar a la posició del cursor"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Mantén premut Dreta i prem [***] per ocultar la contrasenya"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Mantén premut Dreta i prem [abc] per mostrar la contrasenya"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Mantín premut Dreta i prem [***] per ocultar la contrasenya"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Mantín premut Dreta i prem [abc] per mostrar la contrasenya"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Prem [***] per ocultar la contrasenya"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Prem [abc] per mostrar la contrasenya"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Mantén premut Amunt per editar l'entrada"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Mantín premut Amunt per editar l'entrada"
|
||||
STR_KB_TIPS: "Consells:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Prem Avall per tornar al teclat"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Prem ABC per eixir del mode URL"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Mantén premut DEL per esborrar tot el text"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Mantén premut SELECT per al caràcter secundari"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Mantén premut SELECT per MAJÚSCULES o caràcter secundari"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Mantén premut SELECT per minúscules o caràcter secundari"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Mantín premut DEL per esborrar tot el text"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Mantín premut SELECT: caràcter secundari"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Mantín premut SELECT: majúscules o caràcter secundari"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Mantín premut SELECT: minúscules o caràcter secundari"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Prem URL per inserir fragments"
|
||||
STR_SD_FIRMWARE_UPDATE: "Actualització de firmware des de la targeta SD"
|
||||
STR_SELECT_FIRMWARE_FILE: "Seleccioneu un arxiu de firmware (.bin)"
|
||||
STR_SELECT_FIRMWARE_FILE: "Selecciona un arxiu de firmware (.bin)"
|
||||
STR_NO_BIN_FILES: "No s'han trobat arxius .bin"
|
||||
STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Voleu actualitzar el firmware?"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagueu el dispositiu!"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "No apagues el dispositiu!"
|
||||
STR_RECOVERY_MODE: "Mode de recuperació"
|
||||
STR_RECOVERY_MODE_HINT: "Poseu firmware.bin a l'arrel de la targeta SD i seleccioneu-lo"
|
||||
STR_RECOVERY_MODE_HINT: "Posa firmware.bin a l'arrel de la targeta SD i selecciona'l"
|
||||
STR_ADD_HIDDEN_NETWORK: "Afig una xarxa oculta..."
|
||||
STR_ENTER_WIFI_SSID: "Introduïx el nom de la xarxa (SSID)"
|
||||
|
||||
@@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Ẩn đi"
|
||||
STR_SHORT_PWR_BTN: "Nhấn nhanh nút nguồn"
|
||||
STR_ORIENTATION: "Hướng đọc"
|
||||
STR_SIDE_BTN_LAYOUT: "Bố trí nút bên (trình đọc)"
|
||||
STR_TOUCH_READER_CONTROLS: "Điều khiển đọc bằng cảm ứng"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Xoay nút trước theo hướng"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Hành vi nhấn giữ nút"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "TẮT"
|
||||
@@ -98,6 +99,7 @@ STR_USERNAME: "Tên đăng nhập"
|
||||
STR_PASSWORD: "Mật khẩu"
|
||||
STR_SYNC_SERVER_URL: "URL máy chủ đồng bộ"
|
||||
STR_DOCUMENT_MATCHING: "Khớp tài liệu"
|
||||
STR_SEND_METADATA: "Gửi siêu dữ liệu tài liệu"
|
||||
STR_AUTHENTICATE: "Xác thực"
|
||||
STR_KOREADER_USERNAME: "Tên đăng nhập KOReader"
|
||||
STR_KOREADER_PASSWORD: "Mật khẩu KOReader"
|
||||
@@ -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,12 +329,18 @@ 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"
|
||||
STR_DELETE_SERVER: "Xóa máy chủ"
|
||||
STR_OPDS_SERVERS: "Máy chủ OPDS"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Thư mục tải xuống"
|
||||
STR_OPDS_FILENAME_FORMAT: "Định dạng tên tệp"
|
||||
STR_FMT_AUTHOR_TITLE: "Tác giả - Tựa đề"
|
||||
STR_FMT_TITLE_AUTHOR: "Tựa đề - Tác giả"
|
||||
STR_FMT_TITLE: "Tựa đề"
|
||||
STR_AUTO_TURN_ENABLED: "Tự lật trang: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)"
|
||||
STR_MANAGE_FONTS: "Quản lý phông chữ"
|
||||
@@ -379,3 +386,5 @@ STR_FIRMWARE_WRITE_FAILED: "Ghi firmware thất bại"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Không được tắt nguồn!"
|
||||
STR_RECOVERY_MODE: "Chế độ phục hồi"
|
||||
STR_RECOVERY_MODE_HINT: "Đặt firmware.bin ở thư mục gốc thẻ SD rồi chọn"
|
||||
STR_ADD_HIDDEN_NETWORK: "Thêm mạng ẩn..."
|
||||
STR_ENTER_WIFI_SSID: "Nhập tên mạng (SSID)"
|
||||
|
||||
@@ -13,6 +13,11 @@ enum class InflateStatus {
|
||||
|
||||
// Streaming deflate decompressor wrapping uzlib.
|
||||
//
|
||||
// NOTE: retained ONLY for FontDecompressor's tiny one-shot flash-resident group
|
||||
// decompressions, where uzlib's ~1KB state beats tinfl's ~11KB on the
|
||||
// OOM-sensitive render path. All throughput paths (zip entries, PNG IDAT) use
|
||||
// InflateStream (lib/miniz), which decodes several times faster.
|
||||
//
|
||||
// Two modes:
|
||||
// init(false) — one-shot: input is a contiguous buffer, call read() once.
|
||||
// init(true) — streaming: allocates a 32KB ring buffer for back-references
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <JPEGDEC.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -169,11 +171,22 @@ constexpr uint32_t FP_ONE = 1UL << 16;
|
||||
// Static file pointer for JPEGDEC open callback.
|
||||
// Safe in single-threaded embedded context; never accessed concurrently.
|
||||
static HalFile* s_jpegFile = nullptr;
|
||||
static uint8_t s_jpegIoSinceYield = 0;
|
||||
|
||||
static void yieldToIdle() { vTaskDelay(1); }
|
||||
|
||||
static void yieldDuringJpegIo() {
|
||||
if (++s_jpegIoSinceYield < 4) return;
|
||||
s_jpegIoSinceYield = 0;
|
||||
yieldToIdle();
|
||||
}
|
||||
|
||||
void* bmpJpegOpen(const char* /*filename*/, int32_t* size) {
|
||||
if (!s_jpegFile || !*s_jpegFile) return nullptr;
|
||||
s_jpegIoSinceYield = 0;
|
||||
s_jpegFile->seek(0);
|
||||
*size = static_cast<int32_t>(s_jpegFile->size());
|
||||
yieldDuringJpegIo();
|
||||
return s_jpegFile;
|
||||
}
|
||||
|
||||
@@ -187,6 +200,7 @@ int32_t bmpJpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
int32_t n = f->read(pBuf, len);
|
||||
if (n < 0) n = 0;
|
||||
pFile->iPos += n;
|
||||
yieldDuringJpegIo();
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -194,6 +208,7 @@ int32_t bmpJpegSeek(JPEGFILE* pFile, int32_t pos) {
|
||||
auto* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||
if (!f || !f->seek(pos)) return -1;
|
||||
pFile->iPos = pos;
|
||||
yieldDuringJpegIo();
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -236,9 +251,23 @@ struct BmpConvertCtx {
|
||||
std::unique_ptr<FloydSteinbergDitherer> fsDitherer;
|
||||
std::unique_ptr<Atkinson1BitDitherer> atkinson1BitDitherer;
|
||||
|
||||
uint8_t rowsSinceYield;
|
||||
uint8_t blocksSinceYield;
|
||||
bool error;
|
||||
};
|
||||
|
||||
static void yieldDuringDecode(BmpConvertCtx* ctx) {
|
||||
if (++ctx->rowsSinceYield < 8) return;
|
||||
ctx->rowsSinceYield = 0;
|
||||
yieldToIdle();
|
||||
}
|
||||
|
||||
static void yieldDuringDecodeBlock(BmpConvertCtx* ctx) {
|
||||
if (++ctx->blocksSinceYield < 16) return;
|
||||
ctx->blocksSinceYield = 0;
|
||||
yieldToIdle();
|
||||
}
|
||||
|
||||
// Write a fully-assembled output row (grayscale bytes, length outWidth) to BMP
|
||||
static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY) {
|
||||
memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
|
||||
@@ -274,6 +303,7 @@ static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY)
|
||||
}
|
||||
|
||||
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
|
||||
yieldDuringDecode(ctx);
|
||||
}
|
||||
|
||||
// Matches the progressive-JPEG smoothing used by JpegToFramebufferConverter, but stays
|
||||
@@ -396,6 +426,7 @@ static void flushScaledRow(BmpConvertCtx* ctx) {
|
||||
|
||||
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow);
|
||||
ctx->currentOutY++;
|
||||
yieldDuringDecode(ctx);
|
||||
}
|
||||
|
||||
// JPEGDEC draw callback — receives one MCU-width × MCU-height block at a time,
|
||||
@@ -405,6 +436,7 @@ static void flushScaledRow(BmpConvertCtx* ctx) {
|
||||
int bmpDrawCallback(JPEGDRAW* pDraw) {
|
||||
auto* ctx = reinterpret_cast<BmpConvertCtx*>(pDraw->pUser);
|
||||
if (!ctx || ctx->error) return 0;
|
||||
yieldDuringDecodeBlock(ctx);
|
||||
|
||||
const uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
|
||||
const int stride = pDraw->iWidth;
|
||||
@@ -599,6 +631,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& b
|
||||
ctx.smoothScaleY_fp = interpolationStep(ctx.srcHeight, outHeight);
|
||||
ctx.smoothNextOutY = 0;
|
||||
ctx.smoothPrevY = -1;
|
||||
ctx.rowsSinceYield = 0;
|
||||
ctx.blocksSinceYield = 0;
|
||||
ctx.error = false;
|
||||
|
||||
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale
|
||||
|
||||
@@ -278,14 +278,19 @@ 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++;
|
||||
} else if (name == "li") {
|
||||
paragraphCount++;
|
||||
}
|
||||
if (paragraphCount == targetParagraph) {
|
||||
xpath = buildParagraphXPath(spineIndex, path, 0, 0);
|
||||
stopped = true;
|
||||
XML_StopParser(parser, XML_FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
@@ -1,118 +1,81 @@
|
||||
#include "KOReaderCredentialStore.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <MD5Builder.h>
|
||||
#include <ObfuscationUtils.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "KOReaderJsonIO.h"
|
||||
|
||||
// Initialize the static instance
|
||||
KOReaderCredentialStore KOReaderCredentialStore::instance;
|
||||
|
||||
namespace {
|
||||
// File format version (for binary migration)
|
||||
constexpr uint8_t KOREADER_FILE_VERSION = 1;
|
||||
// Default sync server URL. crosspoint-sync speaks the full KOSync protocol, so
|
||||
// pointing at any other kosync server (e.g. https://sync.koreader.rocks:443)
|
||||
// still works via the custom server URL setting.
|
||||
constexpr char DEFAULT_SERVER_URL[] = "https://sync.crosspointreader.com";
|
||||
|
||||
// File paths
|
||||
constexpr char KOREADER_FILE_BIN[] = "/.crosspoint/koreader.bin";
|
||||
constexpr char KOREADER_FILE_JSON[] = "/.crosspoint/koreader.json";
|
||||
constexpr char KOREADER_FILE_BAK[] = "/.crosspoint/koreader.bin.bak";
|
||||
// Default before config version 2. Configs saved without a version stamp and an
|
||||
// empty serverUrl were implicitly syncing here — they get pinned on upgrade.
|
||||
constexpr char LEGACY_DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
|
||||
|
||||
// Default sync server URL
|
||||
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
|
||||
|
||||
// Legacy obfuscation key - "KOReader" in ASCII (only used for binary migration)
|
||||
constexpr uint8_t LEGACY_OBFUSCATION_KEY[] = {0x4B, 0x4F, 0x52, 0x65, 0x61, 0x64, 0x65, 0x72};
|
||||
constexpr size_t LEGACY_KEY_LENGTH = sizeof(LEGACY_OBFUSCATION_KEY);
|
||||
|
||||
void legacyDeobfuscate(std::string& data) {
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
data[i] ^= LEGACY_OBFUSCATION_KEY[i % LEGACY_KEY_LENGTH];
|
||||
}
|
||||
}
|
||||
// Bumped when a change to defaults would alter behavior for existing configs.
|
||||
constexpr uint8_t CONFIG_VERSION = 2;
|
||||
} // namespace
|
||||
|
||||
bool KOReaderCredentialStore::saveToFile() const {
|
||||
Storage.mkdir("/.crosspoint");
|
||||
return KOReaderJsonIO::save(*this, KOREADER_FILE_JSON);
|
||||
void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
|
||||
doc["cfgVersion"] = CONFIG_VERSION;
|
||||
doc["username"] = getUsername();
|
||||
doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
|
||||
doc["serverUrl"] = getServerUrl();
|
||||
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
|
||||
doc["sendMetadata"] = getSendMetadata();
|
||||
doc["syncBehavior"] = static_cast<uint8_t>(getSyncBehavior());
|
||||
}
|
||||
|
||||
bool KOReaderCredentialStore::loadFromFile() {
|
||||
// Try JSON first
|
||||
if (Storage.exists(KOREADER_FILE_JSON)) {
|
||||
String json = Storage.readFile(KOREADER_FILE_JSON);
|
||||
if (!json.isEmpty()) {
|
||||
bool resave = false;
|
||||
bool result = KOReaderJsonIO::load(*this, json.c_str(), &resave);
|
||||
if (result && resave) {
|
||||
saveToFile();
|
||||
bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
|
||||
std::string user = doc["username"] | "";
|
||||
|
||||
bool needsResave = false;
|
||||
std::string pass = extractPassword(doc, needsResave);
|
||||
|
||||
setCredentials(user, pass);
|
||||
setServerUrl(doc["serverUrl"] | "");
|
||||
|
||||
// The default server changed in config v2 (sync.koreader.rocks -> crosspoint-sync).
|
||||
// A pre-v2 config with credentials and no explicit URL was actively syncing
|
||||
// against the old default — pin that URL so the upgrade doesn't switch servers
|
||||
// out from under the user. Fresh setups get the new default.
|
||||
const uint8_t cfgVersion = doc["cfgVersion"] | (uint8_t)1;
|
||||
if (cfgVersion < CONFIG_VERSION) {
|
||||
if (getServerUrl().empty() && hasCredentials()) {
|
||||
LOG_DBG("KRS", "Pre-v2 config used the old default server; pinning %s", LEGACY_DEFAULT_SERVER_URL);
|
||||
setServerUrl(LEGACY_DEFAULT_SERVER_URL);
|
||||
}
|
||||
needsResave = true; // stamp cfgVersion so this migration runs once
|
||||
}
|
||||
|
||||
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
||||
if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) {
|
||||
setMatchMethod(static_cast<DocumentMatchMethod>(method));
|
||||
} else {
|
||||
LOG_DBG("KRS", "Invalid matchMethod %u in JSON, resetting to FILENAME", method);
|
||||
setMatchMethod(DocumentMatchMethod::FILENAME);
|
||||
}
|
||||
setSendMetadata(doc["sendMetadata"] | false);
|
||||
|
||||
const JsonVariantConst behaviorValue = doc["syncBehavior"];
|
||||
const bool missingBehavior = behaviorValue.isNull();
|
||||
uint8_t behavior = behaviorValue | static_cast<uint8_t>(KOReaderSyncBehavior::ASK_EVERY_TIME);
|
||||
if (behavior <= static_cast<uint8_t>(KOReaderSyncBehavior::SMART)) {
|
||||
setSyncBehavior(static_cast<KOReaderSyncBehavior>(behavior));
|
||||
needsResave = needsResave || missingBehavior;
|
||||
} else {
|
||||
LOG_DBG("KRS", "Invalid syncBehavior %u in JSON, resetting to ASK_EVERY_TIME", behavior);
|
||||
setSyncBehavior(KOReaderSyncBehavior::ASK_EVERY_TIME);
|
||||
needsResave = true;
|
||||
}
|
||||
|
||||
if (needsResave) {
|
||||
LOG_DBG("KRS", "Resaved KOReader credentials to update format");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
// Fall back to binary migration
|
||||
if (Storage.exists(KOREADER_FILE_BIN)) {
|
||||
if (loadFromBinaryFile()) {
|
||||
if (saveToFile()) {
|
||||
Storage.rename(KOREADER_FILE_BIN, KOREADER_FILE_BAK);
|
||||
LOG_DBG("KRS", "Migrated koreader.bin to koreader.json");
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERR("KRS", "Failed to save KOReader credentials during migration");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DBG("KRS", "No credentials file found");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool KOReaderCredentialStore::loadFromBinaryFile() {
|
||||
HalFile file;
|
||||
if (!Storage.openFileForRead("KRS", KOREADER_FILE_BIN, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(file, version);
|
||||
if (version != KOREADER_FILE_VERSION) {
|
||||
LOG_DBG("KRS", "Unknown file version: %u", version);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.available()) {
|
||||
serialization::readString(file, username);
|
||||
} else {
|
||||
username.clear();
|
||||
}
|
||||
|
||||
if (file.available()) {
|
||||
serialization::readString(file, password);
|
||||
legacyDeobfuscate(password);
|
||||
} else {
|
||||
password.clear();
|
||||
}
|
||||
|
||||
if (file.available()) {
|
||||
serialization::readString(file, serverUrl);
|
||||
} else {
|
||||
serverUrl.clear();
|
||||
}
|
||||
|
||||
if (file.available()) {
|
||||
uint8_t method;
|
||||
serialization::readPod(file, method);
|
||||
matchMethod = static_cast<DocumentMatchMethod>(method);
|
||||
} else {
|
||||
matchMethod = DocumentMatchMethod::FILENAME;
|
||||
}
|
||||
|
||||
LOG_DBG("KRS", "Loaded KOReader credentials from binary for user: %s", username.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -173,3 +136,16 @@ void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
||||
matchMethod = method;
|
||||
LOG_DBG("KRS", "Set match method: %s", method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||
}
|
||||
|
||||
void KOReaderCredentialStore::setSendMetadata(bool enabled) {
|
||||
sendMetadata = enabled;
|
||||
LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false");
|
||||
}
|
||||
|
||||
void KOReaderCredentialStore::setSyncBehavior(KOReaderSyncBehavior behavior) {
|
||||
if (static_cast<uint8_t>(behavior) > static_cast<uint8_t>(KOReaderSyncBehavior::SMART)) {
|
||||
behavior = KOReaderSyncBehavior::ASK_EVERY_TIME;
|
||||
}
|
||||
syncBehavior = behavior;
|
||||
LOG_DBG("KRS", "Set sync behavior: %s", behavior == KOReaderSyncBehavior::SMART ? "Smart" : "Ask");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#pragma once
|
||||
#include <ArduinoJson.h>
|
||||
#include <PersistableStore.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
@@ -8,36 +11,38 @@ enum class DocumentMatchMethod : uint8_t {
|
||||
BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical)
|
||||
};
|
||||
|
||||
// How manual "Sync Progress" resolves differences after fetching remote progress.
|
||||
enum class KOReaderSyncBehavior : uint8_t {
|
||||
ASK_EVERY_TIME = 0, // Preserve legacy behavior: always show Apply/Upload choices.
|
||||
SMART = 1, // Auto-resolve simple cases using furthest progress.
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton class for storing KOReader sync credentials on the SD card.
|
||||
* Passwords are XOR-obfuscated with the device's unique hardware MAC address
|
||||
* and base64-encoded before writing to JSON (not cryptographically secure,
|
||||
* but prevents casual reading and ties credentials to the specific device).
|
||||
*/
|
||||
class KOReaderCredentialStore {
|
||||
|
||||
class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore> {
|
||||
private:
|
||||
static KOReaderCredentialStore instance;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string serverUrl; // Custom sync server URL (empty = default)
|
||||
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
|
||||
bool sendMetadata = false; // Send document metadata with progress sync
|
||||
KOReaderSyncBehavior syncBehavior = KOReaderSyncBehavior::SMART;
|
||||
|
||||
// Private constructor for singleton
|
||||
KOReaderCredentialStore() = default;
|
||||
~KOReaderCredentialStore() = default;
|
||||
|
||||
bool loadFromBinaryFile();
|
||||
friend class PersistableStore<KOReaderCredentialStore>;
|
||||
|
||||
public:
|
||||
// Delete copy constructor and assignment
|
||||
KOReaderCredentialStore(const KOReaderCredentialStore&) = delete;
|
||||
KOReaderCredentialStore& operator=(const KOReaderCredentialStore&) = delete;
|
||||
|
||||
// Get singleton instance
|
||||
static KOReaderCredentialStore& getInstance() { return instance; }
|
||||
|
||||
// Save/load from SD card
|
||||
bool saveToFile() const;
|
||||
bool loadFromFile();
|
||||
static const char* getFilePath() { return "/.crosspoint/koreader.json"; }
|
||||
void toJson(JsonDocument& doc) const;
|
||||
bool fromJson(JsonVariantConst doc);
|
||||
|
||||
// Credential management
|
||||
void setCredentials(const std::string& user, const std::string& pass);
|
||||
@@ -63,6 +68,14 @@ class KOReaderCredentialStore {
|
||||
// Document matching method
|
||||
void setMatchMethod(DocumentMatchMethod method);
|
||||
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
|
||||
|
||||
// Send metadata setting
|
||||
void setSendMetadata(bool enabled);
|
||||
bool getSendMetadata() const { return sendMetadata; }
|
||||
|
||||
// Sync behavior
|
||||
void setSyncBehavior(KOReaderSyncBehavior behavior);
|
||||
KOReaderSyncBehavior getSyncBehavior() const { return syncBehavior; }
|
||||
};
|
||||
|
||||
// Helper macro to access credential store
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#include "KOReaderJsonIO.h"
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <ObfuscationUtils.h>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
|
||||
namespace KOReaderJsonIO {
|
||||
|
||||
bool save(const KOReaderCredentialStore& store, const char* path) {
|
||||
JsonDocument doc;
|
||||
doc["username"] = store.getUsername();
|
||||
doc["password_obf"] = obfuscation::obfuscateToBase64(store.getPassword());
|
||||
doc["serverUrl"] = store.getServerUrl();
|
||||
doc["matchMethod"] = static_cast<uint8_t>(store.getMatchMethod());
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
return Storage.writeFile(path, json);
|
||||
}
|
||||
|
||||
bool load(KOReaderCredentialStore& store, const char* json, bool* needsResave) {
|
||||
if (needsResave) *needsResave = false;
|
||||
JsonDocument doc;
|
||||
auto error = deserializeJson(doc, json);
|
||||
if (error) {
|
||||
LOG_ERR("KRS", "JSON parse error: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string user = doc["username"] | std::string("");
|
||||
|
||||
bool ok = false;
|
||||
std::string pass = obfuscation::deobfuscateFromBase64(doc["password_obf"] | "", &ok);
|
||||
if (!ok || pass.empty()) {
|
||||
pass = doc["password"] | std::string("");
|
||||
if (!pass.empty() && needsResave) *needsResave = true;
|
||||
}
|
||||
|
||||
store.setCredentials(user, pass);
|
||||
store.setServerUrl(doc["serverUrl"] | std::string(""));
|
||||
|
||||
uint8_t method = doc["matchMethod"] | (uint8_t)0;
|
||||
store.setMatchMethod(static_cast<DocumentMatchMethod>(method));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace KOReaderJsonIO
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user