16 Commits

Author SHA1 Message Date
cottongin
a9f5149444 mod: remove duplicate I18n.h include in HomeActivity.cpp
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 13:26:17 -05:00
cottongin
0222cbf19b mod: convert remaining manual render locks to RAII RenderLock
Replace xSemaphoreTake/Give(renderingMutex) with scoped
RenderLock in EpubReaderActivity popup rendering (bookmark
added/removed, dictionary cache deleted).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 13:24:48 -05:00
cottongin
02f2474e3b mod: adapt mod activities to #774 render() pattern
Migrate 5 mod Activity subclasses from old polling-based
display task pattern to the upstream render() super-class
pattern with freeRTOS notification:

- EpubReaderBookmarkSelectionActivity
- DictionaryWordSelectActivity
- DictionarySuggestionsActivity
- DictionaryDefinitionActivity
- LookedUpWordsActivity

Changes: remove own TaskHandle/SemaphoreHandle/updateRequired,
use requestUpdate() + render(RenderLock&&) override, fix
potential deadlocks around enterNewActivity() calls.

Also fix stale conflict marker in EpubReaderMenuActivity.h.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 13:22:40 -05:00
pablohc
f06e3a0a82 fix: align battery icon based on context (UI / Reader) (#796)
Issues solved: #729 and #739

## Summary

* **What is the goal of this PR?**
Currently, the battery icon and charge percentage were aligned to the
left even for the UI, where they were positioned on the right side of
the screen. This meant that when changing values of different numbers of
digits, the battery would shift, creating a block of icons and text that
was illegible.

* **What changes are included?**
- Add drawBatteryUi() method for right-aligned battery display in UI
headers
- Keep drawBattery() for left-aligned display in reader mode
- Extract drawBatteryIcon() helper to reduce code duplication
- Battery icon now stays fixed at right edge regardless of percentage
digits
- Text adjusts to left of icon in UI mode, to right of icon in reader
mode

## Additional Context

* Add any other information that might be helpful for the reviewer 
* This fix applies to both themes (Base and Lyra).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
2026-02-16 13:14:44 -05:00
Andrew Brandt
a585f219f4 docs: add translators doc (#792)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Add a translators document for us to track which individuals have
volunteered to contribute in which languages.

* **What changes are included?**
Add a new document that includes who the translators are and what
languages they have volunteered for.

## Additional Context

This is primarily to keep a handle on the volunteers coming into the
repo. This will serve as a master list of all volunteer translators.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

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

---------

Signed-off-by: Andrew Brandt <brandt.andrew89@gmail.com>
2026-02-16 13:14:30 -05:00
Lev Roland-Kalb
df6cc637ec docs: Updating webserver.md documentation to align with 1.0.0 features (#906)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Updating webserver.md documentation to align with 1.0.0 features

* **What changes are included?**

Added documentation for the following new features (including replacing
screenshots)

- file renaming
- file moving
- support for uploading any file type
- batch uploads

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

Nothing comes to mind

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-16 13:13:05 -05:00
Lev Roland-Kalb
4cfe155488 fix: Removed white boxes extending passed the bounds of the empty button icon when hint text is blank/null (#884)
## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Empty Button Icons (I.E. Back button in the home menu) were still
rendering the full sized white rectangles going passed the boarders of
the little button nub. This was not visible on the home screen due to
the white background, but it does cause issues if we ever want to have
bmp files displayed while buttons are visible or implement a dark mode.

* **What changes are included?**

Made it so that when a button hint text is empty string or null the
displayed mini button nub does not have a white rectangle extending
passed the bounds of the mini button nub

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

Having that extended rectangle was likely never noticed due to the only
space where that feature is used being the main menu where the
background is completely white. I am working on some new features that
would have an image displayed while there are button hints and noticed
this issue while implementing that.

One other note is that this only affects the Lyra Theme

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_
2026-02-16 13:12:50 -05:00
Uri Tauber
f1966f1e26 feat: User-Interface I18n System (#728)
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.

**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.

- Data Structures:

- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
  - `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
  - `lib/I18n/translations.csv`: single source of truth.

- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.

- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`

This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.

The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.

The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.

To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.

- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.

Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!

**Status:** EDIT: ready to be merged.

As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.

---

Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
2026-02-16 13:12:29 -05:00
Xuan-Son Nguyen
ebcd3a8b94 fix: use RAII render lock everywhere (#916)
Follow-up to
https://github.com/crosspoint-reader/crosspoint-reader/pull/774

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **Refactor**
* Modernized internal synchronization mechanisms across multiple
components to improve code reliability and maintainability. All
functionality remains unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-16 13:03:50 -05:00
Xuan-Son Nguyen
ed8a0feac1 refactor: move render() to Activity super class, use freeRTOS notification (#774)
Currently, each activity has to manage their own `displayTaskLoop` which
adds redundant boilerplate code. The loop is a wait loop which is also
not the best practice, as the `updateRequested` boolean is not protected
by a mutex.

In this PR:
- Move `displayTaskLoop` to the super `Activity` class
- Replace `updateRequested` with freeRTOS's [direct to task
notification](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/03-Direct-to-task-notifications/01-Task-notifications)
- For `ActivityWithSubactivity`, whenever a sub-activity is present, the
parent's `render()` automatically goes inactive

With this change, activities now only need to expose `render()`
function, and anywhere in the code base can call `requestUpdate()` to
request a new rendering pass.

In theory, this change may also make the battery life a bit better,
since one wait loop is removed. Although the equipment in my home lab
wasn't been able to verify it (the electric current is too noisy and
small). Would appreciate if anyone has any insights on this subject.

Update: I managed to hack [a small piece of
code](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage)
that allow tracking CPU idle time.

The CPU load does decrease a bit (1.47% down to 1.39%), which make
sense, because the display task is now sleeping most of the time unless
notified. This should translate to a slightly increase in battery life
in the long run.

```
PR:
[40012] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[40012] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[50017] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[50017] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[60022] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[60022] [IDLE] Idle time: 98.61% (CPU load: 1.39%)

master:
[20012] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[20012] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[30017] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[30017] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[40022] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[40022] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
```

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **Refactor**
* Streamlined rendering architecture by consolidating update mechanisms
across all activities, improving efficiency and consistency.
* Modernized synchronization patterns for display updates to ensure
reliable, conflict-free rendering.

* **Bug Fixes**
* Enhanced rendering stability through improved locking mechanisms and
explicit update requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: znelson <znelson@users.noreply.github.com>
2026-02-16 13:01:42 -05:00
jpirnay
12cc7de49e fix: Add miniz directive to get rid of compilation warning (#858)
## Summary

* I am getting miniz warning during compilation: "Using fopen, ftello,
fseeko, stat() etc. path for file I/O - this path may not support large
files."
* Disable the io module from miniz as it is not used and get rid of the
warning

## Additional Context

* the ZipFile.cpp implementation only uses tinfl_decompressor,
tinfl_init(), and tinfl_decompress() (low-level API) and does all ZIP
file parsing manually using SD card file I/O
* it never uses miniz's high-level file functions like
mz_zip_reader_init_file()
* so we can disable Miniz io-stack be setting MINIZ_NO_STDIO to 1

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? partially, let claude
inspect the codebase
2026-02-16 12:43:25 -05:00
jpirnay
f622e87c10 fix: Correct multiple author display (#856)
## Summary

* If an EPUB has:
```
<dc:creator>J.R.R. Tolkien</dc:creator>
<dc:creator>Christopher Tolkien</dc:creator>
```
the current result for epub.author would provide : "J.R.R.
TolkienChristopher Tolkien" (no separator!)
* The fix will seperate multiple authors: "J.R.R. Tolkien, Christopher
Tolkien"

## Additional Context

* Simple fix in ContentOpfParser - I am not seeing any dependence on the
wrong concatenated result.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO
2026-02-16 12:43:13 -05:00
Dave Allie
24c1df0308 docs: Include dictionary as in-scope (#917)
## Summary

* Include dictionary as in-scope

## Additional Context

* Discussion in
https://github.com/crosspoint-reader/crosspoint-reader/discussions/878

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? No
2026-02-16 12:43:02 -05:00
ThatCrispyToast
6cc68e828a fix: add distro agnostic shebang and clang-format check to clang-format-fix (#840)
## Summary

**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Minor development tooling fix for nonstandard environments (NixOS,
FreeBSD, Guix, etc.)

**What changes are included?**

- environment relative shebang in `clang-format-fix`
- clang-format check in `clang-format-fix`
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-16 12:42:47 -05:00
jpirnay
6097ee03df fix: Auto calculate the settings size on serialization (#832)
* The constant SETTINGS_CONST was hardcoded and needed to be updated
whenever an additional setting was added
* This is no longer necessary as the settings size will be determined
automatically on settings persistence

* New settings need to be added (as previously) in saveToFile - that's
it

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

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

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-02-16 12:42:35 -05:00
Xuan-Son Nguyen
d11ad45e59 perf: apply (micro) optimization on SerializedHyphenationPatterns (#689)
This PR applies a micro optimization on `SerializedHyphenationPatterns`,
which allow reading `rootOffset` directly without having to parse then
cache it.

It should not affect storage space since no new bytes are added.

This also gets rid of the linear cache search whenever
`liangBreakIndexes` is called. In theory, the performance should be
improved a bit, although it may be too small to be noticeable in
practice.

master branch:

```
english: 99.1023%
french: 100%
german: 97.7289%
russian: 97.2167%
spanish: 99.0236%
```

This PR:

```
english: 99.1023%
french: 100%
german: 97.7289%
russian: 97.2167%
spanish: 99.0236%
```

---

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? PARTIALLY - mostly IDE
tab-autocompletions
2026-02-16 12:39:23 -05:00
111 changed files with 23717 additions and 17669 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
.DS_Store .DS_Store
.vscode .vscode
lib/EpdFont/fontsrc lib/EpdFont/fontsrc
lib/I18n/I18nStrings.cpp
*.generated.h *.generated.h
.vs .vs
build build

View File

@@ -25,6 +25,8 @@ usability over "swiss-army-knife" functionality.
* **Library Management:** E.g. Simple, intuitive ways to organize and navigate a collection of books. * **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. * **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. * **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.
### Out-of-Scope ### Out-of-Scope
@@ -34,8 +36,8 @@ usability over "swiss-army-knife" functionality.
* **Active Connectivity:** No RSS readers, News aggregators, or Web browsers. Background Wi-Fi tasks drain the battery * **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. and complicate the single-core CPU's execution.
* **Media Playback:** No Audio players or Audio-books. * **Media Playback:** No Audio players or Audio-books.
* **Complex Reader Features:** No highlighting, notes, or dictionary lookup. These features are better suited for * **Complex Annotation:** No typed out notes. These features are better suited for devices with better input
devices with better input capabilities and more powerful chips. capabilities and more powerful chips.
## 3. Idea Evaluation ## 3. Idea Evaluation

View File

@@ -1,10 +1,18 @@
#!/bin/bash #!/usr/bin/env bash
# Check if clang-format is availible
command -v clang-format >/dev/null 2>&1 || {
printf "'clang-format' not found in current environment\n"
printf "install 'clang', 'clang-tools', or 'clang-format' depending on your distro/os and tooling requirements\n"
exit 1
}
GIT_LS_FILES_FLAGS="" GIT_LS_FILES_FLAGS=""
if [[ "$1" == "-g" ]]; then if [[ "$1" == "-g" ]]; then
GIT_LS_FILES_FLAGS="--modified" GIT_LS_FILES_FLAGS="--modified"
fi fi
# --- Main Logic --- # --- Main Logic ---
# Format all files (or only modified files if -g is passed) # Format all files (or only modified files if -g is passed)

View File

@@ -45,22 +45,9 @@ byte arrays, and emits headers under
`SerializedHyphenationPatterns` descriptor so the reader can keep the automaton `SerializedHyphenationPatterns` descriptor so the reader can keep the automaton
in flash. in flash.
To refresh the firmware assets after updating the `.bin` files, run: A convenient script `update_hyphenation.sh` is used to update all languages.
To use it, run:
``` ```sh
./scripts/generate_hyphenation_trie.py \ ./scripts/update_hypenation.sh
--input lib/Epub/Epub/hyphenation/tries/en.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-en.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/fr.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-fr.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/de.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-de.trie.h
./scripts/generate_hyphenation_trie.py \
--input lib/Epub/Epub/hyphenation/tries/ru.bin \
--output lib/Epub/Epub/hyphenation/generated/hyph-ru.trie.h
``` ```

229
docs/i18n.md Normal file
View File

@@ -0,0 +1,229 @@
# Internationalization (I18N)
This guide explains the multi-language support system in CrossPoint Reader.
## Supported Languages (Updating)
---
## For Developers
### Translation System Architecture
The I18N system uses **per-language YAML files** to maintain translations and a Python script to generate C++ code:
```
lib/I18n/
├── translations/ # One YAML file per language
│ ├── english.yaml
│ ├── spanish.yaml
│ ├── french.yaml
│ └── ...
├── I18n.h
├── I18n.cpp
├── I18nKeys.h # Enums (auto-generated)
├── I18nStrings.h # String array declarations (auto-generated)
└── I18nStrings.cpp # String array definitions (auto-generated)
scripts/
└── gen_i18n.py # Code generator script
```
**Key principle:** All translations are managed in the YAML files under `lib/I18n/translations/`. The Python script generates the necessary C++ code automatically.
---
### YAML File Format
Each language has its own file in `lib/I18n/translations/` (e.g. `spanish.yaml`).
A file looks like this:
```yaml
_language_name: "Español"
_language_code: "SPANISH"
_order: "1"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BOOTING"
STR_BROWSE_FILES: "Buscar archivos"
```
**Metadata keys** (prefixed with `_`):
- `_language_name` — Native display name shown to the user (e.g. "Français")
- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier.
- `_order` — Controls the position in the Language enum (English is always 0)
**Rules:**
- Use UTF-8 encoding
- Every line must follow the format: `KEY: "value"`
- Keys must be valid C++ identifiers (uppercase, strats with STR_)
- Keys must be unique within a file
- String values must be quoted
- Use `\n` for newlines, `\\` for literal backslashes, `\"` for literal quotes inside values
---
### Adding New Strings
To add a new translatable string:
#### 1. Edit the English YAML file
Add the key to `lib/I18n/translations/english.yaml`:
```yaml
STR_MY_NEW_STRING: "My New String"
```
Then add translations in each language file. If a key is missing from a
language file, the generator will automatically use the English text as a
fallback (and print a warning).
#### 2. Run the generator script
```bash
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
```
This automatically:
- Fills missing translations from English
- Updates the `StrId` enum in `I18nKeys.h`
- Regenerates all language arrays in `I18nStrings.cpp`
#### 3. Use in code
```cpp
#include <I18n.h>
// Using the tr() macro (recommended)
renderer.drawText(font, x, y, tr(STR_MY_NEW_STRING));
// Using I18N.get() directly
const char* text = I18N.get(StrId::STR_MY_NEW_STRING);
```
**That's it!** No manual array synchronization needed.
---
### Adding a New Language
To add support for a new language (e.g., Italian):
#### 1. Create a new YAML file
Create `lib/I18n/translations/italian.yaml`:
```yaml
_language_name: "Italiano"
_language_code: "ITALIAN"
_order: "7"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "AVVIO"
```
You only need to include the strings you have translations for. Missing
keys will fall back to English automatically.
#### 2. Run the generator
```bash
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
```
This automatically updates all necessary code.
---
### Modifying Existing Translations
Simply edit the relevant YAML file and regenerate:
```bash
python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/
```
---
### UTF-8 Encoding
The YAML files use UTF-8 encoding. Special characters are automatically converted to C++ UTF-8 hex sequences by the generator.
---
### I18N API Reference
```cpp
// === Convenience Macros (Recommended) ===
// tr(id) - Get translated string without StrId:: prefix
const char* text = tr(STR_SETTINGS_TITLE);
renderer.drawText(font, x, y, tr(STR_BROWSE_FILES));
Serial.printf("Status: %s\n", tr(STR_CONNECTED));
// I18N - Shorthand for I18n::getInstance()
I18N.setLanguage(Language::SPANISH);
Language lang = I18N.getLanguage();
// === Full API ===
// Get the singleton instance
I18n& instance = I18n::getInstance();
// Get translated string (three equivalent ways)
const char* text = tr(STR_SETTINGS_TITLE); // Macro (recommended)
const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call
const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload
// Set language
I18N.setLanguage(Language::SPANISH);
// Get current language
Language lang = I18N.getLanguage();
// Save language setting to file
I18N.saveSettings();
// Load language setting from file
I18N.loadSettings();
// Get character set for font subsetting (static method)
const char* chars = I18n::getCharacterSet(Language::FRENCH);
```
---
## File Storage
Language settings are stored in:
```
/.crosspoint/language.bin
```
This file contains:
- Version byte
- Current language selection (1 byte)
---
## Translation Workflow
### For Developers (Adding Features)
1. Add new strings to `lib/I18n/translations/english.yaml`
2. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/`
3. Use the new `StrId` in your code
4. Request translations from translators
### For Translators
1. Open the YAML file for your language in `lib/I18n/translations/`
2. Add or update translations using the format `STR_KEY: "translated text"`
3. Keep translations concise (E-ink space constraints)
4. Make sure the file is in UTF-8 encoding
5. Run `python3 scripts/gen_i18n.py lib/I18n/translations lib/I18n/` to verify
6. Test on device or submit for review

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 54 KiB

27
docs/translators.md Normal file
View File

@@ -0,0 +1,27 @@
# Translators
Below is a list of users and languages CrossPoint may support in the future.
Note because a language is below does not mean there is official support for the language at this time.
## Contributing
If you'd like to add your name to this list, please open a PR adding yourself and your Github link. Thank you!
## French
- [Spigaw](https://github.com/Spigaw)
## German
- [DavidOrtmann](https://github.com/DavidOrtmann)
## Italian
- [fragolinux](https://github.com/fragolinux)
## Russian
- [madebyKir](https://github.com/madebyKir)
## Spanish
- [yeyeto2788](https://github.com/yeyeto2788)
- [Skrzakk](https://github.com/Skrzakk)
## Swedish
- [dawiik](https://github.com/dawiik)

View File

@@ -1,14 +1,14 @@
# Web Server Guide # Web Server Guide
This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload EPUB files from your computer or phone. This guide explains how to connect your CrossPoint Reader to WiFi and use the built-in web server to upload files from your computer or phone.
## Overview ## Overview
CrossPoint Reader includes a built-in web server that allows you to: CrossPoint Reader includes a built-in web server that allows you to:
- Upload EPUB files wirelessly from any device on the same WiFi network - Upload files wirelessly from any device on the same WiFi network
- Browse and manage files on your device's SD card - Browse and manage files on your device's SD card
- Create folders to organize your ebooks - Create folders to organize your library
- Delete files and folders - Delete files and folders
## Prerequisites ## Prerequisites
@@ -129,34 +129,31 @@ Click **File Manager** to access file management features.
#### Browsing Files #### Browsing Files
- The file manager displays all files and folders on your SD card - The file manager displays all files and folders on your SD card
- **Folders** are highlighted in yellow with a 📁 icon - **Folders** are highlighted in yellow and indicated with a 📁 icon
- **EPUB files** are highlighted in green with a 📗 icon - **EPUB Files** are highlighted in green and indicated with a 📗 icon
- **All Other Files** are not highlighted and indicated with a 📄 icon
- Click on a folder name to navigate into it - Click on a folder name to navigate into it
- Use the breadcrumb navigation at the top to go back to parent folders - Use the breadcrumb navigation at the top to go back to parent folders
<img src="./images/wifi/webserver_files.png" width="600"> <img src="./images/wifi/webserver_files.png" width="600">
#### Uploading EPUB Files #### Uploading Files
1. Click the **+ Add** button in the top-right corner 1. Click the **📤 Upload** button in the top-right corner
2. Select **Upload eBook** from the dropdown menu 2. Click **Choose File** and select a file from your device
3. Click **Choose File** and select an `.epub` file from your device 3. Click **Upload**
4. Click **Upload** 4. A progress bar will show the upload status
5. A progress bar will show the upload status 5. The page will automatically refresh when the upload is complete
6. The page will automatically refresh when the upload is complete
**Note:** Only `.epub` files are accepted. Other file types will be rejected.
<img src="./images/wifi/webserver_upload.png" width="600"> <img src="./images/wifi/webserver_upload.png" width="600">
#### Creating Folders #### Creating Folders
1. Click the **+ Add** button in the top-right corner 1. Click the **📁 New Folder** button in the top-right corner
2. Select **New Folder** from the dropdown menu 2. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
3. Enter a folder name (must not contain characters \" * : < > ? / \\ | and must not be . or ..) 3. Click **Create Folder**
4. Click **Create Folder**
This is useful for organizing your ebooks by genre, author, or series. This is useful for organizing your library by genre, author, series or file type.
#### Deleting Files and Folders #### Deleting Files and Folders
@@ -168,11 +165,25 @@ This is useful for organizing your ebooks by genre, author, or series.
**Note:** Folders must be empty before they can be deleted. **Note:** Folders must be empty before they can be deleted.
#### Moving Files
1. Click the **📂** (folder) icon next to any file
2. Enter a folder name or select one from the dropdown
3. Click **Move** to relocate the file
**Note:** Typing in a nonexistent folder name will result in the following error: "Failed to move: Destination not found"
#### Renaming Files
1. Click the **✏️** (pencil) icon next to any file
2. Enter a file name (must not contain characters \" * : < > ? / \\ | and must not be . or ..)
3. Click **Rename** to permanently rename the file
--- ---
## Command Line File Management ## Command Line File Management
For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md). For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. Detailed documentation can be found [here](./webserver-endpoints.md).
## Security Notes ## Security Notes
@@ -189,7 +200,6 @@ For power users, you can manage files directly from your terminal using `curl` w
- **Supported WiFi:** 2.4GHz networks (802.11 b/g/n) - **Supported WiFi:** 2.4GHz networks (802.11 b/g/n)
- **Web Server Port:** 80 (HTTP) - **Web Server Port:** 80 (HTTP)
- **Maximum Upload Size:** Limited by available SD card space - **Maximum Upload Size:** Limited by available SD card space
- **Supported File Format:** `.epub` only
- **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge) - **Browser Compatibility:** All modern browsers (Chrome, Firefox, Safari, Edge)
--- ---
@@ -198,7 +208,7 @@ For power users, you can manage files directly from your terminal using `curl` w
1. **Organize with folders** - Create folders before uploading to keep your library organized 1. **Organize with folders** - Create folders before uploading to keep your library organized
2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads 2. **Check signal strength** - Stronger signals (`|||` or `||||`) provide faster, more reliable uploads
3. **Upload multiple files** - You can upload files one at a time; the page refreshes after each upload 3. **Upload multiple files** - You can select and upload multiple files at once; the manager will queue them and refresh when the batch is finished
4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction") 4. **Use descriptive names** - Name your folders clearly (e.g., "SciFi", "Mystery", "Non-Fiction")
5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future 5. **Keep credentials saved** - Save your WiFi password for quick reconnection in the future
6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery 6. **Exit when done** - Press **Back** to exit the WiFi screen and save battery

View File

@@ -27,7 +27,7 @@ namespace {
#ifndef OMIT_HYPH_EN #ifndef OMIT_HYPH_EN
// English hyphenation patterns (3/3 minimum prefix/suffix length) // English hyphenation patterns (3/3 minimum prefix/suffix length)
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3); LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
#endif #endif
#ifndef OMIT_HYPH_FR #ifndef OMIT_HYPH_FR
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
@@ -36,7 +36,7 @@ LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
#endif #endif
#ifndef OMIT_HYPH_RU #ifndef OMIT_HYPH_RU
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic); LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
#endif #endif
#ifndef OMIT_HYPH_ES #ifndef OMIT_HYPH_ES
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);

View File

@@ -53,6 +53,8 @@
namespace { namespace {
using EmbeddedAutomaton = SerializedHyphenationPatterns;
struct AugmentedWord { struct AugmentedWord {
std::vector<uint8_t> bytes; std::vector<uint8_t> bytes;
std::vector<size_t> charByteOffsets; std::vector<size_t> charByteOffsets;
@@ -141,59 +143,10 @@ struct AutomatonState {
bool valid() const { return data != nullptr; } bool valid() const { return data != nullptr; }
}; };
// Lightweight descriptor for the entire embedded automaton.
// The blob format is:
// [0..3] - big-endian root offset
// [4....] - node heap containing variable-sized headers + transition data
struct EmbeddedAutomaton {
const uint8_t* data = nullptr;
size_t size = 0;
uint32_t rootOffset = 0;
bool valid() const { return data != nullptr && size >= 4 && rootOffset < size; }
};
// Decode the serialized automaton header and root offset.
EmbeddedAutomaton parseAutomaton(const SerializedHyphenationPatterns& patterns) {
EmbeddedAutomaton automaton;
if (!patterns.data || patterns.size < 4) {
return automaton;
}
automaton.data = patterns.data;
automaton.size = patterns.size;
automaton.rootOffset = (static_cast<uint32_t>(patterns.data[0]) << 24) |
(static_cast<uint32_t>(patterns.data[1]) << 16) |
(static_cast<uint32_t>(patterns.data[2]) << 8) | static_cast<uint32_t>(patterns.data[3]);
if (automaton.rootOffset >= automaton.size) {
automaton.data = nullptr;
automaton.size = 0;
}
return automaton;
}
// Cache parsed automata per blob pointer to avoid reparsing.
const EmbeddedAutomaton& getAutomaton(const SerializedHyphenationPatterns& patterns) {
struct CacheEntry {
const SerializedHyphenationPatterns* key;
EmbeddedAutomaton automaton;
};
static std::vector<CacheEntry> cache;
for (const auto& entry : cache) {
if (entry.key == &patterns) {
return entry.automaton;
}
}
cache.push_back({&patterns, parseAutomaton(patterns)});
return cache.back().automaton;
}
// Interpret the node located at `addr`, returning transition metadata. // Interpret the node located at `addr`, returning transition metadata.
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) { AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
AutomatonState state; AutomatonState state;
if (!automaton.valid() || addr >= automaton.size) { if (addr >= automaton.size) {
return state; return state;
} }
@@ -234,7 +187,7 @@ AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
if (offset + levelsLen > automaton.size) { if (offset + levelsLen > automaton.size) {
return AutomatonState{}; return AutomatonState{};
} }
levelsPtr = automaton.data + offset; levelsPtr = automaton.data + offset - 4u;
} }
if (pos + childCount > remaining) { if (pos + childCount > remaining) {
@@ -344,10 +297,7 @@ std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
return {}; return {};
} }
const EmbeddedAutomaton& automaton = getAutomaton(patterns); const EmbeddedAutomaton& automaton = patterns;
if (!automaton.valid()) {
return {};
}
const AutomatonState root = decodeState(automaton, automaton.rootOffset); const AutomatonState root = decodeState(automaton, automaton.rootOffset);
if (!root.valid()) { if (!root.valid()) {

View File

@@ -5,6 +5,7 @@
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash. // Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
struct SerializedHyphenationPatterns { struct SerializedHyphenationPatterns {
size_t rootOffset;
const std::uint8_t* data; const std::uint8_t* data;
size_t size; size_t size;
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,377 +7,447 @@
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually. // Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t fr_trie_data[] = { alignas(4) constexpr uint8_t fr_trie_data[] = {
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x2B,
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17,
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x04, 0x1F, 0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36,
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C,
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B,
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x48, 0x2C, 0x0B, 0x29, 0x16,
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x3E, 0x0D,
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B,
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0x16, 0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD,
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21,
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63,
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD,
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0,
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x21, 0x6E, 0xFD, 0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61,
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA,
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD,
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x5E, 0x21, 0x74, 0xFC,
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x74,
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70,
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0x73, 0x72, 0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF,
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB,
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0,
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0x6E, 0x75, 0xF2, 0xFD, 0x21,
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0xFF, 0x06,
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21,
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0, 0x74, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2,
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41, 0x02, 0x52, 0x6E, 0x75, 0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD,
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70,
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD,
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x72, 0xFD, 0x21,
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96,
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x96, 0x41, 0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52,
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05,
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75,
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0xC2, 0x02,
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74,
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF,
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0xF4, 0xFF, 0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7,
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD,
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21,
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xFD, 0xA0, 0x00, 0x61,
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0xFE, 0xC8,
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43,
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E,
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0x70, 0x73, 0x72, 0x67, 0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D,
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2,
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x47, 0xFE, 0x47,
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43,
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x63, 0x74, 0x75, 0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F,
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0,
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75,
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0x6C,
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD,
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69, 0xA0, 0x04, 0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21,
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF, 0xC3, 0xFD, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD,
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02,
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x31, 0x21,
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41,
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44, 0x61, 0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41,
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x6F, 0xFE, 0x7B, 0xA0, 0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21,
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63,
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3,
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x74,
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75, 0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E,
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x72, 0x73, 0xFF, 0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9,
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C,
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1,
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x23, 0x21, 0x6E, 0xFD,
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0xFF,
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF,
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xFD, 0x44, 0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9,
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64,
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C,
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x68, 0xFC, 0x92, 0x23, 0x61,
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0xFC, 0x5A,
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79,
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0x6F, 0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69,
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0x6F, 0x73, 0x74, 0x75, 0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0,
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE,
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63,
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x6E, 0xFB, 0x41,
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21,
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D, 0xC3, 0xFC, 0x21, 0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3,
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64,
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD,
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xFE, 0xCA, 0x21, 0x6F,
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0x44,
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5,
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD,
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF, 0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21,
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA,
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xFA, 0xA4, 0xFA, 0xA4, 0xFF,
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0xFD, 0x44,
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5,
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41,
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0xA9, 0xFC, 0x27, 0x21, 0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21,
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2,
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73,
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0x21, 0xA9,
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4,
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0xFC, 0xBD, 0x21, 0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93,
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9,
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD,
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xA0, 0x01, 0xC1, 0x21,
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x81,
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41,
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x73, 0xFE, 0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD,
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0x43, 0x6F, 0x73, 0x75, 0xFF, 0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD,
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0x61, 0xFE, 0xA9, 0x21, 0x69,
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0x21, 0x74,
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD,
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0x25, 0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB,
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42, 0xFD, 0x21, 0x61, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD,
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68,
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA,
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xFB,
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7,
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8, 0xFF, 0xFD, 0x41, 0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C,
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80,
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9,
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0xD7, 0xFF, 0xE4, 0xFD,
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xB9,
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21,
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6, 0xA9, 0xFD, 0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9,
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x42, 0x66, 0x78, 0xFB, 0x18, 0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1,
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41,
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x63, 0x68, 0x75, 0x6F, 0xFF,
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0xFD, 0xC3,
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43,
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x63, 0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F,
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41,
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0xF8, 0xFF, 0xF9,
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41,
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0x69, 0xF7, 0xD2, 0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73,
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF,
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70,
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x41, 0x75, 0xF8, 0x30,
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0xF8,
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41,
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x73, 0xF8, 0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41,
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F, 0x69, 0xF8, 0x73, 0x21, 0x75, 0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA,
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72,
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0xBF, 0xF6, 0xBF, 0x42, 0x63,
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x74,
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2,
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF,
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21, 0xF9, 0xF6, 0x99, 0xF6, 0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF,
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF,
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xC3, 0x62, 0x63, 0x64, 0x65, 0x69,
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0xB1, 0xF8, 0xE6,
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65, 0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85,
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21, 0xF8, 0x85, 0xA0, 0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21,
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73,
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21,
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F,
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x61,
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74,
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74,
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD,
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0xE1, 0xFD, 0x41, 0x74, 0xFE,
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xF6, 0xFD,
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD,
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41,
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0x2E, 0xFF, 0x33, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63,
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22,
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74,
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E,
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD,
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64,
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6,
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xC9, 0xFF, 0xD4,
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02,
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x21, 0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1,
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69, 0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2,
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74,
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x00, 0xE2, 0x65, 0x75, 0xFF,
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0x62, 0xFF,
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43,
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC, 0x65, 0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92,
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65,
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63,
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD,
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0xE7, 0xFF, 0xF6,
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9, 0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21,
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21,
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC,
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48,
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFF, 0xA8, 0xFF, 0xBF,
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x8D,
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75,
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFD, 0x41, 0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70,
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0xFF, 0xFD, 0x42, 0x2E, 0x73,
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x74, 0xFD,
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F,
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0xE2, 0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21,
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70,
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE,
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41,
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xE2, 0x2E, 0x62,
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D,
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0xFC, 0xEE, 0xA0, 0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5,
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72,
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD,
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0x72, 0xFC, 0x41, 0x69,
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42,
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4,
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0x21, 0x69, 0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF,
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9, 0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC,
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7,
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF,
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0x88, 0xA1,
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69,
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0, 0xFC, 0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE,
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0x8A, 0xFD, 0x27, 0xFD, 0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF,
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1,
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC,
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0xF2, 0x5A, 0xA1,
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22, 0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2,
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xF5, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0,
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65,
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1,
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFB, 0x41, 0xFF, 0xFB,
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0xFC,
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73,
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0xFB, 0x34, 0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69,
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08,
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54,
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xF3,
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0xFF, 0xFC,
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5,
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF,
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFC, 0xFB, 0x62, 0x42, 0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E,
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C,
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD,
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x68, 0xF8, 0xC0,
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A,
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61, 0xC3, 0x69, 0x63, 0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39,
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03,
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A,
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x76,
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0x73,
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D,
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2,
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x69, 0x75, 0xC3, 0x6F, 0x65, 0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41,
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9,
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFC, 0x3E, 0xFC, 0x3E, 0x41,
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0xFF, 0xFC,
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21,
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45, 0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C,
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0xEB, 0xFD, 0x42, 0xA9, 0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F,
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D,
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21,
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0x65, 0xF9, 0x7E,
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21,
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x75, 0xFC, 0xA0, 0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00,
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF,
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB,
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x07, 0x62, 0x21, 0xA9,
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0xFA,
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35,
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6,
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x41, 0x75, 0xF8, 0xC2, 0x22, 0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC,
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D,
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x74, 0x79, 0xFE, 0xAE, 0xFE,
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xC2, 0xFF,
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64,
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0xF1, 0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC,
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4, 0x41, 0x6C, 0xF1, 0x8F, 0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21,
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42,
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD,
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0xB5, 0xBC, 0xCE,
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF, 0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61,
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69, 0xFC, 0x22, 0x63, 0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD,
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1,
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70,
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x72, 0xF7, 0x21, 0x68,
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFA,
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD,
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21, 0x41, 0x61, 0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72,
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC,
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69,
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xFF, 0xA5,
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21,
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0x61, 0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA,
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0xFF, 0xDF, 0xFF, 0xEB, 0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61,
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63,
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6,
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0x41, 0x70, 0xED,
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x6D, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70,
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD,
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21,
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79,
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xFD,
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72,
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF6, 0xA6, 0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74,
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xF0, 0x03, 0xFF, 0xFC, 0x45, 0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD,
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61,
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0xFB, 0x9D, 0x21, 0x68, 0xFC,
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xFB, 0xEE,
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41,
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD, 0x6D, 0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1,
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2,
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21,
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7,
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xFF, 0xF3, 0xF7,
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1, 0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00,
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE1, 0x6D, 0x72, 0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65,
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3,
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5,
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0x41,
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x2E,
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20,
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0xFF, 0x4D, 0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC,
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75,
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0x32, 0xFF, 0xFD, 0xC2, 0x00,
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0x65, 0x69,
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD,
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xC4, 0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD,
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0xC4, 0xF4, 0xD1, 0x45, 0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4,
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD,
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2,
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0x61, 0xC3, 0x65,
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4,
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0x79, 0x41, 0x69, 0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5,
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5,
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21,
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0x6F, 0x73, 0xF5, 0x12,
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0x70,
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00,
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66, 0xE2, 0x75, 0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD,
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0, 0x41, 0x6D, 0xF4, 0x02, 0x21, 0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22,
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0,
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0xFF, 0xFC, 0xF3, 0xDA, 0x41,
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xFB, 0x17,
0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6,
0x66, 0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29,
0xC6, 0x00, 0xE2, 0x2E, 0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF,
0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21,
0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA9,
0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65,
0xF3, 0x44, 0x21, 0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9,
0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5,
0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F,
0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x41, 0x69, 0xF3, 0x97,
0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0x6E,
0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41,
0x6F, 0xF6, 0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62,
0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F,
0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21,
0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF7, 0x81, 0x21, 0x72, 0xFC,
0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF7, 0x6F,
0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21,
0x63, 0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC,
0xDC, 0xFB, 0x21, 0x74, 0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D,
0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D,
0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E,
0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x72, 0xFF, 0xFB,
0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1,
0x00, 0xE1, 0x6D, 0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB,
0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06,
0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD,
0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x22,
0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75,
0x79, 0xF1, 0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41,
0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC,
0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73,
0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x6E, 0x73, 0xE9, 0xBA, 0xE9,
0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x00, 0xE1,
0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41,
0x74, 0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF,
0xFD, 0xF2, 0x88, 0x42, 0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73,
0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD,
0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0x41, 0x6D,
0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB,
0xF2, 0x50, 0xF0, 0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42,
0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48,
0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6,
0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0x21, 0x74, 0xFC, 0x21,
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0x6D,
0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80,
0xF0, 0x80, 0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1,
0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0,
0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0,
0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0x41, 0x69, 0xFA,
0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0xFD, 0x22,
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69,
0x6F, 0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0,
0x0B, 0xF4, 0x0D, 0xFF, 0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC,
0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61,
0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F,
0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB,
0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0,
0xEF, 0x48, 0xF0, 0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F,
0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4,
0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65,
0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
}; };
constexpr SerializedHyphenationPatterns fr_patterns = { constexpr SerializedHyphenationPatterns fr_patterns = {
0x1AF0u,
fr_trie_data, fr_trie_data,
sizeof(fr_trie_data), sizeof(fr_trie_data),
}; };

View File

@@ -7,107 +7,107 @@
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually. // Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
alignas(4) constexpr uint8_t it_trie_data[] = { alignas(4) constexpr uint8_t it_trie_data[] = {
0x00, 0x00, 0x05, 0xC4, 0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x0D, 0x16, 0x0B, 0x34,
0x0D, 0x16, 0x0B, 0x34, 0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x01, 0x02, 0x16, 0x02,
0x01, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0B, 0xA0, 0x00, 0x42,
0x0B, 0xA0, 0x00, 0x42, 0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x6D, 0xFD, 0x21, 0x69,
0x6D, 0xFD, 0x21, 0x69, 0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0x21, 0x6F, 0xFD, 0x21,
0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x6D,
0x00, 0xC2, 0x21, 0x6D, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6F, 0xFD,
0xE1, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x61, 0x69, 0x6F, 0xDF,
0x61, 0x69, 0x6F, 0xDF, 0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63,
0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x75, 0xFD, 0x21,
0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x00,
0x6E, 0xFD, 0xA0, 0x00, 0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0xA0, 0x01, 0x52, 0x21,
0xA0, 0x01, 0x52, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x71, 0x21, 0x6F,
0x01, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x6F, 0xFD,
0x61, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x72,
0xFD, 0x22, 0x70, 0x72, 0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0x6C, 0x72, 0xFD, 0xFD,
0x6C, 0x72, 0xFD, 0xFD, 0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x11, 0x25, 0x61, 0x68,
0x11, 0x25, 0x61, 0x68, 0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x72, 0xFD, 0x21, 0x63,
0x72, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xA2, 0x21, 0x65, 0xFD,
0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x72, 0xFF, 0xFC, 0xFF,
0x72, 0xFF, 0xFC, 0xFF, 0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x72,
0x6C, 0x6E, 0x70, 0x72, 0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0x21,
0x5C, 0xFF, 0x5C, 0x21, 0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0xFD, 0x41, 0x6E, 0xFF,
0xFD, 0x41, 0x6E, 0xFF, 0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x61, 0x65,
0xFD, 0x22, 0x61, 0x65, 0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
0x70, 0x72, 0x73, 0x74, 0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x12, 0xFF, 0x20, 0xFF,
0x12, 0xFF, 0x20, 0xFF, 0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0xC2, 0xFF, 0xE6, 0xFF,
0xC2, 0xFF, 0xE6, 0xFF, 0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xA0, 0x00, 0xD1, 0x24,
0xA0, 0x00, 0xD1, 0x24, 0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0xF1, 0xA0, 0x01,
0x61, 0xF1, 0xA0, 0x01, 0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xFD, 0x21, 0x75, 0xDF,
0xFD, 0x21, 0x75, 0xDF, 0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0x02, 0x01, 0x62, 0x63,
0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0xE3, 0xE3, 0xE3, 0xE3,
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0x27, 0xC4, 0xC7, 0xC6,
0x27, 0xC4, 0xC7, 0xC6, 0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xFB, 0xFF, 0xBF,
0xFF, 0xFB, 0xFF, 0xBF, 0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x6E, 0x71, 0x73,
0x6D, 0x6E, 0x71, 0x73, 0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xEB, 0xFF,
0xAA, 0xFF, 0xEB, 0xFF, 0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0x67, 0x6C, 0x6D, 0x6E,
0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0x72, 0x73, 0x74, 0x2E,
0x72, 0x73, 0x74, 0x2E, 0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x4A, 0xFF,
0x5C, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0xFD, 0xD1, 0x02, 0x01,
0xFD, 0xD1, 0x02, 0x01, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E,
0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x33, 0xFF, 0x21, 0xFF,
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x68, 0x69, 0x6C, 0x6D,
0x68, 0x69, 0x6C, 0x6D, 0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0xFC, 0xFE, 0xF9, 0xFE,
0xFC, 0xFE, 0xF9, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0x02, 0x01, 0x2E, 0x27,
0x02, 0x01, 0x2E, 0x27, 0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0x6D, 0x72, 0x73, 0x74,
0x6D, 0x72, 0x73, 0x74, 0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0x2E, 0x27, 0xFE, 0x93,
0x2E, 0x27, 0xFE, 0x93, 0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0x21, 0x72, 0xF8, 0x21,
0x21, 0x72, 0xF8, 0x21, 0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x69, 0xFC, 0x21, 0x65,
0x69, 0xFC, 0x21, 0x65, 0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
0xFD, 0xFD, 0xFD, 0xFF, 0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
0xFD, 0xFD, 0xFD, 0xFF, 0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x82,
0xFD, 0xA0, 0x02, 0x82, 0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xFD, 0xCB, 0x02, 0x01,
0xFD, 0xCB, 0x02, 0x01, 0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0xB1, 0xFD, 0xC3, 0xFD,
0xB1, 0xFD, 0xC3, 0xFD, 0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD,
0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0x8D, 0xA0, 0x02, 0x53,
0x8D, 0xA0, 0x02, 0x53, 0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x68, 0x67, 0x6B, 0x6C,
0x68, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x27, 0xFD, 0x79, 0xFD,
0x27, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x37, 0xFD, 0x37, 0xFD,
0x37, 0xFD, 0x37, 0xFD, 0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x8F, 0x4B, 0x62, 0x63,
0x8F, 0x4B, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xA0,
0x21, 0xFD, 0x21, 0xA0, 0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x70, 0x74, 0x7A, 0x2E,
0x70, 0x74, 0x7A, 0x2E, 0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0xF8, 0xFF, 0xFB, 0xC1,
0xF8, 0xFF, 0xFB, 0xC1, 0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x63, 0xFC, 0xC1, 0x01,
0x63, 0xFC, 0xC1, 0x01, 0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0x06, 0xD2, 0x02, 0x01,
0x06, 0xD2, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A,
0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xE2, 0xFC, 0xD3,
0xFF, 0xE2, 0xFC, 0xD3, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1,
0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0x76, 0x2E, 0x27, 0xFC,
0x76, 0x2E, 0x27, 0xFC, 0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x72, 0xFB, 0xAF, 0xA0,
0x72, 0xFB, 0xAF, 0xA0, 0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0xFF, 0xF9, 0xFF, 0xFD,
0xFF, 0xF9, 0xFF, 0xFD, 0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0x70, 0x74, 0x77, 0x2E,
0x70, 0x74, 0x77, 0x2E, 0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0xCB, 0x02, 0x01, 0x62,
0xCB, 0x02, 0x01, 0x62, 0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFD, 0x9F,
0xFC, 0x32, 0xFD, 0x9F, 0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0xC2, 0xFB, 0xF9, 0xFC,
0xC2, 0xFB, 0xF9, 0xFC, 0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0xC4, 0xFC, 0xED, 0xFD,
0xC4, 0xFC, 0xED, 0xFD, 0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x5D, 0xFE, 0x81, 0xFE,
0x5D, 0xFE, 0x81, 0xFE, 0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xD5, 0xFF, 0xDC,
0xD5, 0xFF, 0xDC,
}; };
constexpr SerializedHyphenationPatterns it_patterns = { constexpr SerializedHyphenationPatterns it_patterns = {
0x5C0u,
it_trie_data, it_trie_data,
sizeof(it_trie_data), sizeof(it_trie_data),
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -327,6 +327,9 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s,
} }
if (self->state == IN_BOOK_AUTHOR) { if (self->state == IN_BOOK_AUTHOR) {
if (!self->author.empty()) {
self->author.append(", "); // Add separator for multiple authors
}
self->author.append(s, len); self->author.append(s, len);
return; return;
} }

96
lib/I18n/I18n.cpp Normal file
View File

@@ -0,0 +1,96 @@
#include "I18n.h"
#include <HalStorage.h>
#include <HardwareSerial.h>
#include <Serialization.h>
#include "I18nStrings.h"
using namespace i18n_strings;
// Settings file path
static constexpr const char* SETTINGS_FILE = "/.crosspoint/language.bin";
static constexpr uint8_t SETTINGS_VERSION = 1;
I18n& I18n::getInstance() {
static I18n instance;
return instance;
}
const char* I18n::get(StrId id) const {
const auto index = static_cast<size_t>(id);
if (index >= static_cast<size_t>(StrId::_COUNT)) {
return "???";
}
// Use generated helper function - no hardcoded switch needed!
const char* const* strings = getStringArray(_language);
return strings[index];
}
void I18n::setLanguage(Language lang) {
if (lang >= Language::_COUNT) {
return;
}
_language = lang;
saveSettings();
}
const char* I18n::getLanguageName(Language lang) const {
const auto index = static_cast<size_t>(lang);
if (index >= static_cast<size_t>(Language::_COUNT)) {
return "???";
}
return LANGUAGE_NAMES[index];
}
void I18n::saveSettings() {
Storage.mkdir("/.crosspoint");
FsFile file;
if (!Storage.openFileForWrite("I18N", SETTINGS_FILE, file)) {
Serial.printf("[I18N] Failed to save settings\n");
return;
}
serialization::writePod(file, SETTINGS_VERSION);
serialization::writePod(file, static_cast<uint8_t>(_language));
file.close();
Serial.printf("[I18N] Settings saved: language=%d\n", static_cast<int>(_language));
}
void I18n::loadSettings() {
FsFile file;
if (!Storage.openFileForRead("I18N", SETTINGS_FILE, file)) {
Serial.printf("[I18N] No settings file, using default (English)\n");
return;
}
uint8_t version;
serialization::readPod(file, version);
if (version != SETTINGS_VERSION) {
Serial.printf("[I18N] Settings version mismatch\n");
file.close();
return;
}
uint8_t lang;
serialization::readPod(file, lang);
if (lang < static_cast<size_t>(Language::_COUNT)) {
_language = static_cast<Language>(lang);
Serial.printf("[I18N] Loaded language: %d\n", static_cast<int>(_language));
}
file.close();
}
// Generate character set for a specific language
const char* I18n::getCharacterSet(Language lang) {
const auto langIndex = static_cast<size_t>(lang);
if (langIndex >= static_cast<size_t>(Language::_COUNT)) {
lang = Language::ENGLISH; // Fallback to first language
}
return CHARACTER_SETS[static_cast<size_t>(lang)];
}

42
lib/I18n/I18n.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
#include "I18nKeys.h"
/**
* Internationalization (i18n) system for CrossPoint Reader
*/
class I18n {
public:
static I18n& getInstance();
// Disable copy
I18n(const I18n&) = delete;
I18n& operator=(const I18n&) = delete;
// Get localized string by ID
const char* get(StrId id) const;
const char* operator[](StrId id) const { return get(id); }
Language getLanguage() const { return _language; }
void setLanguage(Language lang);
const char* getLanguageName(Language lang) const;
void saveSettings();
void loadSettings();
// Get all unique characters used in a specific language
// Returns a sorted string of unique characters
static const char* getCharacterSet(Language lang);
private:
I18n() : _language(Language::ENGLISH) {}
Language _language;
};
// Convenience macros
#define tr(id) I18n::getInstance().get(StrId::id)
#define I18N I18n::getInstance()

397
lib/I18n/I18nKeys.h Normal file
View File

@@ -0,0 +1,397 @@
#pragma once
#include <cstdint>
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
// Forward declaration for string arrays
namespace i18n_strings {
extern const char* const STRINGS_EN[];
extern const char* const STRINGS_ES[];
extern const char* const STRINGS_FR[];
extern const char* const STRINGS_DE[];
extern const char* const STRINGS_CZ[];
extern const char* const STRINGS_PO[];
extern const char* const STRINGS_RU[];
extern const char* const STRINGS_SV[];
} // namespace i18n_strings
// Language enum
enum class Language : uint8_t {
ENGLISH = 0,
SPANISH = 1,
FRENCH = 2,
GERMAN = 3,
CZECH = 4,
PORTUGUESE = 5,
RUSSIAN = 6,
SWEDISH = 7,
_COUNT
};
// Language display names (defined in I18nStrings.cpp)
extern const char* const LANGUAGE_NAMES[];
// Character sets for each language (defined in I18nStrings.cpp)
extern const char* const CHARACTER_SETS[];
// String IDs
enum class StrId : uint16_t {
STR_CROSSPOINT,
STR_BOOTING,
STR_SLEEPING,
STR_ENTERING_SLEEP,
STR_BROWSE_FILES,
STR_FILE_TRANSFER,
STR_SETTINGS_TITLE,
STR_CALIBRE_LIBRARY,
STR_CONTINUE_READING,
STR_NO_OPEN_BOOK,
STR_START_READING,
STR_BOOKS,
STR_NO_BOOKS_FOUND,
STR_SELECT_CHAPTER,
STR_NO_CHAPTERS,
STR_END_OF_BOOK,
STR_EMPTY_CHAPTER,
STR_INDEXING,
STR_MEMORY_ERROR,
STR_PAGE_LOAD_ERROR,
STR_EMPTY_FILE,
STR_OUT_OF_BOUNDS,
STR_LOADING,
STR_LOAD_XTC_FAILED,
STR_LOAD_TXT_FAILED,
STR_LOAD_EPUB_FAILED,
STR_SD_CARD_ERROR,
STR_WIFI_NETWORKS,
STR_NO_NETWORKS,
STR_NETWORKS_FOUND,
STR_SCANNING,
STR_CONNECTING,
STR_CONNECTED,
STR_CONNECTION_FAILED,
STR_CONNECTION_TIMEOUT,
STR_FORGET_NETWORK,
STR_SAVE_PASSWORD,
STR_REMOVE_PASSWORD,
STR_PRESS_OK_SCAN,
STR_PRESS_ANY_CONTINUE,
STR_SELECT_HINT,
STR_HOW_CONNECT,
STR_JOIN_NETWORK,
STR_CREATE_HOTSPOT,
STR_JOIN_DESC,
STR_HOTSPOT_DESC,
STR_STARTING_HOTSPOT,
STR_HOTSPOT_MODE,
STR_CONNECT_WIFI_HINT,
STR_OPEN_URL_HINT,
STR_OR_HTTP_PREFIX,
STR_SCAN_QR_HINT,
STR_CALIBRE_WIRELESS,
STR_CALIBRE_WEB_URL,
STR_CONNECT_WIRELESS,
STR_NETWORK_LEGEND,
STR_MAC_ADDRESS,
STR_CHECKING_WIFI,
STR_ENTER_WIFI_PASSWORD,
STR_ENTER_TEXT,
STR_TO_PREFIX,
STR_CALIBRE_DISCOVERING,
STR_CALIBRE_CONNECTING_TO,
STR_CALIBRE_CONNECTED_TO,
STR_CALIBRE_WAITING_COMMANDS,
STR_CONNECTION_FAILED_RETRYING,
STR_CALIBRE_DISCONNECTED,
STR_CALIBRE_WAITING_TRANSFER,
STR_CALIBRE_TRANSFER_HINT,
STR_CALIBRE_RECEIVING,
STR_CALIBRE_RECEIVED,
STR_CALIBRE_WAITING_MORE,
STR_CALIBRE_FAILED_CREATE_FILE,
STR_CALIBRE_PASSWORD_REQUIRED,
STR_CALIBRE_TRANSFER_INTERRUPTED,
STR_CALIBRE_INSTRUCTION_1,
STR_CALIBRE_INSTRUCTION_2,
STR_CALIBRE_INSTRUCTION_3,
STR_CALIBRE_INSTRUCTION_4,
STR_CAT_DISPLAY,
STR_CAT_READER,
STR_CAT_CONTROLS,
STR_CAT_SYSTEM,
STR_SLEEP_SCREEN,
STR_SLEEP_COVER_MODE,
STR_STATUS_BAR,
STR_HIDE_BATTERY,
STR_EXTRA_SPACING,
STR_TEXT_AA,
STR_SHORT_PWR_BTN,
STR_ORIENTATION,
STR_FRONT_BTN_LAYOUT,
STR_SIDE_BTN_LAYOUT,
STR_LONG_PRESS_SKIP,
STR_FONT_FAMILY,
STR_EXT_READER_FONT,
STR_EXT_CHINESE_FONT,
STR_EXT_UI_FONT,
STR_FONT_SIZE,
STR_LINE_SPACING,
STR_ASCII_LETTER_SPACING,
STR_ASCII_DIGIT_SPACING,
STR_CJK_SPACING,
STR_COLOR_MODE,
STR_SCREEN_MARGIN,
STR_PARA_ALIGNMENT,
STR_HYPHENATION,
STR_TIME_TO_SLEEP,
STR_REFRESH_FREQ,
STR_CALIBRE_SETTINGS,
STR_KOREADER_SYNC,
STR_CHECK_UPDATES,
STR_LANGUAGE,
STR_SELECT_WALLPAPER,
STR_CLEAR_READING_CACHE,
STR_CALIBRE,
STR_USERNAME,
STR_PASSWORD,
STR_SYNC_SERVER_URL,
STR_DOCUMENT_MATCHING,
STR_AUTHENTICATE,
STR_KOREADER_USERNAME,
STR_KOREADER_PASSWORD,
STR_FILENAME,
STR_BINARY,
STR_SET_CREDENTIALS_FIRST,
STR_WIFI_CONN_FAILED,
STR_AUTHENTICATING,
STR_AUTH_SUCCESS,
STR_KOREADER_AUTH,
STR_SYNC_READY,
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_NO_PROGRESS,
STR_FULL_OPT,
STR_NEVER,
STR_IN_READER,
STR_ALWAYS,
STR_IGNORE,
STR_SLEEP,
STR_PAGE_TURN,
STR_PORTRAIT,
STR_LANDSCAPE_CW,
STR_INVERTED,
STR_LANDSCAPE_CCW,
STR_FRONT_LAYOUT_BCLR,
STR_FRONT_LAYOUT_LRBC,
STR_FRONT_LAYOUT_LBCR,
STR_PREV_NEXT,
STR_NEXT_PREV,
STR_BOOKERLY,
STR_NOTO_SANS,
STR_OPEN_DYSLEXIC,
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_MIN_1,
STR_MIN_5,
STR_MIN_10,
STR_MIN_15,
STR_MIN_30,
STR_PAGES_1,
STR_PAGES_5,
STR_PAGES_10,
STR_PAGES_15,
STR_PAGES_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_EXTERNAL_FONT,
STR_BUILTIN_DISABLED,
STR_NO_ENTRIES,
STR_DOWNLOADING,
STR_DOWNLOAD_FAILED,
STR_ERROR_MSG,
STR_UNNAMED,
STR_NO_SERVER_URL,
STR_FETCH_FEED_FAILED,
STR_PARSE_FEED_FAILED,
STR_NETWORK_PREFIX,
STR_IP_ADDRESS_PREFIX,
STR_SCAN_QR_WIFI_HINT,
STR_ERROR_GENERAL_FAILURE,
STR_ERROR_NETWORK_NOT_FOUND,
STR_ERROR_CONNECTION_TIMEOUT,
STR_SD_CARD,
STR_BACK,
STR_EXIT,
STR_HOME,
STR_SAVE,
STR_SELECT,
STR_TOGGLE,
STR_CONFIRM,
STR_CANCEL,
STR_CONNECT,
STR_OPEN,
STR_DOWNLOAD,
STR_RETRY,
STR_YES,
STR_NO,
STR_STATE_ON,
STR_STATE_OFF,
STR_SET,
STR_NOT_SET,
STR_DIR_LEFT,
STR_DIR_RIGHT,
STR_DIR_UP,
STR_DIR_DOWN,
STR_CAPS_ON,
STR_CAPS_OFF,
STR_OK_BUTTON,
STR_ON_MARKER,
STR_SLEEP_COVER_FILTER,
STR_FILTER_CONTRAST,
STR_STATUS_BAR_FULL_PERCENT,
STR_STATUS_BAR_FULL_BOOK,
STR_STATUS_BAR_BOOK_ONLY,
STR_STATUS_BAR_FULL_CHAPTER,
STR_UI_THEME,
STR_THEME_CLASSIC,
STR_THEME_LYRA,
STR_SUNLIGHT_FADING_FIX,
STR_REMAP_FRONT_BUTTONS,
STR_OPDS_BROWSER,
STR_COVER_CUSTOM,
STR_RECENTS,
STR_MENU_RECENT_BOOKS,
STR_NO_RECENT_BOOKS,
STR_CALIBRE_DESC,
STR_FORGET_AND_REMOVE,
STR_FORGET_BUTTON,
STR_CALIBRE_STARTING,
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,
STR_HW_CONFIRM_LABEL,
STR_HW_LEFT_LABEL,
STR_HW_RIGHT_LABEL,
STR_GO_TO_PERCENT,
STR_GO_HOME_BUTTON,
STR_SYNC_PROGRESS,
STR_DELETE_CACHE,
STR_CHAPTER_PREFIX,
STR_PAGES_SEPARATOR,
STR_BOOK_PREFIX,
STR_KBD_SHIFT,
STR_KBD_SHIFT_CAPS,
STR_KBD_LOCK,
STR_CALIBRE_URL_HINT,
STR_PERCENT_STEP_HINT,
STR_SYNCING_TIME,
STR_CALC_HASH,
STR_HASH_FAILED,
STR_FETCH_PROGRESS,
STR_UPLOAD_PROGRESS,
STR_NO_CREDENTIALS_MSG,
STR_KOREADER_SETUP_HINT,
STR_PROGRESS_FOUND,
STR_REMOTE_LABEL,
STR_LOCAL_LABEL,
STR_PAGE_OVERALL_FORMAT,
STR_PAGE_TOTAL_OVERALL_FORMAT,
STR_DEVICE_FROM_FORMAT,
STR_APPLY_REMOTE,
STR_UPLOAD_LOCAL,
STR_NO_REMOTE_MSG,
STR_UPLOAD_PROMPT,
STR_UPLOAD_SUCCESS,
STR_SYNC_FAILED_MSG,
STR_SECTION_PREFIX,
STR_UPLOAD,
STR_BOOK_S_STYLE,
STR_EMBEDDED_STYLE,
STR_OPDS_SERVER_URL,
STR_LETTERBOX_FILL,
STR_DITHERED,
STR_SOLID,
STR_ADD_BOOKMARK,
STR_REMOVE_BOOKMARK,
STR_LOOKUP_WORD,
STR_LOOKUP_HISTORY,
STR_GO_TO_BOOKMARK,
STR_CLOSE_BOOK,
STR_DELETE_DICT_CACHE,
STR_DEFAULT_OPTION,
STR_BOOKMARK_ADDED,
STR_BOOKMARK_REMOVED,
STR_DICT_CACHE_DELETED,
STR_NO_CACHE_TO_DELETE,
STR_TABLE_OF_CONTENTS,
// Sentinel - must be last
_COUNT
};
// Helper function to get string array for a language
inline const char* const* getStringArray(Language lang) {
switch (lang) {
case Language::ENGLISH:
return i18n_strings::STRINGS_EN;
case Language::SPANISH:
return i18n_strings::STRINGS_ES;
case Language::FRENCH:
return i18n_strings::STRINGS_FR;
case Language::GERMAN:
return i18n_strings::STRINGS_DE;
case Language::CZECH:
return i18n_strings::STRINGS_CZ;
case Language::PORTUGUESE:
return i18n_strings::STRINGS_PO;
case Language::RUSSIAN:
return i18n_strings::STRINGS_RU;
case Language::SWEDISH:
return i18n_strings::STRINGS_SV;
default:
return i18n_strings::STRINGS_EN;
}
}
// Helper function to get language count
constexpr uint8_t getLanguageCount() { return static_cast<uint8_t>(Language::_COUNT); }

19
lib/I18n/I18nStrings.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <string>
#include "I18nKeys.h"
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
namespace i18n_strings {
extern const char* const STRINGS_EN[];
extern const char* const STRINGS_ES[];
extern const char* const STRINGS_FR[];
extern const char* const STRINGS_DE[];
extern const char* const STRINGS_CZ[];
extern const char* const STRINGS_PO[];
extern const char* const STRINGS_RU[];
extern const char* const STRINGS_SV[];
} // namespace i18n_strings

View File

@@ -0,0 +1,317 @@
_language_name: "Čeština"
_language_code: "CZECH"
_order: "4"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "SPUŠTĚNÍ"
STR_SLEEPING: "SPÁNEK"
STR_ENTERING_SLEEP: "Vstup do režimu spánku..."
STR_BROWSE_FILES: "Procházet soubory"
STR_FILE_TRANSFER: "Přenos souborů"
STR_SETTINGS_TITLE: "Nastavení"
STR_CALIBRE_LIBRARY: "Knihovna Calibre"
STR_CONTINUE_READING: "Pokračovat ve čtení"
STR_NO_OPEN_BOOK: "Žádná otevřená kniha"
STR_START_READING: "Začněte číst níže"
STR_BOOKS: "Knihy"
STR_NO_BOOKS_FOUND: "Žádné knihy nenalezeny"
STR_SELECT_CHAPTER: "Vybrat kapitolu"
STR_NO_CHAPTERS: "Žádné kapitoly"
STR_END_OF_BOOK: "Konec knihy"
STR_EMPTY_CHAPTER: "Prázdná kapitola"
STR_INDEXING: "Indexování..."
STR_MEMORY_ERROR: "Chyba paměti"
STR_PAGE_LOAD_ERROR: "Chyba načítání stránky"
STR_EMPTY_FILE: "Prázdný soubor"
STR_OUT_OF_BOUNDS: "Mimo hranice"
STR_LOADING: "Načítání..."
STR_LOAD_XTC_FAILED: "Nepodařilo se načíst XTC"
STR_LOAD_TXT_FAILED: "Nepodařilo se načíst TXT"
STR_LOAD_EPUB_FAILED: "Nepodařilo se načíst EPUB"
STR_SD_CARD_ERROR: "Chyba SD karty"
STR_WIFI_NETWORKS: "Wi-Fi sítě"
STR_NO_NETWORKS: "Žádné sítě nenalezeny"
STR_NETWORKS_FOUND: "Nalezeno %zu sítí"
STR_SCANNING: "Skenování..."
STR_CONNECTING: "Připojování..."
STR_CONNECTED: "Připojeno!"
STR_CONNECTION_FAILED: "Připojení se nezdařilo"
STR_CONNECTION_TIMEOUT: "Časový limit připojení"
STR_FORGET_NETWORK: "Zapomenout síť?"
STR_SAVE_PASSWORD: "Uložit heslo pro příště?"
STR_REMOVE_PASSWORD: "Odstranit uložené heslo?"
STR_PRESS_OK_SCAN: "Stiskněte OK pro přeskenování"
STR_PRESS_ANY_CONTINUE: "Pokračujte stiskem libovolné klávesy"
STR_SELECT_HINT: "VLEVO/VPRAVO: Vybrat | OK: Potvrdit"
STR_HOW_CONNECT: "Jak se chcete připojit?"
STR_JOIN_NETWORK: "Připojit se k síti"
STR_CREATE_HOTSPOT: "Vytvořit hotspot"
STR_JOIN_DESC: "Připojit se k existující síti WiFi"
STR_HOTSPOT_DESC: "Vytvořit síť WiFi, ke které se mohou připojit ostatní"
STR_STARTING_HOTSPOT: "Spouštění hotspotu..."
STR_HOTSPOT_MODE: "Režim hotspotu"
STR_CONNECT_WIFI_HINT: "Připojte své zařízení k této síti WiFi"
STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
STR_OR_HTTP_PREFIX: "nebo http://"
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "URL webu Calibre"
STR_CONNECT_WIRELESS: "Připojit jako bezdrátové zařízení"
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola WiFi..."
STR_ENTER_WIFI_PASSWORD: "Zadejte heslo WiFi"
STR_ENTER_TEXT: "Zadejte text"
STR_TO_PREFIX: "pro"
STR_CALIBRE_DISCOVERING: "Prozkoumávání Calibre..."
STR_CALIBRE_CONNECTING_TO: "Připojování k"
STR_CALIBRE_CONNECTED_TO: "Připojeno k"
STR_CALIBRE_WAITING_COMMANDS: "Čekám na příkazy…"
STR_CONNECTION_FAILED_RETRYING: "(Připojení se nezdařilo, opakování pokusu)"
STR_CALIBRE_DISCONNECTED: "Calibre odpojeno"
STR_CALIBRE_WAITING_TRANSFER: "Čekání na přenos..."
STR_CALIBRE_TRANSFER_HINT: "Nezdaří-li se přenos, povolte\\n„Ignorovat volné místo“ v Calibre\\nnastavení pluginu SmartDevice."
STR_CALIBRE_RECEIVING: "Příjem:"
STR_CALIBRE_RECEIVED: "Přijato:"
STR_CALIBRE_WAITING_MORE: "Čekání na další..."
STR_CALIBRE_FAILED_CREATE_FILE: "Nepodařilo se vytvořit soubor"
STR_CALIBRE_PASSWORD_REQUIRED: "Vyžadováno heslo"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Přenos přerušen"
STR_CALIBRE_INSTRUCTION_1: "1) Nainstalujte plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Buďte ve stejné síti WiFi"
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_SYSTEM: "Systém"
STR_SLEEP_SCREEN: "Obrazovka spánku"
STR_SLEEP_COVER_MODE: "Obrazovka spánku Režim krytu"
STR_STATUS_BAR: "Stavový řádek"
STR_HIDE_BATTERY: "Skrýt baterii %"
STR_EXTRA_SPACING: "Extra mezery mezi odstavci"
STR_TEXT_AA: "Vyhlazování textu"
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
STR_ORIENTATION: "Orientace čtení"
STR_FRONT_BTN_LAYOUT: "Rozvržení předních tlačítek"
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
STR_LONG_PRESS_SKIP: "Dlouhé stisknutí Přeskočit kapitolu"
STR_FONT_FAMILY: "Rodina písem čtečky"
STR_EXT_READER_FONT: "Písmo externí čtečky"
STR_EXT_CHINESE_FONT: "Písmo čtečky"
STR_EXT_UI_FONT: "Písmo rozhraní"
STR_FONT_SIZE: "Velikost písma rozhraní"
STR_LINE_SPACING: "Řádkování čtečky"
STR_ASCII_LETTER_SPACING: "Mezery písmen ASCII"
STR_ASCII_DIGIT_SPACING: "Mezery číslic ASCII"
STR_CJK_SPACING: "Mezery CJK"
STR_COLOR_MODE: "Režim barev"
STR_SCREEN_MARGIN: "Okraj obrazovky čtečky"
STR_PARA_ALIGNMENT: "Zarovnání odstavců čtečky"
STR_HYPHENATION: "Dělení slov"
STR_TIME_TO_SLEEP: "Čas do uspání"
STR_REFRESH_FREQ: "Frekvence obnovení"
STR_CALIBRE_SETTINGS: "Nastavení Calibre"
STR_KOREADER_SYNC: "KOReaderu Sync"
STR_CHECK_UPDATES: "Zkontrolovat aktualizace"
STR_LANGUAGE: "Jazyk"
STR_SELECT_WALLPAPER: "Vybrat tapetu"
STR_CLEAR_READING_CACHE: "Vymazat mezipaměť čtení"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Uživatelské jméno"
STR_PASSWORD: "Heslo"
STR_SYNC_SERVER_URL: "URL synch. serveru"
STR_DOCUMENT_MATCHING: "Párování dokumentů"
STR_AUTHENTICATE: "Ověření"
STR_KOREADER_USERNAME: "Uživ. jméno KOReaderu"
STR_KOREADER_PASSWORD: "Heslo KOReaderu"
STR_FILENAME: "Název souboru"
STR_BINARY: "Binární"
STR_SET_CREDENTIALS_FIRST: "Nastavte přihlašovací údaje"
STR_WIFI_CONN_FAILED: "Připojení k Wi-Fi selhalo"
STR_AUTHENTICATING: "Ověřování..."
STR_AUTH_SUCCESS: "Úspěšné ověření!"
STR_KOREADER_AUTH: "Ověření KOReaderu"
STR_SYNC_READY: "Synchronizace KOReaderu je připravena k použití"
STR_AUTH_FAILED: "Ověření selhalo"
STR_DONE: "Hotovo"
STR_CLEAR_CACHE_WARNING_1: "Tímto vymažete všechna data knih v mezipaměti."
STR_CLEAR_CACHE_WARNING_2: "Veškerý průběh čtení bude ztracen!"
STR_CLEAR_CACHE_WARNING_3: "Knihy bude nutné znovu indexovat"
STR_CLEAR_CACHE_WARNING_4: "při opětovném otevření."
STR_CLEARING_CACHE: "Mazání mezipaměti..."
STR_CACHE_CLEARED: "Mezipaměť vymazána"
STR_ITEMS_REMOVED: "položky odstraněny"
STR_FAILED_LOWER: "selhalo"
STR_CLEAR_CACHE_FAILED: "Vymazání mezipaměti se nezdařilo"
STR_CHECK_SERIAL_OUTPUT: "Podrobnosti naleznete v sériovém výstupu"
STR_DARK: "Tmavý"
STR_LIGHT: "Světlý"
STR_CUSTOM: "Vlastní"
STR_COVER: "Obálka"
STR_NONE_OPT: "Žádný"
STR_FIT: "Přizpůsobit"
STR_CROP: "Oříznout"
STR_NO_PROGRESS: "Žádný postup"
STR_FULL_OPT: "Plná"
STR_NEVER: "Nikdy"
STR_IN_READER: "Ve čtečce"
STR_ALWAYS: "Vždy"
STR_IGNORE: "Ignorovat"
STR_SLEEP: "Spánek"
STR_PAGE_TURN: "Otáčení stránek"
STR_PORTRAIT: "Na výšku"
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
STR_INVERTED: "Invertovaný"
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
STR_FRONT_LAYOUT_BCLR: "Zpět, Potvrdit, Vlevo, Vpravo"
STR_FRONT_LAYOUT_LRBC: "Vlevo, Vpravo, Zpět, Potvrdit"
STR_FRONT_LAYOUT_LBCR: "Vlevo, Zpět, Potvrdit, Vpravo"
STR_PREV_NEXT: "Předchozí/Další"
STR_NEXT_PREV: "Další/Předchozí"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Malý"
STR_MEDIUM: "Střední"
STR_LARGE: "Velký"
STR_X_LARGE: "Obří"
STR_TIGHT: "Těsný"
STR_NORMAL: "Normální"
STR_WIDE: "Široký"
STR_JUSTIFY: "Zarovnat do bloku"
STR_ALIGN_LEFT: "Vlevo"
STR_CENTER: "Na střed"
STR_ALIGN_RIGHT: "Vpravo"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 stránka"
STR_PAGES_5: "5 stránek"
STR_PAGES_10: "10 stránek"
STR_PAGES_15: "15 stránek"
STR_PAGES_30: "30 stránek"
STR_UPDATE: "Aktualizace"
STR_CHECKING_UPDATE: "Kontrola aktualizací…"
STR_NEW_UPDATE: "Nová aktualizace k dispozici!"
STR_CURRENT_VERSION: "Aktuální verze:"
STR_NEW_VERSION: "Nová verze:"
STR_UPDATING: "Aktualizace..."
STR_NO_UPDATE: "Žádná aktualizace k dispozici"
STR_UPDATE_FAILED: "Aktualizace selhala"
STR_UPDATE_COMPLETE: "Aktualizace dokončena"
STR_POWER_ON_HINT: "Stiskněte a podržte tlačítko napájení pro opětovné zapnutí"
STR_EXTERNAL_FONT: "Externí písmo"
STR_BUILTIN_DISABLED: "Vestavěné (Zakázáno)"
STR_NO_ENTRIES: "Žádné položky nenalezeny"
STR_DOWNLOADING: "Stahování..."
STR_DOWNLOAD_FAILED: "Stahování selhalo"
STR_ERROR_MSG: "Chyba:"
STR_UNNAMED: "Nepojmenované"
STR_NO_SERVER_URL: "Není nakonfigurována adresa URL serveru"
STR_FETCH_FEED_FAILED: "Načtení kanálu se nezdařilo"
STR_PARSE_FEED_FAILED: "Analyzování kanálu se nezdařilo"
STR_NETWORK_PREFIX: "Síť:"
STR_IP_ADDRESS_PREFIX: "IP adresa:"
STR_SCAN_QR_WIFI_HINT: "nebo naskenujte QR kód telefonem pro připojení k Wi-Fi."
STR_ERROR_GENERAL_FAILURE: "Chyba: Obecná chyba"
STR_ERROR_NETWORK_NOT_FOUND: "Chyba: Síť nenalezena"
STR_ERROR_CONNECTION_TIMEOUT: "Chyba: Časový limit připojení"
STR_SD_CARD: "SD karta"
STR_BACK: "« Zpět"
STR_EXIT: "« Konec"
STR_HOME: "« Domů"
STR_SAVE: "« Uložit"
STR_SELECT: "Vybrat"
STR_TOGGLE: "Přepnout"
STR_CONFIRM: "Potvrdit"
STR_CANCEL: "Zrušit"
STR_CONNECT: "Připojit"
STR_OPEN: "Otevřít"
STR_DOWNLOAD: "Stáhnout"
STR_RETRY: "Zkusit znovu"
STR_YES: "Ano"
STR_NO: "Ne"
STR_STATE_ON: "ZAP"
STR_STATE_OFF: "VYP"
STR_SET: "Nastavit"
STR_NOT_SET: "Nenastaveno"
STR_DIR_LEFT: "Vlevo"
STR_DIR_RIGHT: "Vpravo"
STR_DIR_UP: "Nahoru"
STR_DIR_DOWN: "Dolů"
STR_CAPS_ON: "PÍSMO"
STR_CAPS_OFF: "písmo"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ZAP]"
STR_SLEEP_COVER_FILTER: "Filtr obrazovky spánku"
STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Plný s procenty"
STR_STATUS_BAR_FULL_BOOK: "Plný s pruhem knih"
STR_STATUS_BAR_BOOK_ONLY: "Pouze pruh knih"
STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol"
STR_UI_THEME: "Šablona rozhraní"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
STR_OPDS_BROWSER: "Prohlížeč OPDS"
STR_COVER_CUSTOM: "Obálka + Vlastní"
STR_RECENTS: "Nedávné"
STR_MENU_RECENT_BOOKS: "Nedávné knihy"
STR_NO_RECENT_BOOKS: "Žádné nedávné knihy"
STR_CALIBRE_DESC: "Používat přenosy bezdrátových zařízení Calibre"
STR_FORGET_AND_REMOVE: "Zapomenout síť a odstranit uložené heslo?"
STR_FORGET_BUTTON: "Zapomenout na síť"
STR_CALIBRE_STARTING: "Spuštění Calibre..."
STR_CALIBRE_SETUP: "Nastavení"
STR_CALIBRE_STATUS: "Stav"
STR_CLEAR_BUTTON: "Vymazat"
STR_DEFAULT_VALUE: "Výchozí"
STR_REMAP_PROMPT: "Stiskněte přední tlačítko pro každou roli"
STR_UNASSIGNED: "Nepřiřazeno"
STR_ALREADY_ASSIGNED: "Již přiřazeno"
STR_REMAP_RESET_HINT: "Boční tlačítko Nahoru: Obnovit výchozí rozvržení"
STR_REMAP_CANCEL_HINT: "Boční tlačítko Dolů: Zrušit přemapování"
STR_HW_BACK_LABEL: "Zpět (1. tlačítko)"
STR_HW_CONFIRM_LABEL: "Potvrdit (2. tlačítko)"
STR_HW_LEFT_LABEL: "Vlevo (3. tlačítko)"
STR_HW_RIGHT_LABEL: "Vpravo (4. tlačítko)"
STR_GO_TO_PERCENT: "Přejít na %"
STR_GO_HOME_BUTTON: "Přejít Domů"
STR_SYNC_PROGRESS: "Průběh synchronizace"
STR_DELETE_CACHE: "Smazat mezipaměť knihy"
STR_CHAPTER_PREFIX: "Kapitola:"
STR_PAGES_SEPARATOR: "stránek |"
STR_BOOK_PREFIX: "Kniha:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "ZÁMEK"
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"
STR_FETCH_PROGRESS: "Načítání vzdáleného průběhu..."
STR_UPLOAD_PROGRESS: "Průběh nahrávání..."
STR_NO_CREDENTIALS_MSG: "Přihlašovací údaje nejsou nakonfigurovány"
STR_KOREADER_SETUP_HINT: "Nastavit účet KOReader v Nastavení"
STR_PROGRESS_FOUND: "Nalezen průběh!"
STR_REMOTE_LABEL: "Vzdálené:"
STR_LOCAL_LABEL: "Lokální:"
STR_PAGE_OVERALL_FORMAT: "Stránka %d, celkově %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stránka %d/%d, celkově %.2f%%"
STR_DEVICE_FROM_FORMAT: " Od: %s"
STR_APPLY_REMOTE: "Použít vzdálený postup"
STR_UPLOAD_LOCAL: "Nahrát lokální postup"
STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený postup"
STR_UPLOAD_PROMPT: "Nahrát aktuální pozici?"
STR_UPLOAD_SUCCESS: "Postup nahrán!"
STR_SYNC_FAILED_MSG: "Synchronizace se nezdařila"
STR_SECTION_PREFIX: "Sekce"
STR_UPLOAD: "Nahrát"
STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl"
STR_OPDS_SERVER_URL: "URL serveru OPDS"

View File

@@ -0,0 +1,333 @@
_language_name: "English"
_language_code: "ENGLISH"
_order: "0"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BOOTING"
STR_SLEEPING: "SLEEPING"
STR_ENTERING_SLEEP: "Entering Sleep..."
STR_BROWSE_FILES: "Browse Files"
STR_FILE_TRANSFER: "File Transfer"
STR_SETTINGS_TITLE: "Settings"
STR_CALIBRE_LIBRARY: "Calibre Library"
STR_CONTINUE_READING: "Continue Reading"
STR_NO_OPEN_BOOK: "No open book"
STR_START_READING: "Start reading below"
STR_BOOKS: "Books"
STR_NO_BOOKS_FOUND: "No books found"
STR_SELECT_CHAPTER: "Select Chapter"
STR_NO_CHAPTERS: "No chapters"
STR_END_OF_BOOK: "End of book"
STR_EMPTY_CHAPTER: "Empty chapter"
STR_INDEXING: "Indexing..."
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_LOAD_XTC_FAILED: "Failed to load XTC"
STR_LOAD_TXT_FAILED: "Failed to load TXT"
STR_LOAD_EPUB_FAILED: "Failed to load EPUB"
STR_SD_CARD_ERROR: "SD card error"
STR_WIFI_NETWORKS: "WiFi Networks"
STR_NO_NETWORKS: "No networks found"
STR_NETWORKS_FOUND: "%zu networks found"
STR_SCANNING: "Scanning..."
STR_CONNECTING: "Connecting..."
STR_CONNECTED: "Connected!"
STR_CONNECTION_FAILED: "Connection Failed"
STR_CONNECTION_TIMEOUT: "Connection timeout"
STR_FORGET_NETWORK: "Forget Network?"
STR_SAVE_PASSWORD: "Save password for next time?"
STR_REMOVE_PASSWORD: "Remove saved password?"
STR_PRESS_OK_SCAN: "Press OK to scan again"
STR_PRESS_ANY_CONTINUE: "Press any button to continue"
STR_SELECT_HINT: "LEFT/RIGHT: Select | OK: Confirm"
STR_HOW_CONNECT: "How would you like to connect?"
STR_JOIN_NETWORK: "Join a Network"
STR_CREATE_HOTSPOT: "Create Hotspot"
STR_JOIN_DESC: "Connect to an existing WiFi network"
STR_HOTSPOT_DESC: "Create a WiFi network others can join"
STR_STARTING_HOTSPOT: "Starting Hotspot..."
STR_HOTSPOT_MODE: "Hotspot Mode"
STR_CONNECT_WIFI_HINT: "Connect your device to this WiFi network"
STR_OPEN_URL_HINT: "Open this URL in your browser"
STR_OR_HTTP_PREFIX: "or http://"
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CONNECT_WIRELESS: "Connect as Wireless Device"
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
STR_MAC_ADDRESS: "MAC address:"
STR_CHECKING_WIFI: "Checking WiFi..."
STR_ENTER_WIFI_PASSWORD: "Enter WiFi Password"
STR_ENTER_TEXT: "Enter Text"
STR_TO_PREFIX: "to "
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
STR_CALIBRE_CONNECTING_TO: "Connecting to "
STR_CALIBRE_CONNECTED_TO: "Connected to "
STR_CALIBRE_WAITING_COMMANDS: "Waiting for commands..."
STR_CONNECTION_FAILED_RETRYING: "(Connection failed, retrying)"
STR_CALIBRE_DISCONNECTED: "Calibre disconnected"
STR_CALIBRE_WAITING_TRANSFER: "Waiting for transfer..."
STR_CALIBRE_TRANSFER_HINT: "If transfer fails, enable\\n'Ignore free space' in Calibre's\\nSmartDevice plugin settings."
STR_CALIBRE_RECEIVING: "Receiving: "
STR_CALIBRE_RECEIVED: "Received: "
STR_CALIBRE_WAITING_MORE: "Waiting for more..."
STR_CALIBRE_FAILED_CREATE_FILE: "Failed to create file"
STR_CALIBRE_PASSWORD_REQUIRED: "Password required"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfer interrupted"
STR_CALIBRE_INSTRUCTION_1: "1) Install CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Be on the same WiFi network"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"Send to device\""
STR_CALIBRE_INSTRUCTION_4: "\"Keep this screen open while sending\""
STR_CAT_DISPLAY: "Display"
STR_CAT_READER: "Reader"
STR_CAT_CONTROLS: "Controls"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Sleep Screen"
STR_SLEEP_COVER_MODE: "Sleep Screen Cover Mode"
STR_STATUS_BAR: "Status Bar"
STR_HIDE_BATTERY: "Hide Battery %"
STR_EXTRA_SPACING: "Extra Paragraph Spacing"
STR_TEXT_AA: "Text Anti-Aliasing"
STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation"
STR_FRONT_BTN_LAYOUT: "Front Button Layout"
STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)"
STR_LONG_PRESS_SKIP: "Long-press Chapter Skip"
STR_FONT_FAMILY: "Reader Font Family"
STR_EXT_READER_FONT: "External Reader Font"
STR_EXT_CHINESE_FONT: "Reader Font"
STR_EXT_UI_FONT: "UI Font"
STR_FONT_SIZE: "UI Font Size"
STR_LINE_SPACING: "Reader Line Spacing"
STR_ASCII_LETTER_SPACING: "ASCII Letter Spacing"
STR_ASCII_DIGIT_SPACING: "ASCII Digit Spacing"
STR_CJK_SPACING: "CJK Spacing"
STR_COLOR_MODE: "Color Mode"
STR_SCREEN_MARGIN: "Reader Screen Margin"
STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
STR_HYPHENATION: "Hyphenation"
STR_TIME_TO_SLEEP: "Time to Sleep"
STR_REFRESH_FREQ: "Refresh Frequency"
STR_CALIBRE_SETTINGS: "Calibre Settings"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Check for updates"
STR_LANGUAGE: "Language"
STR_SELECT_WALLPAPER: "Select Wallpaper"
STR_CLEAR_READING_CACHE: "Clear Reading Cache"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Username"
STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL"
STR_DOCUMENT_MATCHING: "Document Matching"
STR_AUTHENTICATE: "Authenticate"
STR_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password"
STR_FILENAME: "Filename"
STR_BINARY: "Binary"
STR_SET_CREDENTIALS_FIRST: "Set credentials first"
STR_WIFI_CONN_FAILED: "WiFi connection failed"
STR_AUTHENTICATING: "Authenticating..."
STR_AUTH_SUCCESS: "Successfully authenticated!"
STR_KOREADER_AUTH: "KOReader Auth"
STR_SYNC_READY: "KOReader sync is ready to use"
STR_AUTH_FAILED: "Authentication Failed"
STR_DONE: "Done"
STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data."
STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!"
STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed"
STR_CLEAR_CACHE_WARNING_4: "when opened again."
STR_CLEARING_CACHE: "Clearing cache..."
STR_CACHE_CLEARED: "Cache Cleared"
STR_ITEMS_REMOVED: "items removed"
STR_FAILED_LOWER: "failed"
STR_CLEAR_CACHE_FAILED: "Failed to clear cache"
STR_CHECK_SERIAL_OUTPUT: "Check serial output for details"
STR_DARK: "Dark"
STR_LIGHT: "Light"
STR_CUSTOM: "Custom"
STR_COVER: "Cover"
STR_NONE_OPT: "None"
STR_FIT: "Fit"
STR_CROP: "Crop"
STR_NO_PROGRESS: "No Progress"
STR_FULL_OPT: "Full"
STR_NEVER: "Never"
STR_IN_READER: "In Reader"
STR_ALWAYS: "Always"
STR_IGNORE: "Ignore"
STR_SLEEP: "Sleep"
STR_PAGE_TURN: "Page Turn"
STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Landscape CW"
STR_INVERTED: "Inverted"
STR_LANDSCAPE_CCW: "Landscape CCW"
STR_FRONT_LAYOUT_BCLR: "Bck, Cnfrm, Lft, Rght"
STR_FRONT_LAYOUT_LRBC: "Lft, Rght, Bck, Cnfrm"
STR_FRONT_LAYOUT_LBCR: "Lft, Bck, Cnfrm, Rght"
STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Small"
STR_MEDIUM: "Medium"
STR_LARGE: "Large"
STR_X_LARGE: "X Large"
STR_TIGHT: "Tight"
STR_NORMAL: "Normal"
STR_WIDE: "Wide"
STR_JUSTIFY: "Justify"
STR_ALIGN_LEFT: "Left"
STR_CENTER: "Center"
STR_ALIGN_RIGHT: "Right"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 page"
STR_PAGES_5: "5 pages"
STR_PAGES_10: "10 pages"
STR_PAGES_15: "15 pages"
STR_PAGES_30: "30 pages"
STR_UPDATE: "Update"
STR_CHECKING_UPDATE: "Checking for update..."
STR_NEW_UPDATE: "New update available!"
STR_CURRENT_VERSION: "Current Version: "
STR_NEW_VERSION: "New Version: "
STR_UPDATING: "Updating..."
STR_NO_UPDATE: "No update available"
STR_UPDATE_FAILED: "Update failed"
STR_UPDATE_COMPLETE: "Update complete"
STR_POWER_ON_HINT: "Press and hold power button to turn back on"
STR_EXTERNAL_FONT: "External Font"
STR_BUILTIN_DISABLED: "Built-in (Disabled)"
STR_NO_ENTRIES: "No entries found"
STR_DOWNLOADING: "Downloading..."
STR_DOWNLOAD_FAILED: "Download failed"
STR_ERROR_MSG: "Error:"
STR_UNNAMED: "Unnamed"
STR_NO_SERVER_URL: "No server URL configured"
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_NETWORK_PREFIX: "Network: "
STR_IP_ADDRESS_PREFIX: "IP Address: "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
STR_ERROR_NETWORK_NOT_FOUND: "Error: Network not found"
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
STR_SD_CARD: "SD card"
STR_BACK: "« Back"
STR_EXIT: "« Exit"
STR_HOME: "« Home"
STR_SAVE: "« Save"
STR_SELECT: "Select"
STR_TOGGLE: "Toggle"
STR_CONFIRM: "Confirm"
STR_CANCEL: "Cancel"
STR_CONNECT: "Connect"
STR_OPEN: "Open"
STR_DOWNLOAD: "Download"
STR_RETRY: "Retry"
STR_YES: "Yes"
STR_NO: "No"
STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF"
STR_SET: "Set"
STR_NOT_SET: "Not Set"
STR_DIR_LEFT: "Left"
STR_DIR_RIGHT: "Right"
STR_DIR_UP: "Up"
STR_DIR_DOWN: "Down"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ON]"
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
STR_FILTER_CONTRAST: "Contrast"
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Percentage"
STR_STATUS_BAR_FULL_BOOK: "Full w/ Book Bar"
STR_STATUS_BAR_BOOK_ONLY: "Book Bar Only"
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser"
STR_COVER_CUSTOM: "Cover + Custom"
STR_RECENTS: "Recents"
STR_MENU_RECENT_BOOKS: "Recent Books"
STR_NO_RECENT_BOOKS: "No recent books"
STR_CALIBRE_DESC: "Use Calibre wireless device transfers"
STR_FORGET_AND_REMOVE: "Forget network and remove saved password?"
STR_FORGET_BUTTON: "Forget network"
STR_CALIBRE_STARTING: "Starting Calibre..."
STR_CALIBRE_SETUP: "Setup"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Clear"
STR_DEFAULT_VALUE: "Default"
STR_REMAP_PROMPT: "Press a front button for each role"
STR_UNASSIGNED: "Unassigned"
STR_ALREADY_ASSIGNED: "Already assigned"
STR_REMAP_RESET_HINT: "Side button Up: Reset to default layout"
STR_REMAP_CANCEL_HINT: "Side button Down: Cancel remapping"
STR_HW_BACK_LABEL: "Back (1st button)"
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
STR_HW_LEFT_LABEL: "Left (3rd button)"
STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_GO_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress"
STR_DELETE_CACHE: "Delete Book Cache"
STR_CHAPTER_PREFIX: "Chapter: "
STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Book: "
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
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"
STR_FETCH_PROGRESS: "Fetching remote progress..."
STR_UPLOAD_PROGRESS: "Uploading progress..."
STR_NO_CREDENTIALS_MSG: "No credentials configured"
STR_KOREADER_SETUP_HINT: "Set up KOReader account in Settings"
STR_PROGRESS_FOUND: "Progress found!"
STR_REMOTE_LABEL: "Remote:"
STR_LOCAL_LABEL: "Local:"
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% overall"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% overall"
STR_DEVICE_FROM_FORMAT: " From: %s"
STR_APPLY_REMOTE: "Apply remote progress"
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_SYNC_FAILED_MSG: "Sync failed"
STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Upload"
STR_BOOK_S_STYLE: "Book's Style"
STR_EMBEDDED_STYLE: "Embedded Style"
STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_LETTERBOX_FILL: "Letterbox Fill"
STR_DITHERED: "Dithered"
STR_SOLID: "Solid"
STR_ADD_BOOKMARK: "Add Bookmark"
STR_REMOVE_BOOKMARK: "Remove Bookmark"
STR_LOOKUP_WORD: "Lookup Word"
STR_LOOKUP_HISTORY: "Lookup Word History"
STR_GO_TO_BOOKMARK: "Go to Bookmark"
STR_CLOSE_BOOK: "Close Book"
STR_DELETE_DICT_CACHE: "Delete Dictionary Cache"
STR_DEFAULT_OPTION: "Default"
STR_BOOKMARK_ADDED: "Bookmark added"
STR_BOOKMARK_REMOVED: "Bookmark removed"
STR_DICT_CACHE_DELETED: "Dictionary cache deleted"
STR_NO_CACHE_TO_DELETE: "No cache to delete"
STR_TABLE_OF_CONTENTS: "Table of Contents"

View File

@@ -0,0 +1,317 @@
_language_name: "Français"
_language_code: "FRENCH"
_order: "2"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "DÉMARRAGE EN COURS"
STR_SLEEPING: "VEILLE"
STR_ENTERING_SLEEP: "Mise en veille…"
STR_BROWSE_FILES: "Fichiers"
STR_FILE_TRANSFER: "Transfert"
STR_SETTINGS_TITLE: "Réglages"
STR_CALIBRE_LIBRARY: "Bibliothèque Calibre"
STR_CONTINUE_READING: "Continuer la lecture"
STR_NO_OPEN_BOOK: "Aucun livre ouvert"
STR_START_READING: "Lisez votre premier livre ci-dessous"
STR_BOOKS: "Livres"
STR_NO_BOOKS_FOUND: "Dossier vide"
STR_SELECT_CHAPTER: "Choix du chapitre"
STR_NO_CHAPTERS: "Aucun chapitre"
STR_END_OF_BOOK: "Fin du livre"
STR_EMPTY_CHAPTER: "Chapitre vide"
STR_INDEXING: "Indexation en cours…"
STR_MEMORY_ERROR: "Erreur de mémoire"
STR_PAGE_LOAD_ERROR: "Erreur de chargement"
STR_EMPTY_FILE: "Fichier vide"
STR_OUT_OF_BOUNDS: "Dépassement de mémoire"
STR_LOADING: "Chargement…"
STR_LOAD_XTC_FAILED: "Erreur de chargement du fichier XTC"
STR_LOAD_TXT_FAILED: "Erreur de chargement du fichier TXT"
STR_LOAD_EPUB_FAILED: "Erreur de chargement du fichier EPUB"
STR_SD_CARD_ERROR: "Carte mémoire absente"
STR_WIFI_NETWORKS: "Réseaux WiFi"
STR_NO_NETWORKS: "Aucun réseau"
STR_NETWORKS_FOUND: "%zu réseaux"
STR_SCANNING: "Recherche de réseaux en cours…"
STR_CONNECTING: "Connexion en cours…"
STR_CONNECTED: "Connecté !"
STR_CONNECTION_FAILED: "Échec de la connexion"
STR_CONNECTION_TIMEOUT: "Délai de connexion dépassé"
STR_FORGET_NETWORK: "Oublier ce réseau ?"
STR_SAVE_PASSWORD: "Enregistrer le mot de passe ?"
STR_REMOVE_PASSWORD: "Supprimer le mot de passe enregistré ?"
STR_PRESS_OK_SCAN: "Appuyez sur OK pour détecter à nouveau"
STR_PRESS_ANY_CONTINUE: "Appuyez sur une touche pour continuer"
STR_SELECT_HINT: "GAUCHE/DROITE: Sélectionner | OK: Valider"
STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?"
STR_JOIN_NETWORK: "Connexion à un réseau"
STR_CREATE_HOTSPOT: "Créer un point daccès"
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis dautres appareils"
STR_STARTING_HOTSPOT: "Création du point daccès en cours…"
STR_HOTSPOT_MODE: "Mode point daccès"
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi"
STR_OPEN_URL_HINT: "Ouvrez cette URL dans votre navigateur"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou scannez le QR code avec votre téléphone"
STR_CALIBRE_WIRELESS: "Connexion à Calibre sans fil"
STR_CALIBRE_WEB_URL: "URL Web Calibre"
STR_CONNECT_WIRELESS: "Se connecter comme appareil sans fil"
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
STR_MAC_ADDRESS: "Adresse MAC :"
STR_CHECKING_WIFI: "Vérification du réseau WiFi..."
STR_ENTER_WIFI_PASSWORD: "Entrez le mot de passe WiFi"
STR_ENTER_TEXT: "Entrez le texte"
STR_TO_PREFIX: "à "
STR_CALIBRE_DISCOVERING: "Recherche de Calibre en cours…"
STR_CALIBRE_CONNECTING_TO: "Connexion à "
STR_CALIBRE_CONNECTED_TO: "Connecté à "
STR_CALIBRE_WAITING_COMMANDS: "En attente de commandes…"
STR_CONNECTION_FAILED_RETRYING: "(Échec de la connexion, nouvelle tentative)"
STR_CALIBRE_DISCONNECTED: "Calibre déconnecté"
STR_CALIBRE_WAITING_TRANSFER: "En attente de transfert…"
STR_CALIBRE_TRANSFER_HINT: "Si le transfert échoue, activez\\nIgnorer lespace libre dans les\\nparamètres du plugin SmartDevice de Calibre."
STR_CALIBRE_RECEIVING: "Réception : "
STR_CALIBRE_RECEIVED: "Reçus : "
STR_CALIBRE_WAITING_MORE: "En attente de données supplémentaires…"
STR_CALIBRE_FAILED_CREATE_FILE: "Échec de la création du fichier"
STR_CALIBRE_PASSWORD_REQUIRED: "Mot de passe requis"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu"
STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : Envoyer vers lappareil"
STR_CALIBRE_INSTRUCTION_4: "“Gardez cet écran ouvert pendant le transfert”"
STR_CAT_DISPLAY: "Affichage"
STR_CAT_READER: "Lecteur"
STR_CAT_CONTROLS: "Commandes"
STR_CAT_SYSTEM: "Système"
STR_SLEEP_SCREEN: "Écran de veille"
STR_SLEEP_COVER_MODE: "Mode dimage de lécran de veille"
STR_STATUS_BAR: "Barre détat"
STR_HIDE_BATTERY: "Masquer % batterie"
STR_EXTRA_SPACING: "Espacement des paragraphes"
STR_TEXT_AA: "Lissage du texte"
STR_SHORT_PWR_BTN: "Appui court bout. alim."
STR_ORIENTATION: "Orientation de lecture"
STR_FRONT_BTN_LAYOUT: "Disposition des boutons avant"
STR_SIDE_BTN_LAYOUT: "Disposition des boutons latéraux"
STR_LONG_PRESS_SKIP: "Appui long pour saut de chapitre"
STR_FONT_FAMILY: "Police de caractères du lecteur"
STR_EXT_READER_FONT: "Police externe"
STR_EXT_CHINESE_FONT: "Police du lecteur"
STR_EXT_UI_FONT: "Police de linterface"
STR_FONT_SIZE: "Taille du texte de linterface"
STR_LINE_SPACING: "Espacement des lignes"
STR_ASCII_LETTER_SPACING: "Espacement des lettres ASCII"
STR_ASCII_DIGIT_SPACING: "Espacement des chiffres ASCII"
STR_CJK_SPACING: "Espacement CJK"
STR_COLOR_MODE: "Mode couleur"
STR_SCREEN_MARGIN: "Marges du lecteur"
STR_PARA_ALIGNMENT: "Alignement des paragraphes"
STR_HYPHENATION: "Césure"
STR_TIME_TO_SLEEP: "Mise en veille automatique"
STR_REFRESH_FREQ: "Fréquence de rafraîchissement"
STR_CALIBRE_SETTINGS: "Réglages Calibre"
STR_KOREADER_SYNC: "Synchronisation KOReader"
STR_CHECK_UPDATES: "Mise à jour"
STR_LANGUAGE: "Langue"
STR_SELECT_WALLPAPER: "Fond décran"
STR_CLEAR_READING_CACHE: "Vider le cache de lecture"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nom dutilisateur"
STR_PASSWORD: "Mot de passe"
STR_SYNC_SERVER_URL: "URL du serveur"
STR_DOCUMENT_MATCHING: "Correspondance"
STR_AUTHENTICATE: "Se connecter"
STR_KOREADER_USERNAME: "Nom dutilisateur"
STR_KOREADER_PASSWORD: "Mot de passe"
STR_FILENAME: "Nom de fichier"
STR_BINARY: "Binaire"
STR_SET_CREDENTIALS_FIRST: "Identifiants manquants"
STR_WIFI_CONN_FAILED: "Échec de connexion WiFi"
STR_AUTHENTICATING: "Connexion en cours…"
STR_AUTH_SUCCESS: "Connexion réussie !"
STR_KOREADER_AUTH: "Auth KOReader"
STR_SYNC_READY: "Synchronisation KOReader prête"
STR_AUTH_FAILED: "Échec de la connexion"
STR_DONE: "OK"
STR_CLEAR_CACHE_WARNING_1: "Le cache de votre bibliothèque sera entièrement vidé"
STR_CLEAR_CACHE_WARNING_2: "Votre progression de lecture sera perdue !"
STR_CLEAR_CACHE_WARNING_3: "Les livres devront être réindexés"
STR_CLEAR_CACHE_WARNING_4: "à leur prochaine ouverture."
STR_CLEARING_CACHE: "Suppression du cache…"
STR_CACHE_CLEARED: "Cache supprimé"
STR_ITEMS_REMOVED: "éléments supprimés"
STR_FAILED_LOWER: "ont échoué"
STR_CLEAR_CACHE_FAILED: "Échec de la suppression du cache"
STR_CHECK_SERIAL_OUTPUT: "Vérifiez la console série pour plus de détails"
STR_DARK: "Sombre"
STR_LIGHT: "Clair"
STR_CUSTOM: "Custom"
STR_COVER: "Couverture"
STR_NONE_OPT: "Aucun"
STR_FIT: "Ajusté"
STR_CROP: "Rogné"
STR_NO_PROGRESS: "Sans progression"
STR_FULL_OPT: "Complète"
STR_NEVER: "Jamais"
STR_IN_READER: "Dans le lecteur"
STR_ALWAYS: "Toujours"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Mise en veille"
STR_PAGE_TURN: "Page suivante"
STR_PORTRAIT: "Portrait"
STR_LANDSCAPE_CW: "Paysage"
STR_INVERTED: "Inversé"
STR_LANDSCAPE_CCW: "Paysage inversé"
STR_FRONT_LAYOUT_BCLR: "Ret, OK, Gauche, Droite"
STR_FRONT_LAYOUT_LRBC: "Gauche, Droite, Ret, OK"
STR_FRONT_LAYOUT_LBCR: "Gauche, Ret, OK, Droite"
STR_PREV_NEXT: "Prec/Suiv"
STR_NEXT_PREV: "Suiv/Prec"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Petite"
STR_MEDIUM: "Moyenne"
STR_LARGE: "Grande"
STR_X_LARGE: "T Grande"
STR_TIGHT: "Serré"
STR_NORMAL: "Normal"
STR_WIDE: "Large"
STR_JUSTIFY: "Justifier"
STR_ALIGN_LEFT: "Gauche"
STR_CENTER: "Centre"
STR_ALIGN_RIGHT: "Droite"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 page"
STR_PAGES_5: "5 pages"
STR_PAGES_10: "10 pages"
STR_PAGES_15: "15 pages"
STR_PAGES_30: "30 pages"
STR_UPDATE: "Mise à jour"
STR_CHECKING_UPDATE: "Recherche de mises à jour en cours…"
STR_NEW_UPDATE: "Nouvelle mise à jour disponible !"
STR_CURRENT_VERSION: "Version actuelle :"
STR_NEW_VERSION: "Nouvelle version : "
STR_UPDATING: "Mise à jour en cours…"
STR_NO_UPDATE: "Aucune mise à jour disponible"
STR_UPDATE_FAILED: "Échec de la mise à jour"
STR_UPDATE_COMPLETE: "Mise à jour effectuée"
STR_POWER_ON_HINT: "Maintenir le bouton dalimentation pour redémarrer"
STR_EXTERNAL_FONT: "Police externe"
STR_BUILTIN_DISABLED: "Intégrée (désactivée)"
STR_NO_ENTRIES: "Aucune entrée trouvée"
STR_DOWNLOADING: "Téléchargement en cours…"
STR_DOWNLOAD_FAILED: "Échec du téléchargement"
STR_ERROR_MSG: "Erreur : "
STR_UNNAMED: "Sans titre"
STR_NO_SERVER_URL: "Aucune URL serveur configurée"
STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux"
STR_PARSE_FEED_FAILED: "Échec de lanalyse du flux"
STR_NETWORK_PREFIX: "Réseau : "
STR_IP_ADDRESS_PREFIX: "Adresse IP : "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général"
STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable"
STR_ERROR_CONNECTION_TIMEOUT: "Erreur : Délai de connexion dépassé"
STR_SD_CARD: "Carte SD"
STR_BACK: "« Retour"
STR_EXIT: "« Sortie"
STR_HOME: "« Accueil"
STR_SAVE: "« Sauver"
STR_SELECT: "OK"
STR_TOGGLE: "Modifier"
STR_CONFIRM: "Confirmer"
STR_CANCEL: "Annuler"
STR_CONNECT: "OK"
STR_OPEN: "Ouvrir"
STR_DOWNLOAD: "Télécharger"
STR_RETRY: "Réessayer"
STR_YES: "Oui"
STR_NO: "Non"
STR_STATE_ON: "ON"
STR_STATE_OFF: "OFF"
STR_SET: "Défini"
STR_NOT_SET: "Non défini"
STR_DIR_LEFT: "Gauche"
STR_DIR_RIGHT: "Droite"
STR_DIR_UP: "Haut"
STR_DIR_DOWN: "Bas"
STR_CAPS_ON: "MAJ"
STR_CAPS_OFF: "maj"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ON]"
STR_SLEEP_COVER_FILTER: "Filtre affichage veille"
STR_FILTER_CONTRAST: "Contraste"
STR_STATUS_BAR_FULL_PERCENT: "Complète + %"
STR_STATUS_BAR_FULL_BOOK: "Complète + barre livre"
STR_STATUS_BAR_BOOK_ONLY: "Barre livre"
STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre"
STR_UI_THEME: "Thème de linterface"
STR_THEME_CLASSIC: "Classique"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil"
STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant"
STR_OPDS_BROWSER: "Navigateur OPDS"
STR_COVER_CUSTOM: "Couverture + Custom"
STR_RECENTS: "Récents"
STR_MENU_RECENT_BOOKS: "Livres récents"
STR_NO_RECENT_BOOKS: "Aucun livre récent"
STR_CALIBRE_DESC: "Utiliser les transferts sans fil Calibre"
STR_FORGET_AND_REMOVE: "Oublier le réseau et supprimer le mot de passe enregistré ?"
STR_FORGET_BUTTON: "Oublier le réseau"
STR_CALIBRE_STARTING: "Démarrage de Calibre..."
STR_CALIBRE_SETUP: "Configuration"
STR_CALIBRE_STATUS: "Statut"
STR_CLEAR_BUTTON: "Effacer"
STR_DEFAULT_VALUE: "Défaut"
STR_REMAP_PROMPT: "Appuyez sur un bouton avant pour chaque rôle"
STR_UNASSIGNED: "Non assigné"
STR_ALREADY_ASSIGNED: "Déjà assigné"
STR_REMAP_RESET_HINT: "Bouton latéral haut : Réinitialiser"
STR_REMAP_CANCEL_HINT: "Bouton latéral bas : Annuler le réglage"
STR_HW_BACK_LABEL: "Retour (1er bouton)"
STR_HW_CONFIRM_LABEL: "OK (2ème bouton)"
STR_HW_LEFT_LABEL: "Gauche (3ème bouton)"
STR_HW_RIGHT_LABEL: "Droite (4ème bouton)"
STR_GO_TO_PERCENT: "Aller à %"
STR_GO_HOME_BUTTON: "Aller à laccueil"
STR_SYNC_PROGRESS: "Synchroniser la progression"
STR_DELETE_CACHE: "Supprimer le cache du livre"
STR_CHAPTER_PREFIX: "Chapitre : "
STR_PAGES_SEPARATOR: " pages | "
STR_BOOK_PREFIX: "Livre : "
STR_KBD_SHIFT: "maj"
STR_KBD_SHIFT_CAPS: "MAJ"
STR_KBD_LOCK: "VERR MAJ"
STR_CALIBRE_URL_HINT: "Pour Calibre, ajoutez /opds à lURL"
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
STR_SYNCING_TIME: "Synchronisation de lheure…"
STR_CALC_HASH: "Calcul du hash du document…"
STR_HASH_FAILED: "Échec du calcul du hash du document"
STR_FETCH_PROGRESS: "Téléchargement de la progression…"
STR_UPLOAD_PROGRESS: "Envoi de la progression…"
STR_NO_CREDENTIALS_MSG: "Aucun identifiant configuré"
STR_KOREADER_SETUP_HINT: "Configurez le compte KOReader dans les réglages"
STR_PROGRESS_FOUND: "Progression trouvée !"
STR_REMOTE_LABEL: "En ligne :"
STR_LOCAL_LABEL: "Locale :"
STR_PAGE_OVERALL_FORMAT: "Page %d, %.2f%% au total"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Page %d/%d, %.2f%% au total"
STR_DEVICE_FROM_FORMAT: " De : %s"
STR_APPLY_REMOTE: "Appliquer la progression en ligne"
STR_UPLOAD_LOCAL: "Envoyer la progression locale"
STR_NO_REMOTE_MSG: "Aucune progression en ligne trouvée"
STR_UPLOAD_PROMPT: "Envoyer la position actuelle ?"
STR_UPLOAD_SUCCESS: "Progression envoyée !"
STR_SYNC_FAILED_MSG: "Échec de la synchronisation"
STR_SECTION_PREFIX: "Section "
STR_UPLOAD: "Envoi"
STR_BOOK_S_STYLE: "Style du livre"
STR_EMBEDDED_STYLE: "Style intégré"
STR_OPDS_SERVER_URL: "URL du serveur OPDS"

View File

@@ -0,0 +1,317 @@
_language_name: "Deutsch"
_language_code: "GERMAN"
_order: "3"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "STARTEN"
STR_SLEEPING: "STANDBY"
STR_ENTERING_SLEEP: "Standby..."
STR_BROWSE_FILES: "Durchsuchen"
STR_FILE_TRANSFER: "Datentransfer"
STR_SETTINGS_TITLE: "Einstellungen"
STR_CALIBRE_LIBRARY: "Calibre-Bibliothek"
STR_CONTINUE_READING: "Weiterlesen"
STR_NO_OPEN_BOOK: "Aktuell kein Buch"
STR_START_READING: "Lesen beginnen"
STR_BOOKS: "Bücher"
STR_NO_BOOKS_FOUND: "Keine Bücher"
STR_SELECT_CHAPTER: "Kapitel auswählen"
STR_NO_CHAPTERS: "Keine Kapitel"
STR_END_OF_BOOK: "Buchende"
STR_EMPTY_CHAPTER: "Kapitelende"
STR_INDEXING: "Indexieren…"
STR_MEMORY_ERROR: "Speicherfehler"
STR_PAGE_LOAD_ERROR: "Seitenladefehler"
STR_EMPTY_FILE: "Leere Datei"
STR_OUT_OF_BOUNDS: "Zu groß"
STR_LOADING: "Laden…"
STR_LOAD_XTC_FAILED: "Ladefehler bei XTC"
STR_LOAD_TXT_FAILED: "Ladefehler bei TXT"
STR_LOAD_EPUB_FAILED: "Ladefehler bei EPUB"
STR_SD_CARD_ERROR: "SD-Karten-Fehler"
STR_WIFI_NETWORKS: "WLAN-Netzwerke"
STR_NO_NETWORKS: "Kein WLAN gefunden"
STR_NETWORKS_FOUND: "%zu WLAN-Netzwerke gefunden"
STR_SCANNING: "Suchen..."
STR_CONNECTING: "Verbinden..."
STR_CONNECTED: "Verbunden!"
STR_CONNECTION_FAILED: "Verbindungsfehler"
STR_CONNECTION_TIMEOUT: "Verbindungs-Timeout"
STR_FORGET_NETWORK: "WLAN vergessen?"
STR_SAVE_PASSWORD: "Passwort speichern?"
STR_REMOVE_PASSWORD: "Passwort entfernen?"
STR_PRESS_OK_SCAN: "OK für neue Suche"
STR_PRESS_ANY_CONTINUE: "Beliebige Taste drücken"
STR_SELECT_HINT: "links/rechts: Auswahl | OK: Best"
STR_HOW_CONNECT: "Wie möchtest du dich verbinden?"
STR_JOIN_NETWORK: "Netzwerk beitreten"
STR_CREATE_HOTSPOT: "Hotspot erstellen"
STR_JOIN_DESC: "Mit einem bestehenden WLAN verbinden"
STR_HOTSPOT_DESC: "WLAN für andere erstellen"
STR_STARTING_HOTSPOT: "Hotspot starten…"
STR_HOTSPOT_MODE: "Hotspot-Modus"
STR_CONNECT_WIFI_HINT: "Gerät mit diesem WLAN verbinden"
STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
STR_OR_HTTP_PREFIX: "oder http://"
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre-Web-URL"
STR_CONNECT_WIRELESS: "Als Drahtlos-Gerät hinzufügen"
STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
STR_MAC_ADDRESS: "MAC-Adresse:"
STR_CHECKING_WIFI: "WLAN prüfen…"
STR_ENTER_WIFI_PASSWORD: "WLAN-Passwort eingeben"
STR_ENTER_TEXT: "Text eingeben"
STR_TO_PREFIX: "bis"
STR_CALIBRE_DISCOVERING: "Calibre finden..."
STR_CALIBRE_CONNECTING_TO: "Verbinden mit"
STR_CALIBRE_CONNECTED_TO: "Verbunden mit"
STR_CALIBRE_WAITING_COMMANDS: "Auf Befehle warten…"
STR_CONNECTION_FAILED_RETRYING: "(Keine Verbindung, wiederholen)"
STR_CALIBRE_DISCONNECTED: "Calibre getrennt"
STR_CALIBRE_WAITING_TRANSFER: "Auf Übertragung warten..."
STR_CALIBRE_TRANSFER_HINT: "Bei Übertragungsfehler \\n'Freien Speicher ign.' in den\\nCalibre-Einstellungen einschalten."
STR_CALIBRE_RECEIVING: "Empfange:"
STR_CALIBRE_RECEIVED: "Empfangen:"
STR_CALIBRE_WAITING_MORE: "Auf mehr warten…"
STR_CALIBRE_FAILED_CREATE_FILE: "Speicherfehler"
STR_CALIBRE_PASSWORD_REQUIRED: "Passwort nötig"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Übertragung unterbrochen"
STR_CALIBRE_INSTRUCTION_1: "1) CrossPoint Reader-Plugin installieren"
STR_CALIBRE_INSTRUCTION_2: "2) Mit selbem WLAN verbinden"
STR_CALIBRE_INSTRUCTION_3: "3) In Calibre: \"An Gerät senden\""
STR_CALIBRE_INSTRUCTION_4: "Bildschirm beim Senden offenlassen"
STR_CAT_DISPLAY: "Anzeige"
STR_CAT_READER: "Lesen"
STR_CAT_CONTROLS: "Bedienung"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Standby-Bild"
STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
STR_STATUS_BAR: "Statusleiste"
STR_HIDE_BATTERY: "Batterie % ausblenden"
STR_EXTRA_SPACING: "Absatzabstand"
STR_TEXT_AA: "Schriftglättung"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung"
STR_FRONT_BTN_LAYOUT: "Vorderes Tastenlayout"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
STR_LONG_PRESS_SKIP: "Langes Drücken springt Kap."
STR_FONT_FAMILY: "Lese-Schriftfamilie"
STR_EXT_READER_FONT: "Externe Schriftart"
STR_EXT_CHINESE_FONT: "Lese-Schriftart"
STR_EXT_UI_FONT: "Menü-Schriftart"
STR_FONT_SIZE: "Schriftgröße"
STR_LINE_SPACING: "Lese-Zeilenabstand"
STR_ASCII_LETTER_SPACING: "ASCII-Zeichenabstand"
STR_ASCII_DIGIT_SPACING: "ASCII-Ziffernabstand"
STR_CJK_SPACING: "CJK-Zeichenabstand"
STR_COLOR_MODE: "Farbmodus"
STR_SCREEN_MARGIN: "Lese-Seitenränder"
STR_PARA_ALIGNMENT: "Lese-Absatzausrichtung"
STR_HYPHENATION: "Silbentrennung"
STR_TIME_TO_SLEEP: "Standby nach"
STR_REFRESH_FREQ: "Anti-Ghosting nach"
STR_CALIBRE_SETTINGS: "Calibre-Einstellungen"
STR_KOREADER_SYNC: "KOReader-Synchr."
STR_CHECK_UPDATES: "Nach Updates suchen"
STR_LANGUAGE: "Sprache"
STR_SELECT_WALLPAPER: "Bildauswahl Standby"
STR_CLEAR_READING_CACHE: "Lese-Cache leeren"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Benutzername"
STR_PASSWORD: "Passwort nötig"
STR_SYNC_SERVER_URL: "Sync-Server-URL"
STR_DOCUMENT_MATCHING: "Dateizuordnung"
STR_AUTHENTICATE: "Authentifizieren"
STR_KOREADER_USERNAME: "KOReader-Benutzername"
STR_KOREADER_PASSWORD: "KOReader-Passwort"
STR_FILENAME: "Dateiname"
STR_BINARY: "Binärdatei"
STR_SET_CREDENTIALS_FIRST: "Zuerst anmelden"
STR_WIFI_CONN_FAILED: "WLAN-Verbindung fehlgeschlagen"
STR_AUTHENTICATING: "Authentifizieren…"
STR_AUTH_SUCCESS: "Erfolgreich authentifiziert!"
STR_KOREADER_AUTH: "KOReader-Auth"
STR_SYNC_READY: "KOReader-Synchronisierung bereit"
STR_AUTH_FAILED: "Authentifizierung fehlg."
STR_DONE: "Erledigt"
STR_CLEAR_CACHE_WARNING_1: "Alle Buch-Caches werden geleert."
STR_CLEAR_CACHE_WARNING_2: "Lesefortschritt wird gelöscht!"
STR_CLEAR_CACHE_WARNING_3: "Bücher müssen beim Öffnen"
STR_CLEAR_CACHE_WARNING_4: "neu eingelesen werden."
STR_CLEARING_CACHE: "Cache leeren…"
STR_CACHE_CLEARED: "Cache geleert"
STR_ITEMS_REMOVED: "Einträge entfernt"
STR_FAILED_LOWER: "fehlgeschlagen"
STR_CLEAR_CACHE_FAILED: "Fehler beim Cache-Leeren"
STR_CHECK_SERIAL_OUTPUT: "Serielle Ausgabe prüfen"
STR_DARK: "Dunkel"
STR_LIGHT: "Hell"
STR_CUSTOM: "Eigenes"
STR_COVER: "Umschlag"
STR_NONE_OPT: "Leer"
STR_FIT: "Anpassen"
STR_CROP: "Zuschnitt"
STR_NO_PROGRESS: "Ohne Fortschr."
STR_FULL_OPT: "Vollst."
STR_NEVER: "Nie"
STR_IN_READER: "Beim Lesen"
STR_ALWAYS: "Immer"
STR_IGNORE: "Ignorieren"
STR_SLEEP: "Standby"
STR_PAGE_TURN: "Umblättern"
STR_PORTRAIT: "Hochformat"
STR_LANDSCAPE_CW: "Querformat rechts"
STR_INVERTED: "Invertiert"
STR_LANDSCAPE_CCW: "Querformat links"
STR_FRONT_LAYOUT_BCLR: "Zurück, Bst, L, R"
STR_FRONT_LAYOUT_LRBC: "L, R, Zurück, Bst"
STR_FRONT_LAYOUT_LBCR: "L, Zurück, Bst, R"
STR_PREV_NEXT: "Zurück/Weiter"
STR_NEXT_PREV: "Weiter/Zuürck"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Klein"
STR_MEDIUM: "Mittel"
STR_LARGE: "Groß"
STR_X_LARGE: "Extragroß"
STR_TIGHT: "Eng"
STR_NORMAL: "Normal"
STR_WIDE: "Breit"
STR_JUSTIFY: "Blocksatz"
STR_ALIGN_LEFT: "Links"
STR_CENTER: "Zentriert"
STR_ALIGN_RIGHT: "Rechts"
STR_MIN_1: "1 Min"
STR_MIN_5: "5 Min"
STR_MIN_10: "10 Min"
STR_MIN_15: "15 Min"
STR_MIN_30: "30 Min"
STR_PAGES_1: "1 Seite"
STR_PAGES_5: "5 Seiten"
STR_PAGES_10: "10 Seiten"
STR_PAGES_15: "15 Seiten"
STR_PAGES_30: "30 Seiten"
STR_UPDATE: "Update"
STR_CHECKING_UPDATE: "Update suchen…"
STR_NEW_UPDATE: "Neues Update verfügbar!"
STR_CURRENT_VERSION: "Aktuelle Version:"
STR_NEW_VERSION: "Neue Version:"
STR_UPDATING: "Aktualisiere…"
STR_NO_UPDATE: "Kein Update verfügbar"
STR_UPDATE_FAILED: "Updatefehler"
STR_UPDATE_COMPLETE: "Update fertig"
STR_POWER_ON_HINT: "An-Knopf lang drücken, um neuzustarten"
STR_EXTERNAL_FONT: "Externe Schrift"
STR_BUILTIN_DISABLED: "Vorinstalliert (aus)"
STR_NO_ENTRIES: "Keine Einträge"
STR_DOWNLOADING: "Herunterladen…"
STR_DOWNLOAD_FAILED: "Ladefehler"
STR_ERROR_MSG: "Fehler:"
STR_UNNAMED: "Unbenannt"
STR_NO_SERVER_URL: "Keine Server-URL konfiguriert"
STR_FETCH_FEED_FAILED: "Feedfehler"
STR_PARSE_FEED_FAILED: "Feed-Format ungültig"
STR_NETWORK_PREFIX: "Netzwerk:"
STR_IP_ADDRESS_PREFIX: "IP-Adresse:"
STR_SCAN_QR_WIFI_HINT: "oder QR-Code mit dem Handy scannen für WLAN."
STR_ERROR_GENERAL_FAILURE: "Fehler: Allgemeiner Fehler"
STR_ERROR_NETWORK_NOT_FOUND: "Fehler: Kein Netzwerk"
STR_ERROR_CONNECTION_TIMEOUT: "Fehler: Zeitüberschreitung"
STR_SD_CARD: "SD-Karte"
STR_BACK: "« Zurück"
STR_EXIT: "« Verlassen"
STR_HOME: "« Start"
STR_SAVE: "« Speichern"
STR_SELECT: "Auswahl"
STR_TOGGLE: "Ändern"
STR_CONFIRM: "Bestätigen"
STR_CANCEL: "Abbrechen"
STR_CONNECT: "Verbinden"
STR_OPEN: "Öffnen"
STR_DOWNLOAD: "Herunterladen"
STR_RETRY: "Wiederh."
STR_YES: "Ja"
STR_NO: "Nein"
STR_STATE_ON: "An"
STR_STATE_OFF: "Aus"
STR_SET: "Gesetzt"
STR_NOT_SET: "Leer"
STR_DIR_LEFT: "Links"
STR_DIR_RIGHT: "Rechts"
STR_DIR_UP: "Hoch"
STR_DIR_DOWN: "Runter"
STR_CAPS_ON: "UMSCH"
STR_CAPS_OFF: "umsch"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[AN]"
STR_SLEEP_COVER_FILTER: "Standby-Coverfilter"
STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Komplett + Prozent"
STR_STATUS_BAR_FULL_BOOK: "Komplett + Buch"
STR_STATUS_BAR_BOOK_ONLY: "Nur Buch"
STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel"
STR_UI_THEME: "System-Design"
STR_THEME_CLASSIC: "Klassisch"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_OPDS_BROWSER: "OPDS-Browser"
STR_COVER_CUSTOM: "Umschlag + Eigenes"
STR_RECENTS: "Zuletzt"
STR_MENU_RECENT_BOOKS: "Zuletzt gelesen"
STR_NO_RECENT_BOOKS: "Keine Bücher"
STR_CALIBRE_DESC: "Calibre-Übertragung (WLAN)"
STR_FORGET_AND_REMOVE: "WLAN entfernen & Passwort löschen?"
STR_FORGET_BUTTON: "WLAN entfernen"
STR_CALIBRE_STARTING: "Calibre starten…"
STR_CALIBRE_SETUP: "Installation"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Leeren"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Entsprechende Vordertaste drücken"
STR_UNASSIGNED: "Leer"
STR_ALREADY_ASSIGNED: "Bereits zugeordnet"
STR_REMAP_RESET_HINT: "Seitentaste hoch: Standard"
STR_REMAP_CANCEL_HINT: "Seitentaste runter: Abbrechen"
STR_HW_BACK_LABEL: "Zurück (1. Taste)"
STR_HW_CONFIRM_LABEL: "Bestätigen (2. Taste)"
STR_HW_LEFT_LABEL: "Links (3. Taste)"
STR_HW_RIGHT_LABEL: "Rechts (4. Taste)"
STR_GO_TO_PERCENT: "Gehe zu %"
STR_GO_HOME_BUTTON: "Zum Anfang"
STR_SYNC_PROGRESS: "Fortschritt synchronisieren"
STR_DELETE_CACHE: "Buch-Cache leeren"
STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " Seiten | "
STR_BOOK_PREFIX: "Buch: "
STR_KBD_SHIFT: "umsch"
STR_KBD_SHIFT_CAPS: "UMSCH"
STR_KBD_LOCK: "FESTST"
STR_CALIBRE_URL_HINT: "Calibre: URL um /opds ergänzen"
STR_PERCENT_STEP_HINT: "links/rechts: 1% hoch/runter: 10%"
STR_SYNCING_TIME: "Zeit synchonisieren…"
STR_CALC_HASH: "Dokument-Hash berechnen…"
STR_HASH_FAILED: "Dokument-Hash fehlgeschlagen"
STR_FETCH_PROGRESS: "Externen Fortschritt abrufen..."
STR_UPLOAD_PROGRESS: "Fortschritt hochladen…"
STR_NO_CREDENTIALS_MSG: "Zugangsdaten fehlen"
STR_KOREADER_SETUP_HINT: "KOReader-Konto unter Einst. anlegen"
STR_PROGRESS_FOUND: "Gefunden!"
STR_REMOTE_LABEL: "Extern:"
STR_LOCAL_LABEL: "Lokal:"
STR_PAGE_OVERALL_FORMAT: " Seite %d, %.2f%% insgesamt"
STR_PAGE_TOTAL_OVERALL_FORMAT: " Seite %d/%d, %.2f%% insgesamt"
STR_DEVICE_FROM_FORMAT: " Von: %s"
STR_APPLY_REMOTE: "Ext. Fortschritt übern."
STR_UPLOAD_LOCAL: "Lokalen Fortschritt hochl."
STR_NO_REMOTE_MSG: "Kein externer Fortschritt"
STR_UPLOAD_PROMPT: "Aktuelle Position hochladen?"
STR_UPLOAD_SUCCESS: "Hochgeladen!"
STR_SYNC_FAILED_MSG: "Fehlgeschlagen"
STR_SECTION_PREFIX: "Abschnitt"
STR_UPLOAD: "Hochladen"
STR_BOOK_S_STYLE: "Buch-Stil"
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
STR_OPDS_SERVER_URL: "OPDS-Server-URL"

View File

@@ -0,0 +1,317 @@
_language_name: "Português (Brasil)"
_language_code: "PORTUGUESE"
_order: "5"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "INICIANDO"
STR_SLEEPING: "EM REPOUSO"
STR_ENTERING_SLEEP: "Entrando em repouso..."
STR_BROWSE_FILES: "Arquivos"
STR_FILE_TRANSFER: "Transferência"
STR_SETTINGS_TITLE: "Configurações"
STR_CALIBRE_LIBRARY: "Biblioteca do Calibre"
STR_CONTINUE_READING: "Continuar lendo"
STR_NO_OPEN_BOOK: "Nenhum livro aberto"
STR_START_READING: "Comece a ler abaixo"
STR_BOOKS: "Livros"
STR_NO_BOOKS_FOUND: "Nenhum livro encontrado"
STR_SELECT_CHAPTER: "Escolher capítulo"
STR_NO_CHAPTERS: "Sem capítulos"
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_EMPTY_FILE: "Arquivo vazio"
STR_OUT_OF_BOUNDS: "Fora dos limites"
STR_LOADING: "Carregando..."
STR_LOAD_XTC_FAILED: "Falha ao carregar XTC"
STR_LOAD_TXT_FAILED: "Falha ao carregar TXT"
STR_LOAD_EPUB_FAILED: "Falha ao carregar EPUB"
STR_SD_CARD_ERROR: "Erro no cartão SD"
STR_WIFI_NETWORKS: "Redes WiFi"
STR_NO_NETWORKS: "Sem redes"
STR_NETWORKS_FOUND: "%zu redes encontradas"
STR_SCANNING: "Procurando..."
STR_CONNECTING: "Conectando..."
STR_CONNECTED: "Conectado!"
STR_CONNECTION_FAILED: "Falha na conexão"
STR_CONNECTION_TIMEOUT: "Tempo limite conexão"
STR_FORGET_NETWORK: "Esquecer rede?"
STR_SAVE_PASSWORD: "Salvar senha a próxima vez?"
STR_REMOVE_PASSWORD: "Remover senha salva?"
STR_PRESS_OK_SCAN: "Pressione OK procurar novamente"
STR_PRESS_ANY_CONTINUE: "Pressione qualquer botão continuar"
STR_SELECT_HINT: "ESQ/DIR: Escolher | OK: Confirmar"
STR_HOW_CONNECT: "Como você gostaria se conectar?"
STR_JOIN_NETWORK: "Entrar em uma rede"
STR_CREATE_HOTSPOT: "Criar hotspot"
STR_JOIN_DESC: "Conecte-se a uma rede WiFi existente"
STR_HOTSPOT_DESC: "Crie uma rede WiFi outras pessoas entrarem"
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
STR_HOTSPOT_MODE: "Modo hotspot"
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede WiFi"
STR_OPEN_URL_HINT: "Abra este URL 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"
STR_CALIBRE_WEB_URL: "URL do Calibre Web"
STR_CONNECT_WIRELESS: "Conectar como dispositivo sem fio"
STR_NETWORK_LEGEND: "* = Criptografada | + = Salva"
STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "Verificando WiFi..."
STR_ENTER_WIFI_PASSWORD: "Digite a senha WiFi"
STR_ENTER_TEXT: "Inserir texto"
STR_TO_PREFIX: "para"
STR_CALIBRE_DISCOVERING: "Procurando o Calibre..."
STR_CALIBRE_CONNECTING_TO: "Conectando a"
STR_CALIBRE_CONNECTED_TO: "Conectado a"
STR_CALIBRE_WAITING_COMMANDS: "Aguardando comandos..."
STR_CONNECTION_FAILED_RETRYING: "(Falha conexão, tentando novamente)"
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
STR_CALIBRE_WAITING_TRANSFER: "Aguardando transferência..."
STR_CALIBRE_TRANSFER_HINT: "Se a transferência falhar, ative\n\\n'Ignorar espaço livre'\\n nas \\nconfigurações do\nplugin SmartDevice\\n Calibre."
STR_CALIBRE_RECEIVING: "Recebendo:"
STR_CALIBRE_RECEIVED: "Recebido:"
STR_CALIBRE_WAITING_MORE: "Aguardando mais..."
STR_CALIBRE_FAILED_CREATE_FILE: "Falha ao criar o arquivo"
STR_CALIBRE_PASSWORD_REQUIRED: "Senha obrigatória"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transf. interrompida"
STR_CALIBRE_INSTRUCTION_1: "1) Instale o plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Esteja mesma rede WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) No Calibre: \"Enviar o 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_SLEEP_COVER_MODE: "Modo capa tela repouso"
STR_STATUS_BAR: "Barra de status"
STR_HIDE_BATTERY: "Ocultar % da bateria"
STR_EXTRA_SPACING: "Espaço de parágrafos extra"
STR_TEXT_AA: "Suavização de texto"
STR_SHORT_PWR_BTN: "Clique curto botão ligar"
STR_ORIENTATION: "Orientação de leitura"
STR_FRONT_BTN_LAYOUT: "Disposição botões frontais"
STR_SIDE_BTN_LAYOUT: "Disposição botões laterais"
STR_LONG_PRESS_SKIP: "Pular capítulo com pressão longa"
STR_FONT_FAMILY: "Fonte do leitor"
STR_EXT_READER_FONT: "Fonte leitor externo"
STR_EXT_CHINESE_FONT: "Fonte do leitor"
STR_EXT_UI_FONT: "Fonte da interface"
STR_FONT_SIZE: "Tam. fonte UI"
STR_LINE_SPACING: "Espaçamento entre linhas"
STR_ASCII_LETTER_SPACING: "Espaçamento letras ASCII"
STR_ASCII_DIGIT_SPACING: "Espaçamento dígitos ASCII"
STR_CJK_SPACING: "Espaçamento CJK"
STR_COLOR_MODE: "Modo de cor"
STR_SCREEN_MARGIN: "Margens da tela"
STR_PARA_ALIGNMENT: "Alinhamento parágrafo"
STR_HYPHENATION: "Hifenização"
STR_TIME_TO_SLEEP: "Tempo para repousar"
STR_REFRESH_FREQ: "Frequência atualização"
STR_CALIBRE_SETTINGS: "Configuração do Calibre"
STR_KOREADER_SYNC: "Sincronização KOReader"
STR_CHECK_UPDATES: "Verificar atualizações"
STR_LANGUAGE: "Idioma"
STR_SELECT_WALLPAPER: "Escolher papel parede"
STR_CLEAR_READING_CACHE: "Limpar cache de leitura"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nome de usuário"
STR_PASSWORD: "Senha"
STR_SYNC_SERVER_URL: "URL servidor sincronização"
STR_DOCUMENT_MATCHING: "Documento correspondente"
STR_AUTHENTICATE: "Autenticar"
STR_KOREADER_USERNAME: "Usuário do KOReader"
STR_KOREADER_PASSWORD: "Senha do KOReader"
STR_FILENAME: "Nome do arquivo"
STR_BINARY: "Binário"
STR_SET_CREDENTIALS_FIRST: "Defina as credenciais primeiro"
STR_WIFI_CONN_FAILED: "Falha na conexão WiFi"
STR_AUTHENTICATING: "Autenticando..."
STR_AUTH_SUCCESS: "Autenticado com sucesso!"
STR_KOREADER_AUTH: "Autenticação KOReader"
STR_SYNC_READY: "A sincronização KOReader está pronta uso"
STR_AUTH_FAILED: "Falha na autenticação"
STR_DONE: "Feito"
STR_CLEAR_CACHE_WARNING_1: "Isso vai limpar todos os dados livros em cache."
STR_CLEAR_CACHE_WARNING_2: "Todo o progresso de leitura será perdido!"
STR_CLEAR_CACHE_WARNING_3: "Os livros precisarão ser reindexados"
STR_CLEAR_CACHE_WARNING_4: "quando forem abertos novamente."
STR_CLEARING_CACHE: "Limpando cache..."
STR_CACHE_CLEARED: "Cache limpo"
STR_ITEMS_REMOVED: "itens removidos"
STR_FAILED_LOWER: "falhou"
STR_CLEAR_CACHE_FAILED: "Falha ao limpar o cache"
STR_CHECK_SERIAL_OUTPUT: "Ver saída serial"
STR_DARK: "Escuro"
STR_LIGHT: "Claro"
STR_CUSTOM: "Personalizado"
STR_COVER: "Capa"
STR_NONE_OPT: "Nenhum"
STR_FIT: "Ajustar"
STR_CROP: "Recortar"
STR_NO_PROGRESS: "Sem progresso"
STR_FULL_OPT: "Completo"
STR_NEVER: "Nunca"
STR_IN_READER: "No leitor"
STR_ALWAYS: "Sempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Repouso"
STR_PAGE_TURN: "Virar página"
STR_PORTRAIT: "Retrato"
STR_LANDSCAPE_CW: "Paisagem H"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Paisagem AH"
STR_FRONT_LAYOUT_BCLR: "Vol, Conf, Esq, Dir"
STR_FRONT_LAYOUT_LRBC: "Esq, Dir, Vol, Conf"
STR_FRONT_LAYOUT_LBCR: "Esq, Vol, Conf, Dir"
STR_PREV_NEXT: "Ant/Próx"
STR_NEXT_PREV: "Próx/Ant"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
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: "Centralizar"
STR_ALIGN_RIGHT: "Direita"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
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: "Verificando atualização..."
STR_NEW_UPDATE: "Nova atualização disponível!"
STR_CURRENT_VERSION: "Versão atual:"
STR_NEW_VERSION: "Nova versão:"
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_EXTERNAL_FONT: "Fonte externa"
STR_BUILTIN_DISABLED: "Integrada (desativada)"
STR_NO_ENTRIES: "Nenhum entries encontrado"
STR_DOWNLOADING: "Baixando..."
STR_DOWNLOAD_FAILED: "Falha no download"
STR_ERROR_MSG: "Erro:"
STR_UNNAMED: "Sem nome"
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_SCAN_QR_WIFI_HINT: "ou escaneie o QR code com seu celular conectar ao WiFi."
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"
STR_SD_CARD: "Cartão SD"
STR_BACK: "« Voltar"
STR_EXIT: "« Sair"
STR_HOME: "« Início"
STR_SAVE: "« Salvar"
STR_SELECT: "Escolher"
STR_TOGGLE: "Alternar"
STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar"
STR_OPEN: "Abrir"
STR_DOWNLOAD: "Baixar"
STR_RETRY: "Tentar novamente"
STR_YES: "Sim"
STR_NO: "Não"
STR_STATE_ON: "LIG."
STR_STATE_OFF: "DESL."
STR_SET: "Definir"
STR_NOT_SET: "Não definido"
STR_DIR_LEFT: "Esquerda"
STR_DIR_RIGHT: "Direita"
STR_DIR_UP: "Cima"
STR_DIR_DOWN: "Baixo"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[LIGADO]"
STR_SLEEP_COVER_FILTER: "Filtro capa tela repouso"
STR_FILTER_CONTRAST: "Contraste"
STR_STATUS_BAR_FULL_PERCENT: "Completa c/ porcentagem"
STR_STATUS_BAR_FULL_BOOK: "Completa c/ barra livro"
STR_STATUS_BAR_BOOK_ONLY: "Só barra do livro"
STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo"
STR_UI_THEME: "Tema da interface"
STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Capa + personalizado"
STR_RECENTS: "Recentes"
STR_MENU_RECENT_BOOKS: "Livros recentes"
STR_NO_RECENT_BOOKS: "Sem livros recentes"
STR_CALIBRE_DESC: "Usar transferências sem fio Calibre"
STR_FORGET_AND_REMOVE: "Esquecer a rede e remover a senha salva?"
STR_FORGET_BUTTON: "Esquecer rede"
STR_CALIBRE_STARTING: "Iniciando Calibre..."
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_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_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: "Excluir cache do livro"
STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: "páginas |"
STR_BOOK_PREFIX: "Livro:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "TRAVAR"
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_FETCH_PROGRESS: "Buscando progresso remoto..."
STR_UPLOAD_PROGRESS: "Enviando progresso..."
STR_NO_CREDENTIALS_MSG: "Nenhuma credencial configurada"
STR_KOREADER_SETUP_HINT: "Configure a conta do KOReader em Config."
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_SECTION_PREFIX: "Seção"
STR_UPLOAD: "Enviar"
STR_BOOK_S_STYLE: "Estilo do livro"
STR_EMBEDDED_STYLE: "Estilo embutido"
STR_OPDS_SERVER_URL: "URL do servidor OPDS"

View File

@@ -0,0 +1,317 @@
_language_name: "Русский"
_language_code: "RUSSIAN"
_order: "6"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "Загрузка"
STR_SLEEPING: "Спящий режим"
STR_ENTERING_SLEEP: "Переход в сон..."
STR_BROWSE_FILES: "Обзор файлов"
STR_FILE_TRANSFER: "Передача файлов"
STR_SETTINGS_TITLE: "Настройки"
STR_CALIBRE_LIBRARY: "Библиотека Calibre"
STR_CONTINUE_READING: "Продолжить чтение"
STR_NO_OPEN_BOOK: "Нет открытой книги"
STR_START_READING: "Начать чтение ниже"
STR_BOOKS: "Книги"
STR_NO_BOOKS_FOUND: "Книги не найдены"
STR_SELECT_CHAPTER: "Выберите главу"
STR_NO_CHAPTERS: "Глав нет"
STR_END_OF_BOOK: "Конец книги"
STR_EMPTY_CHAPTER: "Пустая глава"
STR_INDEXING: "Индексация..."
STR_MEMORY_ERROR: "Ошибка памяти"
STR_PAGE_LOAD_ERROR: "Ошибка загрузки страницы"
STR_EMPTY_FILE: "Пустой файл"
STR_OUT_OF_BOUNDS: "Выход за пределы"
STR_LOADING: "Загрузка..."
STR_LOAD_XTC_FAILED: "Не удалось загрузить XTC"
STR_LOAD_TXT_FAILED: "Не удалось загрузить TXT"
STR_LOAD_EPUB_FAILED: "Не удалось загрузить EPUB"
STR_SD_CARD_ERROR: "Ошибка SD-карты"
STR_WIFI_NETWORKS: "Wi-Fi сети"
STR_NO_NETWORKS: "Сети не найдены"
STR_NETWORKS_FOUND: "Найдено сетей: %zu"
STR_SCANNING: "Сканирование..."
STR_CONNECTING: "Подключение..."
STR_CONNECTED: "Подключено!"
STR_CONNECTION_FAILED: "Ошибка подключения"
STR_CONNECTION_TIMEOUT: "Тайм-аут подключения"
STR_FORGET_NETWORK: "Забыть сеть?"
STR_SAVE_PASSWORD: "Сохранить пароль?"
STR_REMOVE_PASSWORD: "Удалить сохранённый пароль?"
STR_PRESS_OK_SCAN: "Нажмите OK для повторного поиска"
STR_PRESS_ANY_CONTINUE: "Нажмите любую кнопку"
STR_SELECT_HINT: "ВЛЕВО/ВПРАВО: выбор | OK: подтвердить"
STR_HOW_CONNECT: "Как вы хотите подключиться?"
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 по Wi-Fi"
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
STR_CONNECT_WIRELESS: "Подключить как беспроводное устройство"
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
STR_MAC_ADDRESS: "MAC-адрес:"
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Введите пароль Wi-Fi"
STR_ENTER_TEXT: "Введите текст"
STR_TO_PREFIX: "к "
STR_CALIBRE_DISCOVERING: "Поиск Calibre..."
STR_CALIBRE_CONNECTING_TO: "Подключение к "
STR_CALIBRE_CONNECTED_TO: "Подключено к "
STR_CALIBRE_WAITING_COMMANDS: "Ожидание команд..."
STR_CONNECTION_FAILED_RETRYING: "(Ошибка подключения"
STR_CALIBRE_DISCONNECTED: "Соединение с Calibre разорвано"
STR_CALIBRE_WAITING_TRANSFER: "Ожидание передачи..."
STR_CALIBRE_TRANSFER_HINT: "Если передача не удаётся"
STR_CALIBRE_RECEIVING: "Получение:"
STR_CALIBRE_RECEIVED: "Получено:"
STR_CALIBRE_WAITING_MORE: "Ожидание следующих файлов..."
STR_CALIBRE_FAILED_CREATE_FILE: "Не удалось создать файл"
STR_CALIBRE_PASSWORD_REQUIRED: "Требуется пароль"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Передача прервана"
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_SLEEP_COVER_MODE: "Режим обложки сна"
STR_STATUS_BAR: "Строка состояния"
STR_HIDE_BATTERY: "Скрыть % батареи"
STR_EXTRA_SPACING: "Доп. интервал абзаца"
STR_TEXT_AA: "Сглаживание текста"
STR_SHORT_PWR_BTN: "Короткое нажатие PWR"
STR_ORIENTATION: "Ориентация чтения"
STR_FRONT_BTN_LAYOUT: "Боковые кнопки"
STR_SIDE_BTN_LAYOUT: "Боковые кнопки"
STR_LONG_PRESS_SKIP: "Долгое нажатие - смена главы"
STR_FONT_FAMILY: "Шрифт чтения"
STR_EXT_READER_FONT: "Внешний шрифт чтения"
STR_EXT_CHINESE_FONT: "Шрифт CJK"
STR_EXT_UI_FONT: "Шрифт интерфейса"
STR_FONT_SIZE: "Размер шрифта интерфейса"
STR_LINE_SPACING: "Межстрочный интервал"
STR_ASCII_LETTER_SPACING: "Интервал букв ASCII"
STR_ASCII_DIGIT_SPACING: "Интервал цифр ASCII"
STR_CJK_SPACING: "Интервал CJK"
STR_COLOR_MODE: "Цветовой режим"
STR_SCREEN_MARGIN: "Поля экрана"
STR_PARA_ALIGNMENT: "Выравнивание абзаца"
STR_HYPHENATION: "Перенос слов"
STR_TIME_TO_SLEEP: "Сон Через"
STR_REFRESH_FREQ: "Частота обновления"
STR_CALIBRE_SETTINGS: "Настройки Calibre"
STR_KOREADER_SYNC: "Синхронизация KOReader"
STR_CHECK_UPDATES: "Проверить обновления"
STR_LANGUAGE: "Язык"
STR_SELECT_WALLPAPER: "Выбрать обои"
STR_CLEAR_READING_CACHE: "Очистить кэш чтения"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Имя пользователя"
STR_PASSWORD: "Пароль"
STR_SYNC_SERVER_URL: "URL сервера синхронизации"
STR_DOCUMENT_MATCHING: "Сопоставление документов"
STR_AUTHENTICATE: "Авторизация"
STR_KOREADER_USERNAME: "Имя пользователя KOReader"
STR_KOREADER_PASSWORD: "Пароль KOReader"
STR_FILENAME: "Имя файла"
STR_BINARY: "Бинарный"
STR_SET_CREDENTIALS_FIRST: "Сначала укажите данные"
STR_WIFI_CONN_FAILED: "Не удалось подключиться к Wi-Fi"
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: "Проверьте вывод по UART для деталей"
STR_DARK: "Тёмный"
STR_LIGHT: "Светлый"
STR_CUSTOM: "Свой"
STR_COVER: "Обложка"
STR_NONE_OPT: "Нет"
STR_FIT: "Вписать"
STR_CROP: "Обрезать"
STR_NO_PROGRESS: "Без прогресса"
STR_FULL_OPT: "Полная"
STR_NEVER: "Никогда"
STR_IN_READER: "В режиме чтения"
STR_ALWAYS: "Всегда"
STR_IGNORE: "Игнорировать"
STR_SLEEP: "Сон"
STR_PAGE_TURN: "Перелистывание"
STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Ландшафт (CW)"
STR_INVERTED: "Инверсия"
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
STR_FRONT_LAYOUT_BCLR: "Наз"
STR_FRONT_LAYOUT_LRBC: "Лев"
STR_FRONT_LAYOUT_LBCR: "Лев"
STR_PREV_NEXT: "Назад/Вперёд"
STR_NEXT_PREV: "Вперёд/Назад"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
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_MIN_1: "1 мин"
STR_MIN_5: "5 мин"
STR_MIN_10: "10 мин"
STR_MIN_15: "15 мин"
STR_MIN_30: "30 мин"
STR_PAGES_1: "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_EXTERNAL_FONT: "Внешний шрифт"
STR_BUILTIN_DISABLED: "Встроенный (отключён)"
STR_NO_ENTRIES: "Записи не найдены"
STR_DOWNLOADING: "Загрузка..."
STR_DOWNLOAD_FAILED: "Ошибка загрузки"
STR_ERROR_MSG: "Ошибка:"
STR_UNNAMED: "Без имени"
STR_NO_SERVER_URL: "URL сервера не настроен"
STR_FETCH_FEED_FAILED: "Не удалось получить ленту"
STR_PARSE_FEED_FAILED: "Не удалось обработать ленту"
STR_NETWORK_PREFIX: "Сеть:"
STR_IP_ADDRESS_PREFIX: "IP-адрес:"
STR_SCAN_QR_WIFI_HINT: "или отсканируйте QR-код для подключения к Wi-Fi."
STR_ERROR_GENERAL_FAILURE: "Ошибка: Общая ошибка"
STR_ERROR_NETWORK_NOT_FOUND: "Ошибка: Сеть не найдена"
STR_ERROR_CONNECTION_TIMEOUT: "Ошибка: Тайм-аут соединения"
STR_SD_CARD: "SD-карта"
STR_BACK: "« Назад"
STR_EXIT: "« Выход"
STR_HOME: "« Главная"
STR_SAVE: "« Сохранить"
STR_SELECT: "Выбрать"
STR_TOGGLE: "Выбор"
STR_CONFIRM: "Подтв."
STR_CANCEL: "Отмена"
STR_CONNECT: "Подкл."
STR_OPEN: "Открыть"
STR_DOWNLOAD: "Скачать"
STR_RETRY: "Повторить"
STR_YES: "Да"
STR_NO: "Нет"
STR_STATE_ON: "ВКЛ"
STR_STATE_OFF: "ВЫКЛ"
STR_SET: "Установлено"
STR_NOT_SET: "Не установлено"
STR_DIR_LEFT: "Влево"
STR_DIR_RIGHT: "Вправо"
STR_DIR_UP: "Вверх"
STR_DIR_DOWN: "Вниз"
STR_CAPS_ON: "CAPS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ВКЛ]"
STR_SLEEP_COVER_FILTER: "Фильтр обложки сна"
STR_FILTER_CONTRAST: "Контраст"
STR_STATUS_BAR_FULL_PERCENT: "Полная + %"
STR_STATUS_BAR_FULL_BOOK: "Полная + шкала книги"
STR_STATUS_BAR_BOOK_ONLY: "Только шкала книги"
STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы"
STR_UI_THEME: "Тема интерфейса"
STR_THEME_CLASSIC: "Классическая"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_OPDS_BROWSER: "OPDS браузер"
STR_COVER_CUSTOM: "Обложка + Свой"
STR_RECENTS: "Недавние"
STR_MENU_RECENT_BOOKS: "Недавние книги"
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_CHAPTER_PREFIX: "Глава:"
STR_PAGES_SEPARATOR: "стр. |"
STR_BOOK_PREFIX: "Книга:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
STR_CALIBRE_URL_HINT: "Для Calibre добавьте /opds к URL"
STR_PERCENT_STEP_HINT: "Влево/Вправо: 1% Вверх/Вниз: 10%"
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"
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_SECTION_PREFIX: "Раздел"
STR_UPLOAD: "Отправить"
STR_BOOK_S_STYLE: "Стиль книги"
STR_EMBEDDED_STYLE: "Встроенный стиль"
STR_OPDS_SERVER_URL: "URL OPDS сервера"

View File

@@ -0,0 +1,317 @@
_language_name: "Español"
_language_code: "SPANISH"
_order: "1"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "BOOTING"
STR_SLEEPING: "SLEEPING"
STR_ENTERING_SLEEP: "ENTERING SLEEP..."
STR_BROWSE_FILES: "Buscar archivos"
STR_FILE_TRANSFER: "Transferencia de archivos"
STR_SETTINGS_TITLE: "Configuración"
STR_CALIBRE_LIBRARY: "Libreria Calibre"
STR_CONTINUE_READING: "Continuar leyendo"
STR_NO_OPEN_BOOK: "No hay libros abiertos"
STR_START_READING: "Start reading below"
STR_BOOKS: "Libros"
STR_NO_BOOKS_FOUND: "No se encontraron libros"
STR_SELECT_CHAPTER: "Seleccionar capítulo"
STR_NO_CHAPTERS: "Sin capítulos"
STR_END_OF_BOOK: "Fin del libro"
STR_EMPTY_CHAPTER: "Capítulo vacío"
STR_INDEXING: "Indexando..."
STR_MEMORY_ERROR: "Error de memoria"
STR_PAGE_LOAD_ERROR: "Error al cargar la página"
STR_EMPTY_FILE: "Archivo vacío"
STR_OUT_OF_BOUNDS: "Out of bounds"
STR_LOADING: "Cargando..."
STR_LOAD_XTC_FAILED: "Error al cargar XTC"
STR_LOAD_TXT_FAILED: "Error al cargar TXT"
STR_LOAD_EPUB_FAILED: "Error al cargar EPUB"
STR_SD_CARD_ERROR: "Error en la tarjeta SD"
STR_WIFI_NETWORKS: "Redes Wi-Fi"
STR_NO_NETWORKS: "No hay redes disponibles"
STR_NETWORKS_FOUND: "%zu redes encontradas"
STR_SCANNING: "Buscando..."
STR_CONNECTING: "Conectando..."
STR_CONNECTED: "Conectado!"
STR_CONNECTION_FAILED: "Error de conexion"
STR_CONNECTION_TIMEOUT: "Connection timeout"
STR_FORGET_NETWORK: "Olvidar la red?"
STR_SAVE_PASSWORD: "Guardar contraseña para la próxima vez?"
STR_REMOVE_PASSWORD: "Borrar contraseñas guardadas?"
STR_PRESS_OK_SCAN: "Presione OK para buscar de nuevo"
STR_PRESS_ANY_CONTINUE: "Presione cualquier botón para continuar"
STR_SELECT_HINT: "Izquierda/Derecha: Seleccionar | OK: Confirmar"
STR_HOW_CONNECT: "Cómo te gustaría conectarte?"
STR_JOIN_NETWORK: "Unirse a una red"
STR_CREATE_HOTSPOT: "Crear punto de acceso"
STR_JOIN_DESC: "Conectarse a una red Wi-Fi existente"
STR_HOTSPOT_DESC: "Crear una red Wi-Fi para que otros se unan"
STR_STARTING_HOTSPOT: "Iniciando punto de acceso..."
STR_HOTSPOT_MODE: "Modo punto de acceso"
STR_CONNECT_WIFI_HINT: "Conectar su dispositivo a esta red Wi-Fi"
STR_OPEN_URL_HINT: "Abre esta dirección en tu navegador"
STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanee este código QR con su móvil:"
STR_CALIBRE_WIRELESS: "Calibre inalámbrico"
STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre"
STR_CONNECT_WIRELESS: "Conectar como dispositivo inalámbrico"
STR_NETWORK_LEGEND: "* = Cifrado | + = Guardado"
STR_MAC_ADDRESS: "Dirección MAC:"
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Introduzca la contraseña de Wi-Fi"
STR_ENTER_TEXT: "Introduzca el texto"
STR_TO_PREFIX: "a "
STR_CALIBRE_DISCOVERING: "Discovering Calibre..."
STR_CALIBRE_CONNECTING_TO: "Conectándose a"
STR_CALIBRE_CONNECTED_TO: "Conectado a "
STR_CALIBRE_WAITING_COMMANDS: "Esperando comandos..."
STR_CONNECTION_FAILED_RETRYING: "(Error de conexión, intentándolo nuevamente)"
STR_CALIBRE_DISCONNECTED: "Calibre desconectado"
STR_CALIBRE_WAITING_TRANSFER: "Esperando transferencia..."
STR_CALIBRE_TRANSFER_HINT: "Si la transferencia falla, habilite \\n'Ignorar espacio libre' en las configuraciones del \\nplugin smartdevice de calibre."
STR_CALIBRE_RECEIVING: "Recibiendo: "
STR_CALIBRE_RECEIVED: "Recibido: "
STR_CALIBRE_WAITING_MORE: "Esperando más..."
STR_CALIBRE_FAILED_CREATE_FILE: "Error al crear el archivo"
STR_CALIBRE_PASSWORD_REQUIRED: "Contraseña requerida"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Transferencia interrumpida"
STR_CALIBRE_INSTRUCTION_1: "1) Instala CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Conéctese a la misma red Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) En Calibre: \"Enviar a dispotivo\""
STR_CALIBRE_INSTRUCTION_4: "\"Permanezca en esta pantalla mientras se envía\""
STR_CAT_DISPLAY: "Pantalla"
STR_CAT_READER: "Lector"
STR_CAT_CONTROLS: "Control"
STR_CAT_SYSTEM: "Sistema"
STR_SLEEP_SCREEN: "Salva Pantallas"
STR_SLEEP_COVER_MODE: "Modo de salva pantallas"
STR_STATUS_BAR: "Barra de estado"
STR_HIDE_BATTERY: "Ocultar porcentaje de batería"
STR_EXTRA_SPACING: "Espaciado extra de párrafos"
STR_TEXT_AA: "Suavizado de bordes de texto"
STR_SHORT_PWR_BTN: "Clic breve del botón de encendido"
STR_ORIENTATION: "Orientación de la lectura"
STR_FRONT_BTN_LAYOUT: "Diseño de los botones frontales"
STR_SIDE_BTN_LAYOUT: "Diseño de los botones laterales (Lector)"
STR_LONG_PRESS_SKIP: "Pasar a la capítulo al presiónar largamente"
STR_FONT_FAMILY: "Familia de tipografía del lector"
STR_EXT_READER_FONT: "Tipografía externa"
STR_EXT_CHINESE_FONT: "Tipografía (Lectura)"
STR_EXT_UI_FONT: "Tipografía (Pantalla)"
STR_FONT_SIZE: "Tamaño de la fuente (Pantalla)"
STR_LINE_SPACING: "Interlineado (Lectura)"
STR_ASCII_LETTER_SPACING: "Espaciado de letras ASCII"
STR_ASCII_DIGIT_SPACING: "Espaciado de dígitos ASCII"
STR_CJK_SPACING: "Espaciado CJK"
STR_COLOR_MODE: "Modo de color"
STR_SCREEN_MARGIN: "Margen de lectura"
STR_PARA_ALIGNMENT: "Ajuste de parágrafo del lector"
STR_HYPHENATION: "Hyphenation"
STR_TIME_TO_SLEEP: "Tiempo para dormir"
STR_REFRESH_FREQ: "Frecuencia de actualización"
STR_CALIBRE_SETTINGS: "Configuraciones de Calibre"
STR_KOREADER_SYNC: "Síncronización de KOReader"
STR_CHECK_UPDATES: "Verificar actualizaciones"
STR_LANGUAGE: "Idioma"
STR_SELECT_WALLPAPER: "Seleccionar fondo"
STR_CLEAR_READING_CACHE: "Borrar caché de lectura"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Nombre de usuario"
STR_PASSWORD: "Contraseña"
STR_SYNC_SERVER_URL: "URL del servidor de síncronización"
STR_DOCUMENT_MATCHING: "Coincidencia de documentos"
STR_AUTHENTICATE: "Autentificar"
STR_KOREADER_USERNAME: "Nombre de usuario de KOReader"
STR_KOREADER_PASSWORD: "Contraseña de KOReader"
STR_FILENAME: "Nombre del archivo"
STR_BINARY: "Binario"
STR_SET_CREDENTIALS_FIRST: "Configurar credenciales primero"
STR_WIFI_CONN_FAILED: "Falló la conexión Wi-Fi"
STR_AUTHENTICATING: "Autentificando..."
STR_AUTH_SUCCESS: "Autenticación exitsosa!"
STR_KOREADER_AUTH: "Autenticación KOReader"
STR_SYNC_READY: "La síncronización de KOReader está lista para usarse"
STR_AUTH_FAILED: "Falló la autenticación"
STR_DONE: "Hecho"
STR_CLEAR_CACHE_WARNING_1: "Esto borrará todos los datos en cache del libro."
STR_CLEAR_CACHE_WARNING_2: " ¡Se perderá todo el avance de leer!"
STR_CLEAR_CACHE_WARNING_3: "Los libros deberán ser reíndexados"
STR_CLEAR_CACHE_WARNING_4: "cuando se abran de nuevo."
STR_CLEARING_CACHE: "Borrando caché..."
STR_CACHE_CLEARED: "Cache limpia"
STR_ITEMS_REMOVED: "Elementos eliminados"
STR_FAILED_LOWER: "Falló"
STR_CLEAR_CACHE_FAILED: "No se pudo borrar la cache"
STR_CHECK_SERIAL_OUTPUT: "Verifique la salida serial para detalles"
STR_DARK: "Oscuro"
STR_LIGHT: "Claro"
STR_CUSTOM: "Personalizado"
STR_COVER: "Portada"
STR_NONE_OPT: "Ninguno"
STR_FIT: "Ajustar"
STR_CROP: "Recortar"
STR_NO_PROGRESS: "Sin avance"
STR_FULL_OPT: "Completa"
STR_NEVER: "Nunca"
STR_IN_READER: "En el lector"
STR_ALWAYS: "Siempre"
STR_IGNORE: "Ignorar"
STR_SLEEP: "Dormir"
STR_PAGE_TURN: "Paso de página"
STR_PORTRAIT: "Portrato"
STR_LANDSCAPE_CW: "Paisaje sentido horario"
STR_INVERTED: "Invertido"
STR_LANDSCAPE_CCW: "Paisaje sentido antihorario"
STR_FRONT_LAYOUT_BCLR: "Atrás, Confirmar, Izquierda, Derecha"
STR_FRONT_LAYOUT_LRBC: "Izquierda, Derecha, Atrás, Confirmar"
STR_FRONT_LAYOUT_LBCR: "Izquierda, Atrás, Confirmar, Derecha"
STR_PREV_NEXT: "Anterior/Siguiente"
STR_NEXT_PREV: "Siguiente/Anterior"
STR_BOOKERLY: "Relacionado con libros"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Open Dyslexic"
STR_SMALL: "Pequeño"
STR_MEDIUM: "Medio"
STR_LARGE: "Grande"
STR_X_LARGE: "Extra grande"
STR_TIGHT: "Ajustado"
STR_NORMAL: "Normal"
STR_WIDE: "Ancho"
STR_JUSTIFY: "Justificar"
STR_ALIGN_LEFT: "Izquierda"
STR_CENTER: "Centro"
STR_ALIGN_RIGHT: "Derecha"
STR_MIN_1: "1 Minuto"
STR_MIN_5: "10 Minutos"
STR_MIN_10: "5 Minutos"
STR_MIN_15: "15 Minutos"
STR_MIN_30: "30 Minutos"
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: "ActualizaR"
STR_CHECKING_UPDATE: "Verificando actualización..."
STR_NEW_UPDATE: "¡Nueva actualización disponible!"
STR_CURRENT_VERSION: "Versión actual:"
STR_NEW_VERSION: "Nueva versión:"
STR_UPDATING: "Actualizando..."
STR_NO_UPDATE: "No hay actualizaciones disponibles"
STR_UPDATE_FAILED: "Falló la actualización"
STR_UPDATE_COMPLETE: "Actualización completada"
STR_POWER_ON_HINT: "Presione y mantenga presionado el botón de encendido para volver a encender"
STR_EXTERNAL_FONT: "Fuente externa"
STR_BUILTIN_DISABLED: "Incorporado (Desactivado)"
STR_NO_ENTRIES: "No se encontraron elementos"
STR_DOWNLOADING: "Descargando..."
STR_DOWNLOAD_FAILED: "Falló la descarga"
STR_ERROR_MSG: "Error"
STR_UNNAMED: "Sin nombre"
STR_NO_SERVER_URL: "No se ha configurado la url del servidor"
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_NETWORK_PREFIX: "Red: "
STR_IP_ADDRESS_PREFIX: "Dirección IP: "
STR_SCAN_QR_WIFI_HINT: "O escanee el código QR con su teléfono para conectarse a WI-FI."
STR_ERROR_GENERAL_FAILURE: "Error: Fallo general"
STR_ERROR_NETWORK_NOT_FOUND: "Error: Red no encontrada"
STR_ERROR_CONNECTION_TIMEOUT: "Error: Connection timeout"
STR_SD_CARD: "Tarjeta SD"
STR_BACK: "« Atrás"
STR_EXIT: "« SaliR"
STR_HOME: "« Inicio"
STR_SAVE: "« Guardar"
STR_SELECT: "Seleccionar"
STR_TOGGLE: "Cambiar"
STR_CONFIRM: "Confirmar"
STR_CANCEL: "Cancelar"
STR_CONNECT: "Conectar"
STR_OPEN: "Abrir"
STR_DOWNLOAD: "Descargar"
STR_RETRY: "Reintentar"
STR_YES: "Sí"
STR_NO: "No"
STR_STATE_ON: "ENCENDIDO"
STR_STATE_OFF: "APAGADO"
STR_SET: "Configurar"
STR_NOT_SET: "No configurado"
STR_DIR_LEFT: "Izquierda"
STR_DIR_RIGHT: "Derecha"
STR_DIR_UP: "Arriba"
STR_DIR_DOWN: "Abajo"
STR_CAPS_ON: "MAYÚSCULAS"
STR_CAPS_OFF: "caps"
STR_OK_BUTTON: "OK"
STR_ON_MARKER: "[ENCENDIDO]"
STR_SLEEP_COVER_FILTER: "Filtro de salva pantalla y protección de la pantalla"
STR_FILTER_CONTRAST: "Contraste"
STR_STATUS_BAR_FULL_PERCENT: "Completa con porcentaje"
STR_STATUS_BAR_FULL_BOOK: "Completa con progreso del libro"
STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro"
STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos"
STR_UI_THEME: "Estilo de pantalla"
STR_THEME_CLASSIC: "Clásico"
STR_THEME_LYRA: "LYRA"
STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_OPDS_BROWSER: "Navegador opds"
STR_COVER_CUSTOM: "Portada + Personalizado"
STR_RECENTS: "Recientes"
STR_MENU_RECENT_BOOKS: "Libros recientes"
STR_NO_RECENT_BOOKS: "No hay libros recientes"
STR_CALIBRE_DESC: "Utilice las transferencias dispositivos inalámbricos de calibre"
STR_FORGET_AND_REMOVE: "Olvidar la red y eliminar la contraseña guardada?"
STR_FORGET_BUTTON: "Olvidar la red"
STR_CALIBRE_STARTING: "Iniciando calibre..."
STR_CALIBRE_SETUP: "Configuración"
STR_CALIBRE_STATUS: "Estado"
STR_CLEAR_BUTTON: "Borrar"
STR_DEFAULT_VALUE: "Previo"
STR_REMAP_PROMPT: "Presione un botón frontal para cada función"
STR_UNASSIGNED: "No asignado"
STR_ALREADY_ASSIGNED: "Ya asignado"
STR_REMAP_RESET_HINT: "Botón lateral arriba: Restablecer a la configuración previo"
STR_REMAP_CANCEL_HINT: "Botón lateral abajo: Anular reconfiguración"
STR_HW_BACK_LABEL: "Atrás (Primer botón)"
STR_HW_CONFIRM_LABEL: "Confirmar (Segundo botón)"
STR_HW_LEFT_LABEL: "Izquierda (Tercer botón)"
STR_HW_RIGHT_LABEL: "Derecha (Cuarto botón)"
STR_GO_TO_PERCENT: "Ir a %"
STR_GO_HOME_BUTTON: "Volver a inicio"
STR_SYNC_PROGRESS: "Progreso de síncronización"
STR_DELETE_CACHE: "Borrar cache del libro"
STR_CHAPTER_PREFIX: "Capítulo:"
STR_PAGES_SEPARATOR: " Páginas |"
STR_BOOK_PREFIX: "Libro:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "BLOQUEAR"
STR_CALIBRE_URL_HINT: "Para calibre, agregue /opds a su urL"
STR_PERCENT_STEP_HINT: "Izquierda/Derecha: 1% Arriba/Abajo: 10%"
STR_SYNCING_TIME: "Tiempo de síncronización..."
STR_CALC_HASH: "Calculando hash del documento..."
STR_HASH_FAILED: "No se pudo calcular el hash del documento"
STR_FETCH_PROGRESS: "Recuperando progreso remoto..."
STR_UPLOAD_PROGRESS: "Subiendo progreso..."
STR_NO_CREDENTIALS_MSG: "No se han configurado credenciales"
STR_KOREADER_SETUP_HINT: "Configure una cuenta de KOReader en la configuración"
STR_PROGRESS_FOUND: "¡Progreso encontrado!"
STR_REMOTE_LABEL: "Remoto"
STR_LOCAL_LABEL: "Local"
STR_PAGE_OVERALL_FORMAT: "Página %d, %.2f%% Completada"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Página %d / %d, %.2f% Completada"
STR_DEVICE_FROM_FORMAT: " De: %s"
STR_APPLY_REMOTE: "Aplicar progreso remoto"
STR_UPLOAD_LOCAL: "Subir progreso local"
STR_NO_REMOTE_MSG: "No se encontró progreso remoto"
STR_UPLOAD_PROMPT: "Subir posicion actual?"
STR_UPLOAD_SUCCESS: "¡Progreso subido!"
STR_SYNC_FAILED_MSG: "Fallo de síncronización"
STR_SECTION_PREFIX: "Seccion"
STR_UPLOAD: "Subir"
STR_BOOK_S_STYLE: "Estilo del libro"
STR_EMBEDDED_STYLE: "Estilo integrado"
STR_OPDS_SERVER_URL: "URL del servidor OPDS"

View File

@@ -0,0 +1,317 @@
_language_name: "Svenska"
_language_code: "SWEDISH"
_order: "7"
STR_CROSSPOINT: "Crosspoint"
STR_BOOTING: "STARTAR"
STR_SLEEPING: "VILA"
STR_ENTERING_SLEEP: "Går i vila…"
STR_BROWSE_FILES: "Bläddra filer…"
STR_FILE_TRANSFER: "Filöverföring"
STR_SETTINGS_TITLE: "Inställningar"
STR_CALIBRE_LIBRARY: "Calibrebibliotek"
STR_CONTINUE_READING: "Fortsätt läsa"
STR_NO_OPEN_BOOK: "Ingen öppen bok"
STR_START_READING: "Börja läsa nedan"
STR_BOOKS: "Böcker"
STR_NO_BOOKS_FOUND: "Inga böcker hittade"
STR_SELECT_CHAPTER: "Välj kapitel"
STR_NO_CHAPTERS: "Inga kapitel"
STR_END_OF_BOOK: "Slutet på boken"
STR_EMPTY_CHAPTER: "Tomt kapitel"
STR_INDEXING: "Indexerar…"
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_LOAD_XTC_FAILED: "Misslyckades ladda XTC"
STR_LOAD_TXT_FAILED: "Misslyckades ladda TCT"
STR_LOAD_EPUB_FAILED: "Misslyckades ladda EPUB"
STR_SD_CARD_ERROR: "SD-kortfel"
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_CONNECTING: "Ansluter…"
STR_CONNECTED: "Ansluten!"
STR_CONNECTION_FAILED: "Anslutning misslyckades"
STR_CONNECTION_TIMEOUT: "Anslutnings timeout"
STR_FORGET_NETWORK: "Glöm nätverk?"
STR_SAVE_PASSWORD: "Spara lösenord till nästa gång?"
STR_REMOVE_PASSWORD: "Radera sparat lösenord?"
STR_PRESS_OK_SCAN: "Tryck OK för att skanna igen"
STR_PRESS_ANY_CONTINUE: "Tryck valfri knapp för att fortsätta"
STR_SELECT_HINT: "VÄNSTER/HÖGER: Välj OK: Bekräfta"
STR_HOW_CONNECT: "Hur vill du ansluta?"
STR_JOIN_NETWORK: "Anslut till ett nätverk"
STR_CREATE_HOTSPOT: "Skapa surfzon"
STR_JOIN_DESC: "Anslut till ett befintligt trådlöst nätverk"
STR_HOTSPOT_DESC: "Skapa ett trådlöst nätverk andra kan ansluta till"
STR_STARTING_HOTSPOT: "Startar surfzon…"
STR_HOTSPOT_MODE: "Surfzonsläge"
STR_CONNECT_WIFI_HINT: "Anslut din enhet till detta trådlösa nätverk"
STR_OPEN_URL_HINT: "Öppna denna adress i din browser"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
STR_CALIBRE_WIRELESS: "Calibre Trådlöst"
STR_CALIBRE_WEB_URL: "Calibre webbadress"
STR_CONNECT_WIRELESS: "Anslut som trådlös enhet"
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 WiFi-lösenord"
STR_ENTER_TEXT: "Skriv text"
STR_TO_PREFIX: "till"
STR_CALIBRE_DISCOVERING: "Söker Calibre…"
STR_CALIBRE_CONNECTING_TO: "Ansluter till"
STR_CALIBRE_CONNECTED_TO: "Ansluten till"
STR_CALIBRE_WAITING_COMMANDS: "Väntar på kommandon…"
STR_CONNECTION_FAILED_RETRYING: "(Anslutning misslyckades. Försöker igen)"
STR_CALIBRE_DISCONNECTED: "Calibre nedkopplat"
STR_CALIBRE_WAITING_TRANSFER: "Väntar på överföring…"
STR_CALIBRE_TRANSFER_HINT: "Om överföring misslyckas: Aktivera\\n'Ignorera fritt utrymme' i Calibre's\\nSmartDevice plugin settings."
STR_CALIBRE_RECEIVING: "Tar emot:"
STR_CALIBRE_RECEIVED: "Mottaget:"
STR_CALIBRE_WAITING_MORE: "Väntar på mer.."
STR_CALIBRE_FAILED_CREATE_FILE: "Misslyckades att skapa fil"
STR_CALIBRE_PASSWORD_REQUIRED: "Lösenord krävs"
STR_CALIBRE_TRANSFER_INTERRUPTED: "Överföring avbröts"
STR_CALIBRE_INSTRUCTION_1: "1) Installera CrossPoint Reader plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Anslut till samma trådlösa nätverk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: ”Skicka till enhet”"
STR_CALIBRE_INSTRUCTION_4: "”Håll denna skärm öppen under sändning”"
STR_CAT_DISPLAY: "Skärm"
STR_CAT_READER: "Läsare"
STR_CAT_CONTROLS: "Kontroller"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Viloskärm"
STR_SLEEP_COVER_MODE: "Viloskärmens omslagsläge"
STR_STATUS_BAR: "Statusrad"
STR_HIDE_BATTERY: "Dölj batteriprocent"
STR_EXTRA_SPACING: "Extra paragrafmellanrum"
STR_TEXT_AA: "Textkantutjämning"
STR_SHORT_PWR_BTN: "Kort strömknappsklick"
STR_ORIENTATION: "Läsrikting"
STR_FRONT_BTN_LAYOUT: "Frontknappslayout"
STR_SIDE_BTN_LAYOUT: "Sidoknappslayout (Läsare)"
STR_LONG_PRESS_SKIP: "Lång-tryck Kapitelskippning"
STR_FONT_FAMILY: "Eboksläsarens typsnittsfamilj"
STR_EXT_READER_FONT: "Extern Eboksläsartypsnitt"
STR_EXT_CHINESE_FONT: "Eboksläsartypsnitt"
STR_EXT_UI_FONT: "Användargränssnittets typsnitt"
STR_FONT_SIZE: "Användargränssnittets typsnittsstorlek"
STR_LINE_SPACING: "Eboksläsarens linjemellanrum"
STR_ASCII_LETTER_SPACING: "ASCII-bokstavsmellanrum"
STR_ASCII_DIGIT_SPACING: "ASCII-siffermellanrum"
STR_CJK_SPACING: "CJK-mellanrum"
STR_COLOR_MODE: "Färgläge"
STR_SCREEN_MARGIN: "Eboksläsarens skärmmarginal"
STR_PARA_ALIGNMENT: "Eboksläsarens paragraflinjeplacering"
STR_HYPHENATION: "Avstavning"
STR_TIME_TO_SLEEP: "Tid för att gå i vila"
STR_REFRESH_FREQ: "Uppdateringsfrekvens"
STR_CALIBRE_SETTINGS: "Calibreinställningar"
STR_KOREADER_SYNC: "KorReader-synkronisering"
STR_CHECK_UPDATES: "Kolla efter uppdateringar"
STR_LANGUAGE: "Språk"
STR_SELECT_WALLPAPER: "Välj bakgrundsbild"
STR_CLEAR_READING_CACHE: "Rensa Eboksläsarens cache"
STR_CALIBRE: "Calibre"
STR_USERNAME: "Användarnamn"
STR_PASSWORD: "Lösenord"
STR_SYNC_SERVER_URL: "Synkronisera serveradress"
STR_DOCUMENT_MATCHING: "Dokumentmatchning"
STR_AUTHENTICATE: "Autentisera "
STR_KOREADER_USERNAME: "KOReader användarnamn"
STR_KOREADER_PASSWORD: "KOReader lösenord"
STR_FILENAME: "Filnamn"
STR_BINARY: "Binär"
STR_SET_CREDENTIALS_FIRST: "Referenser"
STR_WIFI_CONN_FAILED: "Trådlös anslutning misslyckades"
STR_AUTHENTICATING: "Autentiserar…"
STR_AUTH_SUCCESS: "Lyckad autentisering!"
STR_KOREADER_AUTH: "KORreader autentisering"
STR_SYNC_READY: "KOReader synk är redo att användas"
STR_AUTH_FAILED: "Autentisering misslyckades"
STR_DONE: "Klar"
STR_CLEAR_CACHE_WARNING_1: "Detta rensar all cachad bokdata"
STR_CLEAR_CACHE_WARNING_2: "Alla läsframsteg kommer att försvinna!"
STR_CLEAR_CACHE_WARNING_3: "Böcker kommer att behöva omindexeras"
STR_CLEAR_CACHE_WARNING_4: "när de öppnas på nytt."
STR_CLEARING_CACHE: "Rensar cache…"
STR_CACHE_CLEARED: "Cache rensad!"
STR_ITEMS_REMOVED: "objekt raderade"
STR_FAILED_LOWER: "misslyckades "
STR_CLEAR_CACHE_FAILED: "Misslyckades att rensa cache"
STR_CHECK_SERIAL_OUTPUT: "Kolla seriell utgång för detaljer"
STR_DARK: "Mörk"
STR_LIGHT: "Ljus"
STR_CUSTOM: "Valfri"
STR_COVER: "Omslag"
STR_NONE_OPT: "Ingen öppen bok"
STR_FIT: "Passa"
STR_CROP: "Beskär"
STR_NO_PROGRESS: "Ingen framgång"
STR_FULL_OPT: "Full"
STR_NEVER: "Aldrig"
STR_IN_READER: "I Eboksläsare"
STR_ALWAYS: "Alltid"
STR_IGNORE: "Ignorera"
STR_SLEEP: "Vila"
STR_PAGE_TURN: "Sidvändning"
STR_PORTRAIT: "Porträtt"
STR_LANDSCAPE_CW: "Landskap medurs"
STR_INVERTED: "Inverterad"
STR_LANDSCAPE_CCW: "Landskap moturs"
STR_FRONT_LAYOUT_BCLR: "Bak, Bekr,Vän, Hög"
STR_FRONT_LAYOUT_LRBC: "Vän, Hög, Bak, Bekr"
STR_FRONT_LAYOUT_LBCR: "Vän, Bak, Bekr, Hög"
STR_PREV_NEXT: "Förra/Nästa"
STR_NEXT_PREV: "Nästa/Förra"
STR_BOOKERLY: "Bookerly"
STR_NOTO_SANS: "Noto Sans"
STR_OPEN_DYSLEXIC: "Öppen dyslektisk"
STR_SMALL: "Liten"
STR_MEDIUM: "Medium"
STR_LARGE: "Stor"
STR_X_LARGE: "Extra stor"
STR_TIGHT: "Smal"
STR_NORMAL: "Normal"
STR_WIDE: "Bred"
STR_JUSTIFY: "Rättfärdiga"
STR_ALIGN_LEFT: "Vänster"
STR_CENTER: "Mitten"
STR_ALIGN_RIGHT: "Höger"
STR_MIN_1: "1 min"
STR_MIN_5: "5 min"
STR_MIN_10: "10 min"
STR_MIN_15: "15 min"
STR_MIN_30: "30 min"
STR_PAGES_1: "1 sida"
STR_PAGES_5: "5 sidor"
STR_PAGES_10: "10 sidor"
STR_PAGES_15: "15 sidor"
STR_PAGES_30: "30 sidor"
STR_UPDATE: "Uppdatera"
STR_CHECKING_UPDATE: "Söker uppdatering…"
STR_NEW_UPDATE: "Ny uppdatering tillgänglig!"
STR_CURRENT_VERSION: "Nuvarande version:"
STR_NEW_VERSION: "Ny version:"
STR_UPDATING: "Uppdaterar…"
STR_NO_UPDATE: "Ingen uppdatering tillgänglig"
STR_UPDATE_FAILED: "Uppdatering misslyckades"
STR_UPDATE_COMPLETE: "Uppdatering färdig"
STR_POWER_ON_HINT: "Tryck och håll strömknappen för att sätta på igen"
STR_EXTERNAL_FONT: "Externt typsnitt"
STR_BUILTIN_DISABLED: "Inbyggd (Avstängd)"
STR_NO_ENTRIES: "Inga poster funna"
STR_DOWNLOADING: "Laddar ner…"
STR_DOWNLOAD_FAILED: "Nedladdning misslyckades"
STR_ERROR_MSG: "Fel:"
STR_UNNAMED: "Ej namngiven"
STR_NO_SERVER_URL: "Ingen serveradress konfigurerad"
STR_FETCH_FEED_FAILED: "Misslyckades att hämta flöde"
STR_PARSE_FEED_FAILED: "Misslyckades att analysera flöde"
STR_NETWORK_PREFIX: "Nätverk:"
STR_IP_ADDRESS_PREFIX: "IP-adress;"
STR_SCAN_QR_WIFI_HINT: "eller skanna QR-kod med din telefon för att ansluta till WiFi."
STR_ERROR_GENERAL_FAILURE: "Fel: Generellt fel"
STR_ERROR_NETWORK_NOT_FOUND: "Fel: Nätverk hittades inte"
STR_ERROR_CONNECTION_TIMEOUT: "Fel: Anslutningstimeout"
STR_SD_CARD: "SD-kort"
STR_BACK: "« Bak"
STR_EXIT: "« Avsluta"
STR_HOME: "« Hem"
STR_SAVE: "« Spara"
STR_SELECT: "Välj "
STR_TOGGLE: "Växla"
STR_CONFIRM: "Bekräfta"
STR_CANCEL: "Avbryt"
STR_CONNECT: "Anslut"
STR_OPEN: "Öppna"
STR_DOWNLOAD: "Ladda ner"
STR_RETRY: "Försök igen"
STR_YES: "Ja"
STR_NO: "Nej"
STR_STATE_ON: "PÅ"
STR_STATE_OFF: "AV"
STR_SET: "Inställd"
STR_NOT_SET: "Inte inställd"
STR_DIR_LEFT: "Vänster"
STR_DIR_RIGHT: "Höger"
STR_DIR_UP: "Upp"
STR_DIR_DOWN: "Ner"
STR_CAPS_ON: "VERSALER"
STR_CAPS_OFF: "versaler"
STR_OK_BUTTON: "Okej"
STR_ON_MARKER: "[PÅ]"
STR_SLEEP_COVER_FILTER: "Viloskärmens omslagsfilter"
STR_FILTER_CONTRAST: "Kontrast"
STR_STATUS_BAR_FULL_PERCENT: "Full w/ Procent"
STR_STATUS_BAR_FULL_BOOK: "Full w/ Boklist"
STR_STATUS_BAR_BOOK_ONLY: "Boklist enbart"
STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist"
STR_UI_THEME: "Användargränssnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_OPDS_BROWSER: "OPDS-webbläsare"
STR_COVER_CUSTOM: "Omslag + Valfri"
STR_RECENTS: "Senaste"
STR_MENU_RECENT_BOOKS: "Senaste böckerna"
STR_NO_RECENT_BOOKS: "Inga senaste böcker"
STR_CALIBRE_DESC: "Använd Calibres trådlösa enhetsöverföring"
STR_FORGET_AND_REMOVE: "Glöm nätverk och ta bort sparat lösenord?"
STR_FORGET_BUTTON: "Glöm nätverk"
STR_CALIBRE_STARTING: "Starar Calibre…"
STR_CALIBRE_SETUP: "Inställning"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Rensa"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Tryck en frontknapp för var funktion"
STR_UNASSIGNED: "Otilldelad"
STR_ALREADY_ASSIGNED: "Redan tilldelad"
STR_REMAP_RESET_HINT: "Översta sidoknapp: Återställ standardlayout"
STR_REMAP_CANCEL_HINT: "Nedre sidoknapp: Avbryt tilldelning"
STR_HW_BACK_LABEL: "Bak (Första knapp)"
STR_HW_CONFIRM_LABEL: "Bekräfta (Andra knapp)"
STR_HW_LEFT_LABEL: "Vänster (Tredje knapp)"
STR_HW_RIGHT_LABEL: "Höger (Fjärde knapp)"
STR_GO_TO_PERCENT: "Gå till %"
STR_GO_HOME_BUTTON: "Gå Hem"
STR_SYNC_PROGRESS: "Synkroniseringsframsteg"
STR_DELETE_CACHE: "Radera bokcache"
STR_CHAPTER_PREFIX: "Kapitel:"
STR_PAGES_SEPARATOR: " sidor | "
STR_BOOK_PREFIX: "Bok:"
STR_KBD_SHIFT: "shift"
STR_KBD_SHIFT_CAPS: "SHIFT"
STR_KBD_LOCK: "LOCK"
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"
STR_FETCH_PROGRESS: "Hämtar fjärrframsteg"
STR_UPLOAD_PROGRESS: "Laddar upp framsteg"
STR_NO_CREDENTIALS_MSG: "Inga uppgifter inställda"
STR_KOREADER_SETUP_HINT: "Ställ in KOReaderkonto i Inställningar"
STR_PROGRESS_FOUND: "Framsteg funna!"
STR_REMOTE_LABEL: "Fjärr:"
STR_LOCAL_LABEL: "Lokalt:"
STR_PAGE_OVERALL_FORMAT: "Sida %d, %.2f%% totalt"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Sida %d/%d, %.2f%% totalt"
STR_DEVICE_FROM_FORMAT: " Från: %s"
STR_APPLY_REMOTE: "Använd fjärrframsteg"
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_SYNC_FAILED_MSG: "Synkronisering misslyckades"
STR_SECTION_PREFIX: "Sektion"
STR_UPLOAD: "Uppladdning"
STR_BOOK_S_STYLE: "Bokstil"
STR_EMBEDDED_STYLE: "Inbäddad stil"
STR_OPDS_SERVER_URL: "OPDS-serveradress"

View File

@@ -22,6 +22,7 @@ build_flags =
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1 -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
-DMINIZ_NO_STDIO=1
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 -DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
-DDISABLE_FS_H_WARNING=1 -DDISABLE_FS_H_WARNING=1
# https://libexpat.github.io/doc/api/latest/#XML_GE # https://libexpat.github.io/doc/api/latest/#XML_GE
@@ -44,6 +45,7 @@ board_build.partitions = partitions.csv
extra_scripts = extra_scripts =
pre:scripts/build_html.py pre:scripts/build_html.py
pre:scripts/gen_i18n.py
; Libraries ; Libraries
lib_deps = lib_deps =

620
scripts/gen_i18n.py Executable file
View File

@@ -0,0 +1,620 @@
#!/usr/bin/env python3
"""
Generate I18n C++ files from per-language YAML translations.
Reads YAML files from a translations directory (one file per language) and generates:
- I18nKeys.h: Language enum, StrId enum, helper functions
- I18nStrings.h: String array declarations
- I18nStrings.cpp: String array definitions with all translations
Each YAML file must contain:
_language_name: "Native Name" (e.g. "Español")
_language_code: "ENUM_NAME" (e.g. "SPANISH")
STR_KEY: "translation text"
The English file is the reference. Missing keys in other languages are
automatically filled from English, with a warning.
Usage:
python gen_i18n.py <translations_dir> <output_dir>
Example:
python gen_i18n.py lib/I18n/translations lib/I18n/
"""
import sys
import os
import re
from pathlib import Path
from typing import List, Dict, Tuple
# ---------------------------------------------------------------------------
# YAML file reading (simple key: "value" format, no PyYAML dependency)
# ---------------------------------------------------------------------------
def _unescape_yaml_value(raw: str, filepath: str = "", line_num: int = 0) -> str:
"""
Process escape sequences in a YAML value string.
Recognized escapes: \\\\\\ \\"" \\n → newline
"""
result: List[str] = []
i = 0
while i < len(raw):
if raw[i] == "\\" and i + 1 < len(raw):
nxt = raw[i + 1]
if nxt == "\\":
result.append("\\")
elif nxt == '"':
result.append('"')
elif nxt == "n":
result.append("\n")
else:
raise ValueError(
f"{filepath}:{line_num}: unknown escape '\\{nxt}'"
)
i += 2
else:
result.append(raw[i])
i += 1
return "".join(result)
def parse_yaml_file(filepath: str) -> Dict[str, str]:
"""
Parse a simple YAML file of the form:
key: "value"
Only supports flat key-value pairs with quoted string values.
Aborts on formatting errors.
"""
result = {}
with open(filepath, "r", encoding="utf-8") as f:
for line_num, raw_line in enumerate(f, start=1):
line = raw_line.rstrip("\n\r")
if not line.strip():
continue
match = re.match(r'^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*"(.*)"$', line)
if not match:
raise ValueError(
f"{filepath}:{line_num}: bad format: {line!r}\n"
f' Expected: KEY: "value"'
)
key = match.group(1)
raw_value = match.group(2)
# Un-escape: process character by character to handle
# \\, \", and \n sequences correctly
value = _unescape_yaml_value(raw_value, filepath, line_num)
if key in result:
raise ValueError(f"{filepath}:{line_num}: duplicate key '{key}'")
result[key] = value
return result
# ---------------------------------------------------------------------------
# Load all languages from a directory of YAML files
# ---------------------------------------------------------------------------
def load_translations(
translations_dir: str,
) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]]]:
"""
Read every YAML file in *translations_dir* and return:
language_codes e.g. ["ENGLISH", "SPANISH", ...]
language_names e.g. ["English", "Español", ...]
string_keys ordered list of STR_* keys (from English)
translations {key: [translation_per_language]}
English is always first;
"""
yaml_dir = Path(translations_dir)
if not yaml_dir.is_dir():
raise FileNotFoundError(f"Translations directory not found: {translations_dir}")
yaml_files = sorted(yaml_dir.glob("*.yaml"))
if not yaml_files:
raise FileNotFoundError(f"No .yaml files found in {translations_dir}")
# Parse every file
parsed: Dict[str, Dict[str, str]] = {}
for yf in yaml_files:
parsed[yf.name] = parse_yaml_file(str(yf))
# Identify the English file (must exist)
english_file = None
for name, data in parsed.items():
if data.get("_language_code", "").upper() == "ENGLISH":
english_file = name
break
if english_file is None:
raise ValueError("No YAML file with _language_code: ENGLISH found")
# Order: English first, then by _order metadata (falls back to filename)
def sort_key(fname: str) -> Tuple[int, int, str]:
"""English always first (0), then by _order, then by filename."""
if fname == english_file:
return (0, 0, fname)
order = parsed[fname].get("_order", "999")
try:
order_int = int(order)
except ValueError:
order_int = 999
return (1, order_int, fname)
ordered_files = sorted(parsed, key=sort_key)
# Extract metadata
language_codes: List[str] = []
language_names: List[str] = []
for fname in ordered_files:
data = parsed[fname]
code = data.get("_language_code")
name = data.get("_language_name")
if not code or not name:
raise ValueError(f"{fname}: missing _language_code or _language_name")
language_codes.append(code)
language_names.append(name)
# String keys come from English (order matters)
english_data = parsed[english_file]
string_keys = [k for k in english_data if not k.startswith("_")]
# Validate all keys are valid C++ identifiers
for key in string_keys:
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", key):
raise ValueError(f"Invalid C++ identifier in English file: '{key}'")
# Build translations dict, filling missing keys from English
translations: Dict[str, List[str]] = {}
for key in string_keys:
row: List[str] = []
for fname in ordered_files:
data = parsed[fname]
value = data.get(key, "")
if not value.strip() and fname != english_file:
value = english_data[key]
lang_code = parsed[fname].get("_language_code", fname)
print(f" INFO: '{key}' missing in {lang_code}, using English fallback")
row.append(value)
translations[key] = row
# Warn about extra keys in non-English files
for fname in ordered_files:
if fname == english_file:
continue
data = parsed[fname]
extra = [k for k in data if not k.startswith("_") and k not in english_data]
if extra:
lang_code = data.get("_language_code", fname)
print(f" WARNING: {lang_code} has keys not in English: {', '.join(extra)}")
print(f"Loaded {len(language_codes)} languages, {len(string_keys)} string keys")
return language_codes, language_names, string_keys, translations
# ---------------------------------------------------------------------------
# C++ string escaping
# ---------------------------------------------------------------------------
LANG_ABBREVIATIONS = {
"english": "EN",
"español": "ES", "espanol": "ES",
"italiano": "IT",
"svenska": "SV",
"français": "FR", "francais": "FR",
"deutsch": "DE", "german": "DE",
"português": "PT", "portugues": "PT", "português (brasil)": "PO",
"中文": "ZH", "chinese": "ZH",
"日本語": "JA", "japanese": "JA",
"한국어": "KO", "korean": "KO",
"русский": "RU", "russian": "RU",
"العربية": "AR", "arabic": "AR",
"עברית": "HE", "hebrew": "HE",
"فارسی": "FA", "persian": "FA",
"čeština": "CZ",
}
def get_lang_abbreviation(lang_code: str, lang_name: str) -> str:
"""Return a 2-letter abbreviation for a language."""
lower = lang_name.lower()
if lower in LANG_ABBREVIATIONS:
return LANG_ABBREVIATIONS[lower]
return lang_code[:2].upper()
def escape_cpp_string(s: str) -> List[str]:
r"""
Convert *s* into one or more C++ string literal segments.
Non-ASCII characters are emitted as \xNN hex sequences. After each
hex escape a new segment is started so the compiler doesn't merge
subsequent hex digits into the escape.
Returns a list of string segments (without quotes). For simple ASCII
strings this is a single-element list.
"""
if not s:
return [""]
s = s.replace("\n", "\\n")
# Build a flat list of "tokens", where each token is either a regular
# character sequence or a hex escape. A segment break happens after
# every hex escape.
segments: List[str] = []
current: List[str] = []
i = 0
def _flush() -> None:
segments.append("".join(current))
current.clear()
while i < len(s):
ch = s[i]
if ch == "\\" and i + 1 < len(s):
nxt = s[i + 1]
if nxt in "ntr\"\\":
current.append(ch + nxt)
i += 2
elif nxt == "x" and i + 3 < len(s):
current.append(s[i : i + 4])
_flush() # segment break after hex
i += 4
else:
current.append("\\\\")
i += 1
elif ch == '"':
current.append('\\"')
i += 1
elif ord(ch) < 128:
current.append(ch)
i += 1
else:
for byte in ch.encode("utf-8"):
current.append(f"\\x{byte:02X}")
_flush() # segment break after hex
i += 1
# Flush remaining content
_flush()
return segments
def format_cpp_string_literal(segments: List[str], indent: str = " ") -> List[str]:
"""
Format string segments (from escape_cpp_string) as indented C++ string
literal lines, each wrapped in quotes.
Also wraps long segments to respect ~120 column limit.
"""
# Effective limit for content: 120 - 4 (indent) - 2 (quotes) - 1 (comma/safety) = 113
# Using 113 to match clang-format exactly (120 - 4 - 2 - 1)
MAX_CONTENT_LEN = 113
lines: List[str] = []
for seg in segments:
# Short segment (e.g. hex escape or short text)
if len(seg) <= MAX_CONTENT_LEN:
lines.append(f'{indent}"{seg}"')
continue
# Long segment - wrap it
current = seg
while len(current) > MAX_CONTENT_LEN:
# Find best split point
# Scan forward to find last space <= MAX_CONTENT_LEN
last_space = -1
idx = 0
while idx <= MAX_CONTENT_LEN and idx < len(current):
if current[idx] == ' ':
last_space = idx
# Handle escapes to step correctly
if current[idx] == '\\':
idx += 2
else:
idx += 1
# If we found a space, split after it
if last_space != -1:
# Include the space in the first line
split_point = last_space + 1
lines.append(f'{indent}"{current[:split_point]}"')
current = current[split_point:]
else:
# No space, forced break at MAX_CONTENT_LEN (or slightly less)
cut_at = MAX_CONTENT_LEN
# Don't cut in the middle of an escape sequence
if current[cut_at - 1] == '\\':
cut_at -= 1
lines.append(f'{indent}"{current[:cut_at]}"')
current = current[cut_at:]
if current:
lines.append(f'{indent}"{current}"')
return lines
# ---------------------------------------------------------------------------
# Character-set computation
# ---------------------------------------------------------------------------
def compute_character_set(translations: Dict[str, List[str]], lang_index: int) -> str:
"""Return a sorted string of every unique character used in a language."""
chars = set()
for values in translations.values():
for ch in values[lang_index]:
chars.add(ord(ch))
return "".join(chr(cp) for cp in sorted(chars))
# ---------------------------------------------------------------------------
# Code generators
# ---------------------------------------------------------------------------
def generate_keys_header(
languages: List[str],
language_names: List[str],
string_keys: List[str],
output_path: str,
) -> None:
"""Generate I18nKeys.h."""
lines: List[str] = [
"#pragma once",
"#include <cstdint>",
"",
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
"",
"// Forward declaration for string arrays",
"namespace i18n_strings {",
]
for code, name in zip(languages, language_names):
abbrev = get_lang_abbreviation(code, name)
lines.append(f"extern const char* const STRINGS_{abbrev}[];")
lines.append("} // namespace i18n_strings")
lines.append("")
# Language enum
lines.append("// Language enum")
lines.append("enum class Language : uint8_t {")
for i, lang in enumerate(languages):
lines.append(f" {lang} = {i},")
lines.append(" _COUNT")
lines.append("};")
lines.append("")
# Extern declarations
lines.append("// Language display names (defined in I18nStrings.cpp)")
lines.append("extern const char* const LANGUAGE_NAMES[];")
lines.append("")
lines.append("// Character sets for each language (defined in I18nStrings.cpp)")
lines.append("extern const char* const CHARACTER_SETS[];")
lines.append("")
# StrId enum
lines.append("// String IDs")
lines.append("enum class StrId : uint16_t {")
for key in string_keys:
lines.append(f" {key},")
lines.append(" // Sentinel - must be last")
lines.append(" _COUNT")
lines.append("};")
lines.append("")
# getStringArray helper
lines.append("// Helper function to get string array for a language")
lines.append("inline const char* const* getStringArray(Language lang) {")
lines.append(" switch (lang) {")
for code, name in zip(languages, language_names):
abbrev = get_lang_abbreviation(code, name)
lines.append(f" case Language::{code}:")
lines.append(f" return i18n_strings::STRINGS_{abbrev};")
first_abbrev = get_lang_abbreviation(languages[0], language_names[0])
lines.append(" default:")
lines.append(f" return i18n_strings::STRINGS_{first_abbrev};")
lines.append(" }")
lines.append("}")
lines.append("")
# getLanguageCount helper (single line to match checked-in format)
lines.append("// Helper function to get language count")
lines.append(
"constexpr uint8_t getLanguageCount() "
"{ return static_cast<uint8_t>(Language::_COUNT); }"
)
_write_file(output_path, lines)
def generate_strings_header(
languages: List[str],
language_names: List[str],
output_path: str,
) -> None:
"""Generate I18nStrings.h."""
lines: List[str] = [
"#pragma once",
'#include <string>',
"",
'#include "I18nKeys.h"',
"",
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
"",
"namespace i18n_strings {",
"",
]
for code, name in zip(languages, language_names):
abbrev = get_lang_abbreviation(code, name)
lines.append(f"extern const char* const STRINGS_{abbrev}[];")
lines.append("")
lines.append("} // namespace i18n_strings")
_write_file(output_path, lines)
def generate_strings_cpp(
languages: List[str],
language_names: List[str],
string_keys: List[str],
translations: Dict[str, List[str]],
output_path: str,
) -> None:
"""Generate I18nStrings.cpp."""
lines: List[str] = [
'#include "I18nStrings.h"',
"",
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
"",
]
# LANGUAGE_NAMES array
lines.append("// Language display names")
lines.append("const char* const LANGUAGE_NAMES[] = {")
for name in language_names:
_append_string_entry(lines, name)
lines.append("};")
lines.append("")
# CHARACTER_SETS array
lines.append("// Character sets for each language")
lines.append("const char* const CHARACTER_SETS[] = {")
for lang_idx, name in enumerate(language_names):
charset = compute_character_set(translations, lang_idx)
_append_string_entry(lines, charset, comment=name)
lines.append("};")
lines.append("")
# Per-language string arrays
lines.append("namespace i18n_strings {")
lines.append("")
for lang_idx, (code, name) in enumerate(zip(languages, language_names)):
abbrev = get_lang_abbreviation(code, name)
lines.append(f"const char* const STRINGS_{abbrev}[] = {{")
for key in string_keys:
text = translations[key][lang_idx]
_append_string_entry(lines, text)
lines.append("};")
lines.append("")
lines.append("} // namespace i18n_strings")
lines.append("")
# Compile-time size checks
lines.append("// Compile-time validation of array sizes")
for code, name in zip(languages, language_names):
abbrev = get_lang_abbreviation(code, name)
lines.append(
f"static_assert(sizeof(i18n_strings::STRINGS_{abbrev}) "
f"/ sizeof(i18n_strings::STRINGS_{abbrev}[0]) =="
)
lines.append(" static_cast<size_t>(StrId::_COUNT),")
lines.append(f' "STRINGS_{abbrev} size mismatch");')
_write_file(output_path, lines)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _append_string_entry(
lines: List[str], text: str, comment: str = ""
) -> None:
"""Escape *text*, format as indented C++ lines, append comma (and optional comment)."""
segments = escape_cpp_string(text)
formatted = format_cpp_string_literal(segments)
suffix = f", // {comment}" if comment else ","
formatted[-1] += suffix
lines.extend(formatted)
def _write_file(path: str, lines: List[str]) -> None:
with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(lines))
f.write("\n")
print(f"Generated: {path}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main(translations_dir=None, output_dir=None) -> None:
# Default paths (relative to project root)
default_translations_dir = "lib/I18n/translations"
default_output_dir = "lib/I18n/"
if translations_dir is None or output_dir is None:
if len(sys.argv) == 3:
translations_dir = sys.argv[1]
output_dir = sys.argv[2]
else:
# Default for no arguments or weird arguments (e.g. SCons)
translations_dir = default_translations_dir
output_dir = default_output_dir
if not os.path.isdir(translations_dir):
print(f"Error: Translations directory not found: {translations_dir}")
sys.exit(1)
if not os.path.isdir(output_dir):
print(f"Error: Output directory not found: {output_dir}")
sys.exit(1)
print(f"Reading translations from: {translations_dir}")
print(f"Output directory: {output_dir}")
print()
try:
languages, language_names, string_keys, translations = load_translations(
translations_dir
)
out = Path(output_dir)
generate_keys_header(languages, language_names, string_keys, str(out / "I18nKeys.h"))
generate_strings_header(languages, language_names, str(out / "I18nStrings.h"))
generate_strings_cpp(
languages, language_names, string_keys, translations, str(out / "I18nStrings.cpp")
)
print()
print("✓ Code generation complete!")
print(f" Languages: {len(languages)}")
print(f" String keys: {len(string_keys)}")
except Exception as e:
print(f"\nError: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
else:
try:
Import("env")
print("Running i18n generation script from PlatformIO...")
main()
except NameError:
pass

View File

@@ -33,10 +33,28 @@ def _symbol_from_output(path: pathlib.Path) -> str:
def write_header(path: pathlib.Path, blob: bytes, symbol: str) -> None: def write_header(path: pathlib.Path, blob: bytes, symbol: str) -> None:
# Emit a constexpr header containing the raw bytes plus a SerializedHyphenationPatterns descriptor. # Emit a constexpr header containing the raw bytes plus a SerializedHyphenationPatterns descriptor.
# The binary format has:
# - 4 bytes: big-endian root address
# - levels tape: from byte 4 to root_addr
# - nodes data: from root_addr onwards
if len(blob) < 4:
raise ValueError(f"Blob too small: {len(blob)} bytes")
# Parse root address (big-endian uint32)
root_addr = (blob[0] << 24) | (blob[1] << 16) | (blob[2] << 8) | blob[3]
if root_addr > len(blob):
raise ValueError(f"Root address {root_addr} exceeds blob size {len(blob)}")
# Remove the 4-byte root address and adjust the offset
bytes_literal = _format_bytes(blob[4:])
root_addr_new = root_addr - 4
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
data_symbol = f"{symbol}_trie_data" data_symbol = f"{symbol}_trie_data"
patterns_symbol = f"{symbol}_patterns" patterns_symbol = f"{symbol}_patterns"
bytes_literal = _format_bytes(blob)
content = f"""#pragma once content = f"""#pragma once
#include <cstddef> #include <cstddef>
@@ -50,6 +68,7 @@ alignas(4) constexpr uint8_t {data_symbol}[] = {{
}}; }};
constexpr SerializedHyphenationPatterns {patterns_symbol} = {{ constexpr SerializedHyphenationPatterns {patterns_symbol} = {{
{f"0x{root_addr_new:02X}"}u,
{data_symbol}, {data_symbol},
sizeof({data_symbol}), sizeof({data_symbol}),
}}; }};

24
scripts/update_hypenation.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
process() {
local lang="$1"
mkdir -p "build"
wget -O "build/$lang.bin" "https://github.com/typst/hypher/raw/refs/heads/main/tries/$lang.bin"
python scripts/generate_hyphenation_trie.py \
--input "build/$lang.bin" \
--output "lib/Epub/Epub/hyphenation/generated/hyph-${lang}.trie.h"
}
process en
process fr
process de
process es
process ru
process it

View File

@@ -5,6 +5,7 @@
#include <Serialization.h> #include <Serialization.h>
#include <cstring> #include <cstring>
#include <string>
#include "fontIds.h" #include "fontIds.h"
@@ -21,8 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // SETTINGS_COUNT is now calculated automatically in saveToFile
constexpr uint8_t SETTINGS_COUNT = 31;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
// Validate front button mapping to ensure each hardware button is unique. // Validate front button mapping to ensure each hardware button is unique.
@@ -77,6 +77,69 @@ void applyLegacyFrontButtonLayout(CrossPointSettings& settings) {
} }
} // namespace } // namespace
class SettingsWriter {
public:
bool is_counting = false;
uint8_t item_count = 0;
template <typename T>
void writeItem(FsFile& file, const T& value) {
if (is_counting) {
item_count++;
} else {
serialization::writePod(file, value);
}
}
void writeItemString(FsFile& file, const char* value) {
if (is_counting) {
item_count++;
} else {
serialization::writeString(file, std::string(value));
}
}
};
uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const {
SettingsWriter writer;
writer.is_counting = count_only;
writer.writeItem(file, sleepScreen);
writer.writeItem(file, extraParagraphSpacing);
writer.writeItem(file, shortPwrBtn);
writer.writeItem(file, statusBar);
writer.writeItem(file, orientation);
writer.writeItem(file, frontButtonLayout); // legacy
writer.writeItem(file, sideButtonLayout);
writer.writeItem(file, fontFamily);
writer.writeItem(file, fontSize);
writer.writeItem(file, lineSpacing);
writer.writeItem(file, paragraphAlignment);
writer.writeItem(file, sleepTimeout);
writer.writeItem(file, refreshFrequency);
writer.writeItem(file, screenMargin);
writer.writeItem(file, sleepScreenCoverMode);
writer.writeItemString(file, opdsServerUrl);
writer.writeItem(file, textAntiAliasing);
writer.writeItem(file, hideBatteryPercentage);
writer.writeItem(file, longPressChapterSkip);
writer.writeItem(file, hyphenationEnabled);
writer.writeItemString(file, opdsUsername);
writer.writeItemString(file, opdsPassword);
writer.writeItem(file, sleepScreenCoverFilter);
writer.writeItem(file, uiTheme);
writer.writeItem(file, frontButtonBack);
writer.writeItem(file, frontButtonConfirm);
writer.writeItem(file, frontButtonLeft);
writer.writeItem(file, frontButtonRight);
writer.writeItem(file, fadingFix);
writer.writeItem(file, embeddedStyle);
writer.writeItem(file, sleepScreenLetterboxFill);
// New fields need to be added at end for backward compatibility
return writer.item_count;
}
bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
Storage.mkdir("/.crosspoint"); Storage.mkdir("/.crosspoint");
@@ -86,40 +149,15 @@ bool CrossPointSettings::saveToFile() const {
return false; return false;
} }
// First pass: count the items
uint8_t item_count = writeSettings(outputFile, true); // This will just count, not write
// Write header
serialization::writePod(outputFile, SETTINGS_FILE_VERSION); serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
serialization::writePod(outputFile, SETTINGS_COUNT); serialization::writePod(outputFile, static_cast<uint8_t>(item_count));
serialization::writePod(outputFile, sleepScreen); // Second pass: actually write the settings
serialization::writePod(outputFile, extraParagraphSpacing); writeSettings(outputFile); // This will write the actual data
serialization::writePod(outputFile, shortPwrBtn);
serialization::writePod(outputFile, statusBar);
serialization::writePod(outputFile, orientation);
serialization::writePod(outputFile, frontButtonLayout); // legacy
serialization::writePod(outputFile, sideButtonLayout);
serialization::writePod(outputFile, fontFamily);
serialization::writePod(outputFile, fontSize);
serialization::writePod(outputFile, lineSpacing);
serialization::writePod(outputFile, paragraphAlignment);
serialization::writePod(outputFile, sleepTimeout);
serialization::writePod(outputFile, refreshFrequency);
serialization::writePod(outputFile, screenMargin);
serialization::writePod(outputFile, sleepScreenCoverMode);
serialization::writeString(outputFile, std::string(opdsServerUrl));
serialization::writePod(outputFile, textAntiAliasing);
serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip);
serialization::writePod(outputFile, hyphenationEnabled);
serialization::writeString(outputFile, std::string(opdsUsername));
serialization::writeString(outputFile, std::string(opdsPassword));
serialization::writePod(outputFile, sleepScreenCoverFilter);
serialization::writePod(outputFile, uiTheme);
serialization::writePod(outputFile, frontButtonBack);
serialization::writePod(outputFile, frontButtonConfirm);
serialization::writePod(outputFile, frontButtonLeft);
serialization::writePod(outputFile, frontButtonRight);
serialization::writePod(outputFile, fadingFix);
serialization::writePod(outputFile, embeddedStyle);
serialization::writePod(outputFile, sleepScreenLetterboxFill);
// New fields added at end for backward compatibility
outputFile.close(); outputFile.close();
LOG_DBG("CPS", "Settings saved to file"); LOG_DBG("CPS", "Settings saved to file");

View File

@@ -2,6 +2,9 @@
#include <cstdint> #include <cstdint>
#include <iosfwd> #include <iosfwd>
// Forward declarations
class FsFile;
class CrossPointSettings { class CrossPointSettings {
private: private:
// Private constructor for singleton // Private constructor for singleton
@@ -190,6 +193,9 @@ class CrossPointSettings {
} }
int getReaderFontId() const; int getReaderFontId() const;
// If count_only is true, returns the number of settings items that would be written.
uint8_t writeSettings(FsFile& file, bool count_only = false) const;
bool saveToFile() const; bool saveToFile() const;
bool loadFromFile(); bool loadFromFile();

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <I18n.h>
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@@ -30,36 +32,54 @@ static_assert(kFontFamilyMappingCount > 0, "At least one font family must be ava
// Each entry has a key (for JSON API) and category (for grouping). // Each entry has a key (for JSON API) and category (for grouping).
// ACTION-type entries and entries without a key are device-only. // ACTION-type entries and entries without a key are device-only.
inline std::vector<SettingInfo> getSettingsList() { inline std::vector<SettingInfo> getSettingsList() {
// Build font family options from the compile-time mapping table // Build font family StrId options from the compile-time mapping table
std::vector<std::string> fontFamilyOptions; constexpr StrId kFontFamilyStrIds[] = {
for (size_t i = 0; i < kFontFamilyMappingCount; i++) { #ifndef OMIT_BOOKERLY
fontFamilyOptions.push_back(kFontFamilyMappings[i].name); StrId::STR_BOOKERLY,
} #endif
#ifndef OMIT_NOTOSANS
StrId::STR_NOTO_SANS,
#endif
#ifndef OMIT_OPENDYSLEXIC
StrId::STR_OPEN_DYSLEXIC,
#endif
};
std::vector<StrId> fontFamilyStrIds(std::begin(kFontFamilyStrIds), std::end(kFontFamilyStrIds));
return { return {
// --- Display --- // --- Display ---
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{"Dark", "Light", "Custom", "Cover", "None", "Cover + Custom"}, "sleepScreen", "Display"), {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT,
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}, StrId::STR_COVER_CUSTOM},
"sleepScreenCoverMode", "Display"), "sleepScreen", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode,
{"None", "Contrast", "Inverted"}, "sleepScreenCoverFilter", "Display"), {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum("Letterbox Fill", &CrossPointSettings::sleepScreenLetterboxFill, SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
{"Dithered", "Solid", "None"}, "sleepScreenLetterboxFill", "Display"), {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
"sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_LETTERBOX_FILL, &CrossPointSettings::sleepScreenLetterboxFill,
{StrId::STR_DITHERED, StrId::STR_SOLID, StrId::STR_NONE_OPT},
"sleepScreenLetterboxFill", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum( SettingInfo::Enum(
"Status Bar", &CrossPointSettings::statusBar, StrId::STR_STATUS_BAR, &CrossPointSettings::statusBar,
{"None", "No Progress", "Full w/ Percentage", "Full w/ Book Bar", "Book Bar Only", "Full w/ Chapter Bar"}, {StrId::STR_NONE_OPT, StrId::STR_NO_PROGRESS, StrId::STR_STATUS_BAR_FULL_PERCENT,
"statusBar", "Display"), StrId::STR_STATUS_BAR_FULL_BOOK, StrId::STR_STATUS_BAR_BOOK_ONLY, StrId::STR_STATUS_BAR_FULL_CHAPTER},
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}, "statusBar", StrId::STR_CAT_DISPLAY),
"hideBatteryPercentage", "Display"), SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}, "refreshFrequency", "Display"), StrId::STR_CAT_DISPLAY),
SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Lyra"}, "uiTheme", "Display"), SettingInfo::Enum(
SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix, "fadingFix", "Display"), StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency,
{StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30},
"refreshFrequency", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme,
{StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY),
SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix",
StrId::STR_CAT_DISPLAY),
// --- Reader --- // --- Reader ---
SettingInfo::DynamicEnum( SettingInfo::DynamicEnum(
"Font Family", std::move(fontFamilyOptions), StrId::STR_FONT_FAMILY, std::move(fontFamilyStrIds),
[]() -> uint8_t { []() -> uint8_t {
for (uint8_t i = 0; i < kFontFamilyMappingCount; i++) { for (uint8_t i = 0; i < kFontFamilyMappingCount; i++) {
if (kFontFamilyMappings[i].value == SETTINGS.fontFamily) return i; if (kFontFamilyMappings[i].value == SETTINGS.fontFamily) return i;
@@ -71,71 +91,81 @@ inline std::vector<SettingInfo> getSettingsList() {
SETTINGS.fontFamily = kFontFamilyMappings[idx].value; SETTINGS.fontFamily = kFontFamilyMappings[idx].value;
} }
}, },
"fontFamily", "Reader"), "fontFamily", StrId::STR_CAT_READER),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}, "fontSize", SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize,
"Reader"), {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize",
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}, "lineSpacing", StrId::STR_CAT_READER),
"Reader"), SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing,
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", "Reader"), {StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER),
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin",
{"Justify", "Left", "Center", "Right", "Book's Style"}, "paragraphAlignment", "Reader"), StrId::STR_CAT_READER),
SettingInfo::Toggle("Book's Embedded Style", &CrossPointSettings::embeddedStyle, "embeddedStyle", "Reader"), SettingInfo::Enum(StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment,
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", "Reader"), {StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT,
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, StrId::STR_BOOK_S_STYLE},
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}, "orientation", "Reader"), "paragraphAlignment", StrId::STR_CAT_READER),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing, SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
"extraParagraphSpacing", "Reader"), StrId::STR_CAT_READER),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing, "textAntiAliasing", "Reader"), SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
StrId::STR_CAT_READER),
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW},
"orientation", StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing",
StrId::STR_CAT_READER),
SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing",
StrId::STR_CAT_READER),
// --- Controls --- // --- Controls ---
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}, "sideButtonLayout", "Controls"), {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip",
"Controls"), StrId::STR_CAT_CONTROLS),
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"}, SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn,
"shortPwrBtn", "Controls"), {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN}, "shortPwrBtn",
StrId::STR_CAT_CONTROLS),
// --- System --- // --- System ---
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}, "sleepTimeout", "System"), {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30},
"sleepTimeout", StrId::STR_CAT_SYSTEM),
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- // --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString( SettingInfo::DynamicString(
"KOReader Username", [] { return KOREADER_STORE.getUsername(); }, StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
[](const std::string& v) { [](const std::string& v) {
KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword());
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koUsername", "KOReader Sync"), "koUsername", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicString( SettingInfo::DynamicString(
"KOReader Password", [] { return KOREADER_STORE.getPassword(); }, StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); },
[](const std::string& v) { [](const std::string& v) {
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koPassword", "KOReader Sync"), "koPassword", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicString( SettingInfo::DynamicString(
"Sync Server URL", [] { return KOREADER_STORE.getServerUrl(); }, StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); },
[](const std::string& v) { [](const std::string& v) {
KOREADER_STORE.setServerUrl(v); KOREADER_STORE.setServerUrl(v);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koServerUrl", "KOReader Sync"), "koServerUrl", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicEnum( SettingInfo::DynamicEnum(
"Document Matching", {"Filename", "Binary"}, StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
[] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); }, [] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
[](uint8_t v) { [](uint8_t v) {
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v)); KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koMatchMethod", "KOReader Sync"), "koMatchMethod", StrId::STR_KOREADER_SYNC),
// --- OPDS Browser (web-only, uses CrossPointSettings char arrays) --- // --- OPDS Browser (web-only, uses CrossPointSettings char arrays) ---
SettingInfo::String("OPDS Server URL", SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), "opdsServerUrl", SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl),
"OPDS Browser"), "opdsServerUrl", StrId::STR_OPDS_BROWSER),
SettingInfo::String("OPDS Username", SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername", SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername",
"OPDS Browser"), StrId::STR_OPDS_BROWSER),
SettingInfo::String("OPDS Password", SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword", SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword",
"OPDS Browser"), StrId::STR_OPDS_BROWSER),
}; };
} }

View File

@@ -0,0 +1,58 @@
#include "Activity.h"
void Activity::renderTaskTrampoline(void* param) {
auto* self = static_cast<Activity*>(param);
self->renderTaskLoop();
}
void Activity::renderTaskLoop() {
while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
{
RenderLock lock(*this);
render(std::move(lock));
}
}
}
void Activity::onEnter() {
xTaskCreate(&renderTaskTrampoline, name.c_str(),
8192, // Stack size
this, // Parameters
1, // Priority
&renderTaskHandle // Task handle
);
assert(renderTaskHandle != nullptr && "Failed to create render task");
LOG_DBG("ACT", "Entering activity: %s", name.c_str());
}
void Activity::onExit() {
RenderLock lock(*this); // Ensure we don't delete the task while it's rendering
if (renderTaskHandle) {
vTaskDelete(renderTaskHandle);
renderTaskHandle = nullptr;
}
LOG_DBG("ACT", "Exiting activity: %s", name.c_str());
}
void Activity::requestUpdate() {
// Using direct notification to signal the render task to update
// Increment counter so multiple rapid calls won't be lost
if (renderTaskHandle) {
xTaskNotify(renderTaskHandle, 1, eIncrement);
}
}
void Activity::requestUpdateAndWait() {
// FIXME @ngxson : properly implement this using freeRTOS notification
delay(100);
}
// RenderLock
Activity::RenderLock::RenderLock(Activity& activity) : activity(activity) {
xSemaphoreTake(activity.renderingMutex, portMAX_DELAY);
}
Activity::RenderLock::~RenderLock() { xSemaphoreGive(activity.renderingMutex); }

View File

@@ -1,12 +1,16 @@
#pragma once #pragma once
#include <HardwareSerial.h>
#include <Logging.h> #include <Logging.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <cassert>
#include <string> #include <string>
#include <utility> #include <utility>
class MappedInputManager; #include "GfxRenderer.h"
class GfxRenderer; #include "MappedInputManager.h"
class Activity { class Activity {
protected: protected:
@@ -14,14 +18,44 @@ class Activity {
GfxRenderer& renderer; GfxRenderer& renderer;
MappedInputManager& mappedInput; MappedInputManager& mappedInput;
// Task to render and display the activity
TaskHandle_t renderTaskHandle = nullptr;
[[noreturn]] static void renderTaskTrampoline(void* param);
[[noreturn]] virtual void renderTaskLoop();
// Mutex to protect rendering operations from being deleted mid-render
SemaphoreHandle_t renderingMutex = nullptr;
public: public:
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} : name(std::move(name)), renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
virtual ~Activity() = default; assert(renderingMutex != nullptr && "Failed to create rendering mutex");
virtual void onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); } }
virtual void onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); } virtual ~Activity() {
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
};
class RenderLock;
virtual void onEnter();
virtual void onExit();
virtual void loop() {} virtual void loop() {}
virtual void render(RenderLock&&) {}
virtual void requestUpdate();
virtual void requestUpdateAndWait();
virtual bool skipLoopDelay() { return false; } virtual bool skipLoopDelay() { return false; }
virtual bool preventAutoSleep() { return false; } virtual bool preventAutoSleep() { return false; }
virtual bool isReaderActivity() const { return false; } virtual bool isReaderActivity() const { return false; }
// RAII helper to lock rendering mutex for the duration of a scope.
class RenderLock {
Activity& activity;
public:
explicit RenderLock(Activity& activity);
RenderLock(const RenderLock&) = delete;
RenderLock& operator=(const RenderLock&) = delete;
~RenderLock();
};
}; };

View File

@@ -1,13 +1,31 @@
#include "ActivityWithSubactivity.h" #include "ActivityWithSubactivity.h"
void ActivityWithSubactivity::renderTaskLoop() {
while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
{
RenderLock lock(*this);
if (!subActivity) {
render(std::move(lock));
}
// If subActivity is set, consume the notification but skip parent render
// Note: the sub-activity will call its render() from its own display task
}
}
}
void ActivityWithSubactivity::exitActivity() { void ActivityWithSubactivity::exitActivity() {
// No need to lock, since onExit() already acquires its own lock
if (subActivity) { if (subActivity) {
LOG_DBG("ACT", "Exiting subactivity...");
subActivity->onExit(); subActivity->onExit();
subActivity.reset(); subActivity.reset();
} }
} }
void ActivityWithSubactivity::enterNewActivity(Activity* activity) { void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
// Acquire lock to avoid 2 activities rendering at the same time during transition
RenderLock lock(*this);
subActivity.reset(activity); subActivity.reset(activity);
subActivity->onEnter(); subActivity->onEnter();
} }
@@ -18,7 +36,15 @@ void ActivityWithSubactivity::loop() {
} }
} }
void ActivityWithSubactivity::onExit() { void ActivityWithSubactivity::requestUpdate() {
Activity::onExit(); if (!subActivity) {
exitActivity(); Activity::requestUpdate();
}
// Sub-activity should call their own requestUpdate() from their loop() function
}
void ActivityWithSubactivity::onExit() {
// No need to lock, onExit() already acquires its own lock
exitActivity();
Activity::onExit();
} }

View File

@@ -8,11 +8,15 @@ class ActivityWithSubactivity : public Activity {
std::unique_ptr<Activity> subActivity = nullptr; std::unique_ptr<Activity> subActivity = nullptr;
void exitActivity(); void exitActivity();
void enterNewActivity(Activity* activity); void enterNewActivity(Activity* activity);
[[noreturn]] void renderTaskLoop() override;
public: public:
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity(std::move(name), renderer, mappedInput) {} : Activity(std::move(name), renderer, mappedInput) {}
void loop() override; void loop() override;
// Note: when a subactivity is active, parent requestUpdate() calls are ignored;
// the subactivity should request its own renders. This pauses parent rendering until exit.
void requestUpdate() override;
void onExit() override; void onExit() override;
bool preventAutoSleep() override { return subActivity && subActivity->preventAutoSleep(); } bool preventAutoSleep() override { return subActivity && subActivity->preventAutoSleep(); }
bool skipLoopDelay() override { return subActivity && subActivity->skipLoopDelay(); } bool skipLoopDelay() override { return subActivity && subActivity->skipLoopDelay(); }

View File

@@ -1,6 +1,7 @@
#include "BootActivity.h" #include "BootActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "fontIds.h" #include "fontIds.h"
#include "images/Logo120.h" #include "images/Logo120.h"
@@ -13,8 +14,8 @@ void BootActivity::onEnter() {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120); renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, tr(STR_CROSSPOINT), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, tr(STR_BOOTING));
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@@ -3,6 +3,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <PlaceholderCoverGenerator.h> #include <PlaceholderCoverGenerator.h>
#include <Serialization.h> #include <Serialization.h>
@@ -346,7 +347,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
void SleepActivity::onEnter() { void SleepActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
GUI.drawPopup(renderer, "Entering Sleep..."); GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP));
switch (SETTINGS.sleepScreen) { switch (SETTINGS.sleepScreen) {
case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK): case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK):
@@ -441,8 +442,8 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120); renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, tr(STR_CROSSPOINT), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, tr(STR_SLEEPING));
// Make sleep screen dark unless light is selected in settings // Make sleep screen dark unless light is selected in settings
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) { if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) {

View File

@@ -2,6 +2,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <OpdsStream.h> #include <OpdsStream.h>
#include <WiFi.h> #include <WiFi.h>
@@ -19,30 +20,17 @@ namespace {
constexpr int PAGE_ITEMS = 23; constexpr int PAGE_ITEMS = 23;
} // namespace } // namespace
void OpdsBookBrowserActivity::taskTrampoline(void* param) {
auto* self = static_cast<OpdsBookBrowserActivity*>(param);
self->displayTaskLoop();
}
void OpdsBookBrowserActivity::onEnter() { void OpdsBookBrowserActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
state = BrowserState::CHECK_WIFI; state = BrowserState::CHECK_WIFI;
entries.clear(); entries.clear();
navigationHistory.clear(); navigationHistory.clear();
currentPath = ""; // Root path - user provides full URL in settings currentPath = ""; // Root path - user provides full URL in settings
selectorIndex = 0; selectorIndex = 0;
errorMessage.clear(); errorMessage.clear();
statusMessage = "Checking WiFi..."; statusMessage = tr(STR_CHECKING_WIFI);
updateRequired = true; requestUpdate();
xTaskCreate(&OpdsBookBrowserActivity::taskTrampoline, "OpdsBookBrowserTask",
4096, // Stack size (larger for HTTP operations)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Check WiFi and connect if needed, then fetch feed // Check WiFi and connect if needed, then fetch feed
checkAndConnectWifi(); checkAndConnectWifi();
@@ -54,13 +42,6 @@ void OpdsBookBrowserActivity::onExit() {
// Turn off WiFi when exiting // Turn off WiFi when exiting
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
entries.clear(); entries.clear();
navigationHistory.clear(); navigationHistory.clear();
} }
@@ -80,8 +61,8 @@ void OpdsBookBrowserActivity::loop() {
// WiFi connected - just retry fetching the feed // WiFi connected - just retry fetching the feed
LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch"); LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch");
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = tr(STR_LOADING);
updateRequired = true; requestUpdate();
fetchFeed(currentPath); fetchFeed(currentPath);
} else { } else {
// WiFi not connected - launch WiFi selection // WiFi not connected - launch WiFi selection
@@ -134,50 +115,38 @@ void OpdsBookBrowserActivity::loop() {
if (!entries.empty()) { if (!entries.empty()) {
buttonNavigator.onNextRelease([this] { buttonNavigator.onNextRelease([this] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this] { buttonNavigator.onPreviousRelease([this] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this] { buttonNavigator.onNextContinuous([this] {
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this] { buttonNavigator.onPreviousContinuous([this] {
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
updateRequired = true; requestUpdate();
}); });
} }
} }
} }
void OpdsBookBrowserActivity::displayTaskLoop() { void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void OpdsBookBrowserActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
if (state == BrowserState::CHECK_WIFI) { if (state == BrowserState::CHECK_WIFI) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -185,23 +154,23 @@ void OpdsBookBrowserActivity::render() const {
if (state == BrowserState::LOADING) { if (state == BrowserState::LOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == BrowserState::ERROR) { if (state == BrowserState::ERROR) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Error:"); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_ERROR_MSG));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "Retry", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == BrowserState::DOWNLOADING) { if (state == BrowserState::DOWNLOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, "Downloading..."); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str());
if (downloadTotal > 0) { if (downloadTotal > 0) {
const int barWidth = pageWidth - 100; const int barWidth = pageWidth - 100;
@@ -216,15 +185,15 @@ void OpdsBookBrowserActivity::render() const {
// Browsing state // Browsing state
// Show appropriate button hint based on selected entry type // Show appropriate button hint based on selected entry type
const char* confirmLabel = "Open"; const char* confirmLabel = tr(STR_OPEN);
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) { if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
confirmLabel = "Download"; confirmLabel = tr(STR_DOWNLOAD);
} }
const auto labels = mappedInput.mapLabels("« Back", confirmLabel, "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (entries.empty()) { if (entries.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "No entries found"); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_ENTRIES));
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@@ -259,8 +228,8 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
const char* serverUrl = SETTINGS.opdsServerUrl; const char* serverUrl = SETTINGS.opdsServerUrl;
if (strlen(serverUrl) == 0) { if (strlen(serverUrl) == 0) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "No server URL configured"; errorMessage = tr(STR_NO_SERVER_URL);
updateRequired = true; requestUpdate();
return; return;
} }
@@ -273,16 +242,16 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
OpdsParserStream stream{parser}; OpdsParserStream stream{parser};
if (!HttpDownloader::fetchUrl(url, stream)) { if (!HttpDownloader::fetchUrl(url, stream)) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Failed to fetch feed"; errorMessage = tr(STR_FETCH_FEED_FAILED);
updateRequired = true; requestUpdate();
return; return;
} }
} }
if (!parser) { if (!parser) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Failed to parse feed"; errorMessage = tr(STR_PARSE_FEED_FAILED);
updateRequired = true; requestUpdate();
return; return;
} }
@@ -292,13 +261,13 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
if (entries.empty()) { if (entries.empty()) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "No entries found"; errorMessage = tr(STR_NO_ENTRIES);
updateRequired = true; requestUpdate();
return; return;
} }
state = BrowserState::BROWSING; state = BrowserState::BROWSING;
updateRequired = true; requestUpdate();
} }
void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
@@ -307,10 +276,10 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
currentPath = entry.href; currentPath = entry.href;
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = tr(STR_LOADING);
entries.clear(); entries.clear();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true; requestUpdate();
fetchFeed(currentPath); fetchFeed(currentPath);
} }
@@ -325,10 +294,10 @@ void OpdsBookBrowserActivity::navigateBack() {
navigationHistory.pop_back(); navigationHistory.pop_back();
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = tr(STR_LOADING);
entries.clear(); entries.clear();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true; requestUpdate();
fetchFeed(currentPath); fetchFeed(currentPath);
} }
@@ -339,7 +308,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
statusMessage = book.title; statusMessage = book.title;
downloadProgress = 0; downloadProgress = 0;
downloadTotal = 0; downloadTotal = 0;
updateRequired = true; requestUpdate();
// Build full download URL // Build full download URL
std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
@@ -357,7 +326,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
downloadProgress = downloaded; downloadProgress = downloaded;
downloadTotal = total; downloadTotal = total;
updateRequired = true; requestUpdate();
}); });
if (result == HttpDownloader::OK) { if (result == HttpDownloader::OK) {
@@ -369,11 +338,11 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str()); LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str());
state = BrowserState::BROWSING; state = BrowserState::BROWSING;
updateRequired = true; requestUpdate();
} else { } else {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Download failed"; errorMessage = tr(STR_DOWNLOAD_FAILED);
updateRequired = true; requestUpdate();
} }
} }
@@ -381,8 +350,8 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
// Already connected? Verify connection is valid by checking IP // Already connected? Verify connection is valid by checking IP
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = tr(STR_LOADING);
updateRequired = true; requestUpdate();
fetchFeed(currentPath); fetchFeed(currentPath);
return; return;
} }
@@ -393,7 +362,7 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
void OpdsBookBrowserActivity::launchWifiSelection() { void OpdsBookBrowserActivity::launchWifiSelection() {
state = BrowserState::WIFI_SELECTION; state = BrowserState::WIFI_SELECTION;
updateRequired = true; requestUpdate();
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); })); [this](const bool connected) { onWifiSelectionComplete(connected); }));
@@ -405,8 +374,8 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
if (connected) { if (connected) {
LOG_DBG("OPDS", "WiFi connected via selection, fetching feed"); LOG_DBG("OPDS", "WiFi connected via selection, fetching feed");
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = tr(STR_LOADING);
updateRequired = true; requestUpdate();
fetchFeed(currentPath); fetchFeed(currentPath);
} else { } else {
LOG_DBG("OPDS", "WiFi selection cancelled/failed"); LOG_DBG("OPDS", "WiFi selection cancelled/failed");
@@ -415,7 +384,7 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
WiFi.disconnect(); WiFi.disconnect();
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "WiFi connection failed"; errorMessage = tr(STR_WIFI_CONN_FAILED);
updateRequired = true; requestUpdate();
} }
} }

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <OpdsParser.h> #include <OpdsParser.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -34,13 +31,10 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
bool updateRequired = false;
BrowserState state = BrowserState::LOADING; BrowserState state = BrowserState::LOADING;
std::vector<OpdsEntry> entries; std::vector<OpdsEntry> entries;
std::vector<std::string> navigationHistory; // Stack of previous feed paths for back navigation std::vector<std::string> navigationHistory; // Stack of previous feed paths for back navigation
@@ -53,10 +47,6 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void checkAndConnectWifi(); void checkAndConnectWifi();
void launchWifiSelection(); void launchWifiSelection();
void onWifiSelectionComplete(bool connected); void onWifiSelectionComplete(bool connected);

View File

@@ -4,6 +4,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <Utf8.h> #include <Utf8.h>
#include <PlaceholderCoverGenerator.h> #include <PlaceholderCoverGenerator.h>
#include <Xtc.h> #include <Xtc.h>
@@ -20,11 +21,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
void HomeActivity::taskTrampoline(void* param) {
auto* self = static_cast<HomeActivity*>(param);
self->displayTaskLoop();
}
int HomeActivity::getMenuItemCount() const { int HomeActivity::getMenuItemCount() const {
int count = 4; // My Library, Recents, File transfer, Settings int count = 4; // My Library, Recents, File transfer, Settings
if (!recentBooks.empty()) { if (!recentBooks.empty()) {
@@ -68,7 +64,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
if (!Epub::isValidThumbnailBmp(coverPath)) { if (!Epub::isValidThumbnailBmp(coverPath)) {
if (!showingLoading) { if (!showingLoading) {
showingLoading = true; showingLoading = true;
popupRect = GUI.drawPopup(renderer, "Loading..."); popupRect = GUI.drawPopup(renderer, tr(STR_LOADING));
} }
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
@@ -119,7 +115,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
} }
coverRendered = false; coverRendered = false;
updateRequired = true; requestUpdate();
} }
} }
progress++; progress++;
@@ -132,8 +128,6 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
void HomeActivity::onEnter() { void HomeActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Check if OPDS browser URL is configured // Check if OPDS browser URL is configured
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
@@ -143,28 +137,12 @@ void HomeActivity::onEnter() {
loadRecentBooks(metrics.homeRecentBooksCount); loadRecentBooks(metrics.homeRecentBooksCount);
// Trigger first update // Trigger first update
updateRequired = true; requestUpdate();
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void HomeActivity::onExit() { void HomeActivity::onExit() {
Activity::onExit(); Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
// Free the stored cover buffer if any // Free the stored cover buffer if any
freeCoverBuffer(); freeCoverBuffer();
} }
@@ -216,12 +194,12 @@ void HomeActivity::loop() {
buttonNavigator.onNext([this, menuCount] { buttonNavigator.onNext([this, menuCount] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this, menuCount] { buttonNavigator.onPrevious([this, menuCount] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount);
updateRequired = true; requestUpdate();
}); });
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
@@ -250,19 +228,7 @@ void HomeActivity::loop() {
} }
} }
void HomeActivity::displayTaskLoop() { void HomeActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void HomeActivity::render() {
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
@@ -277,10 +243,11 @@ void HomeActivity::render() {
std::bind(&HomeActivity::storeCoverBuffer, this)); std::bind(&HomeActivity::storeCoverBuffer, this));
// Build menu items dynamically // Build menu items dynamically
std::vector<const char*> menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"}; std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
tr(STR_SETTINGS_TITLE)};
if (hasOpdsUrl) { if (hasOpdsUrl) {
// Insert OPDS Browser after My Library // Insert OPDS Browser after My Library
menuItems.insert(menuItems.begin() + 2, "OPDS Browser"); menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
} }
GUI.drawButtonMenu( GUI.drawButtonMenu(
@@ -291,14 +258,14 @@ void HomeActivity::render() {
static_cast<int>(menuItems.size()), selectorIndex - recentBooks.size(), static_cast<int>(menuItems.size()), selectorIndex - recentBooks.size(),
[&menuItems](int index) { return std::string(menuItems[index]); }, nullptr); [&menuItems](int index) { return std::string(menuItems[index]); }, nullptr);
const auto labels = mappedInput.mapLabels("", "Select", "Up", "Down"); const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
if (!firstRenderDone) { if (!firstRenderDone) {
firstRenderDone = true; firstRenderDone = true;
updateRequired = true; requestUpdate();
} else if (!recentsLoaded && !recentsLoading) { } else if (!recentsLoaded && !recentsLoading) {
recentsLoading = true; recentsLoading = true;
loadRecentCovers(metrics.homeCoverHeight); loadRecentCovers(metrics.homeCoverHeight);

View File

@@ -1,8 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <vector> #include <vector>
@@ -14,11 +10,8 @@ struct RecentBook;
struct Rect; struct Rect;
class HomeActivity final : public Activity { class HomeActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false;
bool recentsLoading = false; bool recentsLoading = false;
bool recentsLoaded = false; bool recentsLoaded = false;
bool firstRenderDone = false; bool firstRenderDone = false;
@@ -34,9 +27,6 @@ class HomeActivity final : public Activity {
const std::function<void()> onFileTransferOpen; const std::function<void()> onFileTransferOpen;
const std::function<void()> onOpdsBrowserOpen; const std::function<void()> onOpdsBrowserOpen;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
int getMenuItemCount() const; int getMenuItemCount() const;
bool storeCoverBuffer(); // Store frame buffer for cover image bool storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover bool restoreCoverBuffer(); // Restore frame buffer from stored cover
@@ -60,4 +50,5 @@ class HomeActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <algorithm> #include <algorithm>
@@ -66,11 +67,6 @@ void sortFileList(std::vector<std::string>& strs) {
}); });
} }
void MyLibraryActivity::taskTrampoline(void* param) {
auto* self = static_cast<MyLibraryActivity*>(param);
self->displayTaskLoop();
}
void MyLibraryActivity::loadFiles() { void MyLibraryActivity::loadFiles() {
files.clear(); files.clear();
@@ -109,33 +105,14 @@ void MyLibraryActivity::loadFiles() {
void MyLibraryActivity::onEnter() { void MyLibraryActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
loadFiles(); loadFiles();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true;
xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", requestUpdate();
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void MyLibraryActivity::onExit() { void MyLibraryActivity::onExit() {
Activity::onExit(); Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
files.clear(); files.clear();
} }
@@ -146,7 +123,6 @@ void MyLibraryActivity::loop() {
basepath = "/"; basepath = "/";
loadFiles(); loadFiles();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true;
return; return;
} }
@@ -162,7 +138,7 @@ void MyLibraryActivity::loop() {
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
loadFiles(); loadFiles();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true; requestUpdate();
} else { } else {
onSelectBook(basepath + files[selectorIndex]); onSelectBook(basepath + files[selectorIndex]);
return; return;
@@ -183,7 +159,7 @@ void MyLibraryActivity::loop() {
const std::string dirName = oldPath.substr(pos + 1) + "/"; const std::string dirName = oldPath.substr(pos + 1) + "/";
selectorIndex = findEntry(dirName); selectorIndex = findEntry(dirName);
updateRequired = true; requestUpdate();
} else { } else {
onGoHome(); onGoHome();
} }
@@ -194,51 +170,39 @@ void MyLibraryActivity::loop() {
buttonNavigator.onNextRelease([this, listSize] { buttonNavigator.onNextRelease([this, listSize] {
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, listSize] { buttonNavigator.onPreviousRelease([this, listSize] {
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this, listSize, pageItems] { buttonNavigator.onNextContinuous([this, listSize, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
updateRequired = true; requestUpdate();
}); });
} }
void MyLibraryActivity::displayTaskLoop() { void MyLibraryActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void MyLibraryActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str(); auto folderName = basepath == "/" ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1).c_str();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName);
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
if (files.empty()) { if (files.empty()) {
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No books found"); renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_BOOKS_FOUND));
} else { } else {
GUI.drawList( GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex, renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
@@ -246,7 +210,8 @@ void MyLibraryActivity::render() const {
} }
// Help text // Help text
const auto labels = mappedInput.mapLabels(basepath == "/" ? "« Home" : "« Back", "Open", "Up", "Down"); const auto labels = mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP),
tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,8 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -13,12 +9,9 @@
class MyLibraryActivity final : public Activity { class MyLibraryActivity final : public Activity {
private: private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
size_t selectorIndex = 0; size_t selectorIndex = 0;
bool updateRequired = false;
// Files state // Files state
std::string basepath = "/"; std::string basepath = "/";
@@ -28,10 +21,6 @@ class MyLibraryActivity final : public Activity {
const std::function<void(const std::string& path)> onSelectBook; const std::function<void(const std::string& path)> onSelectBook;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
// Data loading // Data loading
void loadFiles(); void loadFiles();
size_t findEntry(const std::string& name) const; size_t findEntry(const std::string& name) const;
@@ -48,4 +37,5 @@ class MyLibraryActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <algorithm> #include <algorithm>
@@ -15,11 +16,6 @@ namespace {
constexpr unsigned long GO_HOME_MS = 1000; constexpr unsigned long GO_HOME_MS = 1000;
} // namespace } // namespace
void RecentBooksActivity::taskTrampoline(void* param) {
auto* self = static_cast<RecentBooksActivity*>(param);
self->displayTaskLoop();
}
void RecentBooksActivity::loadRecentBooks() { void RecentBooksActivity::loadRecentBooks() {
recentBooks.clear(); recentBooks.clear();
const auto& books = RECENT_BOOKS.getBooks(); const auto& books = RECENT_BOOKS.getBooks();
@@ -37,34 +33,15 @@ void RecentBooksActivity::loadRecentBooks() {
void RecentBooksActivity::onEnter() { void RecentBooksActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Load data // Load data
loadRecentBooks(); loadRecentBooks();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true; requestUpdate();
xTaskCreate(&RecentBooksActivity::taskTrampoline, "RecentBooksActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void RecentBooksActivity::onExit() { void RecentBooksActivity::onExit() {
Activity::onExit(); Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
recentBooks.clear(); recentBooks.clear();
} }
@@ -87,52 +64,40 @@ void RecentBooksActivity::loop() {
buttonNavigator.onNextRelease([this, listSize] { buttonNavigator.onNextRelease([this, listSize] {
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, listSize] { buttonNavigator.onPreviousRelease([this, listSize] {
selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this, listSize, pageItems] { buttonNavigator.onNextContinuous([this, listSize, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { buttonNavigator.onPreviousContinuous([this, listSize, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems);
updateRequired = true; requestUpdate();
}); });
} }
void RecentBooksActivity::displayTaskLoop() { void RecentBooksActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void RecentBooksActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Recent Books"); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_MENU_RECENT_BOOKS));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
// Recent tab // Recent tab
if (recentBooks.empty()) { if (recentBooks.empty()) {
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No recent books"); renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_RECENT_BOOKS));
} else { } else {
GUI.drawList( GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex, renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
@@ -141,7 +106,7 @@ void RecentBooksActivity::render() const {
} }
// Help text // Help text
const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down"); const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h> #include <I18n.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -13,12 +11,9 @@
class RecentBooksActivity final : public Activity { class RecentBooksActivity final : public Activity {
private: private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
size_t selectorIndex = 0; size_t selectorIndex = 0;
bool updateRequired = false;
// Recent tab state // Recent tab state
std::vector<RecentBook> recentBooks; std::vector<RecentBook> recentBooks;
@@ -27,10 +22,6 @@ class RecentBooksActivity final : public Activity {
const std::function<void(const std::string& path)> onSelectBook; const std::function<void(const std::string& path)> onSelectBook;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
// Data loading // Data loading
void loadRecentBooks(); void loadRecentBooks();
@@ -42,4 +33,5 @@ class RecentBooksActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -2,6 +2,7 @@
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
@@ -14,16 +15,10 @@ namespace {
constexpr const char* HOSTNAME = "crosspoint"; constexpr const char* HOSTNAME = "crosspoint";
} // namespace } // namespace
void CalibreConnectActivity::taskTrampoline(void* param) {
auto* self = static_cast<CalibreConnectActivity*>(param);
self->displayTaskLoop();
}
void CalibreConnectActivity::onEnter() { void CalibreConnectActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); requestUpdate();
updateRequired = true;
state = CalibreConnectState::WIFI_SELECTION; state = CalibreConnectState::WIFI_SELECTION;
connectedIP.clear(); connectedIP.clear();
connectedSSID.clear(); connectedSSID.clear();
@@ -35,13 +30,6 @@ void CalibreConnectActivity::onEnter() {
lastCompleteAt = 0; lastCompleteAt = 0;
exitRequested = false; exitRequested = false;
xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
if (WiFi.status() != WL_CONNECTED) { if (WiFi.status() != WL_CONNECTED) {
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); })); [this](const bool connected) { onWifiSelectionComplete(connected); }));
@@ -63,14 +51,6 @@ void CalibreConnectActivity::onExit() {
delay(30); delay(30);
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
delay(30); delay(30);
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) { void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) {
@@ -92,7 +72,7 @@ void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) {
void CalibreConnectActivity::startWebServer() { void CalibreConnectActivity::startWebServer() {
state = CalibreConnectState::SERVER_STARTING; state = CalibreConnectState::SERVER_STARTING;
updateRequired = true; requestUpdate();
if (MDNS.begin(HOSTNAME)) { if (MDNS.begin(HOSTNAME)) {
// mDNS is optional for the Calibre plugin but still helpful for users. // mDNS is optional for the Calibre plugin but still helpful for users.
@@ -104,10 +84,10 @@ void CalibreConnectActivity::startWebServer() {
if (webServer->isRunning()) { if (webServer->isRunning()) {
state = CalibreConnectState::SERVER_RUNNING; state = CalibreConnectState::SERVER_RUNNING;
updateRequired = true; requestUpdate();
} else { } else {
state = CalibreConnectState::ERROR; state = CalibreConnectState::ERROR;
updateRequired = true; requestUpdate();
} }
} }
@@ -178,7 +158,7 @@ void CalibreConnectActivity::loop() {
changed = true; changed = true;
} }
if (changed) { if (changed) {
updateRequired = true; requestUpdate();
} }
} }
@@ -188,19 +168,7 @@ void CalibreConnectActivity::loop() {
} }
} }
void CalibreConnectActivity::displayTaskLoop() { void CalibreConnectActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void CalibreConnectActivity::render() const {
if (state == CalibreConnectState::SERVER_RUNNING) { if (state == CalibreConnectState::SERVER_RUNNING) {
renderer.clearScreen(); renderer.clearScreen();
renderServerRunning(); renderServerRunning();
@@ -211,9 +179,9 @@ void CalibreConnectActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
if (state == CalibreConnectState::SERVER_STARTING) { if (state == CalibreConnectState::SERVER_STARTING) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Calibre...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CALIBRE_STARTING), true, EpdFontFamily::BOLD);
} else if (state == CalibreConnectState::ERROR) { } else if (state == CalibreConnectState::ERROR) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Calibre setup failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD);
} }
renderer.displayBuffer(); renderer.displayBuffer();
} }
@@ -223,31 +191,32 @@ void CalibreConnectActivity::renderServerRunning() const {
constexpr int SMALL_SPACING = 20; constexpr int SMALL_SPACING = 20;
constexpr int SECTION_SPACING = 40; constexpr int SECTION_SPACING = 40;
constexpr int TOP_PADDING = 14; constexpr int TOP_PADDING = 14;
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Connect to Calibre", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CALIBRE_WIRELESS), true, EpdFontFamily::BOLD);
int y = 55 + TOP_PADDING; int y = 55 + TOP_PADDING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Network", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD);
y += LINE_SPACING; y += LINE_SPACING;
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str()); renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING,
(std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str());
y += LINE_SPACING * 2 + SECTION_SPACING; y += LINE_SPACING * 2 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Setup", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD);
y += LINE_SPACING; y += LINE_SPACING;
renderer.drawCenteredText(SMALL_FONT_ID, y, "1) Install CrossPoint Reader plugin"); renderer.drawCenteredText(SMALL_FONT_ID, y, tr(STR_CALIBRE_INSTRUCTION_1));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, "2) Be on the same WiFi network"); renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, tr(STR_CALIBRE_INSTRUCTION_2));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, "3) In Calibre: \"Send to device\""); renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, tr(STR_CALIBRE_INSTRUCTION_3));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, "Keep this screen open while sending"); renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, tr(STR_CALIBRE_INSTRUCTION_4));
y += SMALL_SPACING * 3 + SECTION_SPACING; y += SMALL_SPACING * 3 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Status", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD);
y += LINE_SPACING; y += LINE_SPACING;
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
std::string label = "Receiving"; std::string label = tr(STR_CALIBRE_RECEIVING);
if (!currentUploadName.empty()) { if (!currentUploadName.empty()) {
label += ": " + currentUploadName; label += ": " + currentUploadName;
if (label.length() > 34) { if (label.length() > 34) {
@@ -263,13 +232,13 @@ void CalibreConnectActivity::renderServerRunning() const {
} }
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
std::string msg = "Received: " + lastCompleteName; std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName;
if (msg.length() > 36) { if (msg.length() > 36) {
msg.replace(33, msg.length() - 33, "..."); msg.replace(33, msg.length() - 33, "...");
} }
renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str());
} }
const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -17,9 +14,6 @@ enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING
* but renders Calibre-specific instructions instead of the web transfer UI. * but renders Calibre-specific instructions instead of the web transfer UI.
*/ */
class CalibreConnectActivity final : public ActivityWithSubactivity { class CalibreConnectActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
CalibreConnectState state = CalibreConnectState::WIFI_SELECTION; CalibreConnectState state = CalibreConnectState::WIFI_SELECTION;
const std::function<void()> onComplete; const std::function<void()> onComplete;
@@ -34,9 +28,6 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
unsigned long lastCompleteAt = 0; unsigned long lastCompleteAt = 0;
bool exitRequested = false; bool exitRequested = false;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void renderServerRunning() const; void renderServerRunning() const;
void onWifiSelectionComplete(bool connected); void onWifiSelectionComplete(bool connected);
@@ -50,6 +41,7 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
bool skipLoopDelay() override { return webServer && webServer->isRunning(); } bool skipLoopDelay() override { return webServer && webServer->isRunning(); }
bool preventAutoSleep() override { return webServer && webServer->isRunning(); } bool preventAutoSleep() override { return webServer && webServer->isRunning(); }
}; };

View File

@@ -3,6 +3,7 @@
#include <DNSServer.h> #include <DNSServer.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <qrcode.h> #include <qrcode.h>
@@ -29,17 +30,10 @@ DNSServer* dnsServer = nullptr;
constexpr uint16_t DNS_PORT = 53; constexpr uint16_t DNS_PORT = 53;
} // namespace } // namespace
void CrossPointWebServerActivity::taskTrampoline(void* param) {
auto* self = static_cast<CrossPointWebServerActivity*>(param);
self->displayTaskLoop();
}
void CrossPointWebServerActivity::onEnter() { void CrossPointWebServerActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
LOG_DBG("WEBACT] [MEM", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap());
renderingMutex = xSemaphoreCreateMutex();
// Reset state // Reset state
state = WebServerActivityState::MODE_SELECTION; state = WebServerActivityState::MODE_SELECTION;
@@ -48,14 +42,7 @@ void CrossPointWebServerActivity::onEnter() {
connectedIP.clear(); connectedIP.clear();
connectedSSID.clear(); connectedSSID.clear();
lastHandleClientTime = 0; lastHandleClientTime = 0;
updateRequired = true; requestUpdate();
xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Launch network mode selection subactivity // Launch network mode selection subactivity
LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity..."); LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity...");
@@ -68,7 +55,7 @@ void CrossPointWebServerActivity::onEnter() {
void CrossPointWebServerActivity::onExit() { void CrossPointWebServerActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
LOG_DBG("WEBACT] [MEM", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
state = WebServerActivityState::SHUTTING_DOWN; state = WebServerActivityState::SHUTTING_DOWN;
@@ -103,27 +90,7 @@ void CrossPointWebServerActivity::onExit() {
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
delay(30); // Allow WiFi hardware to power down delay(30); // Allow WiFi hardware to power down
LOG_DBG("WEBACT] [MEM", "Free heap after WiFi disconnect: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
// Acquire mutex before deleting task
LOG_DBG("WEBACT", "Acquiring rendering mutex before task deletion...");
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// Delete the display task
LOG_DBG("WEBACT", "Deleting display task...");
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
LOG_DBG("WEBACT", "Display task deleted");
}
// Delete the mutex
LOG_DBG("WEBACT", "Deleting mutex...");
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
LOG_DBG("WEBACT", "Mutex deleted");
LOG_DBG("WEBACT] [MEM", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
} }
void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) { void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) {
@@ -165,7 +132,7 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
} else { } else {
// AP mode - start access point // AP mode - start access point
state = WebServerActivityState::AP_STARTING; state = WebServerActivityState::AP_STARTING;
updateRequired = true; requestUpdate();
startAccessPoint(); startAccessPoint();
} }
} }
@@ -200,7 +167,7 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
void CrossPointWebServerActivity::startAccessPoint() { void CrossPointWebServerActivity::startAccessPoint() {
LOG_DBG("WEBACT", "Starting Access Point mode..."); LOG_DBG("WEBACT", "Starting Access Point mode...");
LOG_DBG("WEBACT] [MEM", "Free heap before AP start: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap());
// Configure and start the AP // Configure and start the AP
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
@@ -248,7 +215,7 @@ void CrossPointWebServerActivity::startAccessPoint() {
dnsServer->start(DNS_PORT, "*", apIP); dnsServer->start(DNS_PORT, "*", apIP);
LOG_DBG("WEBACT", "DNS server started for captive portal"); LOG_DBG("WEBACT", "DNS server started for captive portal");
LOG_DBG("WEBACT] [MEM", "Free heap after AP start: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap after AP start: %d bytes", ESP.getFreeHeap());
// Start the web server // Start the web server
startWebServer(); startWebServer();
@@ -267,9 +234,10 @@ void CrossPointWebServerActivity::startWebServer() {
// Force an immediate render since we're transitioning from a subactivity // Force an immediate render since we're transitioning from a subactivity
// that had its own rendering task. We need to make sure our display is shown. // that had its own rendering task. We need to make sure our display is shown.
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
render(); RenderLock lock(*this);
xSemaphoreGive(renderingMutex); render(std::move(lock));
}
LOG_DBG("WEBACT", "Rendered File Transfer screen"); LOG_DBG("WEBACT", "Rendered File Transfer screen");
} else { } else {
LOG_ERR("WEBACT", "ERROR: Failed to start web server!"); LOG_ERR("WEBACT", "ERROR: Failed to start web server!");
@@ -312,7 +280,7 @@ void CrossPointWebServerActivity::loop() {
LOG_DBG("WEBACT", "WiFi disconnected! Status: %d", wifiStatus); LOG_DBG("WEBACT", "WiFi disconnected! Status: %d", wifiStatus);
// Show error and exit gracefully // Show error and exit gracefully
state = WebServerActivityState::SHUTTING_DOWN; state = WebServerActivityState::SHUTTING_DOWN;
updateRequired = true; requestUpdate();
return; return;
} }
// Log weak signal warnings // Log weak signal warnings
@@ -368,19 +336,7 @@ void CrossPointWebServerActivity::loop() {
} }
} }
void CrossPointWebServerActivity::displayTaskLoop() { void CrossPointWebServerActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void CrossPointWebServerActivity::render() const {
// Only render our own UI when server is running // Only render our own UI when server is running
// Subactivities handle their own rendering // Subactivities handle their own rendering
if (state == WebServerActivityState::SERVER_RUNNING) { if (state == WebServerActivityState::SERVER_RUNNING) {
@@ -390,7 +346,7 @@ void CrossPointWebServerActivity::render() const {
} else if (state == WebServerActivityState::AP_STARTING) { } else if (state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen(); renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_STARTING_HOTSPOT), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
} }
} }
@@ -421,21 +377,20 @@ void CrossPointWebServerActivity::renderServerRunning() const {
// Use consistent line spacing // Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines constexpr int LINE_SPACING = 28; // Space between lines
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD);
if (isApMode) { if (isApMode) {
// AP mode display - center the content block // AP mode display - center the content block
int startY = 55; int startY = 55;
renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_HOTSPOT_MODE), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, "Connect your device to this WiFi network"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, tr(STR_CONNECT_WIFI_HINT));
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, tr(STR_SCAN_QR_WIFI_HINT));
"or scan QR code with your phone to connect to Wifi.");
// Show QR code for URL // Show QR code for URL
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
@@ -446,24 +401,24 @@ void CrossPointWebServerActivity::renderServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD);
// Show IP address as fallback // Show IP address as fallback
std::string ipUrl = "or http://" + connectedIP + "/"; std::string ipUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + connectedIP + "/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Open this URL in your browser"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_OPEN_URL_HINT));
// Show QR code for URL // Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, tr(STR_SCAN_QR_HINT));
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl);
} else { } else {
// STA mode display (original behavior) // STA mode display (original behavior)
const int startY = 65; const int startY = 65;
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str());
std::string ipInfo = "IP Address: " + connectedIP; std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str());
// Show web server URL prominently // Show web server URL prominently
@@ -471,16 +426,16 @@ void CrossPointWebServerActivity::renderServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD);
// Also show hostname URL // Also show hostname URL
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/"; std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, tr(STR_OPEN_URL_HINT));
// Show QR code for URL // Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_SCAN_QR_HINT));
} }
const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -31,9 +28,6 @@ enum class WebServerActivityState {
* - Cleans up the server and shuts down WiFi on exit * - Cleans up the server and shuts down WiFi on exit
*/ */
class CrossPointWebServerActivity final : public ActivityWithSubactivity { class CrossPointWebServerActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
WebServerActivityState state = WebServerActivityState::MODE_SELECTION; WebServerActivityState state = WebServerActivityState::MODE_SELECTION;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
@@ -51,9 +45,6 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
// Performance monitoring // Performance monitoring
unsigned long lastHandleClientTime = 0; unsigned long lastHandleClientTime = 0;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void renderServerRunning() const; void renderServerRunning() const;
void onNetworkModeSelected(NetworkMode mode); void onNetworkModeSelected(NetworkMode mode);
@@ -69,6 +60,7 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
bool skipLoopDelay() override { return webServer && webServer->isRunning(); } bool skipLoopDelay() override { return webServer && webServer->isRunning(); }
bool preventAutoSleep() override { return webServer && webServer->isRunning(); } bool preventAutoSleep() override { return webServer && webServer->isRunning(); }
}; };

View File

@@ -1,6 +1,7 @@
#include "NetworkModeSelectionActivity.h" #include "NetworkModeSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
@@ -8,50 +9,19 @@
namespace { namespace {
constexpr int MENU_ITEM_COUNT = 3; constexpr int MENU_ITEM_COUNT = 3;
const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Join a Network", "Connect to Calibre", "Create Hotspot"};
const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {
"Connect to an existing WiFi network",
"Use Calibre wireless device transfers",
"Create a WiFi network others can join",
};
} // namespace } // namespace
void NetworkModeSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<NetworkModeSelectionActivity*>(param);
self->displayTaskLoop();
}
void NetworkModeSelectionActivity::onEnter() { void NetworkModeSelectionActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Reset selection // Reset selection
selectedIndex = 0; selectedIndex = 0;
// Trigger first update // Trigger first update
updateRequired = true; requestUpdate();
xTaskCreate(&NetworkModeSelectionActivity::taskTrampoline, "NetworkModeTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void NetworkModeSelectionActivity::onExit() { void NetworkModeSelectionActivity::onExit() { Activity::onExit(); }
Activity::onExit();
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void NetworkModeSelectionActivity::loop() { void NetworkModeSelectionActivity::loop() {
// Handle back button - cancel // Handle back button - cancel
@@ -75,38 +45,32 @@ void NetworkModeSelectionActivity::loop() {
// Handle navigation // Handle navigation
buttonNavigator.onNext([this] { buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT); selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this] { buttonNavigator.onPrevious([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT); selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT);
updateRequired = true; requestUpdate();
}); });
} }
void NetworkModeSelectionActivity::displayTaskLoop() { void NetworkModeSelectionActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void NetworkModeSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD);
// Draw subtitle // Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?"); renderer.drawCenteredText(UI_10_FONT_ID, 50, tr(STR_HOW_CONNECT));
// Menu items and descriptions
static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS,
StrId::STR_CREATE_HOTSPOT};
static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC,
StrId::STR_HOTSPOT_DESC};
// Draw menu items centered on screen // Draw menu items centered on screen
constexpr int itemHeight = 50; // Height for each menu item (including description) constexpr int itemHeight = 50; // Height for each menu item (including description)
@@ -123,12 +87,12 @@ void NetworkModeSelectionActivity::render() const {
// Draw text: black=false (white text) when selected (on black background) // Draw text: black=false (white text) when selected (on black background)
// black=true (black text) when not selected (on white background) // black=true (black text) when not selected (on white background)
renderer.drawText(UI_10_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected); renderer.drawText(UI_10_FONT_ID, 30, itemY, I18N.get(menuItems[i]), /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected); renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, I18N.get(menuDescs[i]), /*black=*/!isSelected);
} }
// Draw help text at bottom // Draw help text at bottom
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
@@ -21,19 +18,13 @@ enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT };
* The onCancel callback is called if the user presses back. * The onCancel callback is called if the user presses back.
*/ */
class NetworkModeSelectionActivity final : public Activity { class NetworkModeSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int selectedIndex = 0; int selectedIndex = 0;
bool updateRequired = false;
const std::function<void(NetworkMode)> onModeSelected; const std::function<void(NetworkMode)> onModeSelected;
const std::function<void()> onCancel; const std::function<void()> onCancel;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public: public:
explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void(NetworkMode)>& onModeSelected, const std::function<void(NetworkMode)>& onModeSelected,
@@ -42,4 +33,5 @@ class NetworkModeSelectionActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -1,6 +1,7 @@
#include "WifiSelectionActivity.h" #include "WifiSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <WiFi.h> #include <WiFi.h>
@@ -12,21 +13,15 @@
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
void WifiSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<WifiSelectionActivity*>(param);
self->displayTaskLoop();
}
void WifiSelectionActivity::onEnter() { void WifiSelectionActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Load saved WiFi credentials - SD card operations need lock as we use SPI // Load saved WiFi credentials - SD card operations need lock as we use SPI
// for both // for both
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
WIFI_STORE.loadFromFile(); RenderLock lock(*this);
xSemaphoreGive(renderingMutex); WIFI_STORE.loadFromFile();
}
// Reset state // Reset state
selectedNetworkIndex = 0; selectedNetworkIndex = 0;
@@ -44,18 +39,13 @@ void WifiSelectionActivity::onEnter() {
// Cache MAC address for display // Cache MAC address for display
uint8_t mac[6]; uint8_t mac[6];
WiFi.macAddress(mac); WiFi.macAddress(mac);
char macStr[32]; char macStr[64];
snprintf(macStr, sizeof(macStr), "MAC address: %02x-%02x-%02x-%02x-%02x-%02x", mac[0], mac[1], mac[2], mac[3], mac[4], snprintf(macStr, sizeof(macStr), "%s %02x-%02x-%02x-%02x-%02x-%02x", tr(STR_MAC_ADDRESS), mac[0], mac[1], mac[2],
mac[5]); mac[3], mac[4], mac[5]);
cachedMacAddress = std::string(macStr); cachedMacAddress = std::string(macStr);
// Task creation // Trigger first update to show scanning message
xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask", requestUpdate();
4096, // Stack size (larger for WiFi operations)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Attempt to auto-connect to the last network // Attempt to auto-connect to the last network
if (allowAutoConnect) { if (allowAutoConnect) {
@@ -70,7 +60,7 @@ void WifiSelectionActivity::onEnter() {
usedSavedPassword = true; usedSavedPassword = true;
autoConnecting = true; autoConnecting = true;
attemptConnection(); attemptConnection();
updateRequired = true; requestUpdate();
return; return;
} }
} }
@@ -83,45 +73,25 @@ void WifiSelectionActivity::onEnter() {
void WifiSelectionActivity::onExit() { void WifiSelectionActivity::onExit() {
Activity::onExit(); Activity::onExit();
LOG_DBG("WIFI] [MEM", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); LOG_DBG("WIFI", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
// Stop any ongoing WiFi scan // Stop any ongoing WiFi scan
LOG_DBG("WIFI", "Deleting WiFi scan..."); LOG_DBG("WIFI", "Deleting WiFi scan...");
WiFi.scanDelete(); WiFi.scanDelete();
LOG_DBG("WIFI] [MEM", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap()); LOG_DBG("WIFI", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap());
// Note: We do NOT disconnect WiFi here - the parent activity // Note: We do NOT disconnect WiFi here - the parent activity
// (CrossPointWebServerActivity) manages WiFi connection state. We just clean // (CrossPointWebServerActivity) manages WiFi connection state. We just clean
// up the scan and task. // up the scan and task.
// Acquire mutex before deleting task to ensure task isn't using it LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
// This prevents hangs/crashes if the task holds the mutex when deleted
LOG_DBG("WIFI", "Acquiring rendering mutex before task deletion...");
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// Delete the display task (we now hold the mutex, so task is blocked if it
// needs it)
LOG_DBG("WIFI", "Deleting display task...");
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
LOG_DBG("WIFI", "Display task deleted");
}
// Now safe to delete the mutex since we own it
LOG_DBG("WIFI", "Deleting mutex...");
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
LOG_DBG("WIFI", "Mutex deleted");
LOG_DBG("WIFI] [MEM", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
} }
void WifiSelectionActivity::startWifiScan() { void WifiSelectionActivity::startWifiScan() {
autoConnecting = false; autoConnecting = false;
state = WifiSelectionState::SCANNING; state = WifiSelectionState::SCANNING;
networks.clear(); networks.clear();
updateRequired = true; requestUpdate();
// Set WiFi mode to station // Set WiFi mode to station
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
@@ -142,7 +112,7 @@ void WifiSelectionActivity::processWifiScanResults() {
if (scanResult == WIFI_SCAN_FAILED) { if (scanResult == WIFI_SCAN_FAILED) {
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true; requestUpdate();
return; return;
} }
@@ -191,7 +161,7 @@ void WifiSelectionActivity::processWifiScanResults() {
WiFi.scanDelete(); WiFi.scanDelete();
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
selectedNetworkIndex = 0; selectedNetworkIndex = 0;
updateRequired = true; requestUpdate();
} }
void WifiSelectionActivity::selectNetwork(const int index) { void WifiSelectionActivity::selectNetwork(const int index) {
@@ -221,9 +191,8 @@ void WifiSelectionActivity::selectNetwork(const int index) {
// Show password entry // Show password entry
state = WifiSelectionState::PASSWORD_ENTRY; state = WifiSelectionState::PASSWORD_ENTRY;
// Don't allow screen updates while changing activity // Don't allow screen updates while changing activity
xSemaphoreTake(renderingMutex, portMAX_DELAY);
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Enter WiFi Password", renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD),
"", // No initial text "", // No initial text
50, // Y position 50, // Y position
64, // Max password length 64, // Max password length
@@ -234,11 +203,9 @@ void WifiSelectionActivity::selectNetwork(const int index) {
}, },
[this] { [this] {
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
updateRequired = true;
exitActivity(); exitActivity();
requestUpdate();
})); }));
updateRequired = true;
xSemaphoreGive(renderingMutex);
} else { } else {
// Connect directly for open networks // Connect directly for open networks
attemptConnection(); attemptConnection();
@@ -250,7 +217,7 @@ void WifiSelectionActivity::attemptConnection() {
connectionStartTime = millis(); connectionStartTime = millis();
connectedIP.clear(); connectedIP.clear();
connectionError.clear(); connectionError.clear();
updateRequired = true; requestUpdate();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
@@ -278,16 +245,17 @@ void WifiSelectionActivity::checkConnectionStatus() {
// Save this as the last connected network - SD card operations need lock as // Save this as the last connected network - SD card operations need lock as
// we use SPI for both // we use SPI for both
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
WIFI_STORE.setLastConnectedSsid(selectedSSID); RenderLock lock(*this);
xSemaphoreGive(renderingMutex); WIFI_STORE.setLastConnectedSsid(selectedSSID);
}
// If we entered a new password, ask if user wants to save it // If we entered a new password, ask if user wants to save it
// Otherwise, immediately complete so parent can start web server // Otherwise, immediately complete so parent can start web server
if (!usedSavedPassword && !enteredPassword.empty()) { if (!usedSavedPassword && !enteredPassword.empty()) {
state = WifiSelectionState::SAVE_PROMPT; state = WifiSelectionState::SAVE_PROMPT;
savePromptSelection = 0; // Default to "Yes" savePromptSelection = 0; // Default to "Yes"
updateRequired = true; requestUpdate();
} else { } else {
// Using saved password or open network - complete immediately // Using saved password or open network - complete immediately
LOG_DBG("WIFI", LOG_DBG("WIFI",
@@ -299,21 +267,21 @@ void WifiSelectionActivity::checkConnectionStatus() {
} }
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) {
connectionError = "Error: General failure"; connectionError = tr(STR_ERROR_GENERAL_FAILURE);
if (status == WL_NO_SSID_AVAIL) { if (status == WL_NO_SSID_AVAIL) {
connectionError = "Error: Network not found"; connectionError = tr(STR_ERROR_NETWORK_NOT_FOUND);
} }
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; requestUpdate();
return; return;
} }
// Check for timeout // Check for timeout
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
WiFi.disconnect(); WiFi.disconnect();
connectionError = "Error: Connection timeout"; connectionError = tr(STR_ERROR_CONNECTION_TIMEOUT);
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; requestUpdate();
return; return;
} }
} }
@@ -348,20 +316,19 @@ void WifiSelectionActivity::loop() {
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (savePromptSelection > 0) { if (savePromptSelection > 0) {
savePromptSelection--; savePromptSelection--;
updateRequired = true; requestUpdate();
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (savePromptSelection < 1) { if (savePromptSelection < 1) {
savePromptSelection++; savePromptSelection++;
updateRequired = true; requestUpdate();
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (savePromptSelection == 0) { if (savePromptSelection == 0) {
// User chose "Yes" - save the password // User chose "Yes" - save the password
xSemaphoreTake(renderingMutex, portMAX_DELAY); RenderLock lock(*this);
WIFI_STORE.addCredential(selectedSSID, enteredPassword); WIFI_STORE.addCredential(selectedSSID, enteredPassword);
xSemaphoreGive(renderingMutex);
} }
// Complete - parent will start web server // Complete - parent will start web server
onComplete(true); onComplete(true);
@@ -378,20 +345,19 @@ void WifiSelectionActivity::loop() {
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
if (forgetPromptSelection > 0) { if (forgetPromptSelection > 0) {
forgetPromptSelection--; forgetPromptSelection--;
updateRequired = true; requestUpdate();
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
if (forgetPromptSelection < 1) { if (forgetPromptSelection < 1) {
forgetPromptSelection++; forgetPromptSelection++;
updateRequired = true; requestUpdate();
} }
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (forgetPromptSelection == 1) { if (forgetPromptSelection == 1) {
RenderLock lock(*this);
// User chose "Forget network" - forget the network // User chose "Forget network" - forget the network
xSemaphoreTake(renderingMutex, portMAX_DELAY);
WIFI_STORE.removeCredential(selectedSSID); WIFI_STORE.removeCredential(selectedSSID);
xSemaphoreGive(renderingMutex);
// Update the network list to reflect the change // Update the network list to reflect the change
const auto network = find_if(networks.begin(), networks.end(), const auto network = find_if(networks.begin(), networks.end(),
[this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; }); [this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; });
@@ -430,7 +396,7 @@ void WifiSelectionActivity::loop() {
// Go back to network list on failure for non-saved credentials // Go back to network list on failure for non-saved credentials
state = WifiSelectionState::NETWORK_LIST; state = WifiSelectionState::NETWORK_LIST;
} }
updateRequired = true; requestUpdate();
return; return;
} }
} }
@@ -465,7 +431,7 @@ void WifiSelectionActivity::loop() {
selectedSSID = networks[selectedNetworkIndex].ssid; selectedSSID = networks[selectedNetworkIndex].ssid;
state = WifiSelectionState::FORGET_PROMPT; state = WifiSelectionState::FORGET_PROMPT;
forgetPromptSelection = 0; // Default to "Cancel" forgetPromptSelection = 0; // Default to "Cancel"
updateRequired = true; requestUpdate();
return; return;
} }
} }
@@ -473,12 +439,12 @@ void WifiSelectionActivity::loop() {
// Handle navigation // Handle navigation
buttonNavigator.onNext([this] { buttonNavigator.onNext([this] {
selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size());
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this] { buttonNavigator.onPrevious([this] {
selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size());
updateRequired = true; requestUpdate();
}); });
} }
} }
@@ -500,32 +466,14 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi
return " "; // Very weak return " "; // Very weak
} }
void WifiSelectionActivity::displayTaskLoop() { void WifiSelectionActivity::render(Activity::RenderLock&&) {
while (true) { // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning
// If a subactivity is active, yield CPU time but don't render // from the keyboard subactivity back to the main activity
if (subActivity) { if (state == WifiSelectionState::PASSWORD_ENTRY) {
vTaskDelay(10 / portTICK_PERIOD_MS); requestUpdateAndWait();
continue; return;
}
// Don't render if we're in PASSWORD_ENTRY state - we're just transitioning
// from the keyboard subactivity back to the main activity
if (state == WifiSelectionState::PASSWORD_ENTRY) {
vTaskDelay(10 / portTICK_PERIOD_MS);
continue;
}
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
} }
}
void WifiSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
switch (state) { switch (state) {
@@ -563,14 +511,14 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD);
if (networks.empty()) { if (networks.empty()) {
// No networks found or scan failed // No networks found or scan failed
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found"); renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS));
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again"); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN));
} else { } else {
// Calculate how many networks we can display // Calculate how many networks we can display
constexpr int startY = 60; constexpr int startY = 60;
@@ -625,8 +573,8 @@ void WifiSelectionActivity::renderNetworkList() const {
} }
// Show network count // Show network count
char countStr[32]; char countStr[64];
snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size()); snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size());
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
} }
@@ -634,12 +582,12 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str()); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text // Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, tr(STR_NETWORK_LEGEND));
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
const char* forgetLabel = hasSavedPassword ? "Forget" : ""; const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : "";
const auto labels = mappedInput.mapLabels("« Back", "Connect", forgetLabel, "Refresh"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_CONNECT), forgetLabel, tr(STR_RETRY));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@@ -649,11 +597,11 @@ void WifiSelectionActivity::renderConnecting() const {
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
if (state == WifiSelectionState::SCANNING) { if (state == WifiSelectionState::SCANNING) {
renderer.drawCenteredText(UI_10_FONT_ID, top, "Scanning..."); renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_SCANNING));
} else { } else {
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connecting...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_CONNECTING), true, EpdFontFamily::BOLD);
std::string ssidInfo = "to " + selectedSSID; std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 25) { if (ssidInfo.length() > 25) {
ssidInfo.replace(22, ssidInfo.length() - 22, "..."); ssidInfo.replace(22, ssidInfo.length() - 22, "...");
} }
@@ -666,19 +614,19 @@ void WifiSelectionActivity::renderConnected() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 4) / 2; const auto top = (pageHeight - height * 4) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 30, "Connected!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 30, tr(STR_CONNECTED), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top + 10, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + 10, ssidInfo.c_str());
const std::string ipInfo = "IP Address: " + connectedIP; const std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str());
// Use centralized button hints // Use centralized button hints
const auto labels = mappedInput.mapLabels("", "Continue", "", ""); const auto labels = mappedInput.mapLabels("", tr(STR_DONE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@@ -688,15 +636,15 @@ void WifiSelectionActivity::renderSavePrompt() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connected!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_CONNECTED), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Save password for next time?"); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, tr(STR_SAVE_PASSWORD));
// Draw Yes/No buttons // Draw Yes/No buttons
const int buttonY = top + 80; const int buttonY = top + 80;
@@ -707,20 +655,22 @@ void WifiSelectionActivity::renderSavePrompt() const {
// Draw "Yes" button // Draw "Yes" button
if (savePromptSelection == 0) { if (savePromptSelection == 0) {
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]"); std::string text = "[" + std::string(tr(STR_YES)) + "]";
renderer.drawText(UI_10_FONT_ID, startX, buttonY, text.c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes"); renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, tr(STR_YES));
} }
// Draw "No" button // Draw "No" button
if (savePromptSelection == 1) { if (savePromptSelection == 1) {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]"); std::string text = "[" + std::string(tr(STR_NO)) + "]";
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, text.c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No"); renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, tr(STR_NO));
} }
// Use centralized button hints // Use centralized button hints
const auto labels = mappedInput.mapLabels("« Skip", "Select", "Left", "Right"); const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@@ -729,11 +679,11 @@ void WifiSelectionActivity::renderConnectionFailed() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 2) / 2; const auto top = (pageHeight - height * 2) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 20, "Connection Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str());
// Use centralized button hints // Use centralized button hints
const auto labels = mappedInput.mapLabels("« Back", "Continue", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@@ -743,14 +693,15 @@ void WifiSelectionActivity::renderForgetPrompt() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_FORGET_NETWORK), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID;
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Forget network and remove saved password?"); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, tr(STR_FORGET_AND_REMOVE));
// Draw Cancel/Forget network buttons // Draw Cancel/Forget network buttons
const int buttonY = top + 80; const int buttonY = top + 80;
@@ -761,19 +712,21 @@ void WifiSelectionActivity::renderForgetPrompt() const {
// Draw "Cancel" button // Draw "Cancel" button
if (forgetPromptSelection == 0) { if (forgetPromptSelection == 0) {
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Cancel]"); std::string text = "[" + std::string(tr(STR_CANCEL)) + "]";
renderer.drawText(UI_10_FONT_ID, startX, buttonY, text.c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Cancel"); renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, tr(STR_CANCEL));
} }
// Draw "Forget network" button // Draw "Forget network" button
if (forgetPromptSelection == 1) { if (forgetPromptSelection == 1) {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[Forget network]"); std::string text = "[" + std::string(tr(STR_FORGET_BUTTON)) + "]";
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, text.c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "Forget network"); renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, tr(STR_FORGET_BUTTON));
} }
// Use centralized button hints // Use centralized button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "Left", "Right"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <cstdint> #include <cstdint>
#include <functional> #include <functional>
@@ -45,10 +42,8 @@ enum class WifiSelectionState {
* The onComplete callback receives true if connected successfully, false if cancelled. * The onComplete callback receives true if connected successfully, false if cancelled.
*/ */
class WifiSelectionActivity final : public ActivityWithSubactivity { class WifiSelectionActivity final : public ActivityWithSubactivity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
bool updateRequired = false;
WifiSelectionState state = WifiSelectionState::SCANNING; WifiSelectionState state = WifiSelectionState::SCANNING;
int selectedNetworkIndex = 0; int selectedNetworkIndex = 0;
std::vector<WifiNetworkInfo> networks; std::vector<WifiNetworkInfo> networks;
@@ -85,9 +80,6 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000;
unsigned long connectionStartTime = 0; unsigned long connectionStartTime = 0;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
void renderNetworkList() const; void renderNetworkList() const;
void renderPasswordEntry() const; void renderPasswordEntry() const;
void renderConnecting() const; void renderConnecting() const;
@@ -112,6 +104,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
// Get the IP address after successful connection // Get the IP address after successful connection
const std::string& getConnectedIP() const { return connectedIP; } const std::string& getConnectedIP() const { return connectedIP; }

View File

@@ -11,40 +11,14 @@
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
void DictionaryDefinitionActivity::taskTrampoline(void* param) {
auto* self = static_cast<DictionaryDefinitionActivity*>(param);
self->displayTaskLoop();
}
void DictionaryDefinitionActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void DictionaryDefinitionActivity::onEnter() { void DictionaryDefinitionActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
wrapText(); wrapText();
updateRequired = true; requestUpdate();
xTaskCreate(&DictionaryDefinitionActivity::taskTrampoline, "DictDefTask", 4096, this, 1, &displayTaskHandle);
} }
void DictionaryDefinitionActivity::onExit() { void DictionaryDefinitionActivity::onExit() {
Activity::onExit(); Activity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -442,12 +416,12 @@ void DictionaryDefinitionActivity::loop() {
if (prevPage && currentPage > 0) { if (prevPage && currentPage > 0) {
currentPage--; currentPage--;
updateRequired = true; requestUpdate();
} }
if (nextPage && currentPage < totalPages - 1) { if (nextPage && currentPage < totalPages - 1) {
currentPage++; currentPage++;
updateRequired = true; requestUpdate();
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
@@ -465,7 +439,7 @@ void DictionaryDefinitionActivity::loop() {
} }
} }
void DictionaryDefinitionActivity::renderScreen() { void DictionaryDefinitionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
const bool landscape = orientation == CrossPointSettings::ORIENTATION::LANDSCAPE_CW || const bool landscape = orientation == CrossPointSettings::ORIENTATION::LANDSCAPE_CW ||

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -27,6 +24,7 @@ class DictionaryDefinitionActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
// A positioned text segment within a wrapped line (pre-calculated x offset and style). // A positioned text segment within a wrapped line (pre-calculated x offset and style).
@@ -61,17 +59,10 @@ class DictionaryDefinitionActivity final : public Activity {
int currentPage = 0; int currentPage = 0;
int linesPerPage = 0; int linesPerPage = 0;
int totalPages = 0; int totalPages = 0;
bool updateRequired = false;
bool firstRender = true; bool firstRender = true;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::vector<TextAtom> parseHtml(const std::string& html); std::vector<TextAtom> parseHtml(const std::string& html);
static std::string decodeEntity(const std::string& entity); static std::string decodeEntity(const std::string& entity);
static bool isRenderableCodepoint(uint32_t cp); static bool isRenderableCodepoint(uint32_t cp);
void wrapText(); void wrapText();
void renderScreen();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
}; };

View File

@@ -8,39 +8,13 @@
#include "fontIds.h" #include "fontIds.h"
#include "util/Dictionary.h" #include "util/Dictionary.h"
void DictionarySuggestionsActivity::taskTrampoline(void* param) {
auto* self = static_cast<DictionarySuggestionsActivity*>(param);
self->displayTaskLoop();
}
void DictionarySuggestionsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void DictionarySuggestionsActivity::onEnter() { void DictionarySuggestionsActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); requestUpdate();
updateRequired = true;
xTaskCreate(&DictionarySuggestionsActivity::taskTrampoline, "DictSugTask", 4096, this, 1, &displayTaskHandle);
} }
void DictionarySuggestionsActivity::onExit() { void DictionarySuggestionsActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void DictionarySuggestionsActivity::loop() { void DictionarySuggestionsActivity::loop() {
@@ -49,7 +23,7 @@ void DictionarySuggestionsActivity::loop() {
if (pendingBackFromDef) { if (pendingBackFromDef) {
pendingBackFromDef = false; pendingBackFromDef = false;
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
} }
if (pendingExitToReader) { if (pendingExitToReader) {
pendingExitToReader = false; pendingExitToReader = false;
@@ -68,12 +42,12 @@ void DictionarySuggestionsActivity::loop() {
buttonNavigator.onNext([this] { buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(suggestions.size())); selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(suggestions.size()));
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this] { buttonNavigator.onPrevious([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(suggestions.size())); selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(suggestions.size()));
updateRequired = true; requestUpdate();
}); });
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
@@ -81,10 +55,13 @@ void DictionarySuggestionsActivity::loop() {
std::string definition = Dictionary::lookup(selected); std::string definition = Dictionary::lookup(selected);
if (definition.empty()) { if (definition.empty()) {
GUI.drawPopup(renderer, "Not found"); {
renderer.displayBuffer(HalDisplay::FAST_REFRESH); Activity::RenderLock lock(*this);
GUI.drawPopup(renderer, "Not found");
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
vTaskDelay(1000 / portTICK_PERIOD_MS); vTaskDelay(1000 / portTICK_PERIOD_MS);
updateRequired = true; requestUpdate();
return; return;
} }
@@ -100,7 +77,7 @@ void DictionarySuggestionsActivity::loop() {
} }
} }
void DictionarySuggestionsActivity::renderScreen() { void DictionarySuggestionsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
const auto orient = renderer.getOrientation(); const auto orient = renderer.getOrientation();

View File

@@ -1,8 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -28,6 +24,7 @@ class DictionarySuggestionsActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
std::string originalWord; std::string originalWord;
@@ -39,15 +36,7 @@ class DictionarySuggestionsActivity final : public ActivityWithSubactivity {
const std::function<void()> onDone; const std::function<void()> onDone;
int selectedIndex = 0; int selectedIndex = 0;
bool updateRequired = false;
bool pendingBackFromDef = false; bool pendingBackFromDef = false;
bool pendingExitToReader = false; bool pendingExitToReader = false;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
void renderScreen();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
}; };

View File

@@ -14,45 +14,19 @@
#include "util/Dictionary.h" #include "util/Dictionary.h"
#include "util/LookupHistory.h" #include "util/LookupHistory.h"
void DictionaryWordSelectActivity::taskTrampoline(void* param) {
auto* self = static_cast<DictionaryWordSelectActivity*>(param);
self->displayTaskLoop();
}
void DictionaryWordSelectActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void DictionaryWordSelectActivity::onEnter() { void DictionaryWordSelectActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
extractWords(); extractWords();
mergeHyphenatedWords(); mergeHyphenatedWords();
if (!rows.empty()) { if (!rows.empty()) {
currentRow = static_cast<int>(rows.size()) / 3; currentRow = static_cast<int>(rows.size()) / 3;
currentWordInRow = 0; currentWordInRow = 0;
} }
updateRequired = true; requestUpdate();
xTaskCreate(&DictionaryWordSelectActivity::taskTrampoline, "DictWordSelTask", 4096, this, 1, &displayTaskHandle);
} }
void DictionaryWordSelectActivity::onExit() { void DictionaryWordSelectActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
bool DictionaryWordSelectActivity::isLandscape() const { bool DictionaryWordSelectActivity::isLandscape() const {
@@ -231,7 +205,7 @@ void DictionaryWordSelectActivity::loop() {
if (pendingBackFromDef) { if (pendingBackFromDef) {
pendingBackFromDef = false; pendingBackFromDef = false;
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
} }
if (pendingExitToReader) { if (pendingExitToReader) {
pendingExitToReader = false; pendingExitToReader = false;
@@ -353,25 +327,28 @@ void DictionaryWordSelectActivity::loop() {
std::string cleaned = Dictionary::cleanWord(rawWord); std::string cleaned = Dictionary::cleanWord(rawWord);
if (cleaned.empty()) { if (cleaned.empty()) {
GUI.drawPopup(renderer, "No word"); {
renderer.displayBuffer(HalDisplay::FAST_REFRESH); Activity::RenderLock lock(*this);
GUI.drawPopup(renderer, "No word");
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
vTaskDelay(1000 / portTICK_PERIOD_MS); vTaskDelay(1000 / portTICK_PERIOD_MS);
updateRequired = true; requestUpdate();
return; return;
} }
// Show looking up popup, then release mutex so display task can run Rect popupLayout;
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
Rect popupLayout = GUI.drawPopup(renderer, "Looking up..."); Activity::RenderLock lock(*this);
xSemaphoreGive(renderingMutex); popupLayout = GUI.drawPopup(renderer, "Looking up...");
}
bool cancelled = false; bool cancelled = false;
std::string definition = Dictionary::lookup( std::string definition = Dictionary::lookup(
cleaned, cleaned,
[this, &popupLayout](int percent) { [this, &popupLayout](int percent) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); Activity::RenderLock lock(*this);
GUI.fillPopupProgress(renderer, popupLayout, percent); GUI.fillPopupProgress(renderer, popupLayout, percent);
xSemaphoreGive(renderingMutex);
}, },
[this, &cancelled]() -> bool { [this, &cancelled]() -> bool {
mappedInput.update(); mappedInput.update();
@@ -383,7 +360,7 @@ void DictionaryWordSelectActivity::loop() {
}); });
if (cancelled) { if (cancelled) {
updateRequired = true; requestUpdate();
return; return;
} }
@@ -417,10 +394,13 @@ void DictionaryWordSelectActivity::loop() {
return; return;
} }
GUI.drawPopup(renderer, "Not found"); {
renderer.displayBuffer(HalDisplay::FAST_REFRESH); Activity::RenderLock lock(*this);
GUI.drawPopup(renderer, "Not found");
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
vTaskDelay(1500 / portTICK_PERIOD_MS); vTaskDelay(1500 / portTICK_PERIOD_MS);
updateRequired = true; requestUpdate();
return; return;
} }
@@ -430,11 +410,11 @@ void DictionaryWordSelectActivity::loop() {
} }
if (changed) { if (changed) {
updateRequired = true; requestUpdate();
} }
} }
void DictionaryWordSelectActivity::renderScreen() { void DictionaryWordSelectActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
// Render the page content // Render the page content

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Epub/Page.h> #include <Epub/Page.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -31,6 +28,7 @@ class DictionaryWordSelectActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
struct WordInfo { struct WordInfo {
@@ -64,19 +62,12 @@ class DictionaryWordSelectActivity final : public ActivityWithSubactivity {
std::vector<Row> rows; std::vector<Row> rows;
int currentRow = 0; int currentRow = 0;
int currentWordInRow = 0; int currentWordInRow = 0;
bool updateRequired = false;
bool pendingBackFromDef = false; bool pendingBackFromDef = false;
bool pendingExitToReader = false; bool pendingExitToReader = false;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool isLandscape() const; bool isLandscape() const;
bool isInverted() const; bool isInverted() const;
void extractWords(); void extractWords();
void mergeHyphenatedWords(); void mergeHyphenatedWords();
void renderScreen();
void drawHints(); void drawHints();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
}; };

View File

@@ -4,6 +4,7 @@
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <PlaceholderCoverGenerator.h> #include <PlaceholderCoverGenerator.h>
@@ -67,11 +68,6 @@ void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
} // namespace } // namespace
void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderActivity::onEnter() { void EpubReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
@@ -83,8 +79,6 @@ void EpubReaderActivity::onEnter() {
// NOTE: This affects layout math and must be applied before any render calls. // NOTE: This affects layout math and must be applied before any render calls.
applyReaderOrientation(renderer, SETTINGS.orientation); applyReaderOrientation(renderer, SETTINGS.orientation);
renderingMutex = xSemaphoreCreateMutex();
epub->setupCacheDir(); epub->setupCacheDir();
FsFile f; FsFile f;
@@ -179,14 +173,7 @@ void EpubReaderActivity::onEnter() {
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath()); RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
// Trigger first update // Trigger first update
updateRequired = true; requestUpdate();
xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void EpubReaderActivity::onExit() { void EpubReaderActivity::onExit() {
@@ -195,14 +182,6 @@ void EpubReaderActivity::onExit() {
// Reset orientation back to portrait for the rest of the UI // Reset orientation back to portrait for the rest of the UI
renderer.setOrientation(GfxRenderer::Orientation::Portrait); renderer.setOrientation(GfxRenderer::Orientation::Portrait);
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
APP_STATE.readerActivityLoadCount = 0; APP_STATE.readerActivityLoadCount = 0;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
section.reset(); section.reset();
@@ -217,7 +196,7 @@ void EpubReaderActivity::loop() {
if (pendingSubactivityExit) { if (pendingSubactivityExit) {
pendingSubactivityExit = false; pendingSubactivityExit = false;
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
skipNextButtonCheck = true; // Skip button processing to ignore stale events skipNextButtonCheck = true; // Skip button processing to ignore stale events
} }
// Deferred go home: process after subActivity->loop() returns to avoid race condition // Deferred go home: process after subActivity->loop() returns to avoid race condition
@@ -257,8 +236,6 @@ void EpubReaderActivity::loop() {
// Enter reader menu activity. // Enter reader menu activity.
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY);
const int currentPage = section ? section->currentPage + 1 : 0; const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0; const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f; float bookProgress = 0.0f;
@@ -276,7 +253,6 @@ void EpubReaderActivity::loop() {
SETTINGS.orientation, hasDictionary, isBookmarked, epub->getCachePath(), SETTINGS.orientation, hasDictionary, isBookmarked, epub->getCachePath(),
[this](const uint8_t orientation) { onReaderMenuBack(orientation); }, [this](const uint8_t orientation) { onReaderMenuBack(orientation); },
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
xSemaphoreGive(renderingMutex);
} }
// Long press BACK (1s+) goes to file selection // Long press BACK (1s+) goes to file selection
@@ -313,7 +289,7 @@ void EpubReaderActivity::loop() {
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount() - 1; currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX; nextPageNumber = UINT16_MAX;
updateRequired = true; requestUpdate();
return; return;
} }
@@ -321,18 +297,19 @@ void EpubReaderActivity::loop() {
if (skipChapter) { if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
nextPageNumber = 0; RenderLock lock(*this);
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; nextPageNumber = 0;
section.reset(); currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
xSemaphoreGive(renderingMutex); section.reset();
updateRequired = true; }
requestUpdate();
return; return;
} }
// No current section, attempt to rerender the book // No current section, attempt to rerender the book
if (!section) { if (!section) {
updateRequired = true; requestUpdate();
return; return;
} }
@@ -341,25 +318,27 @@ void EpubReaderActivity::loop() {
section->currentPage--; section->currentPage--;
} else { } else {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
nextPageNumber = UINT16_MAX; RenderLock lock(*this);
currentSpineIndex--; nextPageNumber = UINT16_MAX;
section.reset(); currentSpineIndex--;
xSemaphoreGive(renderingMutex); section.reset();
}
} }
updateRequired = true; requestUpdate();
} else { } else {
if (section->currentPage < section->pageCount - 1) { if (section->currentPage < section->pageCount - 1) {
section->currentPage++; section->currentPage++;
} else { } else {
// We don't want to delete the section mid-render, so grab the semaphore // We don't want to delete the section mid-render, so grab the semaphore
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
nextPageNumber = 0; RenderLock lock(*this);
currentSpineIndex++; nextPageNumber = 0;
section.reset(); currentSpineIndex++;
xSemaphoreGive(renderingMutex); section.reset();
}
} }
updateRequired = true; requestUpdate();
} }
} }
@@ -370,7 +349,7 @@ void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
applyOrientation(orientation); applyOrientation(orientation);
// Force a half refresh on the next render to clear menu/popup artifacts // Force a half refresh on the next render to clear menu/popup artifacts
pagesUntilFullRefresh = 1; pagesUntilFullRefresh = 1;
updateRequired = true; requestUpdate();
} }
// Translate an absolute percent into a spine index plus a normalized position // Translate an absolute percent into a spine index plus a normalized position
@@ -426,13 +405,14 @@ void EpubReaderActivity::jumpToPercent(int percent) {
pendingSpineProgress = 1.0f; pendingSpineProgress = 1.0f;
} }
// Reset state so renderScreen() reloads and repositions on the target spine. // Reset state so render() reloads and repositions on the target spine.
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
currentSpineIndex = targetSpineIndex; RenderLock lock(*this);
nextPageNumber = 0; currentSpineIndex = targetSpineIndex;
pendingPercentJump = true; nextPageNumber = 0;
section.reset(); pendingPercentJump = true;
xSemaphoreGive(renderingMutex); section.reset();
}
} }
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
@@ -497,29 +477,31 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
} }
BookmarkStore::addBookmark(epub->getCachePath(), currentSpineIndex, page, snippet); BookmarkStore::addBookmark(epub->getCachePath(), currentSpineIndex, page, snippet);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
GUI.drawPopup(renderer, "Bookmark added"); RenderLock lock(*this);
renderer.displayBuffer(HalDisplay::FAST_REFRESH); GUI.drawPopup(renderer, tr(STR_BOOKMARK_ADDED));
xSemaphoreGive(renderingMutex); renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
vTaskDelay(750 / portTICK_PERIOD_MS); vTaskDelay(750 / portTICK_PERIOD_MS);
// Exit the menu and return to reading — the bookmark indicator will show on re-render, // Exit the menu and return to reading — the bookmark indicator will show on re-render,
// and next menu open will reflect the updated state. // and next menu open will reflect the updated state.
exitActivity(); exitActivity();
pagesUntilFullRefresh = 1; pagesUntilFullRefresh = 1;
updateRequired = true; requestUpdate();
break; break;
} }
case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: { case EpubReaderMenuActivity::MenuAction::REMOVE_BOOKMARK: {
const int page = section ? section->currentPage : 0; const int page = section ? section->currentPage : 0;
BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, page); BookmarkStore::removeBookmark(epub->getCachePath(), currentSpineIndex, page);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
GUI.drawPopup(renderer, "Bookmark removed"); RenderLock lock(*this);
renderer.displayBuffer(HalDisplay::FAST_REFRESH); GUI.drawPopup(renderer, tr(STR_BOOKMARK_REMOVED));
xSemaphoreGive(renderingMutex); renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
vTaskDelay(750 / portTICK_PERIOD_MS); vTaskDelay(750 / portTICK_PERIOD_MS);
exitActivity(); exitActivity();
pagesUntilFullRefresh = 1; pagesUntilFullRefresh = 1;
updateRequired = true; requestUpdate();
break; break;
} }
case EpubReaderMenuActivity::MenuAction::GO_TO_BOOKMARK: { case EpubReaderMenuActivity::MenuAction::GO_TO_BOOKMARK: {
@@ -533,13 +515,12 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
const int spineIdx = currentSpineIndex; const int spineIdx = currentSpineIndex;
const std::string path = epub->getPath(); const std::string path = epub->getPath();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderChapterSelectionActivity( enterNewActivity(new EpubReaderChapterSelectionActivity(
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
[this] { [this] {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this](const int newSpineIndex) { [this](const int newSpineIndex) {
if (currentSpineIndex != newSpineIndex) { if (currentSpineIndex != newSpineIndex) {
@@ -548,7 +529,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset(); section.reset();
} }
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this](const int newSpineIndex, const int newPage) { [this](const int newSpineIndex, const int newPage) {
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
@@ -557,21 +538,19 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset(); section.reset();
} }
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
xSemaphoreGive(renderingMutex);
} }
// If no TOC either, just return to reader (menu already closed by callback) // If no TOC either, just return to reader (menu already closed by callback)
break; break;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderBookmarkSelectionActivity( enterNewActivity(new EpubReaderBookmarkSelectionActivity(
this->renderer, this->mappedInput, epub, std::move(bookmarks), epub->getCachePath(), this->renderer, this->mappedInput, epub, std::move(bookmarks), epub->getCachePath(),
[this] { [this] {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this](const int newSpineIndex, const int newPage) { [this](const int newSpineIndex, const int newPage) {
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
@@ -580,23 +559,24 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset(); section.reset();
} }
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
xSemaphoreGive(renderingMutex);
break; break;
} }
case EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE: { case EpubReaderMenuActivity::MenuAction::DELETE_DICT_CACHE: {
if (Dictionary::cacheExists()) { if (Dictionary::cacheExists()) {
Dictionary::deleteCache(); Dictionary::deleteCache();
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
GUI.drawPopup(renderer, "Dictionary cache deleted"); RenderLock lock(*this);
renderer.displayBuffer(HalDisplay::FAST_REFRESH); GUI.drawPopup(renderer, tr(STR_DICT_CACHE_DELETED));
xSemaphoreGive(renderingMutex); renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
} else { } else {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
GUI.drawPopup(renderer, "No cache to delete"); RenderLock lock(*this);
renderer.displayBuffer(HalDisplay::FAST_REFRESH); GUI.drawPopup(renderer, tr(STR_NO_CACHE_TO_DELETE));
xSemaphoreGive(renderingMutex); renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
} }
vTaskDelay(1500 / portTICK_PERIOD_MS); vTaskDelay(1500 / portTICK_PERIOD_MS);
break; break;
@@ -608,8 +588,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
const int spineIdx = currentSpineIndex; const int spineIdx = currentSpineIndex;
const std::string path = epub->getPath(); const std::string path = epub->getPath();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// 1. Close the menu // 1. Close the menu
exitActivity(); exitActivity();
@@ -618,7 +596,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
[this] { [this] {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this](const int newSpineIndex) { [this](const int newSpineIndex) {
if (currentSpineIndex != newSpineIndex) { if (currentSpineIndex != newSpineIndex) {
@@ -627,7 +605,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset(); section.reset();
} }
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this](const int newSpineIndex, const int newPage) { [this](const int newSpineIndex, const int newPage) {
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
@@ -636,10 +614,9 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset(); section.reset();
} }
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
xSemaphoreGive(renderingMutex);
break; break;
} }
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: { case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
@@ -650,7 +627,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
} }
const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderPercentSelectionActivity( enterNewActivity(new EpubReaderPercentSelectionActivity(
renderer, mappedInput, initialPercent, renderer, mappedInput, initialPercent,
@@ -658,59 +634,65 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
// Apply the new position and exit back to the reader. // Apply the new position and exit back to the reader.
jumpToPercent(percent); jumpToPercent(percent);
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
// Cancel selection and return to the reader. // Cancel selection and return to the reader.
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
xSemaphoreGive(renderingMutex);
break; break;
} }
case EpubReaderMenuActivity::MenuAction::LOOKUP: { case EpubReaderMenuActivity::MenuAction::LOOKUP: {
xSemaphoreTake(renderingMutex, portMAX_DELAY); // Gather data we need while holding the render lock
// Compute margins (same logic as renderScreen)
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, std::unique_ptr<Page> pageForLookup;
&orientedMarginLeft); int readerFontId;
orientedMarginTop += SETTINGS.screenMargin; std::string bookCachePath;
orientedMarginLeft += SETTINGS.screenMargin; uint8_t currentOrientation;
orientedMarginRight += SETTINGS.screenMargin;
orientedMarginBottom += SETTINGS.screenMargin;
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
auto metrics = UITheme::getInstance().getMetrics();
const bool showProgressBar =
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
}
// Load the current page
auto pageForLookup = section ? section->loadPageFromSectionFile() : nullptr;
const int readerFontId = SETTINGS.getReaderFontId();
const std::string bookCachePath = epub->getCachePath();
const uint8_t currentOrientation = SETTINGS.orientation;
// Get first word of next page for cross-page hyphenation
std::string nextPageFirstWord; std::string nextPageFirstWord;
if (section && section->currentPage < section->pageCount - 1) { {
int savedPage = section->currentPage; RenderLock lock(*this);
section->currentPage = savedPage + 1;
auto nextPage = section->loadPageFromSectionFile(); // Compute margins (same logic as render)
section->currentPage = savedPage; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
if (nextPage && !nextPage->elements.empty()) { &orientedMarginLeft);
const auto* firstLine = static_cast<const PageLine*>(nextPage->elements[0].get()); orientedMarginTop += SETTINGS.screenMargin;
if (firstLine->getBlock() && !firstLine->getBlock()->getWords().empty()) { orientedMarginLeft += SETTINGS.screenMargin;
nextPageFirstWord = firstLine->getBlock()->getWords().front(); orientedMarginRight += SETTINGS.screenMargin;
orientedMarginBottom += SETTINGS.screenMargin;
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
auto metrics = UITheme::getInstance().getMetrics();
const bool showProgressBar =
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::BOOK_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_BOOK_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR;
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
(showProgressBar ? (metrics.bookProgressBarHeight + progressBarMarginTop) : 0);
}
// Load the current page
pageForLookup = section ? section->loadPageFromSectionFile() : nullptr;
readerFontId = SETTINGS.getReaderFontId();
bookCachePath = epub->getCachePath();
currentOrientation = SETTINGS.orientation;
// Get first word of next page for cross-page hyphenation
if (section && section->currentPage < section->pageCount - 1) {
int savedPage = section->currentPage;
section->currentPage = savedPage + 1;
auto nextPage = section->loadPageFromSectionFile();
section->currentPage = savedPage;
if (nextPage && !nextPage->elements.empty()) {
const auto* firstLine = static_cast<const PageLine*>(nextPage->elements[0].get());
if (firstLine->getBlock() && !firstLine->getBlock()->getWords().empty()) {
nextPageFirstWord = firstLine->getBlock()->getWords().front();
}
} }
} }
} }
// Lock released — safe to call enterNewActivity which takes its own lock
exitActivity(); exitActivity();
if (pageForLookup) { if (pageForLookup) {
@@ -718,18 +700,13 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
renderer, mappedInput, std::move(pageForLookup), readerFontId, orientedMarginLeft, orientedMarginTop, renderer, mappedInput, std::move(pageForLookup), readerFontId, orientedMarginLeft, orientedMarginTop,
bookCachePath, currentOrientation, [this]() { pendingSubactivityExit = true; }, nextPageFirstWord)); bookCachePath, currentOrientation, [this]() { pendingSubactivityExit = true; }, nextPageFirstWord));
} }
xSemaphoreGive(renderingMutex);
break; break;
} }
case EpubReaderMenuActivity::MenuAction::LOOKED_UP_WORDS: { case EpubReaderMenuActivity::MenuAction::LOOKED_UP_WORDS: {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new LookedUpWordsActivity( enterNewActivity(new LookedUpWordsActivity(
renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation, renderer, mappedInput, epub->getCachePath(), SETTINGS.getReaderFontId(), SETTINGS.orientation,
[this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; })); [this]() { pendingSubactivityExit = true; }, [this]() { pendingSubactivityExit = true; }));
xSemaphoreGive(renderingMutex);
break; break;
} }
case EpubReaderMenuActivity::MenuAction::GO_HOME: { case EpubReaderMenuActivity::MenuAction::GO_HOME: {
@@ -738,34 +715,34 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
break; break;
} }
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
if (epub) { RenderLock lock(*this);
// 2. BACKUP: Read current progress if (epub) {
// We use the current variables that track our position // 2. BACKUP: Read current progress
uint16_t backupSpine = currentSpineIndex; // We use the current variables that track our position
uint16_t backupPage = section->currentPage; uint16_t backupSpine = currentSpineIndex;
uint16_t backupPageCount = section->pageCount; uint16_t backupPage = section->currentPage;
uint16_t backupPageCount = section->pageCount;
section.reset(); section.reset();
// 3. WIPE: Clear the cache directory // 3. WIPE: Clear the cache directory
epub->clearCache(); epub->clearCache();
// 4. RESTORE: Re-setup the directory and rewrite the progress file // 4. RESTORE: Re-setup the directory and rewrite the progress file
epub->setupCacheDir(); epub->setupCacheDir();
saveProgress(backupSpine, backupPage, backupPageCount); saveProgress(backupSpine, backupPage, backupPageCount);
// 5. Remove from recent books so the home screen doesn't show a stale/placeholder cover // 5. Remove from recent books so the home screen doesn't show a stale/placeholder cover
RECENT_BOOKS.removeBook(epub->getPath()); RECENT_BOOKS.removeBook(epub->getPath());
}
} }
xSemaphoreGive(renderingMutex);
// Defer go home to avoid race condition with display task // Defer go home to avoid race condition with display task
pendingGoHome = true; pendingGoHome = true;
break; break;
} }
case EpubReaderMenuActivity::MenuAction::SYNC: { case EpubReaderMenuActivity::MenuAction::SYNC: {
if (KOREADER_STORE.hasCredentials()) { if (KOREADER_STORE.hasCredentials()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
const int currentPage = section ? section->currentPage : 0; const int currentPage = section ? section->currentPage : 0;
const int totalPages = section ? section->pageCount : 0; const int totalPages = section ? section->pageCount : 0;
exitActivity(); exitActivity();
@@ -784,7 +761,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
} }
pendingSubactivityExit = true; pendingSubactivityExit = true;
})); }));
xSemaphoreGive(renderingMutex);
} }
break; break;
} }
@@ -802,39 +778,28 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
} }
// Preserve current reading position so we can restore after reflow. // Preserve current reading position so we can restore after reflow.
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
if (section) { RenderLock lock(*this);
cachedSpineIndex = currentSpineIndex; if (section) {
cachedChapterTotalPageCount = section->pageCount; cachedSpineIndex = currentSpineIndex;
nextPageNumber = section->currentPage; cachedChapterTotalPageCount = section->pageCount;
} nextPageNumber = section->currentPage;
// Persist the selection so the reader keeps the new orientation on next launch.
SETTINGS.orientation = orientation;
SETTINGS.saveToFile();
// Update renderer orientation to match the new logical coordinate system.
applyReaderOrientation(renderer, SETTINGS.orientation);
// Reset section to force re-layout in the new orientation.
section.reset();
xSemaphoreGive(renderingMutex);
}
void EpubReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
} }
vTaskDelay(10 / portTICK_PERIOD_MS);
// Persist the selection so the reader keeps the new orientation on next launch.
SETTINGS.orientation = orientation;
SETTINGS.saveToFile();
// Update renderer orientation to match the new logical coordinate system.
applyReaderOrientation(renderer, SETTINGS.orientation);
// Reset section to force re-layout in the new orientation.
section.reset();
} }
} }
// TODO: Failure handling // TODO: Failure handling
void EpubReaderActivity::renderScreen() { void EpubReaderActivity::render(Activity::RenderLock&& lock) {
if (!epub) { if (!epub) {
return; return;
} }
@@ -851,7 +816,7 @@ void EpubReaderActivity::renderScreen() {
// Show end of book screen // Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) { if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@@ -892,7 +857,7 @@ void EpubReaderActivity::renderScreen() {
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) {
LOG_DBG("ERS", "Cache not found, building..."); LOG_DBG("ERS", "Cache not found, building...");
const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); }; const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
@@ -940,7 +905,7 @@ void EpubReaderActivity::renderScreen() {
if (section->pageCount == 0) { if (section->pageCount == 0) {
LOG_DBG("ERS", "No pages to render"); LOG_DBG("ERS", "No pages to render");
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -948,7 +913,7 @@ void EpubReaderActivity::renderScreen() {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) { if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount); LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -960,7 +925,9 @@ void EpubReaderActivity::renderScreen() {
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache"); LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
section->clearCache(); section->clearCache();
section.reset(); section.reset();
return renderScreen(); requestUpdate(); // Try again after clearing cache
// TODO: prevent infinite loop if the page keeps failing to load for some reason
return;
} }
const auto start = millis(); const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
@@ -1145,8 +1112,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
} }
if (showBattery) { if (showBattery) {
GUI.drawBattery(renderer, Rect{orientedMarginLeft + 1, textY, metrics.batteryWidth, metrics.batteryHeight}, GUI.drawBatteryLeft(renderer, Rect{orientedMarginLeft + 1, textY, metrics.batteryWidth, metrics.batteryHeight},
showBatteryPercentage); showBatteryPercentage);
} }
if (showChapterTitle) { if (showChapterTitle) {
@@ -1167,8 +1134,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
std::string title; std::string title;
int titleWidth; int titleWidth;
if (tocIndex == -1) { if (tocIndex == -1) {
title = "Unnamed"; title = tr(STR_UNNAMED);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed"); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
} else { } else {
const auto tocItem = epub->getTocItem(tocIndex); const auto tocItem = epub->getTocItem(tocIndex);
title = tocItem.title; title = tocItem.title;

View File

@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <Epub.h> #include <Epub.h>
#include <Epub/Section.h> #include <Epub/Section.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "DictionaryWordSelectActivity.h" #include "DictionaryWordSelectActivity.h"
#include "EpubReaderMenuActivity.h" #include "EpubReaderMenuActivity.h"
@@ -13,8 +10,6 @@
class EpubReaderActivity final : public ActivityWithSubactivity { class EpubReaderActivity final : public ActivityWithSubactivity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr; std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0; int currentSpineIndex = 0;
int nextPageNumber = 0; int nextPageNumber = 0;
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
@@ -25,7 +20,6 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
bool pendingPercentJump = false; bool pendingPercentJump = false;
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
float pendingSpineProgress = 0.0f; float pendingSpineProgress = 0.0f;
bool updateRequired = false;
bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free
bool pendingGoHome = false; // Defer go home to avoid race condition with display task bool pendingGoHome = false; // Defer go home to avoid race condition with display task
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
@@ -33,9 +27,6 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight, void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft); int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
@@ -56,10 +47,11 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&& lock) override;
// Defer low-power mode and auto-sleep while a section is loading/building. // Defer low-power mode and auto-sleep while a section is loading/building.
// !section covers the period before the Section object is created (including // !section covers the period before the Section object is created (including
// cover prerendering in onEnter). loadingSection covers the full !section block // cover prerendering in onEnter). loadingSection covers the full !section block
// in renderScreen (including createSectionFile), during which section is non-null // in render (including createSectionFile), during which section is non-null
// but the section file is still being built. // but the section file is still being built.
bool preventAutoSleep() override { return !section || loadingSection; } bool preventAutoSleep() override { return !section || loadingSection; }
}; };

View File

@@ -42,36 +42,13 @@ std::string EpubReaderBookmarkSelectionActivity::getPageSuffix(const Bookmark& b
return " - Page " + std::to_string(bookmark.pageNumber + 1); return " - Page " + std::to_string(bookmark.pageNumber + 1);
} }
void EpubReaderBookmarkSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderBookmarkSelectionActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderBookmarkSelectionActivity::onEnter() { void EpubReaderBookmarkSelectionActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
requestUpdate();
renderingMutex = xSemaphoreCreateMutex();
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderBookmarkSelectionActivity::taskTrampoline, "BookmarkSelTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void EpubReaderBookmarkSelectionActivity::onExit() { void EpubReaderBookmarkSelectionActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void EpubReaderBookmarkSelectionActivity::loop() { void EpubReaderBookmarkSelectionActivity::loop() {
@@ -83,7 +60,6 @@ void EpubReaderBookmarkSelectionActivity::loop() {
const int totalItems = getTotalItems(); const int totalItems = getTotalItems();
if (totalItems == 0) { if (totalItems == 0) {
// All bookmarks deleted, go back
if (mappedInput.wasReleased(MappedInputManager::Button::Back) || if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
onGoBack(); onGoBack();
@@ -91,14 +67,11 @@ void EpubReaderBookmarkSelectionActivity::loop() {
return; return;
} }
// Delete confirmation mode: wait for confirm (delete) or back (cancel)
if (deleteConfirmMode) { if (deleteConfirmMode) {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (ignoreNextConfirmRelease) { if (ignoreNextConfirmRelease) {
// Ignore the release from the initial long press
ignoreNextConfirmRelease = false; ignoreNextConfirmRelease = false;
} else { } else {
// Confirm delete
BookmarkStore::removeBookmark(cachePath, bookmarks[pendingDeleteIndex].spineIndex, BookmarkStore::removeBookmark(cachePath, bookmarks[pendingDeleteIndex].spineIndex,
bookmarks[pendingDeleteIndex].pageNumber); bookmarks[pendingDeleteIndex].pageNumber);
bookmarks.erase(bookmarks.begin() + pendingDeleteIndex); bookmarks.erase(bookmarks.begin() + pendingDeleteIndex);
@@ -106,25 +79,24 @@ void EpubReaderBookmarkSelectionActivity::loop() {
selectorIndex = std::max(0, static_cast<int>(bookmarks.size()) - 1); selectorIndex = std::max(0, static_cast<int>(bookmarks.size()) - 1);
} }
deleteConfirmMode = false; deleteConfirmMode = false;
updateRequired = true; requestUpdate();
} }
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
deleteConfirmMode = false; deleteConfirmMode = false;
ignoreNextConfirmRelease = false; ignoreNextConfirmRelease = false;
updateRequired = true; requestUpdate();
} }
return; return;
} }
// Detect long press on Confirm to trigger delete
constexpr unsigned long DELETE_HOLD_MS = 700; constexpr unsigned long DELETE_HOLD_MS = 700;
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= DELETE_HOLD_MS) { if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= DELETE_HOLD_MS) {
if (totalItems > 0 && selectorIndex >= 0 && selectorIndex < totalItems) { if (totalItems > 0 && selectorIndex >= 0 && selectorIndex < totalItems) {
deleteConfirmMode = true; deleteConfirmMode = true;
ignoreNextConfirmRelease = true; ignoreNextConfirmRelease = true;
pendingDeleteIndex = selectorIndex; pendingDeleteIndex = selectorIndex;
updateRequired = true; requestUpdate();
} }
return; return;
} }
@@ -144,38 +116,26 @@ void EpubReaderBookmarkSelectionActivity::loop() {
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, totalItems] { buttonNavigator.onPreviousRelease([this, totalItems] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this, totalItems, pageItems] { buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
} }
void EpubReaderBookmarkSelectionActivity::displayTaskLoop() { void EpubReaderBookmarkSelectionActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderBookmarkSelectionActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
@@ -191,7 +151,6 @@ void EpubReaderBookmarkSelectionActivity::renderScreen() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
const int totalItems = getTotalItems(); const int totalItems = getTotalItems();
// Title
const int titleX = const int titleX =
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Bookmark", EpdFontFamily::BOLD)) / 2; contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Bookmark", EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Go to Bookmark", true, EpdFontFamily::BOLD); renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Go to Bookmark", true, EpdFontFamily::BOLD);
@@ -213,7 +172,6 @@ void EpubReaderBookmarkSelectionActivity::renderScreen() {
const std::string suffix = getPageSuffix(bookmarks[itemIndex]); const std::string suffix = getPageSuffix(bookmarks[itemIndex]);
const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, suffix.c_str()); const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, suffix.c_str());
// Truncate the prefix (chapter + snippet) to leave room for the page suffix
const std::string prefix = getBookmarkPrefix(bookmarks[itemIndex]); const std::string prefix = getBookmarkPrefix(bookmarks[itemIndex]);
const std::string truncatedPrefix = const std::string truncatedPrefix =
renderer.truncatedText(UI_10_FONT_ID, prefix.c_str(), maxLabelWidth - suffixWidth); renderer.truncatedText(UI_10_FONT_ID, prefix.c_str(), maxLabelWidth - suffixWidth);
@@ -225,7 +183,6 @@ void EpubReaderBookmarkSelectionActivity::renderScreen() {
} }
if (deleteConfirmMode && pendingDeleteIndex < static_cast<int>(bookmarks.size())) { if (deleteConfirmMode && pendingDeleteIndex < static_cast<int>(bookmarks.size())) {
// Draw delete confirmation overlay
const std::string suffix = getPageSuffix(bookmarks[pendingDeleteIndex]); const std::string suffix = getPageSuffix(bookmarks[pendingDeleteIndex]);
std::string msg = "Delete bookmark" + suffix + "?"; std::string msg = "Delete bookmark" + suffix + "?";

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Epub.h> #include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory> #include <memory>
#include <vector> #include <vector>
@@ -15,32 +12,19 @@ class EpubReaderBookmarkSelectionActivity final : public ActivityWithSubactivity
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
std::vector<Bookmark> bookmarks; std::vector<Bookmark> bookmarks;
std::string cachePath; std::string cachePath;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false;
bool deleteConfirmMode = false; bool deleteConfirmMode = false;
bool ignoreNextConfirmRelease = false; bool ignoreNextConfirmRelease = false;
int pendingDeleteIndex = 0; int pendingDeleteIndex = 0;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(int newSpineIndex, int newPage)> onSelectBookmark; const std::function<void(int newSpineIndex, int newPage)> onSelectBookmark;
// Number of items that fit on a page, derived from logical screen height.
int getPageItems() const; int getPageItems() const;
int getTotalItems() const; int getTotalItems() const;
// Build the prefix portion of a bookmark label (chapter + snippet, without page suffix)
std::string getBookmarkPrefix(const Bookmark& bookmark) const; std::string getBookmarkPrefix(const Bookmark& bookmark) const;
// Build the page suffix (e.g. " - Page 5")
static std::string getPageSuffix(const Bookmark& bookmark); static std::string getPageSuffix(const Bookmark& bookmark);
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public: public:
explicit EpubReaderBookmarkSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit EpubReaderBookmarkSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::shared_ptr<Epub>& epub,
@@ -57,4 +41,5 @@ class EpubReaderBookmarkSelectionActivity final : public ActivityWithSubactivity
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -1,6 +1,7 @@
#include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderChapterSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
@@ -24,11 +25,6 @@ int EpubReaderChapterSelectionActivity::getPageItems() const {
return std::max(1, availableHeight / lineHeight); return std::max(1, availableHeight / lineHeight);
} }
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderChapterSelectionActivity::onEnter() { void EpubReaderChapterSelectionActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
@@ -36,35 +32,16 @@ void EpubReaderChapterSelectionActivity::onEnter() {
return; return;
} }
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
if (selectorIndex == -1) { if (selectorIndex == -1) {
selectorIndex = 0; selectorIndex = 0;
} }
// Trigger first update // Trigger first update
updateRequired = true; requestUpdate();
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void EpubReaderChapterSelectionActivity::onExit() { void EpubReaderChapterSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); }
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionActivity::loop() { void EpubReaderChapterSelectionActivity::loop() {
if (subActivity) { if (subActivity) {
@@ -88,38 +65,26 @@ void EpubReaderChapterSelectionActivity::loop() {
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, totalItems] { buttonNavigator.onPreviousRelease([this, totalItems] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this, totalItems, pageItems] { buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
} }
void EpubReaderChapterSelectionActivity::displayTaskLoop() { void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderChapterSelectionActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
@@ -140,8 +105,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
// Manual centering to honor content gutters. // Manual centering to honor content gutters.
const int titleX = const int titleX =
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Chapter", EpdFontFamily::BOLD)) / 2; contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Go to Chapter", true, EpdFontFamily::BOLD); renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems; const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Highlight only the content area, not the hint gutters. // Highlight only the content area, not the hint gutters.
@@ -163,7 +128,7 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
} }
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Epub.h> #include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory> #include <memory>
@@ -12,14 +9,12 @@
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
std::shared_ptr<Epub> epub; std::shared_ptr<Epub> epub;
std::string epubPath; std::string epubPath;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int currentSpineIndex = 0; int currentSpineIndex = 0;
int currentPage = 0; int currentPage = 0;
int totalPagesInSpine = 0; int totalPagesInSpine = 0;
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(int newSpineIndex)> onSelectSpineIndex; const std::function<void(int newSpineIndex)> onSelectSpineIndex;
const std::function<void(int newSpineIndex, int newPage)> onSyncPosition; const std::function<void(int newSpineIndex, int newPage)> onSyncPosition;
@@ -31,10 +26,6 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
// Total TOC items count // Total TOC items count
int getTotalItems() const; int getTotalItems() const;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public: public:
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::string& epubPath, const std::shared_ptr<Epub>& epub, const std::string& epubPath,
@@ -54,4 +45,5 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -1,6 +1,7 @@
#include "EpubReaderMenuActivity.h" #include "EpubReaderMenuActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
@@ -8,39 +9,10 @@
void EpubReaderMenuActivity::onEnter() { void EpubReaderMenuActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex(); requestUpdate();
updateRequired = true;
xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubMenuTask", 4096, this, 1, &displayTaskHandle);
} }
void EpubReaderMenuActivity::onExit() { void EpubReaderMenuActivity::onExit() { ActivityWithSubactivity::onExit(); }
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderMenuActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderMenuActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderMenuActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderMenuActivity::loop() { void EpubReaderMenuActivity::loop() {
if (subActivity) { if (subActivity) {
@@ -51,12 +23,12 @@ void EpubReaderMenuActivity::loop() {
// Handle navigation // Handle navigation
buttonNavigator.onNext([this] { buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size())); selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this] { buttonNavigator.onPrevious([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size())); selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
updateRequired = true; requestUpdate();
}); });
// Use local variables for items we need to check after potential deletion // Use local variables for items we need to check after potential deletion
@@ -65,7 +37,7 @@ void EpubReaderMenuActivity::loop() {
if (selectedAction == MenuAction::ROTATE_SCREEN) { if (selectedAction == MenuAction::ROTATE_SCREEN) {
// Cycle orientation preview locally; actual rotation happens on menu exit. // Cycle orientation preview locally; actual rotation happens on menu exit.
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
updateRequired = true; requestUpdate();
return; return;
} }
if (selectedAction == MenuAction::LETTERBOX_FILL) { if (selectedAction == MenuAction::LETTERBOX_FILL) {
@@ -73,7 +45,7 @@ void EpubReaderMenuActivity::loop() {
int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT; int idx = (letterboxFillToIndex() + 1) % LETTERBOX_FILL_OPTION_COUNT;
pendingLetterboxFill = indexToLetterboxFill(idx); pendingLetterboxFill = indexToLetterboxFill(idx);
saveLetterboxFill(); saveLetterboxFill();
updateRequired = true; requestUpdate();
return; return;
} }
@@ -92,7 +64,7 @@ void EpubReaderMenuActivity::loop() {
} }
} }
void EpubReaderMenuActivity::renderScreen() { void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto orientation = renderer.getOrientation(); const auto orientation = renderer.getOrientation();
@@ -121,9 +93,10 @@ void EpubReaderMenuActivity::renderScreen() {
// Progress summary // Progress summary
std::string progressLine; std::string progressLine;
if (totalPages > 0) { if (totalPages > 0) {
progressLine = "Chapter: " + std::to_string(currentPage) + "/" + std::to_string(totalPages) + " pages | "; progressLine = std::string(tr(STR_CHAPTER_PREFIX)) + std::to_string(currentPage) + "/" +
std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR));
} }
progressLine += "Book: " + std::to_string(bookProgressPercent) + "%"; progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str());
// Menu Items // Menu Items
@@ -139,24 +112,24 @@ void EpubReaderMenuActivity::renderScreen() {
renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true); renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true);
} }
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, menuItems[i].label.c_str(), !isSelected); renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected);
if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { if (menuItems[i].action == MenuAction::ROTATE_SCREEN) {
// Render current orientation value on the right edge of the content area. // Render current orientation value on the right edge of the content area.
const auto value = orientationLabels[pendingOrientation]; const char* value = I18N.get(orientationLabels[pendingOrientation]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
} }
if (menuItems[i].action == MenuAction::LETTERBOX_FILL) { if (menuItems[i].action == MenuAction::LETTERBOX_FILL) {
// Render current letterbox fill value on the right edge of the content area. // Render current letterbox fill value on the right edge of the content area.
const auto value = letterboxFillLabels[letterboxFillToIndex()]; const char* value = I18N.get(letterboxFillLabels[letterboxFillToIndex()]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); const auto width = renderer.getTextWidth(UI_10_FONT_ID, value);
renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected);
} }
} }
// Footer / Hints // Footer / Hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,8 +1,6 @@
#pragma once #pragma once
#include <Epub.h> #include <Epub.h>
#include <freertos/FreeRTOS.h> #include <I18n.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -55,28 +53,29 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
struct MenuItem { struct MenuItem {
MenuAction action; MenuAction action;
std::string label; StrId labelId;
}; };
std::vector<MenuItem> menuItems; std::vector<MenuItem> menuItems;
int selectedIndex = 0; int selectedIndex = 0;
bool updateRequired = false;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
std::string title = "Reader Menu"; std::string title = "Reader Menu";
uint8_t pendingOrientation = 0; uint8_t pendingOrientation = 0;
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; const std::vector<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
StrId::STR_LANDSCAPE_CCW};
std::string bookCachePath; std::string bookCachePath;
// Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None // Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL; uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4; // Default + 3 modes static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4; // Default + 3 modes
const std::vector<const char*> letterboxFillLabels = {"Default", "Dithered", "Solid", "None"}; const std::vector<StrId> letterboxFillLabels = {StrId::STR_DEFAULT_OPTION, StrId::STR_DITHERED, StrId::STR_SOLID,
StrId::STR_NONE_OPT};
int currentPage = 0; int currentPage = 0;
int totalPages = 0; int totalPages = 0;
int bookProgressPercent = 0; int bookProgressPercent = 0;
@@ -106,29 +105,26 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
static std::vector<MenuItem> buildMenuItems(bool hasDictionary, bool isBookmarked) { static std::vector<MenuItem> buildMenuItems(bool hasDictionary, bool isBookmarked) {
std::vector<MenuItem> items; std::vector<MenuItem> items;
if (isBookmarked) { if (isBookmarked) {
items.push_back({MenuAction::REMOVE_BOOKMARK, "Remove Bookmark"}); items.push_back({MenuAction::REMOVE_BOOKMARK, StrId::STR_REMOVE_BOOKMARK});
} else { } else {
items.push_back({MenuAction::ADD_BOOKMARK, "Add Bookmark"}); items.push_back({MenuAction::ADD_BOOKMARK, StrId::STR_ADD_BOOKMARK});
} }
if (hasDictionary) { if (hasDictionary) {
items.push_back({MenuAction::LOOKUP, "Lookup Word"}); items.push_back({MenuAction::LOOKUP, StrId::STR_LOOKUP_WORD});
items.push_back({MenuAction::LOOKED_UP_WORDS, "Lookup Word History"}); items.push_back({MenuAction::LOOKED_UP_WORDS, StrId::STR_LOOKUP_HISTORY});
} }
items.push_back({MenuAction::ROTATE_SCREEN, "Reading Orientation"}); items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION});
items.push_back({MenuAction::LETTERBOX_FILL, "Letterbox Fill"}); items.push_back({MenuAction::LETTERBOX_FILL, StrId::STR_LETTERBOX_FILL});
items.push_back({MenuAction::SELECT_CHAPTER, "Table of Contents"}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_TABLE_OF_CONTENTS});
items.push_back({MenuAction::GO_TO_BOOKMARK, "Go to Bookmark"}); items.push_back({MenuAction::GO_TO_BOOKMARK, StrId::STR_GO_TO_BOOKMARK});
items.push_back({MenuAction::GO_TO_PERCENT, "Go to %"}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::GO_HOME, "Close Book"}); items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
items.push_back({MenuAction::SYNC, "Sync Progress"}); items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
items.push_back({MenuAction::DELETE_CACHE, "Delete Book Cache"}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
if (hasDictionary) { if (hasDictionary) {
items.push_back({MenuAction::DELETE_DICT_CACHE, "Delete Dictionary Cache"}); items.push_back({MenuAction::DELETE_DICT_CACHE, StrId::STR_DELETE_DICT_CACHE});
} }
return items; return items;
} }
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
}; };

View File

@@ -1,6 +1,7 @@
#include "EpubReaderPercentSelectionActivity.h" #include "EpubReaderPercentSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
@@ -15,41 +16,10 @@ constexpr int kLargeStep = 10;
void EpubReaderPercentSelectionActivity::onEnter() { void EpubReaderPercentSelectionActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
// Set up rendering task and mark first frame dirty. // Set up rendering task and mark first frame dirty.
renderingMutex = xSemaphoreCreateMutex(); requestUpdate();
updateRequired = true;
xTaskCreate(&EpubReaderPercentSelectionActivity::taskTrampoline, "EpubPercentSlider", 4096, this, 1,
&displayTaskHandle);
} }
void EpubReaderPercentSelectionActivity::onExit() { void EpubReaderPercentSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); }
ActivityWithSubactivity::onExit();
// Ensure the render task is stopped before freeing the mutex.
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderPercentSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderPercentSelectionActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderPercentSelectionActivity::displayTaskLoop() {
while (true) {
// Render only when the view is dirty and no subactivity is running.
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
// Apply delta and clamp within 0-100. // Apply delta and clamp within 0-100.
@@ -59,7 +29,7 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
} else if (percent > 100) { } else if (percent > 100) {
percent = 100; percent = 100;
} }
updateRequired = true; requestUpdate();
} }
void EpubReaderPercentSelectionActivity::loop() { void EpubReaderPercentSelectionActivity::loop() {
@@ -86,11 +56,11 @@ void EpubReaderPercentSelectionActivity::loop() {
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
} }
void EpubReaderPercentSelectionActivity::renderScreen() { void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
// Title and numeric percent value. // Title and numeric percent value.
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Position", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_GO_TO_PERCENT), true, EpdFontFamily::BOLD);
const std::string percentText = std::to_string(percent) + "%"; const std::string percentText = std::to_string(percent) + "%";
renderer.drawCenteredText(UI_12_FONT_ID, 90, percentText.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 90, percentText.c_str(), true, EpdFontFamily::BOLD);
@@ -115,10 +85,10 @@ void EpubReaderPercentSelectionActivity::renderScreen() {
renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true); renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true);
// Hint text for step sizes. // Hint text for step sizes.
renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, "Left/Right: 1% Up/Down: 10%", true); renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, tr(STR_PERCENT_STEP_HINT), true);
// Button hints follow the current front button layout. // Button hints follow the current front button layout.
const auto labels = mappedInput.mapLabels("« Back", "Select", "-", "+"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
@@ -23,15 +20,12 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
// Current percent value (0-100) shown on the slider. // Current percent value (0-100) shown on the slider.
int percent = 0; int percent = 0;
// Render dirty flag for the task loop.
bool updateRequired = false;
// FreeRTOS task and mutex for rendering.
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
// Callback invoked when the user confirms a percent. // Callback invoked when the user confirms a percent.
@@ -39,10 +33,6 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity
// Callback invoked when the user cancels the slider. // Callback invoked when the user cancels the slider.
const std::function<void()> onCancel; const std::function<void()> onCancel;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
// Render the slider UI.
void renderScreen();
// Change the current percent by a delta and clamp within bounds. // Change the current percent by a delta and clamp within bounds.
void adjustPercent(int delta); void adjustPercent(int delta);
}; };

View File

@@ -1,6 +1,7 @@
#include "KOReaderSyncActivity.h" #include "KOReaderSyncActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <WiFi.h> #include <WiFi.h>
#include <esp_sntp.h> #include <esp_sntp.h>
@@ -40,11 +41,6 @@ void syncTimeWithNTP() {
} }
} // namespace } // namespace
void KOReaderSyncActivity::taskTrampoline(void* param) {
auto* self = static_cast<KOReaderSyncActivity*>(param);
self->displayTaskLoop();
}
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
exitActivity(); exitActivity();
@@ -56,19 +52,21 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
LOG_DBG("KOSync", "WiFi connected, starting sync"); LOG_DBG("KOSync", "WiFi connected, starting sync");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNCING; RenderLock lock(*this);
statusMessage = "Syncing time..."; state = SYNCING;
xSemaphoreGive(renderingMutex); statusMessage = tr(STR_SYNCING_TIME);
updateRequired = true; }
requestUpdate();
// Sync time with NTP before making API requests // Sync time with NTP before making API requests
syncTimeWithNTP(); syncTimeWithNTP();
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
statusMessage = "Calculating document hash..."; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); statusMessage = tr(STR_CALC_HASH);
updateRequired = true; }
requestUpdate();
performSync(); performSync();
} }
@@ -81,41 +79,44 @@ void KOReaderSyncActivity::performSync() {
documentHash = KOReaderDocumentId::calculate(epubPath); documentHash = KOReaderDocumentId::calculate(epubPath);
} }
if (documentHash.empty()) { if (documentHash.empty()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNC_FAILED; RenderLock lock(*this);
statusMessage = "Failed to calculate document hash"; state = SYNC_FAILED;
xSemaphoreGive(renderingMutex); statusMessage = tr(STR_HASH_FAILED);
updateRequired = true; }
requestUpdate();
return; return;
} }
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str()); LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
statusMessage = "Fetching remote progress..."; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); statusMessage = tr(STR_FETCH_PROGRESS);
updateRequired = true; }
vTaskDelay(10 / portTICK_PERIOD_MS); requestUpdateAndWait();
// Fetch remote progress // Fetch remote progress
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
if (result == KOReaderSyncClient::NOT_FOUND) { if (result == KOReaderSyncClient::NOT_FOUND) {
// No remote progress - offer to upload // No remote progress - offer to upload
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = NO_REMOTE_PROGRESS; RenderLock lock(*this);
hasRemoteProgress = false; state = NO_REMOTE_PROGRESS;
xSemaphoreGive(renderingMutex); hasRemoteProgress = false;
updateRequired = true; }
requestUpdate();
return; return;
} }
if (result != KOReaderSyncClient::OK) { if (result != KOReaderSyncClient::OK) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNC_FAILED; RenderLock lock(*this);
statusMessage = KOReaderSyncClient::errorString(result); state = SYNC_FAILED;
xSemaphoreGive(renderingMutex); statusMessage = KOReaderSyncClient::errorString(result);
updateRequired = true; }
requestUpdate();
return; return;
} }
@@ -128,20 +129,22 @@ void KOReaderSyncActivity::performSync() {
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine}; CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
localProgress = ProgressMapper::toKOReader(epub, localPos); localProgress = ProgressMapper::toKOReader(epub, localPos);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SHOWING_RESULT; RenderLock lock(*this);
selectedOption = 0; // Default to "Apply" state = SHOWING_RESULT;
xSemaphoreGive(renderingMutex); selectedOption = 0; // Default to "Apply"
updateRequired = true; }
requestUpdate();
} }
void KOReaderSyncActivity::performUpload() { void KOReaderSyncActivity::performUpload() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = UPLOADING; RenderLock lock(*this);
statusMessage = "Uploading progress..."; state = UPLOADING;
xSemaphoreGive(renderingMutex); statusMessage = tr(STR_UPLOAD_PROGRESS);
updateRequired = true; }
vTaskDelay(10 / portTICK_PERIOD_MS); requestUpdate();
requestUpdateAndWait();
// Convert current position to KOReader format // Convert current position to KOReader format
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine}; CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
@@ -155,36 +158,29 @@ void KOReaderSyncActivity::performUpload() {
const auto result = KOReaderSyncClient::updateProgress(progress); const auto result = KOReaderSyncClient::updateProgress(progress);
if (result != KOReaderSyncClient::OK) { if (result != KOReaderSyncClient::OK) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = SYNC_FAILED; RenderLock lock(*this);
statusMessage = KOReaderSyncClient::errorString(result); state = SYNC_FAILED;
xSemaphoreGive(renderingMutex); statusMessage = KOReaderSyncClient::errorString(result);
updateRequired = true; }
requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = UPLOAD_COMPLETE; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = UPLOAD_COMPLETE;
updateRequired = true; }
requestUpdate();
} }
void KOReaderSyncActivity::onEnter() { void KOReaderSyncActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
xTaskCreate(&KOReaderSyncActivity::taskTrampoline, "KOSyncTask",
4096, // Stack size (larger for network operations)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Check for credentials first // Check for credentials first
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
state = NO_CREDENTIALS; state = NO_CREDENTIALS;
updateRequired = true; requestUpdate();
return; return;
} }
@@ -196,8 +192,8 @@ void KOReaderSyncActivity::onEnter() {
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
LOG_DBG("KOSync", "Already connected to WiFi"); LOG_DBG("KOSync", "Already connected to WiFi");
state = SYNCING; state = SYNCING;
statusMessage = "Syncing time..."; statusMessage = tr(STR_SYNCING_TIME);
updateRequired = true; requestUpdate();
// Perform sync directly (will be handled in loop) // Perform sync directly (will be handled in loop)
xTaskCreate( xTaskCreate(
@@ -205,10 +201,11 @@ void KOReaderSyncActivity::onEnter() {
auto* self = static_cast<KOReaderSyncActivity*>(param); auto* self = static_cast<KOReaderSyncActivity*>(param);
// Sync time first // Sync time first
syncTimeWithNTP(); syncTimeWithNTP();
xSemaphoreTake(self->renderingMutex, portMAX_DELAY); {
self->statusMessage = "Calculating document hash..."; RenderLock lock(*self);
xSemaphoreGive(self->renderingMutex); self->statusMessage = tr(STR_CALC_HASH);
self->updateRequired = true; }
self->requestUpdate();
self->performSync(); self->performSync();
vTaskDelete(nullptr); vTaskDelete(nullptr);
}, },
@@ -230,30 +227,9 @@ void KOReaderSyncActivity::onExit() {
delay(100); delay(100);
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
delay(100); delay(100);
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void KOReaderSyncActivity::displayTaskLoop() { void KOReaderSyncActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void KOReaderSyncActivity::render() {
if (subActivity) { if (subActivity) {
return; return;
} }
@@ -261,13 +237,13 @@ void KOReaderSyncActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD);
if (state == NO_CREDENTIALS) { if (state == NO_CREDENTIALS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No credentials configured", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_CREDENTIALS_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Set up KOReader account in Settings"); renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_KOREADER_SETUP_HINT));
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -281,40 +257,41 @@ void KOReaderSyncActivity::render() {
if (state == SHOWING_RESULT) { if (state == SHOWING_RESULT) {
// Show comparison // Show comparison
renderer.drawCenteredText(UI_10_FONT_ID, 120, "Progress found!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 120, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD);
// Get chapter names from TOC // Get chapter names from TOC
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex); const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
const std::string remoteChapter = (remoteTocIndex >= 0) const std::string remoteChapter =
? epub->getTocItem(remoteTocIndex).title (remoteTocIndex >= 0) ? epub->getTocItem(remoteTocIndex).title
: ("Section " + std::to_string(remotePosition.spineIndex + 1)); : (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
const std::string localChapter = (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title const std::string localChapter =
: ("Section " + std::to_string(currentSpineIndex + 1)); (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
// Remote progress - chapter and page // Remote progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 160, "Remote:", true); renderer.drawText(UI_10_FONT_ID, 20, 160, tr(STR_REMOTE_LABEL), true);
char remoteChapterStr[128]; char remoteChapterStr[128];
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str()); snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr); renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr);
char remotePageStr[64]; char remotePageStr[64];
snprintf(remotePageStr, sizeof(remotePageStr), " Page %d, %.2f%% overall", remotePosition.pageNumber + 1, snprintf(remotePageStr, sizeof(remotePageStr), tr(STR_PAGE_OVERALL_FORMAT), remotePosition.pageNumber + 1,
remoteProgress.percentage * 100); remoteProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr); renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr);
if (!remoteProgress.device.empty()) { if (!remoteProgress.device.empty()) {
char deviceStr[64]; char deviceStr[64];
snprintf(deviceStr, sizeof(deviceStr), " From: %s", remoteProgress.device.c_str()); snprintf(deviceStr, sizeof(deviceStr), tr(STR_DEVICE_FROM_FORMAT), remoteProgress.device.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr); renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr);
} }
// Local progress - chapter and page // Local progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 270, "Local:", true); renderer.drawText(UI_10_FONT_ID, 20, 270, tr(STR_LOCAL_LABEL), true);
char localChapterStr[128]; char localChapterStr[128];
snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str()); snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr); renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr);
char localPageStr[64]; char localPageStr[64];
snprintf(localPageStr, sizeof(localPageStr), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine, snprintf(localPageStr, sizeof(localPageStr), tr(STR_PAGE_TOTAL_OVERALL_FORMAT), currentPage + 1, totalPagesInSpine,
localProgress.percentage * 100); localProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr); renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
@@ -325,45 +302,45 @@ void KOReaderSyncActivity::render() {
if (selectedOption == 0) { if (selectedOption == 0) {
renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight); renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight);
} }
renderer.drawText(UI_10_FONT_ID, 20, optionY, "Apply remote progress", selectedOption != 0); renderer.drawText(UI_10_FONT_ID, 20, optionY, tr(STR_APPLY_REMOTE), selectedOption != 0);
// Upload option // Upload option
if (selectedOption == 1) { if (selectedOption == 1) {
renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight); renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight);
} }
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, "Upload local progress", selectedOption != 1); renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, tr(STR_UPLOAD_LOCAL), selectedOption != 1);
// Bottom button hints: show Back and Select // Bottom button hints: show Back and Select
const auto labels = mappedInput.mapLabels("Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == NO_REMOTE_PROGRESS) { if (state == NO_REMOTE_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_REMOTE_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?"); renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_UPLOAD_PROMPT));
const auto labels = mappedInput.mapLabels("Back", "Upload", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_UPLOAD), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == UPLOAD_COMPLETE) { if (state == UPLOAD_COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Progress uploaded!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == SYNC_FAILED) { if (state == SYNC_FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Sync failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -388,11 +365,11 @@ void KOReaderSyncActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
updateRequired = true; requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
updateRequired = true; requestUpdate();
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Epub.h> #include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
@@ -45,6 +42,7 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; }
private: private:
@@ -66,10 +64,6 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
int currentPage; int currentPage;
int totalPagesInSpine; int totalPagesInSpine;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
State state = WIFI_SELECTION; State state = WIFI_SELECTION;
std::string statusMessage; std::string statusMessage;
std::string documentHash; std::string documentHash;
@@ -91,8 +85,4 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
void onWifiSelectionComplete(bool success); void onWifiSelectionComplete(bool success);
void performSync(); void performSync();
void performUpload(); void performUpload();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
}; };

View File

@@ -12,41 +12,15 @@
#include "util/Dictionary.h" #include "util/Dictionary.h"
#include "util/LookupHistory.h" #include "util/LookupHistory.h"
void LookedUpWordsActivity::taskTrampoline(void* param) {
auto* self = static_cast<LookedUpWordsActivity*>(param);
self->displayTaskLoop();
}
void LookedUpWordsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void LookedUpWordsActivity::onEnter() { void LookedUpWordsActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
words = LookupHistory::load(cachePath); words = LookupHistory::load(cachePath);
std::reverse(words.begin(), words.end()); std::reverse(words.begin(), words.end());
updateRequired = true; requestUpdate();
xTaskCreate(&LookedUpWordsActivity::taskTrampoline, "LookedUpTask", 4096, this, 1, &displayTaskHandle);
} }
void LookedUpWordsActivity::onExit() { void LookedUpWordsActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void LookedUpWordsActivity::loop() { void LookedUpWordsActivity::loop() {
@@ -55,7 +29,7 @@ void LookedUpWordsActivity::loop() {
if (pendingBackFromDef) { if (pendingBackFromDef) {
pendingBackFromDef = false; pendingBackFromDef = false;
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
} }
if (pendingExitToReader) { if (pendingExitToReader) {
pendingExitToReader = false; pendingExitToReader = false;
@@ -87,13 +61,13 @@ void LookedUpWordsActivity::loop() {
selectedIndex = std::max(0, static_cast<int>(words.size()) - 1); selectedIndex = std::max(0, static_cast<int>(words.size()) - 1);
} }
deleteConfirmMode = false; deleteConfirmMode = false;
updateRequired = true; requestUpdate();
} }
} }
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
deleteConfirmMode = false; deleteConfirmMode = false;
ignoreNextConfirmRelease = false; ignoreNextConfirmRelease = false;
updateRequired = true; requestUpdate();
} }
return; return;
} }
@@ -104,7 +78,7 @@ void LookedUpWordsActivity::loop() {
deleteConfirmMode = true; deleteConfirmMode = true;
ignoreNextConfirmRelease = true; ignoreNextConfirmRelease = true;
pendingDeleteIndex = selectedIndex; pendingDeleteIndex = selectedIndex;
updateRequired = true; requestUpdate();
return; return;
} }
@@ -113,30 +87,37 @@ void LookedUpWordsActivity::loop() {
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, totalItems); selectedIndex = ButtonNavigator::nextIndex(selectedIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, totalItems] { buttonNavigator.onPreviousRelease([this, totalItems] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, totalItems); selectedIndex = ButtonNavigator::previousIndex(selectedIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this, totalItems, pageItems] { buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, totalItems, pageItems); selectedIndex = ButtonNavigator::nextPageIndex(selectedIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, totalItems, pageItems); selectedIndex = ButtonNavigator::previousPageIndex(selectedIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const std::string& headword = words[selectedIndex]; const std::string& headword = words[selectedIndex];
Rect popupLayout = GUI.drawPopup(renderer, "Looking up..."); Rect popupLayout;
{
Activity::RenderLock lock(*this);
popupLayout = GUI.drawPopup(renderer, "Looking up...");
}
std::string definition = Dictionary::lookup( std::string definition = Dictionary::lookup(
headword, [this, &popupLayout](int percent) { GUI.fillPopupProgress(renderer, popupLayout, percent); }); headword, [this, &popupLayout](int percent) {
Activity::RenderLock lock(*this);
GUI.fillPopupProgress(renderer, popupLayout, percent);
});
if (!definition.empty()) { if (!definition.empty()) {
enterNewActivity(new DictionaryDefinitionActivity( enterNewActivity(new DictionaryDefinitionActivity(
@@ -166,10 +147,13 @@ void LookedUpWordsActivity::loop() {
return; return;
} }
GUI.drawPopup(renderer, "Not found"); {
renderer.displayBuffer(HalDisplay::FAST_REFRESH); Activity::RenderLock lock(*this);
GUI.drawPopup(renderer, "Not found");
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
vTaskDelay(1500 / portTICK_PERIOD_MS); vTaskDelay(1500 / portTICK_PERIOD_MS);
updateRequired = true; requestUpdate();
return; return;
} }
@@ -190,7 +174,7 @@ int LookedUpWordsActivity::getPageItems() const {
return std::max(1, contentHeight / metrics.listRowHeight); return std::max(1, contentHeight / metrics.listRowHeight);
} }
void LookedUpWordsActivity::renderScreen() { void LookedUpWordsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
const auto orient = renderer.getOrientation(); const auto orient = renderer.getOrientation();

View File

@@ -1,8 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -25,6 +21,7 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
std::string cachePath; std::string cachePath;
@@ -35,7 +32,6 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity {
std::vector<std::string> words; std::vector<std::string> words;
int selectedIndex = 0; int selectedIndex = 0;
bool updateRequired = false;
bool pendingBackFromDef = false; bool pendingBackFromDef = false;
bool pendingExitToReader = false; bool pendingExitToReader = false;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
@@ -45,11 +41,5 @@ class LookedUpWordsActivity final : public ActivityWithSubactivity {
bool ignoreNextConfirmRelease = false; bool ignoreNextConfirmRelease = false;
int pendingDeleteIndex = 0; int pendingDeleteIndex = 0;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int getPageItems() const; int getPageItems() const;
void renderScreen();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
}; };

View File

@@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <Serialization.h> #include <Serialization.h>
#include <Utf8.h> #include <Utf8.h>
@@ -25,11 +26,6 @@ constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
} // namespace } // namespace
void TxtReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<TxtReaderActivity*>(param);
self->displayTaskLoop();
}
void TxtReaderActivity::onEnter() { void TxtReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
@@ -55,8 +51,6 @@ void TxtReaderActivity::onEnter() {
break; break;
} }
renderingMutex = xSemaphoreCreateMutex();
txt->setupCacheDir(); txt->setupCacheDir();
// Prerender covers and thumbnails on first open so Home and Sleep screens are instant. // Prerender covers and thumbnails on first open so Home and Sleep screens are instant.
@@ -106,14 +100,7 @@ void TxtReaderActivity::onEnter() {
RECENT_BOOKS.addBook(filePath, fileName, "", txt->getThumbBmpPath()); RECENT_BOOKS.addBook(filePath, fileName, "", txt->getThumbBmpPath());
// Trigger first update // Trigger first update
updateRequired = true; requestUpdate();
xTaskCreate(&TxtReaderActivity::taskTrampoline, "TxtReaderActivityTask",
6144, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void TxtReaderActivity::onExit() { void TxtReaderActivity::onExit() {
@@ -122,14 +109,6 @@ void TxtReaderActivity::onExit() {
// Reset orientation back to portrait for the rest of the UI // Reset orientation back to portrait for the rest of the UI
renderer.setOrientation(GfxRenderer::Orientation::Portrait); renderer.setOrientation(GfxRenderer::Orientation::Portrait);
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
pageOffsets.clear(); pageOffsets.clear();
currentPageLines.clear(); currentPageLines.clear();
APP_STATE.readerActivityLoadCount = 0; APP_STATE.readerActivityLoadCount = 0;
@@ -175,22 +154,10 @@ void TxtReaderActivity::loop() {
if (prevTriggered && currentPage > 0) { if (prevTriggered && currentPage > 0) {
currentPage--; currentPage--;
updateRequired = true; requestUpdate();
} else if (nextTriggered && currentPage < totalPages - 1) { } else if (nextTriggered && currentPage < totalPages - 1) {
currentPage++; currentPage++;
updateRequired = true; requestUpdate();
}
}
void TxtReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
} }
} }
@@ -257,7 +224,7 @@ void TxtReaderActivity::buildPageIndex() {
LOG_DBG("TRS", "Building page index for %zu bytes...", fileSize); LOG_DBG("TRS", "Building page index for %zu bytes...", fileSize);
GUI.drawPopup(renderer, "Indexing..."); GUI.drawPopup(renderer, tr(STR_INDEXING));
while (offset < fileSize) { while (offset < fileSize) {
std::vector<std::string> tempLines; std::vector<std::string> tempLines;
@@ -413,7 +380,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
return !outLines.empty(); return !outLines.empty();
} }
void TxtReaderActivity::renderScreen() { void TxtReaderActivity::render(Activity::RenderLock&&) {
if (!txt) { if (!txt) {
return; return;
} }
@@ -425,7 +392,7 @@ void TxtReaderActivity::renderScreen() {
if (pageOffsets.empty()) { if (pageOffsets.empty()) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty file", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_FILE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@@ -583,8 +550,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
} }
if (showBattery) { if (showBattery) {
GUI.drawBattery(renderer, Rect{orientedMarginLeft, textY, metrics.batteryWidth, metrics.batteryHeight}, GUI.drawBatteryLeft(renderer, Rect{orientedMarginLeft, textY, metrics.batteryWidth, metrics.batteryHeight},
showBatteryPercentage); showBatteryPercentage);
} }
if (showTitle) { if (showTitle) {

View File

@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <Txt.h> #include <Txt.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <vector> #include <vector>
@@ -12,12 +9,11 @@
class TxtReaderActivity final : public ActivityWithSubactivity { class TxtReaderActivity final : public ActivityWithSubactivity {
std::unique_ptr<Txt> txt; std::unique_ptr<Txt> txt;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentPage = 0; int currentPage = 0;
int totalPages = 1; int totalPages = 1;
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
@@ -33,9 +29,6 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
int cachedScreenMargin = 0; int cachedScreenMargin = 0;
uint8_t cachedParagraphAlignment = CrossPointSettings::LEFT_ALIGN; uint8_t cachedParagraphAlignment = CrossPointSettings::LEFT_ALIGN;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderPage(); void renderPage();
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
@@ -57,6 +50,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
// Defer low-power mode and auto-sleep while the reader is initializing // Defer low-power mode and auto-sleep while the reader is initializing
// (cover prerendering, page index building on first open). // (cover prerendering, page index building on first open).
bool preventAutoSleep() override { return !initialized; } bool preventAutoSleep() override { return !initialized; }

View File

@@ -10,6 +10,7 @@
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <PlaceholderCoverGenerator.h> #include <PlaceholderCoverGenerator.h>
@@ -26,11 +27,6 @@ constexpr unsigned long skipPageMs = 700;
constexpr unsigned long goHomeMs = 1000; constexpr unsigned long goHomeMs = 1000;
} // namespace } // namespace
void XtcReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<XtcReaderActivity*>(param);
self->displayTaskLoop();
}
void XtcReaderActivity::onEnter() { void XtcReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
@@ -38,8 +34,6 @@ void XtcReaderActivity::onEnter() {
return; return;
} }
renderingMutex = xSemaphoreCreateMutex();
xtc->setupCacheDir(); xtc->setupCacheDir();
// Load saved progress // Load saved progress
@@ -93,27 +87,12 @@ void XtcReaderActivity::onEnter() {
RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor(), xtc->getThumbBmpPath()); RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor(), xtc->getThumbBmpPath());
// Trigger first update // Trigger first update
updateRequired = true; requestUpdate();
xTaskCreate(&XtcReaderActivity::taskTrampoline, "XtcReaderActivityTask",
4096, // Stack size (smaller than EPUB since no parsing needed)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void XtcReaderActivity::onExit() { void XtcReaderActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
APP_STATE.readerActivityLoadCount = 0; APP_STATE.readerActivityLoadCount = 0;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
xtc.reset(); xtc.reset();
@@ -129,20 +108,18 @@ void XtcReaderActivity::loop() {
// Enter chapter selection activity // Enter chapter selection activity
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity(new XtcReaderChapterSelectionActivity( enterNewActivity(new XtcReaderChapterSelectionActivity(
this->renderer, this->mappedInput, xtc, currentPage, this->renderer, this->mappedInput, xtc, currentPage,
[this] { [this] {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this](const uint32_t newPage) { [this](const uint32_t newPage) {
currentPage = newPage; currentPage = newPage;
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
xSemaphoreGive(renderingMutex);
} }
} }
@@ -179,7 +156,7 @@ void XtcReaderActivity::loop() {
// Handle end of book // Handle end of book
if (currentPage >= xtc->getPageCount()) { if (currentPage >= xtc->getPageCount()) {
currentPage = xtc->getPageCount() - 1; currentPage = xtc->getPageCount() - 1;
updateRequired = true; requestUpdate();
return; return;
} }
@@ -192,29 +169,17 @@ void XtcReaderActivity::loop() {
} else { } else {
currentPage = 0; currentPage = 0;
} }
updateRequired = true; requestUpdate();
} else if (nextTriggered) { } else if (nextTriggered) {
currentPage += skipAmount; currentPage += skipAmount;
if (currentPage >= xtc->getPageCount()) { if (currentPage >= xtc->getPageCount()) {
currentPage = xtc->getPageCount(); // Allow showing "End of book" currentPage = xtc->getPageCount(); // Allow showing "End of book"
} }
updateRequired = true; requestUpdate();
} }
} }
void XtcReaderActivity::displayTaskLoop() { void XtcReaderActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void XtcReaderActivity::renderScreen() {
if (!xtc) { if (!xtc) {
return; return;
} }
@@ -223,7 +188,7 @@ void XtcReaderActivity::renderScreen() {
if (currentPage >= xtc->getPageCount()) { if (currentPage >= xtc->getPageCount()) {
// Show end of book screen // Show end of book screen
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@@ -252,7 +217,7 @@ void XtcReaderActivity::renderPage() {
if (!pageBuffer) { if (!pageBuffer) {
LOG_ERR("XTR", "Failed to allocate page buffer (%lu bytes)", pageBufferSize); LOG_ERR("XTR", "Failed to allocate page buffer (%lu bytes)", pageBufferSize);
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_MEMORY_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@@ -263,7 +228,7 @@ void XtcReaderActivity::renderPage() {
LOG_ERR("XTR", "Failed to load page %lu", currentPage); LOG_ERR("XTR", "Failed to load page %lu", currentPage);
free(pageBuffer); free(pageBuffer);
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }

View File

@@ -8,25 +8,18 @@
#pragma once #pragma once
#include <Xtc.h> #include <Xtc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
class XtcReaderActivity final : public ActivityWithSubactivity { class XtcReaderActivity final : public ActivityWithSubactivity {
std::shared_ptr<Xtc> xtc; std::shared_ptr<Xtc> xtc;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
uint32_t currentPage = 0; uint32_t currentPage = 0;
int pagesUntilFullRefresh = 0; int pagesUntilFullRefresh = 0;
bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderPage(); void renderPage();
void saveProgress() const; void saveProgress() const;
void loadProgress(); void loadProgress();
@@ -41,4 +34,5 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -1,6 +1,7 @@
#include "XtcReaderChapterSelectionActivity.h" #include "XtcReaderChapterSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm> #include <algorithm>
@@ -37,11 +38,6 @@ int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) co
return 0; return 0;
} }
void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<XtcReaderChapterSelectionActivity*>(param);
self->displayTaskLoop();
}
void XtcReaderChapterSelectionActivity::onEnter() { void XtcReaderChapterSelectionActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
@@ -49,29 +45,12 @@ void XtcReaderChapterSelectionActivity::onEnter() {
return; return;
} }
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = findChapterIndexForPage(currentPage); selectorIndex = findChapterIndexForPage(currentPage);
updateRequired = true; requestUpdate();
xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void XtcReaderChapterSelectionActivity::onExit() { void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
Activity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void XtcReaderChapterSelectionActivity::loop() { void XtcReaderChapterSelectionActivity::loop() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
@@ -88,38 +67,26 @@ void XtcReaderChapterSelectionActivity::loop() {
buttonNavigator.onNextRelease([this, totalItems] { buttonNavigator.onNextRelease([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousRelease([this, totalItems] { buttonNavigator.onPreviousRelease([this, totalItems] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onNextContinuous([this, totalItems, pageItems] { buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true; requestUpdate();
}); });
} }
void XtcReaderChapterSelectionActivity::displayTaskLoop() { void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void XtcReaderChapterSelectionActivity::renderScreen() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
@@ -138,14 +105,14 @@ void XtcReaderChapterSelectionActivity::renderScreen() {
const int pageItems = getPageItems(); const int pageItems = getPageItems();
// Manual centering to honor content gutters. // Manual centering to honor content gutters.
const int titleX = const int titleX =
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Select Chapter", EpdFontFamily::BOLD)) / 2; contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2;
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Select Chapter", true, EpdFontFamily::BOLD); renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD);
const auto& chapters = xtc->getChapters(); const auto& chapters = xtc->getChapters();
if (chapters.empty()) { if (chapters.empty()) {
// Center the empty state within the gutter-safe content region. // Center the empty state within the gutter-safe content region.
const int emptyX = contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, "No chapters")) / 2; const int emptyX = contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, tr(STR_NO_CHAPTERS))) / 2;
renderer.drawText(UI_10_FONT_ID, emptyX, 120 + contentY, "No chapters"); renderer.drawText(UI_10_FONT_ID, emptyX, 120 + contentY, tr(STR_NO_CHAPTERS));
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@@ -155,13 +122,13 @@ void XtcReaderChapterSelectionActivity::renderScreen() {
renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30); renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30);
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) { for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
const auto& chapter = chapters[i]; const auto& chapter = chapters[i];
const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str(); const char* title = chapter.name.empty() ? tr(STR_UNNAMED) : chapter.name.c_str();
renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, i != selectorIndex); renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, i != selectorIndex);
} }
// Skip button hints in landscape CW mode (they overlap content) // Skip button hints in landscape CW mode (they overlap content)
if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) {
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }

View File

@@ -1,8 +1,5 @@
#pragma once #pragma once
#include <Xtc.h> #include <Xtc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory> #include <memory>
@@ -11,22 +8,16 @@
class XtcReaderChapterSelectionActivity final : public Activity { class XtcReaderChapterSelectionActivity final : public Activity {
std::shared_ptr<Xtc> xtc; std::shared_ptr<Xtc> xtc;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
uint32_t currentPage = 0; uint32_t currentPage = 0;
int selectorIndex = 0; int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(uint32_t newPage)> onSelectPage; const std::function<void(uint32_t newPage)> onSelectPage;
int getPageItems() const; int getPageItems() const;
int findChapterIndexForPage(uint32_t page) const; int findChapterIndexForPage(uint32_t page) const;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public: public:
explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Xtc>& xtc, uint32_t currentPage, const std::shared_ptr<Xtc>& xtc, uint32_t currentPage,
@@ -40,4 +31,5 @@ class XtcReaderChapterSelectionActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
}; };

View File

@@ -1,6 +1,7 @@
#include "ButtonRemapActivity.h" #include "ButtonRemapActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
@@ -16,15 +17,9 @@ constexpr uint8_t kUnassigned = 0xFF;
constexpr unsigned long kErrorDisplayMs = 1500; constexpr unsigned long kErrorDisplayMs = 1500;
} // namespace } // namespace
void ButtonRemapActivity::taskTrampoline(void* param) {
auto* self = static_cast<ButtonRemapActivity*>(param);
self->displayTaskLoop();
}
void ButtonRemapActivity::onEnter() { void ButtonRemapActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Start with all roles unassigned to avoid duplicate blocking. // Start with all roles unassigned to avoid duplicate blocking.
currentStep = 0; currentStep = 0;
tempMapping[0] = kUnassigned; tempMapping[0] = kUnassigned;
@@ -33,25 +28,20 @@ void ButtonRemapActivity::onEnter() {
tempMapping[3] = kUnassigned; tempMapping[3] = kUnassigned;
errorMessage.clear(); errorMessage.clear();
errorUntil = 0; errorUntil = 0;
updateRequired = true; requestUpdate();
xTaskCreate(&ButtonRemapActivity::taskTrampoline, "ButtonRemapTask", 4096, this, 1, &displayTaskHandle);
} }
void ButtonRemapActivity::onExit() { void ButtonRemapActivity::onExit() { Activity::onExit(); }
Activity::onExit();
// Ensure display task is stopped outside of active rendering.
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void ButtonRemapActivity::loop() { void ButtonRemapActivity::loop() {
// Clear any temporary warning after its timeout.
if (errorUntil > 0 && millis() > errorUntil) {
errorMessage.clear();
errorUntil = 0;
requestUpdate();
return;
}
// Side buttons: // Side buttons:
// - Up: reset mapping to defaults and exit. // - Up: reset mapping to defaults and exit.
// - Down: cancel without saving. // - Down: cancel without saving.
@@ -72,60 +62,39 @@ void ButtonRemapActivity::loop() {
return; return;
} }
// Wait for the UI to refresh before accepting another assignment. {
// This avoids rapid double-presses that can advance the step without a visible redraw. // Wait for the UI to refresh before accepting another assignment.
if (updateRequired) { // This avoids rapid double-presses that can advance the step without a visible redraw.
return; requestUpdateAndWait();
}
// Wait for a front button press to assign to the current role. // Wait for a front button press to assign to the current role.
const int pressedButton = mappedInput.getPressedFrontButton(); const int pressedButton = mappedInput.getPressedFrontButton();
if (pressedButton < 0) { if (pressedButton < 0) {
return; return;
}
// Update temporary mapping and advance the remap step.
// Only accept the press if this hardware button isn't already assigned elsewhere.
if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) {
updateRequired = true;
return;
}
tempMapping[currentStep] = static_cast<uint8_t>(pressedButton);
currentStep++;
if (currentStep >= kRoleCount) {
// All roles assigned; save to settings and exit.
applyTempMapping();
SETTINGS.saveToFile();
onBack();
return;
}
updateRequired = true;
}
[[noreturn]] void ButtonRemapActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
// Ensure render calls are serialized with UI thread changes.
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
updateRequired = false;
xSemaphoreGive(renderingMutex);
} }
// Clear any temporary warning after its timeout. // Update temporary mapping and advance the remap step.
if (errorUntil > 0 && millis() > errorUntil) { // Only accept the press if this hardware button isn't already assigned elsewhere.
errorMessage.clear(); if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) {
errorUntil = 0; requestUpdate();
updateRequired = true; return;
}
tempMapping[currentStep] = static_cast<uint8_t>(pressedButton);
currentStep++;
if (currentStep >= kRoleCount) {
// All roles assigned; save to settings and exit.
applyTempMapping();
SETTINGS.saveToFile();
onBack();
return;
} }
vTaskDelay(50 / portTICK_PERIOD_MS); requestUpdate();
} }
} }
void ButtonRemapActivity::render() { void ButtonRemapActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
@@ -138,8 +107,8 @@ void ButtonRemapActivity::render() {
return "-"; return "-";
}; };
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Remap Front Buttons", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_REMAP_FRONT_BUTTONS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 40, "Press a front button for each role"); renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_REMAP_PROMPT));
for (uint8_t i = 0; i < kRoleCount; i++) { for (uint8_t i = 0; i < kRoleCount; i++) {
const int y = 70 + i * 30; const int y = 70 + i * 30;
@@ -154,7 +123,7 @@ void ButtonRemapActivity::render() {
renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected);
// Show currently assigned hardware button (or unassigned). // Show currently assigned hardware button (or unassigned).
const char* assigned = (tempMapping[i] == kUnassigned) ? "Unassigned" : getHardwareName(tempMapping[i]); const char* assigned = (tempMapping[i] == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(tempMapping[i]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned); const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected);
} }
@@ -165,8 +134,8 @@ void ButtonRemapActivity::render() {
} }
// Provide side button actions at the bottom of the screen (split across two lines). // Provide side button actions at the bottom of the screen (split across two lines).
renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset to default layout", true); renderer.drawCenteredText(SMALL_FONT_ID, 250, tr(STR_REMAP_RESET_HINT), true);
renderer.drawCenteredText(SMALL_FONT_ID, 280, "Side button Down: Cancel remapping", true); renderer.drawCenteredText(SMALL_FONT_ID, 280, tr(STR_REMAP_CANCEL_HINT), true);
// Live preview of logical labels under front buttons. // Live preview of logical labels under front buttons.
// This mirrors the on-device front button order: Back, Confirm, Left, Right. // This mirrors the on-device front button order: Back, Confirm, Left, Right.
@@ -189,7 +158,7 @@ bool ButtonRemapActivity::validateUnassigned(const uint8_t pressedButton) {
// Block reusing a hardware button already assigned to another role. // Block reusing a hardware button already assigned to another role.
for (uint8_t i = 0; i < kRoleCount; i++) { for (uint8_t i = 0; i < kRoleCount; i++) {
if (tempMapping[i] == pressedButton && i != currentStep) { if (tempMapping[i] == pressedButton && i != currentStep) {
errorMessage = "Already assigned"; errorMessage = tr(STR_ALREADY_ASSIGNED);
errorUntil = millis() + kErrorDisplayMs; errorUntil = millis() + kErrorDisplayMs;
return false; return false;
} }
@@ -200,27 +169,27 @@ bool ButtonRemapActivity::validateUnassigned(const uint8_t pressedButton) {
const char* ButtonRemapActivity::getRoleName(const uint8_t roleIndex) const { const char* ButtonRemapActivity::getRoleName(const uint8_t roleIndex) const {
switch (roleIndex) { switch (roleIndex) {
case 0: case 0:
return "Back"; return tr(STR_BACK);
case 1: case 1:
return "Confirm"; return tr(STR_CONFIRM);
case 2: case 2:
return "Left"; return tr(STR_DIR_LEFT);
case 3: case 3:
default: default:
return "Right"; return tr(STR_DIR_RIGHT);
} }
} }
const char* ButtonRemapActivity::getHardwareName(const uint8_t buttonIndex) const { const char* ButtonRemapActivity::getHardwareName(const uint8_t buttonIndex) const {
switch (buttonIndex) { switch (buttonIndex) {
case CrossPointSettings::FRONT_HW_BACK: case CrossPointSettings::FRONT_HW_BACK:
return "Back (1st button)"; return tr(STR_HW_BACK_LABEL);
case CrossPointSettings::FRONT_HW_CONFIRM: case CrossPointSettings::FRONT_HW_CONFIRM:
return "Confirm (2nd button)"; return tr(STR_HW_CONFIRM_LABEL);
case CrossPointSettings::FRONT_HW_LEFT: case CrossPointSettings::FRONT_HW_LEFT:
return "Left (3rd button)"; return tr(STR_HW_LEFT_LABEL);
case CrossPointSettings::FRONT_HW_RIGHT: case CrossPointSettings::FRONT_HW_RIGHT:
return "Right (4th button)"; return tr(STR_HW_RIGHT_LABEL);
default: default:
return "Unknown"; return "Unknown";
} }

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -17,12 +14,10 @@ class ButtonRemapActivity final : public Activity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
// Rendering task state. // Rendering task state.
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
// Callback used to exit the remap flow back to the settings list. // Callback used to exit the remap flow back to the settings list.
const std::function<void()> onBack; const std::function<void()> onBack;
@@ -34,11 +29,6 @@ class ButtonRemapActivity final : public Activity {
unsigned long errorUntil = 0; unsigned long errorUntil = 0;
std::string errorMessage; std::string errorMessage;
// FreeRTOS task helpers.
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
// Commit temporary mapping to settings. // Commit temporary mapping to settings.
void applyTempMapping(); void applyTempMapping();
// Returns false if a hardware button is already assigned to a different role. // Returns false if a hardware button is already assigned to a different role.

View File

@@ -1,6 +1,7 @@
#include "CalibreSettingsActivity.h" #include "CalibreSettingsActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <cstring> #include <cstring>
@@ -12,40 +13,17 @@
namespace { namespace {
constexpr int MENU_ITEMS = 3; constexpr int MENU_ITEMS = 3;
const char* menuNames[MENU_ITEMS] = {"OPDS Server URL", "Username", "Password"}; const StrId menuNames[MENU_ITEMS] = {StrId::STR_CALIBRE_WEB_URL, StrId::STR_USERNAME, StrId::STR_PASSWORD};
} // namespace } // namespace
void CalibreSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<CalibreSettingsActivity*>(param);
self->displayTaskLoop();
}
void CalibreSettingsActivity::onEnter() { void CalibreSettingsActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0; selectedIndex = 0;
updateRequired = true; requestUpdate();
xTaskCreate(&CalibreSettingsActivity::taskTrampoline, "CalibreSettingsTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void CalibreSettingsActivity::onExit() { void CalibreSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); }
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void CalibreSettingsActivity::loop() { void CalibreSettingsActivity::loop() {
if (subActivity) { if (subActivity) {
@@ -66,23 +44,21 @@ void CalibreSettingsActivity::loop() {
// Handle navigation // Handle navigation
buttonNavigator.onNext([this] { buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS; selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this] { buttonNavigator.onPrevious([this] {
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
updateRequired = true; requestUpdate();
}); });
} }
void CalibreSettingsActivity::handleSelection() { void CalibreSettingsActivity::handleSelection() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (selectedIndex == 0) { if (selectedIndex == 0) {
// OPDS Server URL // OPDS Server URL
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "OPDS Server URL", SETTINGS.opdsServerUrl, 10, renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 10,
127, // maxLength 127, // maxLength
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@@ -90,17 +66,17 @@ void CalibreSettingsActivity::handleSelection() {
SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0';
SETTINGS.saveToFile(); SETTINGS.saveToFile();
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} else if (selectedIndex == 1) { } else if (selectedIndex == 1) {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Username", SETTINGS.opdsUsername, 10, renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 10,
63, // maxLength 63, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
@@ -108,17 +84,17 @@ void CalibreSettingsActivity::handleSelection() {
SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0';
SETTINGS.saveToFile(); SETTINGS.saveToFile();
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} else if (selectedIndex == 2) { } else if (selectedIndex == 2) {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Password", SETTINGS.opdsPassword, 10, renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 10,
63, // maxLength 63, // maxLength
false, // not password mode false, // not password mode
[this](const std::string& password) { [this](const std::string& password) {
@@ -126,39 +102,25 @@ void CalibreSettingsActivity::handleSelection() {
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
SETTINGS.saveToFile(); SETTINGS.saveToFile();
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} }
xSemaphoreGive(renderingMutex);
} }
void CalibreSettingsActivity::displayTaskLoop() { void CalibreSettingsActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void CalibreSettingsActivity::render() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
// Draw info text about Calibre // Draw info text about Calibre
renderer.drawCenteredText(UI_10_FONT_ID, 40, "For Calibre, add /opds to your URL"); renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_CALIBRE_URL_HINT));
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30);
@@ -168,23 +130,26 @@ void CalibreSettingsActivity::render() {
const int settingY = 70 + i * 30; const int settingY = 70 + i * 30;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected);
// Draw status for each setting // Draw status for each setting
const char* status = "[Not Set]"; std::string status = std::string("[") + tr(STR_NOT_SET) + "]";
if (i == 0) { if (i == 0) {
status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]"; status = (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
} else if (i == 1) { } else if (i == 1) {
status = (strlen(SETTINGS.opdsUsername) > 0) ? "[Set]" : "[Not Set]"; status = (strlen(SETTINGS.opdsUsername) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
} else if (i == 2) { } else if (i == 2) {
status = (strlen(SETTINGS.opdsPassword) > 0) ? "[Set]" : "[Not Set]"; status = (strlen(SETTINGS.opdsPassword) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
} }
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected);
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
@@ -21,18 +18,12 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
bool updateRequired = false;
int selectedIndex = 0; int selectedIndex = 0;
const std::function<void()> onBack; const std::function<void()> onBack;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
void handleSelection(); void handleSelection();
}; };

View File

@@ -2,101 +2,67 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
void ClearCacheActivity::taskTrampoline(void* param) {
auto* self = static_cast<ClearCacheActivity*>(param);
self->displayTaskLoop();
}
void ClearCacheActivity::onEnter() { void ClearCacheActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
state = WARNING; state = WARNING;
updateRequired = true; requestUpdate();
xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void ClearCacheActivity::onExit() { void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); }
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD void ClearCacheActivity::render(Activity::RenderLock&&) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void ClearCacheActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void ClearCacheActivity::render() {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CLEAR_READING_CACHE), true, EpdFontFamily::BOLD);
if (state == WARNING) { if (state == WARNING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, "All reading progress will be lost!", true, renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, tr(STR_CLEAR_CACHE_WARNING_2), true,
EpdFontFamily::BOLD); EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Books will need to be re-indexed", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, tr(STR_CLEAR_CACHE_WARNING_3), true);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, "when opened again.", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, tr(STR_CLEAR_CACHE_WARNING_4), true);
const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_CLEAR_BUTTON), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == CLEARING) { if (state == CLEARING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == SUCCESS) { if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Cache Cleared", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_CACHE_CLEARED), true, EpdFontFamily::BOLD);
String resultText = String(clearedCount) + " items removed"; std::string resultText = std::to_string(clearedCount) + " " + std::string(tr(STR_ITEMS_REMOVED));
if (failedCount > 0) { if (failedCount > 0) {
resultText += ", " + String(failedCount) + " failed"; resultText += ", " + std::to_string(failedCount) + " " + std::string(tr(STR_FAILED_LOWER));
} }
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Failed to clear cache", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_CLEAR_CACHE_FAILED), true,
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Check serial output for details"); EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, tr(STR_CHECK_SERIAL_OUTPUT));
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -112,7 +78,7 @@ void ClearCacheActivity::clearCache() {
LOG_DBG("CLEAR_CACHE", "Failed to open cache directory"); LOG_DBG("CLEAR_CACHE", "Failed to open cache directory");
if (root) root.close(); if (root) root.close();
state = FAILED; state = FAILED;
updateRequired = true; requestUpdate();
return; return;
} }
@@ -147,18 +113,18 @@ void ClearCacheActivity::clearCache() {
LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount); LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount);
state = SUCCESS; state = SUCCESS;
updateRequired = true; requestUpdate();
} }
void ClearCacheActivity::loop() { void ClearCacheActivity::loop() {
if (state == WARNING) { if (state == WARNING) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear"); LOG_DBG("CLEAR_CACHE", "User confirmed, starting cache clear");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = CLEARING; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = CLEARING;
updateRequired = true; }
vTaskDelay(10 / portTICK_PERIOD_MS); requestUpdateAndWait();
clearCache(); clearCache();
} }

View File

@@ -1,9 +1,5 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
@@ -17,21 +13,16 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
enum State { WARNING, CLEARING, SUCCESS, FAILED }; enum State { WARNING, CLEARING, SUCCESS, FAILED };
State state = WARNING; State state = WARNING;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
const std::function<void()> goBack; const std::function<void()> goBack;
int clearedCount = 0; int clearedCount = 0;
int failedCount = 0; int failedCount = 0;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
void clearCache(); void clearCache();
}; };

View File

@@ -1,6 +1,7 @@
#include "KOReaderAuthActivity.h" #include "KOReaderAuthActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
@@ -10,28 +11,25 @@
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
void KOReaderAuthActivity::taskTrampoline(void* param) {
auto* self = static_cast<KOReaderAuthActivity*>(param);
self->displayTaskLoop();
}
void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
exitActivity(); exitActivity();
if (!success) { if (!success) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = FAILED; RenderLock lock(*this);
errorMessage = "WiFi connection failed"; state = FAILED;
xSemaphoreGive(renderingMutex); errorMessage = tr(STR_WIFI_CONN_FAILED);
updateRequired = true; }
requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = AUTHENTICATING; RenderLock lock(*this);
statusMessage = "Authenticating..."; state = AUTHENTICATING;
xSemaphoreGive(renderingMutex); statusMessage = tr(STR_AUTHENTICATING);
updateRequired = true; }
requestUpdate();
performAuthentication(); performAuthentication();
} }
@@ -39,38 +37,30 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
void KOReaderAuthActivity::performAuthentication() { void KOReaderAuthActivity::performAuthentication() {
const auto result = KOReaderSyncClient::authenticate(); const auto result = KOReaderSyncClient::authenticate();
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
if (result == KOReaderSyncClient::OK) { RenderLock lock(*this);
state = SUCCESS; if (result == KOReaderSyncClient::OK) {
statusMessage = "Successfully authenticated!"; state = SUCCESS;
} else { statusMessage = tr(STR_AUTH_SUCCESS);
state = FAILED; } else {
errorMessage = KOReaderSyncClient::errorString(result); state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result);
}
} }
xSemaphoreGive(renderingMutex); requestUpdate();
updateRequired = true;
} }
void KOReaderAuthActivity::onEnter() { void KOReaderAuthActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
xTaskCreate(&KOReaderAuthActivity::taskTrampoline, "KOAuthTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Turn on WiFi // Turn on WiFi
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
// Check if already connected // Check if already connected
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
state = AUTHENTICATING; state = AUTHENTICATING;
statusMessage = "Authenticating..."; statusMessage = tr(STR_AUTHENTICATING);
updateRequired = true; requestUpdate();
// Perform authentication in a separate task // Perform authentication in a separate task
xTaskCreate( xTaskCreate(
@@ -96,35 +86,11 @@ void KOReaderAuthActivity::onExit() {
delay(100); delay(100);
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
delay(100); delay(100);
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void KOReaderAuthActivity::displayTaskLoop() { void KOReaderAuthActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void KOReaderAuthActivity::render() {
if (subActivity) {
return;
}
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_AUTH), true, EpdFontFamily::BOLD);
if (state == AUTHENTICATING) { if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
@@ -133,20 +99,20 @@ void KOReaderAuthActivity::render() {
} }
if (state == SUCCESS) { if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use"); renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_SYNC_READY));
const auto labels = mappedInput.mapLabels("Done", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_DONE), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
@@ -20,15 +17,12 @@ class KOReaderAuthActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; } bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; }
private: private:
enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED }; enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED };
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
State state = WIFI_SELECTION; State state = WIFI_SELECTION;
std::string statusMessage; std::string statusMessage;
std::string errorMessage; std::string errorMessage;
@@ -37,8 +31,4 @@ class KOReaderAuthActivity final : public ActivityWithSubactivity {
void onWifiSelectionComplete(bool success); void onWifiSelectionComplete(bool success);
void performAuthentication(); void performAuthentication();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
}; };

View File

@@ -1,6 +1,7 @@
#include "KOReaderSettingsActivity.h" #include "KOReaderSettingsActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <cstring> #include <cstring>
@@ -13,40 +14,18 @@
namespace { namespace {
constexpr int MENU_ITEMS = 5; constexpr int MENU_ITEMS = 5;
const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"}; const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL,
StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE};
} // namespace } // namespace
void KOReaderSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<KOReaderSettingsActivity*>(param);
self->displayTaskLoop();
}
void KOReaderSettingsActivity::onEnter() { void KOReaderSettingsActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0; selectedIndex = 0;
updateRequired = true; requestUpdate();
xTaskCreate(&KOReaderSettingsActivity::taskTrampoline, "KOReaderSettingsTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
} }
void KOReaderSettingsActivity::onExit() { void KOReaderSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); }
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void KOReaderSettingsActivity::loop() { void KOReaderSettingsActivity::loop() {
if (subActivity) { if (subActivity) {
@@ -67,51 +46,49 @@ void KOReaderSettingsActivity::loop() {
// Handle navigation // Handle navigation
buttonNavigator.onNext([this] { buttonNavigator.onNext([this] {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS; selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
updateRequired = true; requestUpdate();
}); });
buttonNavigator.onPrevious([this] { buttonNavigator.onPrevious([this] {
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
updateRequired = true; requestUpdate();
}); });
} }
void KOReaderSettingsActivity::handleSelection() { void KOReaderSettingsActivity::handleSelection() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (selectedIndex == 0) { if (selectedIndex == 0) {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "KOReader Username", KOREADER_STORE.getUsername(), 10, renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 10,
64, // maxLength 64, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword()); KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword());
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} else if (selectedIndex == 1) { } else if (selectedIndex == 1) {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "KOReader Password", KOREADER_STORE.getPassword(), 10, renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 10,
64, // maxLength 64, // maxLength
false, // show characters false, // show characters
[this](const std::string& password) { [this](const std::string& password) {
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password); KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} else if (selectedIndex == 2) { } else if (selectedIndex == 2) {
// Sync Server URL - prefill with https:// if empty to save typing // Sync Server URL - prefill with https:// if empty to save typing
@@ -119,7 +96,7 @@ void KOReaderSettingsActivity::handleSelection() {
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Sync Server URL", prefillUrl, 10, renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 10,
128, // maxLength - URLs can be long 128, // maxLength - URLs can be long
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@@ -128,11 +105,11 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.setServerUrl(urlToSave); KOREADER_STORE.setServerUrl(urlToSave);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
}, },
[this]() { [this]() {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} else if (selectedIndex == 3) { } else if (selectedIndex == 3) {
// Document Matching - toggle between Filename and Binary // Document Matching - toggle between Filename and Binary
@@ -141,43 +118,28 @@ void KOReaderSettingsActivity::handleSelection() {
(current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME; (current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME;
KOREADER_STORE.setMatchMethod(newMethod); KOREADER_STORE.setMatchMethod(newMethod);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
updateRequired = true; requestUpdate();
} else if (selectedIndex == 4) { } else if (selectedIndex == 4) {
// Authenticate // Authenticate
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly // Can't authenticate without credentials - just show message briefly
xSemaphoreGive(renderingMutex);
return; return;
} }
exitActivity(); exitActivity();
enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] { enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; requestUpdate();
})); }));
} }
xSemaphoreGive(renderingMutex);
} }
void KOReaderSettingsActivity::displayTaskLoop() { void KOReaderSettingsActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void KOReaderSettingsActivity::render() {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30);
@@ -187,28 +149,31 @@ void KOReaderSettingsActivity::render() {
const int settingY = 60 + i * 30; const int settingY = 60 + i * 30;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected);
// Draw status for each item // Draw status for each item
const char* status = ""; std::string status = "";
if (i == 0) { if (i == 0) {
status = KOREADER_STORE.getUsername().empty() ? "[Not Set]" : "[Set]"; status = std::string("[") + (KOREADER_STORE.getUsername().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]";
} else if (i == 1) { } else if (i == 1) {
status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]"; status = std::string("[") + (KOREADER_STORE.getPassword().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]";
} else if (i == 2) { } else if (i == 2) {
status = KOREADER_STORE.getServerUrl().empty() ? "[Default]" : "[Custom]"; status =
std::string("[") + (KOREADER_STORE.getServerUrl().empty() ? tr(STR_DEFAULT_VALUE) : tr(STR_CUSTOM)) + "]";
} else if (i == 3) { } else if (i == 3) {
status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]"; status = std::string("[") +
(KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? tr(STR_FILENAME) : tr(STR_BINARY)) +
"]";
} else if (i == 4) { } else if (i == 4) {
status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"; status = KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} }
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected);
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional> #include <functional>
@@ -21,18 +18,13 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
private: private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
bool updateRequired = false;
int selectedIndex = 0; int selectedIndex = 0;
const std::function<void()> onBack; const std::function<void()> onBack;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
void handleSelection(); void handleSelection();
}; };

View File

@@ -0,0 +1,94 @@
#include "LanguageSelectActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "fontIds.h"
void LanguageSelectActivity::onEnter() {
Activity::onEnter();
totalItems = getLanguageCount();
// Set current selection based on current language
selectedIndex = static_cast<int>(I18N.getLanguage());
requestUpdate();
}
void LanguageSelectActivity::onExit() { Activity::onExit(); }
void LanguageSelectActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex + totalItems - 1) % totalItems;
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % totalItems;
requestUpdate();
}
}
void LanguageSelectActivity::handleSelection() {
{
RenderLock lock(*this);
I18N.setLanguage(static_cast<Language>(selectedIndex));
}
// Return to previous page
onBack();
}
void LanguageSelectActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
constexpr int rowHeight = 30;
// Title
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_LANGUAGE), true, EpdFontFamily::BOLD);
// Current language marker
const int currentLang = static_cast<int>(I18N.getLanguage());
// Draw options
for (int i = 0; i < totalItems; i++) {
const int itemY = 60 + i * rowHeight;
const bool isSelected = (i == selectedIndex);
const bool isCurrent = (i == currentLang);
// Draw selection highlight
if (isSelected) {
renderer.fillRect(0, itemY - 2, pageWidth - 1, rowHeight);
}
// Draw language name - get it from i18n system
const char* langName = I18N.getLanguageName(static_cast<Language>(i));
renderer.drawText(UI_10_FONT_ID, 20, itemY, langName, !isSelected);
// Draw current selection marker
if (isCurrent) {
const char* marker = tr(STR_ON_MARKER);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, marker);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, itemY, marker, !isSelected);
}
}
// Button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <GfxRenderer.h>
#include <I18n.h>
#include <functional>
#include "../ActivityWithSubactivity.h"
#include "components/UITheme.h"
class MappedInputManager;
/**
* Activity for selecting UI language
*/
class LanguageSelectActivity final : public Activity {
public:
explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: Activity("LanguageSelect", renderer, mappedInput), onBack(onBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
void handleSelection();
std::function<void()> onBack;
int selectedIndex = 0;
int totalItems = 0;
};

View File

@@ -1,6 +1,7 @@
#include "OtaUpdateActivity.h" #include "OtaUpdateActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
@@ -9,11 +10,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/OtaUpdater.h" #include "network/OtaUpdater.h"
void OtaUpdateActivity::taskTrampoline(void* param) {
auto* self = static_cast<OtaUpdateActivity*>(param);
self->displayTaskLoop();
}
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
exitActivity(); exitActivity();
@@ -25,48 +21,43 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
LOG_DBG("OTA", "WiFi connected, checking for update"); LOG_DBG("OTA", "WiFi connected, checking for update");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = CHECKING_FOR_UPDATE; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = CHECKING_FOR_UPDATE;
updateRequired = true; }
vTaskDelay(10 / portTICK_PERIOD_MS); requestUpdateAndWait();
const auto res = updater.checkForUpdate(); const auto res = updater.checkForUpdate();
if (res != OtaUpdater::OK) { if (res != OtaUpdater::OK) {
LOG_DBG("OTA", "Update check failed: %d", res); LOG_DBG("OTA", "Update check failed: %d", res);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = FAILED; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = FAILED;
updateRequired = true; }
requestUpdate();
return; return;
} }
if (!updater.isUpdateNewer()) { if (!updater.isUpdateNewer()) {
LOG_DBG("OTA", "No new update available"); LOG_DBG("OTA", "No new update available");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = NO_UPDATE; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = NO_UPDATE;
updateRequired = true; }
requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = WAITING_CONFIRMATION; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = WAITING_CONFIRMATION;
updateRequired = true; }
requestUpdate();
} }
void OtaUpdateActivity::onEnter() { void OtaUpdateActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
xTaskCreate(&OtaUpdateActivity::taskTrampoline, "OtaUpdateActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Turn on WiFi immediately // Turn on WiFi immediately
LOG_DBG("OTA", "Turning on WiFi..."); LOG_DBG("OTA", "Turning on WiFi...");
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
@@ -85,30 +76,9 @@ void OtaUpdateActivity::onExit() {
delay(100); // Allow disconnect frame to be sent delay(100); // Allow disconnect frame to be sent
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
delay(100); // Allow WiFi hardware to fully power down delay(100); // Allow WiFi hardware to fully power down
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
} }
void OtaUpdateActivity::displayTaskLoop() { void OtaUpdateActivity::render(Activity::RenderLock&&) {
while (true) {
if (updateRequired || updater.getRender()) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void OtaUpdateActivity::render() {
if (subActivity) { if (subActivity) {
// Subactivity handles its own rendering // Subactivity handles its own rendering
return; return;
@@ -128,27 +98,27 @@ void OtaUpdateActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_UPDATE), true, EpdFontFamily::BOLD);
if (state == CHECKING_FOR_UPDATE) { if (state == CHECKING_FOR_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_CHECKING_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == WAITING_CONFIRMATION) { if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 200, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION); renderer.drawText(UI_10_FONT_ID, 20, 250, (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str()); renderer.drawText(UI_10_FONT_ID, 20, 270, (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == UPDATE_IN_PROGRESS) { if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 310, tr(STR_UPDATING), true, EpdFontFamily::BOLD);
renderer.drawRect(20, 350, pageWidth - 40, 50); renderer.drawRect(20, 350, pageWidth - 40, 50);
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42); renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
renderer.drawCenteredText(UI_10_FONT_ID, 420, renderer.drawCenteredText(UI_10_FONT_ID, 420,
@@ -161,20 +131,20 @@ void OtaUpdateActivity::render() {
} }
if (state == NO_UPDATE) { if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FINISHED) { if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on"); renderer.drawCenteredText(UI_10_FONT_ID, 350, tr(STR_POWER_ON_HINT));
renderer.displayBuffer(); renderer.displayBuffer();
state = SHUTTING_DOWN; state = SHUTTING_DOWN;
return; return;
@@ -182,6 +152,11 @@ void OtaUpdateActivity::render() {
} }
void OtaUpdateActivity::loop() { void OtaUpdateActivity::loop() {
// TODO @ngxson : refactor this logic later
if (updater.getRender()) {
requestUpdate();
}
if (subActivity) { if (subActivity) {
subActivity->loop(); subActivity->loop();
return; return;
@@ -190,26 +165,29 @@ void OtaUpdateActivity::loop() {
if (state == WAITING_CONFIRMATION) { if (state == WAITING_CONFIRMATION) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
LOG_DBG("OTA", "New update available, starting download..."); LOG_DBG("OTA", "New update available, starting download...");
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = UPDATE_IN_PROGRESS; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = UPDATE_IN_PROGRESS;
updateRequired = true; }
vTaskDelay(10 / portTICK_PERIOD_MS); requestUpdate();
requestUpdateAndWait();
const auto res = updater.installUpdate(); const auto res = updater.installUpdate();
if (res != OtaUpdater::OK) { if (res != OtaUpdater::OK) {
LOG_DBG("OTA", "Update failed: %d", res); LOG_DBG("OTA", "Update failed: %d", res);
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = FAILED; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = FAILED;
updateRequired = true; }
requestUpdate();
return; return;
} }
xSemaphoreTake(renderingMutex, portMAX_DELAY); {
state = FINISHED; RenderLock lock(*this);
xSemaphoreGive(renderingMutex); state = FINISHED;
updateRequired = true; }
requestUpdate();
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {

View File

@@ -1,7 +1,4 @@
#pragma once #pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "activities/ActivityWithSubactivity.h" #include "activities/ActivityWithSubactivity.h"
#include "network/OtaUpdater.h" #include "network/OtaUpdater.h"
@@ -21,18 +18,12 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
// Can't initialize this to 0 or the first render doesn't happen // Can't initialize this to 0 or the first render doesn't happen
static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111; static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
const std::function<void()> goBack; const std::function<void()> goBack;
State state = WIFI_SELECTION; State state = WIFI_SELECTION;
unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE; unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE;
OtaUpdater updater; OtaUpdater updater;
void onWifiSelectionComplete(bool success); void onWifiSelectionComplete(bool success);
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
public: public:
explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
@@ -41,5 +32,6 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override;
bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; }
}; };

Some files were not shown because too many files have changed in this diff Show More