Compare commits
24 Commits
mod/sync-u
...
d02e21a48f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d02e21a48f | ||
|
|
6ec5fc5603 | ||
|
|
9125a7ce68 | ||
|
|
dc6562a51c | ||
|
|
530d43997b | ||
|
|
97c33141bd | ||
|
|
2a32d8a182 | ||
|
|
d6f38d4441 | ||
|
|
513d111634 | ||
|
|
ad9137cfdf | ||
|
|
5c80cface7 | ||
|
|
86d3774a8f | ||
|
|
7ba5978848 | ||
|
|
3d47c081f2 | ||
|
|
6702060960 | ||
|
|
0bc6747483 | ||
|
|
00666377de | ||
|
|
22b77edddf | ||
|
|
2e673c753d | ||
|
|
1a30826981 | ||
|
|
50e6ef9bd8 | ||
|
|
a616f42cb4 | ||
|
|
0508bfc1f7 | ||
|
|
6c3a615fac |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
6
SCOPE.md
6
SCOPE.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
```
|
||||||
|
|||||||
237
docs/i18n.md
Normal file
237
docs/i18n.md
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# Internationalization (I18N)
|
||||||
|
|
||||||
|
This guide explains the multi-language support system in CrossPoint Reader.
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
- English
|
||||||
|
- French
|
||||||
|
- German
|
||||||
|
- Portuguese
|
||||||
|
- Spanish
|
||||||
|
- Swedish
|
||||||
|
- Czech
|
||||||
|
- Russian
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## 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 |
35
docs/translators.md
Normal file
35
docs/translators.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 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)
|
||||||
|
- [CaptainFrito](https://github.com/CaptainFrito)
|
||||||
|
|
||||||
|
## German
|
||||||
|
- [DavidOrtmann](https://github.com/DavidOrtmann)
|
||||||
|
|
||||||
|
## Czech
|
||||||
|
- [brbla](https://github.com/brbla)
|
||||||
|
|
||||||
|
## Portuguese (Brazil)
|
||||||
|
- [yagofarias](https://github.com/yagofarias)
|
||||||
|
|
||||||
|
## Italian
|
||||||
|
- [fragolinux](https://github.com/fragolinux)
|
||||||
|
|
||||||
|
## Russian
|
||||||
|
- [madebyKir](https://github.com/madebyKir)
|
||||||
|
- [mrtnvgr](https://github.com/mrtnvgr)
|
||||||
|
|
||||||
|
## Spanish
|
||||||
|
- [yeyeto2788](https://github.com/yeyeto2788)
|
||||||
|
- [Skrzakk](https://github.com/Skrzakk)
|
||||||
|
|
||||||
|
## Swedish
|
||||||
|
- [dawiik](https://github.com/dawiik)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
#include <PngToBmpConverter.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
@@ -76,6 +77,54 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
bookMetadata.author = opfParser.author;
|
bookMetadata.author = opfParser.author;
|
||||||
bookMetadata.language = opfParser.language;
|
bookMetadata.language = opfParser.language;
|
||||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
|
|
||||||
|
// Guide-based cover fallback: if no cover found via metadata/properties,
|
||||||
|
// try extracting the image reference from the guide's cover page XHTML
|
||||||
|
if (bookMetadata.coverItemHref.empty() && !opfParser.guideCoverPageHref.empty()) {
|
||||||
|
LOG_DBG("EBP", "No cover from metadata, trying guide cover page: %s", opfParser.guideCoverPageHref.c_str());
|
||||||
|
size_t coverPageSize;
|
||||||
|
uint8_t* coverPageData = readItemContentsToBytes(opfParser.guideCoverPageHref, &coverPageSize, true);
|
||||||
|
if (coverPageData) {
|
||||||
|
const std::string coverPageHtml(reinterpret_cast<char*>(coverPageData), coverPageSize);
|
||||||
|
free(coverPageData);
|
||||||
|
|
||||||
|
// Determine base path of the cover page for resolving relative image references
|
||||||
|
std::string coverPageBase;
|
||||||
|
const auto lastSlash = opfParser.guideCoverPageHref.rfind('/');
|
||||||
|
if (lastSlash != std::string::npos) {
|
||||||
|
coverPageBase = opfParser.guideCoverPageHref.substr(0, lastSlash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for image references: xlink:href="..." (SVG) and src="..." (img)
|
||||||
|
std::string imageRef;
|
||||||
|
for (const char* pattern : {"xlink:href=\"", "src=\""}) {
|
||||||
|
auto pos = coverPageHtml.find(pattern);
|
||||||
|
while (pos != std::string::npos) {
|
||||||
|
pos += strlen(pattern);
|
||||||
|
const auto endPos = coverPageHtml.find('"', pos);
|
||||||
|
if (endPos != std::string::npos) {
|
||||||
|
const auto ref = coverPageHtml.substr(pos, endPos - pos);
|
||||||
|
// Check if it's an image file
|
||||||
|
if (ref.length() >= 4) {
|
||||||
|
const auto ext = ref.substr(ref.length() - 4);
|
||||||
|
if (ext == ".png" || ext == ".jpg" || ext == "jpeg" || ext == ".gif") {
|
||||||
|
imageRef = ref;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = coverPageHtml.find(pattern, pos);
|
||||||
|
}
|
||||||
|
if (!imageRef.empty()) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageRef.empty()) {
|
||||||
|
bookMetadata.coverItemHref = FsHelpers::normalisePath(coverPageBase + imageRef);
|
||||||
|
LOG_DBG("EBP", "Found cover image from guide: %s", bookMetadata.coverItemHref.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||||
|
|
||||||
if (!opfParser.tocNcxPath.empty()) {
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
@@ -486,12 +535,44 @@ bool Epub::generateCoverBmp(bool cropped) const {
|
|||||||
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
}
|
}
|
||||||
LOG_DBG("EBP", "Generated BMP from cover image, success: %s", success ? "yes" : "no");
|
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
return success;
|
return success;
|
||||||
} else {
|
|
||||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||||
|
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||||
|
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||||
|
|
||||||
|
FsFile coverPng;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||||
|
coverPng.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile coverBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||||
|
coverPng.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
|
||||||
|
coverPng.close();
|
||||||
|
coverBmp.close();
|
||||||
|
Storage.remove(coverPngTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
|
||||||
|
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +630,40 @@ bool Epub::generateThumbBmp(int height) const {
|
|||||||
}
|
}
|
||||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||||
return success;
|
return success;
|
||||||
|
} else if (coverImageHref.substr(coverImageHref.length() - 4) == ".png") {
|
||||||
|
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||||
|
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||||
|
|
||||||
|
FsFile coverPng;
|
||||||
|
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||||
|
coverPng.close();
|
||||||
|
|
||||||
|
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile thumbBmp;
|
||||||
|
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||||
|
coverPng.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||||
|
int THUMB_TARGET_HEIGHT = height;
|
||||||
|
const bool success =
|
||||||
|
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
||||||
|
coverPng.close();
|
||||||
|
thumbBmp.close();
|
||||||
|
Storage.remove(coverPngTempPath.c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
|
||||||
|
Storage.remove(getThumbBmpPath(height).c_str());
|
||||||
|
}
|
||||||
|
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,29 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
|
|||||||
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||||
|
// Images don't use fontId or text rendering
|
||||||
|
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PageImage::serialize(FsFile& file) {
|
||||||
|
serialization::writePod(file, xPos);
|
||||||
|
serialization::writePod(file, yPos);
|
||||||
|
|
||||||
|
// serialize ImageBlock
|
||||||
|
return imageBlock->serialize(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
|
||||||
|
int16_t xPos;
|
||||||
|
int16_t yPos;
|
||||||
|
serialization::readPod(file, xPos);
|
||||||
|
serialization::readPod(file, yPos);
|
||||||
|
|
||||||
|
auto ib = ImageBlock::deserialize(file);
|
||||||
|
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||||
|
}
|
||||||
|
|
||||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
for (auto& element : elements) {
|
for (auto& element : elements) {
|
||||||
element->render(renderer, fontId, xOffset, yOffset);
|
element->render(renderer, fontId, xOffset, yOffset);
|
||||||
@@ -36,8 +59,9 @@ bool Page::serialize(FsFile& file) const {
|
|||||||
serialization::writePod(file, count);
|
serialization::writePod(file, count);
|
||||||
|
|
||||||
for (const auto& el : elements) {
|
for (const auto& el : elements) {
|
||||||
// Only PageLine exists currently
|
// Use getTag() method to determine type
|
||||||
serialization::writePod(file, static_cast<uint8_t>(TAG_PageLine));
|
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||||
|
|
||||||
if (!el->serialize(file)) {
|
if (!el->serialize(file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -59,6 +83,9 @@ std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
|||||||
if (tag == TAG_PageLine) {
|
if (tag == TAG_PageLine) {
|
||||||
auto pl = PageLine::deserialize(file);
|
auto pl = PageLine::deserialize(file);
|
||||||
page->elements.push_back(std::move(pl));
|
page->elements.push_back(std::move(pl));
|
||||||
|
} else if (tag == TAG_PageImage) {
|
||||||
|
auto pi = PageImage::deserialize(file);
|
||||||
|
page->elements.push_back(std::move(pi));
|
||||||
} else {
|
} else {
|
||||||
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "blocks/ImageBlock.h"
|
||||||
#include "blocks/TextBlock.h"
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
enum PageElementTag : uint8_t {
|
enum PageElementTag : uint8_t {
|
||||||
TAG_PageLine = 1,
|
TAG_PageLine = 1,
|
||||||
|
TAG_PageImage = 2, // New tag
|
||||||
};
|
};
|
||||||
|
|
||||||
// represents something that has been added to a page
|
// represents something that has been added to a page
|
||||||
@@ -19,6 +22,7 @@ class PageElement {
|
|||||||
virtual ~PageElement() = default;
|
virtual ~PageElement() = default;
|
||||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||||
virtual bool serialize(FsFile& file) = 0;
|
virtual bool serialize(FsFile& file) = 0;
|
||||||
|
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||||
};
|
};
|
||||||
|
|
||||||
// a line from a block element
|
// a line from a block element
|
||||||
@@ -30,9 +34,23 @@ class PageLine final : public PageElement {
|
|||||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
bool serialize(FsFile& file) override;
|
bool serialize(FsFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// New PageImage class
|
||||||
|
class PageImage final : public PageElement {
|
||||||
|
std::shared_ptr<ImageBlock> imageBlock;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||||
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||||
|
bool serialize(FsFile& file) override;
|
||||||
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
|
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||||
|
};
|
||||||
|
|
||||||
class Page {
|
class Page {
|
||||||
public:
|
public:
|
||||||
// the list of block index and line numbers on this page
|
// the list of block index and line numbers on this page
|
||||||
@@ -40,4 +58,10 @@ class Page {
|
|||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
bool serialize(FsFile& file) const;
|
bool serialize(FsFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||||
|
|
||||||
|
// Check if page contains any images (used to force full refresh)
|
||||||
|
bool hasImages() const {
|
||||||
|
return std::any_of(elements.begin(), elements.end(),
|
||||||
|
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||||
sizeof(uint32_t);
|
sizeof(uint32_t);
|
||||||
@@ -181,6 +181,11 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
viewportHeight, hyphenationEnabled, embeddedStyle);
|
viewportHeight, hyphenationEnabled, embeddedStyle);
|
||||||
std::vector<uint32_t> lut = {};
|
std::vector<uint32_t> lut = {};
|
||||||
|
|
||||||
|
// Derive the content base directory and image cache path prefix for the parser
|
||||||
|
size_t lastSlash = localPath.find_last_of('/');
|
||||||
|
std::string contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||||
|
std::string imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||||
|
|
||||||
CssParser* cssParser = nullptr;
|
CssParser* cssParser = nullptr;
|
||||||
if (embeddedStyle) {
|
if (embeddedStyle) {
|
||||||
cssParser = epub->getCssParser();
|
cssParser = epub->getCssParser();
|
||||||
@@ -190,11 +195,12 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChapterHtmlSlimParser visitor(
|
ChapterHtmlSlimParser visitor(
|
||||||
tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||||
viewportHeight, hyphenationEnabled,
|
viewportHeight, hyphenationEnabled,
|
||||||
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
|
||||||
embeddedStyle, popupFn, cssParser);
|
embeddedStyle, contentBase, imageBasePath, popupFn, cssParser);
|
||||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||||
success = visitor.parseAndBuildPages();
|
success = visitor.parseAndBuildPages();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
|||||||
class Block {
|
class Block {
|
||||||
public:
|
public:
|
||||||
virtual ~Block() = default;
|
virtual ~Block() = default;
|
||||||
virtual void layout(GfxRenderer& renderer) = 0;
|
|
||||||
virtual BlockType getType() = 0;
|
virtual BlockType getType() = 0;
|
||||||
virtual bool isEmpty() = 0;
|
virtual bool isEmpty() = 0;
|
||||||
virtual void finish() {}
|
virtual void finish() {}
|
||||||
|
|||||||
174
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
174
lib/Epub/Epub/blocks/ImageBlock.cpp
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#include "ImageBlock.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
#include "../converters/DitherUtils.h"
|
||||||
|
#include "../converters/ImageDecoderFactory.h"
|
||||||
|
|
||||||
|
// Cache file format:
|
||||||
|
// - uint16_t width
|
||||||
|
// - uint16_t height
|
||||||
|
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||||
|
|
||||||
|
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||||
|
: imagePath(imagePath), width(width), height(height) {}
|
||||||
|
|
||||||
|
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string getCachePath(const std::string& imagePath) {
|
||||||
|
// Replace extension with .pxc (pixel cache)
|
||||||
|
size_t dotPos = imagePath.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
return imagePath.substr(0, dotPos) + ".pxc";
|
||||||
|
}
|
||||||
|
return imagePath + ".pxc";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||||
|
int expectedHeight) {
|
||||||
|
FsFile cacheFile;
|
||||||
|
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t cachedWidth, cachedHeight;
|
||||||
|
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||||
|
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||||
|
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||||
|
if (widthDiff > 1 || heightDiff > 1) {
|
||||||
|
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||||
|
expectedHeight);
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||||
|
expectedWidth = cachedWidth;
|
||||||
|
expectedHeight = cachedHeight;
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||||
|
|
||||||
|
// Read and render row by row to minimize memory usage
|
||||||
|
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||||
|
uint8_t* rowBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||||
|
if (!rowBuffer) {
|
||||||
|
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < cachedHeight; row++) {
|
||||||
|
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
|
||||||
|
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||||
|
free(rowBuffer);
|
||||||
|
cacheFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int destY = y + row;
|
||||||
|
for (int col = 0; col < cachedWidth; col++) {
|
||||||
|
int byteIdx = col / 4;
|
||||||
|
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
|
||||||
|
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||||
|
|
||||||
|
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(rowBuffer);
|
||||||
|
cacheFile.close();
|
||||||
|
LOG_DBG("IMG", "Cache render complete");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||||
|
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||||
|
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Bounds check render position using logical screen dimensions
|
||||||
|
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||||
|
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||||
|
screenHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to render from cache first
|
||||||
|
std::string cachePath = getCachePath(imagePath);
|
||||||
|
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||||
|
return; // Successfully rendered from cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cache - need to decode the image
|
||||||
|
// Check if image file exists
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||||
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t fileSize = file.size();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (fileSize == 0) {
|
||||||
|
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
RenderConfig config;
|
||||||
|
config.x = x;
|
||||||
|
config.y = y;
|
||||||
|
config.maxWidth = width;
|
||||||
|
config.maxHeight = height;
|
||||||
|
config.useGrayscale = true;
|
||||||
|
config.useDithering = true;
|
||||||
|
config.performanceMode = false;
|
||||||
|
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||||
|
config.cachePath = cachePath; // Enable caching during decode
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||||
|
if (!decoder) {
|
||||||
|
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||||
|
|
||||||
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||||
|
if (!success) {
|
||||||
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Decode successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageBlock::serialize(FsFile& file) {
|
||||||
|
serialization::writeString(file, imagePath);
|
||||||
|
serialization::writePod(file, width);
|
||||||
|
serialization::writePod(file, height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
||||||
|
std::string path;
|
||||||
|
serialization::readString(file, path);
|
||||||
|
int16_t w, h;
|
||||||
|
serialization::readPod(file, w);
|
||||||
|
serialization::readPod(file, h);
|
||||||
|
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||||
|
}
|
||||||
31
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
31
lib/Epub/Epub/blocks/ImageBlock.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Block.h"
|
||||||
|
|
||||||
|
class ImageBlock final : public Block {
|
||||||
|
public:
|
||||||
|
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
||||||
|
~ImageBlock() override = default;
|
||||||
|
|
||||||
|
const std::string& getImagePath() const { return imagePath; }
|
||||||
|
int16_t getWidth() const { return width; }
|
||||||
|
int16_t getHeight() const { return height; }
|
||||||
|
|
||||||
|
bool imageExists() const;
|
||||||
|
|
||||||
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
|
bool isEmpty() override { return false; }
|
||||||
|
|
||||||
|
void render(GfxRenderer& renderer, const int x, const int y);
|
||||||
|
bool serialize(FsFile& file);
|
||||||
|
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string imagePath;
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
};
|
||||||
@@ -33,7 +33,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
||||||
static_cast<uint8_t>(w[2]) == 0x83) {
|
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||||
const char* visiblePtr = w.c_str() + 3;
|
const char* visiblePtr = w.c_str() + 3;
|
||||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, std::string("\xe2\x80\x83").c_str());
|
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83");
|
||||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||||
startX = wordX + prefixWidth;
|
startX = wordX + prefixWidth;
|
||||||
underlineWidth = visibleWidth;
|
underlineWidth = visibleWidth;
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class TextBlock final : public Block {
|
|||||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool isEmpty() override { return words.empty(); }
|
||||||
void layout(GfxRenderer& renderer) override {};
|
|
||||||
// given a renderer works out where to break the words into lines
|
// given a renderer works out where to break the words into lines
|
||||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||||
BlockType getType() override { return TEXT_BLOCK; }
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
|
|||||||
40
lib/Epub/Epub/converters/DitherUtils.h
Normal file
40
lib/Epub/Epub/converters/DitherUtils.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// 4x4 Bayer matrix for ordered dithering
|
||||||
|
inline const uint8_t bayer4x4[4][4] = {
|
||||||
|
{0, 8, 2, 10},
|
||||||
|
{12, 4, 14, 6},
|
||||||
|
{3, 11, 1, 9},
|
||||||
|
{15, 7, 13, 5},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply Bayer dithering and quantize to 4 levels (0-3)
|
||||||
|
// Stateless - works correctly with any pixel processing order
|
||||||
|
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||||
|
int bayer = bayer4x4[y & 3][x & 3];
|
||||||
|
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
|
||||||
|
|
||||||
|
int adjusted = gray + dither;
|
||||||
|
if (adjusted < 0) adjusted = 0;
|
||||||
|
if (adjusted > 255) adjusted = 255;
|
||||||
|
|
||||||
|
if (adjusted < 64) return 0;
|
||||||
|
if (adjusted < 128) return 1;
|
||||||
|
if (adjusted < 192) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a pixel respecting the current render mode for grayscale support
|
||||||
|
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
|
||||||
|
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
|
||||||
|
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
|
||||||
|
renderer.drawPixel(x, y, true);
|
||||||
|
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
|
||||||
|
renderer.drawPixel(x, y, false);
|
||||||
|
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
|
||||||
|
renderer.drawPixel(x, y, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
42
lib/Epub/Epub/converters/ImageDecoderFactory.cpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#include "ImageDecoderFactory.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "JpegToFramebufferConverter.h"
|
||||||
|
#include "PngToFramebufferConverter.h"
|
||||||
|
|
||||||
|
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||||
|
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||||
|
std::string ext = imagePath;
|
||||||
|
size_t dotPos = ext.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
ext = ext.substr(dotPos);
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ext = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||||
|
if (!jpegDecoder) {
|
||||||
|
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||||
|
}
|
||||||
|
return jpegDecoder.get();
|
||||||
|
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||||
|
if (!pngDecoder) {
|
||||||
|
pngDecoder.reset(new PngToFramebufferConverter());
|
||||||
|
}
|
||||||
|
return pngDecoder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||||
20
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
20
lib/Epub/Epub/converters/ImageDecoderFactory.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class JpegToFramebufferConverter;
|
||||||
|
class PngToFramebufferConverter;
|
||||||
|
|
||||||
|
class ImageDecoderFactory {
|
||||||
|
public:
|
||||||
|
// Returns non-owning pointer - factory owns the decoder lifetime
|
||||||
|
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
|
||||||
|
static bool isFormatSupported(const std::string& imagePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
|
||||||
|
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
|
||||||
|
};
|
||||||
17
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
17
lib/Epub/Epub/converters/ImageToFramebufferDecoder.cpp
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||||
|
if (width * height > MAX_SOURCE_PIXELS) {
|
||||||
|
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
||||||
|
format.c_str(), MAX_SOURCE_PIXELS);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||||
|
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||||
|
imagePath.c_str());
|
||||||
|
}
|
||||||
40
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
40
lib/Epub/Epub/converters/ImageToFramebufferDecoder.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
struct ImageDimensions {
|
||||||
|
int16_t width;
|
||||||
|
int16_t height;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderConfig {
|
||||||
|
int x, y;
|
||||||
|
int maxWidth, maxHeight;
|
||||||
|
bool useGrayscale = true;
|
||||||
|
bool useDithering = true;
|
||||||
|
bool performanceMode = false;
|
||||||
|
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
|
||||||
|
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
virtual ~ImageToFramebufferDecoder() = default;
|
||||||
|
|
||||||
|
virtual bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) = 0;
|
||||||
|
|
||||||
|
virtual bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const = 0;
|
||||||
|
|
||||||
|
virtual const char* getFormatName() const = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Size validation helpers
|
||||||
|
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
||||||
|
|
||||||
|
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||||
|
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||||
|
};
|
||||||
297
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
297
lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#include "JpegToFramebufferConverter.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <SdFat.h>
|
||||||
|
#include <picojpeg.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "DitherUtils.h"
|
||||||
|
#include "PixelCache.h"
|
||||||
|
|
||||||
|
struct JpegContext {
|
||||||
|
FsFile& file;
|
||||||
|
uint8_t buffer[512];
|
||||||
|
size_t bufferPos;
|
||||||
|
size_t bufferFilled;
|
||||||
|
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||||
|
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JpegContext context(file);
|
||||||
|
pjpeg_image_info_t imageInfo;
|
||||||
|
|
||||||
|
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (status != 0) {
|
||||||
|
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = imageInfo.m_width;
|
||||||
|
out.height = imageInfo.m_height;
|
||||||
|
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||||
|
const RenderConfig& config) {
|
||||||
|
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
FsFile file;
|
||||||
|
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
||||||
|
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JpegContext context(file);
|
||||||
|
pjpeg_image_info_t imageInfo;
|
||||||
|
|
||||||
|
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
||||||
|
if (status != 0) {
|
||||||
|
LOG_ERR("JPG", "picojpeg init failed: %d", status);
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output dimensions
|
||||||
|
int destWidth, destHeight;
|
||||||
|
float scale;
|
||||||
|
|
||||||
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
|
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||||
|
destWidth = config.maxWidth;
|
||||||
|
destHeight = config.maxHeight;
|
||||||
|
scale = (float)destWidth / imageInfo.m_width;
|
||||||
|
} else {
|
||||||
|
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||||
|
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth)
|
||||||
|
? (float)config.maxWidth / imageInfo.m_width
|
||||||
|
: 1.0f;
|
||||||
|
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
|
||||||
|
? (float)config.maxHeight / imageInfo.m_height
|
||||||
|
: 1.0f;
|
||||||
|
scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (scale > 1.0f) scale = 1.0f;
|
||||||
|
|
||||||
|
destWidth = (int)(imageInfo.m_width * scale);
|
||||||
|
destHeight = (int)(imageInfo.m_height * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
||||||
|
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
||||||
|
|
||||||
|
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
|
||||||
|
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
const int screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Allocate pixel cache if cachePath is provided
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching = !config.cachePath.empty();
|
||||||
|
if (caching) {
|
||||||
|
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||||
|
caching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mcuX = 0;
|
||||||
|
int mcuY = 0;
|
||||||
|
|
||||||
|
while (mcuY < imageInfo.m_MCUSPerCol) {
|
||||||
|
status = pjpeg_decode_mcu();
|
||||||
|
if (status == PJPG_NO_MORE_BLOCKS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (status != 0) {
|
||||||
|
LOG_ERR("JPG", "MCU decode failed: %d", status);
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source position in image coordinates
|
||||||
|
int srcStartX = mcuX * imageInfo.m_MCUWidth;
|
||||||
|
int srcStartY = mcuY * imageInfo.m_MCUHeight;
|
||||||
|
|
||||||
|
switch (imageInfo.m_scanType) {
|
||||||
|
case PJPG_GRAYSCALE:
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH1V1:
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH2V1:
|
||||||
|
for (int row = 0; row < 8; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 16; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
int blockIndex = (col < 8) ? 0 : 1;
|
||||||
|
int pixelIndex = row * 8 + (col % 8);
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH1V2:
|
||||||
|
for (int row = 0; row < 16; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 8; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
int blockIndex = (row < 8) ? 0 : 1;
|
||||||
|
int pixelIndex = (row % 8) * 8 + col;
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PJPG_YH2V2:
|
||||||
|
for (int row = 0; row < 16; row++) {
|
||||||
|
int srcY = srcStartY + row;
|
||||||
|
int destY = config.y + (int)(srcY * scale);
|
||||||
|
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
||||||
|
for (int col = 0; col < 16; col++) {
|
||||||
|
int srcX = srcStartX + col;
|
||||||
|
int destX = config.x + (int)(srcX * scale);
|
||||||
|
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
||||||
|
int blockX = (col < 8) ? 0 : 1;
|
||||||
|
int blockY = (row < 8) ? 0 : 1;
|
||||||
|
int blockIndex = blockY * 2 + blockX;
|
||||||
|
int pixelIndex = (row % 8) * 8 + (col % 8);
|
||||||
|
int blockOffset = blockIndex * 64;
|
||||||
|
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
|
||||||
|
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
|
||||||
|
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
|
||||||
|
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
||||||
|
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
||||||
|
if (caching) cache.setPixel(destX, destY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mcuX++;
|
||||||
|
if (mcuX >= imageInfo.m_MCUSPerRow) {
|
||||||
|
mcuX = 0;
|
||||||
|
mcuY++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "Decoding complete");
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
// Write cache file if caching was enabled
|
||||||
|
if (caching) {
|
||||||
|
cache.writeToFile(config.cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||||
|
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
||||||
|
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
|
||||||
|
|
||||||
|
if (context->bufferPos >= context->bufferFilled) {
|
||||||
|
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
|
||||||
|
if (readCount <= 0) {
|
||||||
|
*pBytes_actually_read = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
context->bufferFilled = readCount;
|
||||||
|
context->bufferPos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
|
||||||
|
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
|
||||||
|
|
||||||
|
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
|
||||||
|
context->bufferPos += bytesToCopy;
|
||||||
|
*pBytes_actually_read = bytesToCopy;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
|
std::string ext = extension;
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
return (ext == ".jpg" || ext == ".jpeg");
|
||||||
|
}
|
||||||
24
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
24
lib/Epub/Epub/converters/JpegToFramebufferConverter.h
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||||
|
|
||||||
|
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||||
|
|
||||||
|
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||||
|
return getDimensionsStatic(imagePath, dims);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool supportsFormat(const std::string& extension);
|
||||||
|
const char* getFormatName() const override { return "JPEG"; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||||
|
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||||
|
};
|
||||||
82
lib/Epub/Epub/converters/PixelCache.h
Normal file
82
lib/Epub/Epub/converters/PixelCache.h
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Cache buffer for storing 2-bit pixels (4 levels) during decode.
|
||||||
|
// Packs 4 pixels per byte, MSB first.
|
||||||
|
struct PixelCache {
|
||||||
|
uint8_t* buffer;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int bytesPerRow;
|
||||||
|
int originX; // config.x - to convert screen coords to cache coords
|
||||||
|
int originY; // config.y
|
||||||
|
|
||||||
|
PixelCache() : buffer(nullptr), width(0), height(0), bytesPerRow(0), originX(0), originY(0) {}
|
||||||
|
PixelCache(const PixelCache&) = delete;
|
||||||
|
PixelCache& operator=(const PixelCache&) = delete;
|
||||||
|
|
||||||
|
static constexpr size_t MAX_CACHE_BYTES = 256 * 1024; // 256KB limit for embedded targets
|
||||||
|
|
||||||
|
bool allocate(int w, int h, int ox, int oy) {
|
||||||
|
width = w;
|
||||||
|
height = h;
|
||||||
|
originX = ox;
|
||||||
|
originY = oy;
|
||||||
|
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||||
|
size_t bufferSize = (size_t)bytesPerRow * h;
|
||||||
|
if (bufferSize > MAX_CACHE_BYTES) {
|
||||||
|
LOG_ERR("IMG", "Cache buffer too large: %d bytes for %dx%d (limit %d)", bufferSize, w, h, MAX_CACHE_BYTES);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
buffer = (uint8_t*)malloc(bufferSize);
|
||||||
|
if (buffer) {
|
||||||
|
memset(buffer, 0, bufferSize);
|
||||||
|
LOG_DBG("IMG", "Allocated cache buffer: %d bytes for %dx%d", bufferSize, w, h);
|
||||||
|
}
|
||||||
|
return buffer != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPixel(int screenX, int screenY, uint8_t value) {
|
||||||
|
if (!buffer) return;
|
||||||
|
int localX = screenX - originX;
|
||||||
|
int localY = screenY - originY;
|
||||||
|
if (localX < 0 || localX >= width || localY < 0 || localY >= height) return;
|
||||||
|
|
||||||
|
int byteIdx = localY * bytesPerRow + localX / 4;
|
||||||
|
int bitShift = 6 - (localX % 4) * 2; // MSB first: pixel 0 at bits 6-7
|
||||||
|
buffer[byteIdx] = (buffer[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writeToFile(const std::string& cachePath) {
|
||||||
|
if (!buffer) return false;
|
||||||
|
|
||||||
|
FsFile cacheFile;
|
||||||
|
if (!Storage.openFileForWrite("IMG", cachePath, cacheFile)) {
|
||||||
|
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t w = width;
|
||||||
|
uint16_t h = height;
|
||||||
|
cacheFile.write(&w, 2);
|
||||||
|
cacheFile.write(&h, 2);
|
||||||
|
cacheFile.write(buffer, bytesPerRow * height);
|
||||||
|
cacheFile.close();
|
||||||
|
|
||||||
|
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePath.c_str(), width, height, 4 + bytesPerRow * height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~PixelCache() {
|
||||||
|
if (buffer) {
|
||||||
|
free(buffer);
|
||||||
|
buffer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
362
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
362
lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
#include "PngToFramebufferConverter.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <PNGdec.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <SdFat.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
#include "DitherUtils.h"
|
||||||
|
#include "PixelCache.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Context struct passed through PNGdec callbacks to avoid global mutable state.
|
||||||
|
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||||
|
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
|
||||||
|
struct PngContext {
|
||||||
|
GfxRenderer* renderer;
|
||||||
|
const RenderConfig* config;
|
||||||
|
int screenWidth;
|
||||||
|
int screenHeight;
|
||||||
|
|
||||||
|
// Scaling state
|
||||||
|
float scale;
|
||||||
|
int srcWidth;
|
||||||
|
int srcHeight;
|
||||||
|
int dstWidth;
|
||||||
|
int dstHeight;
|
||||||
|
int lastDstY; // Track last rendered destination Y to avoid duplicates
|
||||||
|
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching;
|
||||||
|
|
||||||
|
uint8_t* grayLineBuffer;
|
||||||
|
|
||||||
|
PngContext()
|
||||||
|
: renderer(nullptr),
|
||||||
|
config(nullptr),
|
||||||
|
screenWidth(0),
|
||||||
|
screenHeight(0),
|
||||||
|
scale(1.0f),
|
||||||
|
srcWidth(0),
|
||||||
|
srcHeight(0),
|
||||||
|
dstWidth(0),
|
||||||
|
dstHeight(0),
|
||||||
|
lastDstY(-1),
|
||||||
|
caching(false),
|
||||||
|
grayLineBuffer(nullptr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// File I/O callbacks use pFile->fHandle to access the FsFile*,
|
||||||
|
// avoiding the need for global file state.
|
||||||
|
void* pngOpenWithHandle(const char* filename, int32_t* size) {
|
||||||
|
FsFile* f = new FsFile();
|
||||||
|
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
|
||||||
|
delete f;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
*size = f->size();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pngCloseWithHandle(void* handle) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(handle);
|
||||||
|
if (f) {
|
||||||
|
f->close();
|
||||||
|
delete f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||||
|
if (!f) return 0;
|
||||||
|
return f->read(pBuf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||||
|
if (!f) return -1;
|
||||||
|
return f->seek(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
|
||||||
|
// We heap-allocate it on demand rather than using a static instance, so this memory
|
||||||
|
// is only consumed while actually decoding/querying PNG images. This is critical on
|
||||||
|
// the ESP32-C3 where total RAM is ~320 KB.
|
||||||
|
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||||
|
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||||
|
|
||||||
|
// Convert entire source line to grayscale with alpha blending to white background.
|
||||||
|
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||||
|
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||||
|
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||||
|
switch (pixelType) {
|
||||||
|
case PNG_PIXEL_GRAYSCALE:
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_TRUECOLOR:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t* p = &pPixels[x * 3];
|
||||||
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_INDEXED:
|
||||||
|
if (palette) {
|
||||||
|
if (hasAlpha) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t idx = pPixels[x];
|
||||||
|
uint8_t* p = &palette[idx * 3];
|
||||||
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
uint8_t alpha = palette[768 + idx];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t* p = &palette[pPixels[x] * 3];
|
||||||
|
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memcpy(grayLine, pPixels, width);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_GRAY_ALPHA:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t gray = pPixels[x * 2];
|
||||||
|
uint8_t alpha = pPixels[x * 2 + 1];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
uint8_t* p = &pPixels[x * 4];
|
||||||
|
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||||
|
uint8_t alpha = p[3];
|
||||||
|
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
memset(grayLine, 128, width);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||||
|
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||||
|
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||||
|
|
||||||
|
int srcY = pDraw->y;
|
||||||
|
int srcWidth = ctx->srcWidth;
|
||||||
|
|
||||||
|
// Calculate destination Y with scaling
|
||||||
|
int dstY = (int)(srcY * ctx->scale);
|
||||||
|
|
||||||
|
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||||
|
if (dstY == ctx->lastDstY) return 1;
|
||||||
|
ctx->lastDstY = dstY;
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (dstY >= ctx->dstHeight) return 1;
|
||||||
|
|
||||||
|
int outY = ctx->config->y + dstY;
|
||||||
|
if (outY >= ctx->screenHeight) return 1;
|
||||||
|
|
||||||
|
// Convert entire source line to grayscale (improves cache locality)
|
||||||
|
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||||
|
pDraw->iHasAlpha);
|
||||||
|
|
||||||
|
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||||
|
int dstWidth = ctx->dstWidth;
|
||||||
|
int outXBase = ctx->config->x;
|
||||||
|
int screenWidth = ctx->screenWidth;
|
||||||
|
bool useDithering = ctx->config->useDithering;
|
||||||
|
bool caching = ctx->caching;
|
||||||
|
|
||||||
|
int srcX = 0;
|
||||||
|
int error = 0;
|
||||||
|
|
||||||
|
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||||
|
int outX = outXBase + dstX;
|
||||||
|
if (outX < screenWidth) {
|
||||||
|
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||||
|
|
||||||
|
uint8_t ditheredGray;
|
||||||
|
if (useDithering) {
|
||||||
|
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
ditheredGray = gray / 85;
|
||||||
|
if (ditheredGray > 3) ditheredGray = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||||
|
error += srcWidth;
|
||||||
|
while (error >= dstWidth) {
|
||||||
|
error -= dstWidth;
|
||||||
|
srcX++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||||
|
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PNG* png = new (std::nothrow) PNG();
|
||||||
|
if (!png) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = png->getWidth();
|
||||||
|
out.height = png->getHeight();
|
||||||
|
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||||
|
const RenderConfig& config) {
|
||||||
|
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
|
||||||
|
|
||||||
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
|
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||||
|
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
|
||||||
|
PNG* png = new (std::nothrow) PNG();
|
||||||
|
if (!png) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate PNG decoder");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PngContext ctx;
|
||||||
|
ctx.renderer = &renderer;
|
||||||
|
ctx.config = &config;
|
||||||
|
ctx.screenWidth = renderer.getScreenWidth();
|
||||||
|
ctx.screenHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||||
|
pngDrawCallback);
|
||||||
|
if (rc != PNG_SUCCESS) {
|
||||||
|
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate output dimensions
|
||||||
|
ctx.srcWidth = png->getWidth();
|
||||||
|
ctx.srcHeight = png->getHeight();
|
||||||
|
|
||||||
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
|
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||||
|
ctx.dstWidth = config.maxWidth;
|
||||||
|
ctx.dstHeight = config.maxHeight;
|
||||||
|
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||||
|
} else {
|
||||||
|
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||||
|
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||||
|
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||||
|
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||||
|
|
||||||
|
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||||
|
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||||
|
}
|
||||||
|
ctx.lastDstY = -1; // Reset row tracking
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||||
|
ctx.scale, png->getBpp());
|
||||||
|
|
||||||
|
if (png->getBpp() != 8) {
|
||||||
|
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||||
|
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||||
|
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||||
|
if (!ctx.grayLineBuffer) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate cache buffer using SCALED dimensions
|
||||||
|
ctx.caching = !config.cachePath.empty();
|
||||||
|
if (ctx.caching) {
|
||||||
|
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
|
||||||
|
ctx.caching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long decodeStart = millis();
|
||||||
|
rc = png->decode(&ctx, 0);
|
||||||
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
|
free(ctx.grayLineBuffer);
|
||||||
|
ctx.grayLineBuffer = nullptr;
|
||||||
|
|
||||||
|
if (rc != PNG_SUCCESS) {
|
||||||
|
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
png->close();
|
||||||
|
delete png;
|
||||||
|
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
|
||||||
|
|
||||||
|
// Write cache file if caching was enabled and buffer was allocated
|
||||||
|
if (ctx.caching) {
|
||||||
|
ctx.cache.writeToFile(config.cachePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
|
std::string ext = extension;
|
||||||
|
for (auto& c : ext) {
|
||||||
|
c = tolower(c);
|
||||||
|
}
|
||||||
|
return (ext == ".png");
|
||||||
|
}
|
||||||
17
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
17
lib/Epub/Epub/converters/PngToFramebufferConverter.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
class PngToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||||
|
public:
|
||||||
|
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||||
|
|
||||||
|
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||||
|
|
||||||
|
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||||
|
return getDimensionsStatic(imagePath, dims);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool supportsFormat(const std::string& extension);
|
||||||
|
const char* getFormatName() const override { return "PNG"; }
|
||||||
|
};
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// 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);
|
||||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||||
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -1,11 +1,15 @@
|
|||||||
#include "ChapterHtmlSlimParser.h"
|
#include "ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include "../../Epub.h"
|
||||||
#include "../Page.h"
|
#include "../Page.h"
|
||||||
|
#include "../converters/ImageDecoderFactory.h"
|
||||||
|
#include "../converters/ImageToFramebufferDecoder.h"
|
||||||
#include "../htmlEntities.h"
|
#include "../htmlEntities.h"
|
||||||
|
|
||||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
@@ -156,30 +160,125 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||||
// TODO: Start processing image tags
|
std::string src;
|
||||||
std::string alt = "[Image]";
|
std::string alt;
|
||||||
if (atts != nullptr) {
|
if (atts != nullptr) {
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "alt") == 0) {
|
if (strcmp(atts[i], "src") == 0) {
|
||||||
if (strlen(atts[i + 1]) > 0) {
|
src = atts[i + 1];
|
||||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
} else if (strcmp(atts[i], "alt") == 0) {
|
||||||
}
|
alt = atts[i + 1];
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!src.empty()) {
|
||||||
|
LOG_DBG("EHP", "Found image: src=%s", src.c_str());
|
||||||
|
|
||||||
|
{
|
||||||
|
// Resolve the image path relative to the HTML file
|
||||||
|
std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src);
|
||||||
|
|
||||||
|
// Create a unique filename for the cached image
|
||||||
|
std::string ext;
|
||||||
|
size_t extPos = resolvedPath.rfind('.');
|
||||||
|
if (extPos != std::string::npos) {
|
||||||
|
ext = resolvedPath.substr(extPos);
|
||||||
|
}
|
||||||
|
std::string cachedImagePath = self->imageBasePath + std::to_string(self->imageCounter++) + ext;
|
||||||
|
|
||||||
|
// Extract image to cache file
|
||||||
|
FsFile cachedImageFile;
|
||||||
|
bool extractSuccess = false;
|
||||||
|
if (Storage.openFileForWrite("EHP", cachedImagePath, cachedImageFile)) {
|
||||||
|
extractSuccess = self->epub->readItemContentsToStream(resolvedPath, cachedImageFile, 4096);
|
||||||
|
cachedImageFile.flush();
|
||||||
|
cachedImageFile.close();
|
||||||
|
delay(50); // Give SD card time to sync
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractSuccess) {
|
||||||
|
// Get image dimensions
|
||||||
|
ImageDimensions dims = {0, 0};
|
||||||
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath);
|
||||||
|
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
|
||||||
|
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||||
|
|
||||||
|
// Scale to fit viewport while maintaining aspect ratio
|
||||||
|
int maxWidth = self->viewportWidth;
|
||||||
|
int maxHeight = self->viewportHeight;
|
||||||
|
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
||||||
|
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
|
||||||
|
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (scale > 1.0f) scale = 1.0f;
|
||||||
|
|
||||||
|
int displayWidth = (int)(dims.width * scale);
|
||||||
|
int displayHeight = (int)(dims.height * scale);
|
||||||
|
|
||||||
|
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
|
||||||
|
|
||||||
|
// Create page for image - only break if image won't fit remaining space
|
||||||
|
if (self->currentPage && !self->currentPage->elements.empty() &&
|
||||||
|
(self->currentPageNextY + displayHeight > self->viewportHeight)) {
|
||||||
|
self->completePageFn(std::move(self->currentPage));
|
||||||
|
self->currentPage.reset(new Page());
|
||||||
|
if (!self->currentPage) {
|
||||||
|
LOG_ERR("EHP", "Failed to create new page");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->currentPageNextY = 0;
|
||||||
|
} else if (!self->currentPage) {
|
||||||
|
self->currentPage.reset(new Page());
|
||||||
|
if (!self->currentPage) {
|
||||||
|
LOG_ERR("EHP", "Failed to create initial page");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->currentPageNextY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ImageBlock and add to page
|
||||||
|
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight);
|
||||||
|
if (!imageBlock) {
|
||||||
|
LOG_ERR("EHP", "Failed to create ImageBlock");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int xPos = (self->viewportWidth - displayWidth) / 2;
|
||||||
|
auto pageImage = std::make_shared<PageImage>(imageBlock, xPos, self->currentPageNextY);
|
||||||
|
if (!pageImage) {
|
||||||
|
LOG_ERR("EHP", "Failed to create PageImage");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self->currentPage->elements.push_back(pageImage);
|
||||||
|
self->currentPageNextY += displayHeight;
|
||||||
|
|
||||||
|
self->depth += 1;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
LOG_ERR("EHP", "Failed to get image dimensions");
|
||||||
|
Storage.remove(cachedImagePath.c_str());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERR("EHP", "Failed to extract image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to alt text if image processing fails
|
||||||
|
if (!alt.empty()) {
|
||||||
|
alt = "[Image: " + alt + "]";
|
||||||
|
self->startNewTextBlock(centeredBlockStyle);
|
||||||
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||||
|
self->depth += 1;
|
||||||
|
self->characterData(userData, alt.c_str(), alt.length());
|
||||||
|
// Skip any child content (skip until parent as we pre-advanced depth above)
|
||||||
|
self->skipUntilDepth = self->depth - 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No alt text, skip
|
||||||
|
self->skipUntilDepth = self->depth;
|
||||||
|
self->depth += 1;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("EHP", "Image alt: %s", alt.c_str());
|
|
||||||
|
|
||||||
self->startNewTextBlock(centeredBlockStyle);
|
|
||||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
|
||||||
// Advance depth before processing character data (like you would for an element with text)
|
|
||||||
self->depth += 1;
|
|
||||||
self->characterData(userData, alt.c_str(), alt.length());
|
|
||||||
|
|
||||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
|
||||||
self->skipUntilDepth = self->depth - 1;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||||
|
|||||||
@@ -7,16 +7,19 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "../ParsedText.h"
|
#include "../ParsedText.h"
|
||||||
|
#include "../blocks/ImageBlock.h"
|
||||||
#include "../blocks/TextBlock.h"
|
#include "../blocks/TextBlock.h"
|
||||||
#include "../css/CssParser.h"
|
#include "../css/CssParser.h"
|
||||||
#include "../css/CssStyle.h"
|
#include "../css/CssStyle.h"
|
||||||
|
|
||||||
class Page;
|
class Page;
|
||||||
class GfxRenderer;
|
class GfxRenderer;
|
||||||
|
class Epub;
|
||||||
|
|
||||||
#define MAX_WORD_SIZE 200
|
#define MAX_WORD_SIZE 200
|
||||||
|
|
||||||
class ChapterHtmlSlimParser {
|
class ChapterHtmlSlimParser {
|
||||||
|
std::shared_ptr<Epub> epub;
|
||||||
const std::string& filepath;
|
const std::string& filepath;
|
||||||
GfxRenderer& renderer;
|
GfxRenderer& renderer;
|
||||||
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
std::function<void(std::unique_ptr<Page>)> completePageFn;
|
||||||
@@ -43,6 +46,9 @@ class ChapterHtmlSlimParser {
|
|||||||
bool hyphenationEnabled;
|
bool hyphenationEnabled;
|
||||||
const CssParser* cssParser;
|
const CssParser* cssParser;
|
||||||
bool embeddedStyle;
|
bool embeddedStyle;
|
||||||
|
std::string contentBase;
|
||||||
|
std::string imageBasePath;
|
||||||
|
int imageCounter = 0;
|
||||||
|
|
||||||
// Style tracking (replaces depth-based approach)
|
// Style tracking (replaces depth-based approach)
|
||||||
struct StyleStackEntry {
|
struct StyleStackEntry {
|
||||||
@@ -68,15 +74,17 @@ class ChapterHtmlSlimParser {
|
|||||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
|
||||||
const float lineCompression, const bool extraParagraphSpacing,
|
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||||
const bool embeddedStyle, const std::function<void()>& popupFn = nullptr,
|
const bool embeddedStyle, const std::string& contentBase,
|
||||||
|
const std::string& imageBasePath, const std::function<void()>& popupFn = nullptr,
|
||||||
const CssParser* cssParser = nullptr)
|
const CssParser* cssParser = nullptr)
|
||||||
|
|
||||||
: filepath(filepath),
|
: epub(epub),
|
||||||
|
filepath(filepath),
|
||||||
renderer(renderer),
|
renderer(renderer),
|
||||||
fontId(fontId),
|
fontId(fontId),
|
||||||
lineCompression(lineCompression),
|
lineCompression(lineCompression),
|
||||||
@@ -88,7 +96,9 @@ class ChapterHtmlSlimParser {
|
|||||||
completePageFn(completePageFn),
|
completePageFn(completePageFn),
|
||||||
popupFn(popupFn),
|
popupFn(popupFn),
|
||||||
cssParser(cssParser),
|
cssParser(cssParser),
|
||||||
embeddedStyle(embeddedStyle) {}
|
embeddedStyle(embeddedStyle),
|
||||||
|
contentBase(contentBase),
|
||||||
|
imageBasePath(imageBasePath) {}
|
||||||
|
|
||||||
~ChapterHtmlSlimParser() = default;
|
~ChapterHtmlSlimParser() = default;
|
||||||
bool parseAndBuildPages();
|
bool parseAndBuildPages();
|
||||||
|
|||||||
@@ -296,23 +296,22 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
// parse the guide
|
// parse the guide
|
||||||
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
||||||
std::string type;
|
std::string type;
|
||||||
std::string textHref;
|
std::string guideHref;
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "type") == 0) {
|
if (strcmp(atts[i], "type") == 0) {
|
||||||
type = atts[i + 1];
|
type = atts[i + 1];
|
||||||
if (type == "text" || type == "start") {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
LOG_DBG("COF", "Skipping non-text reference in guide: %s", type.c_str());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (strcmp(atts[i], "href") == 0) {
|
} else if (strcmp(atts[i], "href") == 0) {
|
||||||
textHref = FsHelpers::normalisePath(self->baseContentPath + atts[i + 1]);
|
guideHref = FsHelpers::normalisePath(self->baseContentPath + atts[i + 1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) {
|
if (!guideHref.empty()) {
|
||||||
LOG_DBG("COF", "Found %s reference in guide: %s.", type.c_str(), textHref.c_str());
|
if (type == "text" || (type == "start" && !self->textReferenceHref.empty())) {
|
||||||
self->textReferenceHref = textHref;
|
LOG_DBG("COF", "Found %s reference in guide: %s", type.c_str(), guideHref.c_str());
|
||||||
|
self->textReferenceHref = guideHref;
|
||||||
|
} else if ((type == "cover" || type == "cover-page") && self->guideCoverPageHref.empty()) {
|
||||||
|
LOG_DBG("COF", "Found cover reference in guide: %s", guideHref.c_str());
|
||||||
|
self->guideCoverPageHref = guideHref;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -327,6 +326,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class ContentOpfParser final : public Print {
|
|||||||
std::string tocNcxPath;
|
std::string tocNcxPath;
|
||||||
std::string tocNavPath; // EPUB 3 nav document path
|
std::string tocNavPath; // EPUB 3 nav document path
|
||||||
std::string coverItemHref;
|
std::string coverItemHref;
|
||||||
|
std::string guideCoverPageHref; // Guide reference with type="cover" or "cover-page" (points to XHTML wrapper)
|
||||||
std::string textReferenceHref;
|
std::string textReferenceHref;
|
||||||
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
||||||
|
|
||||||
|
|||||||
@@ -74,13 +74,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int w = 0, h = 0;
|
int w = 0, h = 0;
|
||||||
fontMap.at(fontId).getTextDimensions(text, &w, &h, style);
|
fontIt->second.getTextDimensions(text, &w, &h, style);
|
||||||
return w;
|
return w;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +101,12 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto font = fontMap.at(fontId);
|
const auto& font = fontIt->second;
|
||||||
|
|
||||||
// no printable characters
|
// no printable characters
|
||||||
if (!font.hasPrintableChars(text, style)) {
|
if (!font.hasPrintableChars(text, style)) {
|
||||||
@@ -709,52 +711,58 @@ int GfxRenderer::getScreenHeight() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
return fontIt->second.getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t cp;
|
uint32_t cp;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
|
const auto& font = fontIt->second;
|
||||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||||
width += fontMap.at(fontId).getGlyph(cp, EpdFontFamily::REGULAR)->advanceX;
|
width += font.getGlyph(cp, EpdFontFamily::REGULAR)->advanceX;
|
||||||
}
|
}
|
||||||
return width;
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
|
return fontIt->second.getData(EpdFontFamily::REGULAR)->advanceY;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextHeight(const int fontId) const {
|
int GfxRenderer::getTextHeight(const int fontId) const {
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||||
@@ -764,11 +772,13 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fontMap.count(fontId) == 0) {
|
const auto fontIt = fontMap.find(fontId);
|
||||||
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto font = fontMap.at(fontId);
|
|
||||||
|
const auto& font = fontIt->second;
|
||||||
|
|
||||||
// No printable characters
|
// No printable characters
|
||||||
if (!font.hasPrintableChars(text, style)) {
|
if (!font.hasPrintableChars(text, style)) {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class GfxRenderer {
|
|||||||
|
|
||||||
// Grayscale functions
|
// Grayscale functions
|
||||||
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
||||||
|
RenderMode getRenderMode() const { return renderMode; }
|
||||||
void copyGrayscaleLsbBuffers() const;
|
void copyGrayscaleLsbBuffers() const;
|
||||||
void copyGrayscaleMsbBuffers() const;
|
void copyGrayscaleMsbBuffers() const;
|
||||||
void displayGrayBuffer() const;
|
void displayGrayBuffer() const;
|
||||||
|
|||||||
96
lib/I18n/I18n.cpp
Normal file
96
lib/I18n/I18n.cpp
Normal 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
42
lib/I18n/I18n.h
Normal 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()
|
||||||
381
lib/I18n/I18nKeys.h
Normal file
381
lib/I18n/I18nKeys.h
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
#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,
|
||||||
|
// 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
19
lib/I18n/I18nStrings.h
Normal 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
|
||||||
317
lib/I18n/translations/czech.yaml
Normal file
317
lib/I18n/translations/czech.yaml
Normal 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"
|
||||||
317
lib/I18n/translations/english.yaml
Normal file
317
lib/I18n/translations/english.yaml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
_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"
|
||||||
317
lib/I18n/translations/french.yaml
Normal file
317
lib/I18n/translations/french.yaml
Normal 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 d’accès"
|
||||||
|
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
|
||||||
|
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis d’autres appareils"
|
||||||
|
STR_STARTING_HOTSPOT: "Création du point d’accès en cours…"
|
||||||
|
STR_HOTSPOT_MODE: "Mode point d’accè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\\n’Ignorer l’espace 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 l’appareil’"
|
||||||
|
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 d’image 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 l’interface"
|
||||||
|
STR_FONT_SIZE: "Taille du texte de l’interface"
|
||||||
|
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 d’utilisateur"
|
||||||
|
STR_PASSWORD: "Mot de passe"
|
||||||
|
STR_SYNC_SERVER_URL: "URL du serveur"
|
||||||
|
STR_DOCUMENT_MATCHING: "Correspondance"
|
||||||
|
STR_AUTHENTICATE: "Se connecter"
|
||||||
|
STR_KOREADER_USERNAME: "Nom d’utilisateur"
|
||||||
|
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 d’alimentation 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 l’analyse 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 l’interface"
|
||||||
|
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 à l’accueil"
|
||||||
|
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 à l’URL"
|
||||||
|
STR_PERCENT_STEP_HINT: "Gauche/Droite : 1% Haut/Bas : 10%"
|
||||||
|
STR_SYNCING_TIME: "Synchronisation de l’heure…"
|
||||||
|
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"
|
||||||
317
lib/I18n/translations/german.yaml
Normal file
317
lib/I18n/translations/german.yaml
Normal 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"
|
||||||
317
lib/I18n/translations/portuguese.yaml
Normal file
317
lib/I18n/translations/portuguese.yaml
Normal 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 Wi‑Fi"
|
||||||
|
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 Wi‑Fi existente"
|
||||||
|
STR_HOTSPOT_DESC: "Crie uma rede Wi‑Fi outras pessoas entrarem"
|
||||||
|
STR_STARTING_HOTSPOT: "Iniciando hotspot..."
|
||||||
|
STR_HOTSPOT_MODE: "Modo hotspot"
|
||||||
|
STR_CONNECT_WIFI_HINT: "Conecte seu dispositivo a esta rede Wi‑Fi"
|
||||||
|
STR_OPEN_URL_HINT: "Abra este URL seu navegador"
|
||||||
|
STR_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 Wi‑Fi..."
|
||||||
|
STR_ENTER_WIFI_PASSWORD: "Digite a senha Wi‑Fi"
|
||||||
|
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 Wi‑Fi"
|
||||||
|
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 Wi‑Fi"
|
||||||
|
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 Wi‑Fi."
|
||||||
|
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"
|
||||||
317
lib/I18n/translations/russian.yaml
Normal file
317
lib/I18n/translations/russian.yaml
Normal 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 сервера"
|
||||||
317
lib/I18n/translations/spanish.yaml
Normal file
317
lib/I18n/translations/spanish.yaml
Normal 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"
|
||||||
317
lib/I18n/translations/swedish.yaml
Normal file
317
lib/I18n/translations/swedish.yaml
Normal 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"
|
||||||
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
858
lib/PngToBmpConverter/PngToBmpConverter.cpp
Normal file
@@ -0,0 +1,858 @@
|
|||||||
|
#include "PngToBmpConverter.h"
|
||||||
|
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <miniz.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "BitmapHelpers.h"
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// IMAGE PROCESSING OPTIONS - Same as JpegToBmpConverter for consistency
|
||||||
|
// ============================================================================
|
||||||
|
constexpr bool USE_8BIT_OUTPUT = false;
|
||||||
|
constexpr bool USE_ATKINSON = true;
|
||||||
|
constexpr bool USE_FLOYD_STEINBERG = false;
|
||||||
|
constexpr bool USE_PRESCALE = true;
|
||||||
|
constexpr int TARGET_MAX_WIDTH = 480;
|
||||||
|
constexpr int TARGET_MAX_HEIGHT = 800;
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// PNG constants
|
||||||
|
static constexpr uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10};
|
||||||
|
|
||||||
|
// PNG color types
|
||||||
|
enum PngColorType : uint8_t {
|
||||||
|
PNG_COLOR_GRAYSCALE = 0,
|
||||||
|
PNG_COLOR_RGB = 2,
|
||||||
|
PNG_COLOR_PALETTE = 3,
|
||||||
|
PNG_COLOR_GRAYSCALE_ALPHA = 4,
|
||||||
|
PNG_COLOR_RGBA = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// PNG filter types
|
||||||
|
enum PngFilter : uint8_t {
|
||||||
|
PNG_FILTER_NONE = 0,
|
||||||
|
PNG_FILTER_SUB = 1,
|
||||||
|
PNG_FILTER_UP = 2,
|
||||||
|
PNG_FILTER_AVERAGE = 3,
|
||||||
|
PNG_FILTER_PAETH = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read a big-endian 32-bit value from file
|
||||||
|
static bool readBE32(FsFile& file, uint32_t& value) {
|
||||||
|
uint8_t buf[4];
|
||||||
|
if (file.read(buf, 4) != 4) return false;
|
||||||
|
value = (static_cast<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
|
||||||
|
(static_cast<uint32_t>(buf[2]) << 8) | buf[3];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BMP writing helpers (same as JpegToBmpConverter)
|
||||||
|
inline void write16(Print& out, const uint16_t value) {
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void write32(Print& out, const uint32_t value) {
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
out.write((value >> 16) & 0xFF);
|
||||||
|
out.write((value >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void write32Signed(Print& out, const int32_t value) {
|
||||||
|
out.write(value & 0xFF);
|
||||||
|
out.write((value >> 8) & 0xFF);
|
||||||
|
out.write((value >> 16) & 0xFF);
|
||||||
|
out.write((value >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
||||||
|
const int bytesPerRow = (width + 3) / 4 * 4;
|
||||||
|
const int imageSize = bytesPerRow * height;
|
||||||
|
const uint32_t paletteSize = 256 * 4;
|
||||||
|
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
|
||||||
|
|
||||||
|
bmpOut.write('B');
|
||||||
|
bmpOut.write('M');
|
||||||
|
write32(bmpOut, fileSize);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, 14 + 40 + paletteSize);
|
||||||
|
|
||||||
|
write32(bmpOut, 40);
|
||||||
|
write32Signed(bmpOut, width);
|
||||||
|
write32Signed(bmpOut, -height);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write16(bmpOut, 8);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, imageSize);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 256);
|
||||||
|
write32(bmpOut, 256);
|
||||||
|
|
||||||
|
for (int i = 0; i < 256; i++) {
|
||||||
|
bmpOut.write(static_cast<uint8_t>(i));
|
||||||
|
bmpOut.write(static_cast<uint8_t>(i));
|
||||||
|
bmpOut.write(static_cast<uint8_t>(i));
|
||||||
|
bmpOut.write(static_cast<uint8_t>(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
|
||||||
|
const int bytesPerRow = (width + 31) / 32 * 4;
|
||||||
|
const int imageSize = bytesPerRow * height;
|
||||||
|
const uint32_t fileSize = 62 + imageSize;
|
||||||
|
|
||||||
|
bmpOut.write('B');
|
||||||
|
bmpOut.write('M');
|
||||||
|
write32(bmpOut, fileSize);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, 62);
|
||||||
|
|
||||||
|
write32(bmpOut, 40);
|
||||||
|
write32Signed(bmpOut, width);
|
||||||
|
write32Signed(bmpOut, -height);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, imageSize);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2);
|
||||||
|
write32(bmpOut, 2);
|
||||||
|
|
||||||
|
uint8_t palette[8] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
|
||||||
|
for (const uint8_t i : palette) {
|
||||||
|
bmpOut.write(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
|
||||||
|
const int bytesPerRow = (width * 2 + 31) / 32 * 4;
|
||||||
|
const int imageSize = bytesPerRow * height;
|
||||||
|
const uint32_t fileSize = 70 + imageSize;
|
||||||
|
|
||||||
|
bmpOut.write('B');
|
||||||
|
bmpOut.write('M');
|
||||||
|
write32(bmpOut, fileSize);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, 70);
|
||||||
|
|
||||||
|
write32(bmpOut, 40);
|
||||||
|
write32Signed(bmpOut, width);
|
||||||
|
write32Signed(bmpOut, -height);
|
||||||
|
write16(bmpOut, 1);
|
||||||
|
write16(bmpOut, 2);
|
||||||
|
write32(bmpOut, 0);
|
||||||
|
write32(bmpOut, imageSize);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 2835);
|
||||||
|
write32(bmpOut, 4);
|
||||||
|
write32(bmpOut, 4);
|
||||||
|
|
||||||
|
uint8_t palette[16] = {0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x00,
|
||||||
|
0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0xFF, 0xFF, 0x00};
|
||||||
|
for (const uint8_t i : palette) {
|
||||||
|
bmpOut.write(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paeth predictor function per PNG spec
|
||||||
|
static inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) {
|
||||||
|
int p = static_cast<int>(a) + b - c;
|
||||||
|
int pa = p > a ? p - a : a - p;
|
||||||
|
int pb = p > b ? p - b : b - p;
|
||||||
|
int pc = p > c ? p - c : c - p;
|
||||||
|
if (pa <= pb && pa <= pc) return a;
|
||||||
|
if (pb <= pc) return b;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context for streaming PNG decompression
|
||||||
|
struct PngDecodeContext {
|
||||||
|
FsFile& file;
|
||||||
|
|
||||||
|
// PNG image properties
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint8_t bitDepth;
|
||||||
|
uint8_t colorType;
|
||||||
|
uint8_t bytesPerPixel; // after expanding sub-byte depths
|
||||||
|
uint32_t rawRowBytes; // bytes per raw row (without filter byte)
|
||||||
|
|
||||||
|
// Scanline buffers
|
||||||
|
uint8_t* currentRow; // current defiltered scanline
|
||||||
|
uint8_t* previousRow; // previous defiltered scanline
|
||||||
|
|
||||||
|
// zlib decompression state
|
||||||
|
mz_stream zstream;
|
||||||
|
bool zstreamInitialized;
|
||||||
|
|
||||||
|
// Chunk reading state
|
||||||
|
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
|
||||||
|
bool idatFinished; // no more IDAT chunks
|
||||||
|
|
||||||
|
// File read buffer for feeding zlib
|
||||||
|
uint8_t readBuf[2048];
|
||||||
|
|
||||||
|
// Palette for indexed color (type 3)
|
||||||
|
uint8_t palette[256 * 3];
|
||||||
|
int paletteSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the next IDAT chunk header, skipping non-IDAT chunks
|
||||||
|
// Returns true if an IDAT chunk was found
|
||||||
|
static bool findNextIdatChunk(PngDecodeContext& ctx) {
|
||||||
|
while (true) {
|
||||||
|
uint32_t chunkLen;
|
||||||
|
if (!readBE32(ctx.file, chunkLen)) return false;
|
||||||
|
|
||||||
|
uint8_t chunkType[4];
|
||||||
|
if (ctx.file.read(chunkType, 4) != 4) return false;
|
||||||
|
|
||||||
|
if (memcmp(chunkType, "IDAT", 4) == 0) {
|
||||||
|
ctx.chunkBytesRemaining = chunkLen;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip this chunk's data + 4-byte CRC
|
||||||
|
// Use seek to skip efficiently
|
||||||
|
if (!ctx.file.seekCur(chunkLen + 4)) return false;
|
||||||
|
|
||||||
|
// If we hit IEND, there are no more chunks
|
||||||
|
if (memcmp(chunkType, "IEND", 4) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed compressed data to zlib from IDAT chunks
|
||||||
|
// Returns number of bytes made available in zstream, or -1 on error
|
||||||
|
static int feedZlibInput(PngDecodeContext& ctx) {
|
||||||
|
if (ctx.idatFinished) return 0;
|
||||||
|
|
||||||
|
// If current IDAT chunk is exhausted, skip its CRC and find next
|
||||||
|
while (ctx.chunkBytesRemaining == 0) {
|
||||||
|
// Skip 4-byte CRC of previous IDAT
|
||||||
|
if (!ctx.file.seekCur(4)) return -1;
|
||||||
|
|
||||||
|
if (!findNextIdatChunk(ctx)) {
|
||||||
|
ctx.idatFinished = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read from current IDAT chunk
|
||||||
|
size_t toRead = sizeof(ctx.readBuf);
|
||||||
|
if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining;
|
||||||
|
|
||||||
|
int bytesRead = ctx.file.read(ctx.readBuf, toRead);
|
||||||
|
if (bytesRead <= 0) return -1;
|
||||||
|
|
||||||
|
ctx.chunkBytesRemaining -= bytesRead;
|
||||||
|
ctx.zstream.next_in = ctx.readBuf;
|
||||||
|
ctx.zstream.avail_in = bytesRead;
|
||||||
|
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decompress exactly 'needed' bytes into 'dest'
|
||||||
|
static bool decompressBytes(PngDecodeContext& ctx, uint8_t* dest, size_t needed) {
|
||||||
|
ctx.zstream.next_out = dest;
|
||||||
|
ctx.zstream.avail_out = needed;
|
||||||
|
|
||||||
|
while (ctx.zstream.avail_out > 0) {
|
||||||
|
if (ctx.zstream.avail_in == 0) {
|
||||||
|
int fed = feedZlibInput(ctx);
|
||||||
|
if (fed < 0) return false;
|
||||||
|
if (fed == 0) {
|
||||||
|
// Try one more inflate to flush
|
||||||
|
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
|
||||||
|
if (ctx.zstream.avail_out == 0) break;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
|
||||||
|
if (ret != MZ_OK && ret != MZ_STREAM_END && ret != MZ_BUF_ERROR) {
|
||||||
|
LOG_ERR("PNG", "zlib inflate error: %d", ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ret == MZ_STREAM_END) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.zstream.avail_out == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one scanline: decompress filter byte + raw bytes, then unfilter
|
||||||
|
static bool decodeScanline(PngDecodeContext& ctx) {
|
||||||
|
// Decompress filter byte
|
||||||
|
uint8_t filterType;
|
||||||
|
if (!decompressBytes(ctx, &filterType, 1)) return false;
|
||||||
|
|
||||||
|
// Decompress raw row data into currentRow
|
||||||
|
if (!decompressBytes(ctx, ctx.currentRow, ctx.rawRowBytes)) return false;
|
||||||
|
|
||||||
|
// Apply reverse filter
|
||||||
|
const int bpp = ctx.bytesPerPixel;
|
||||||
|
|
||||||
|
switch (filterType) {
|
||||||
|
case PNG_FILTER_NONE:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_SUB:
|
||||||
|
for (uint32_t i = bpp; i < ctx.rawRowBytes; i++) {
|
||||||
|
ctx.currentRow[i] += ctx.currentRow[i - bpp];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_UP:
|
||||||
|
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||||
|
ctx.currentRow[i] += ctx.previousRow[i];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_AVERAGE:
|
||||||
|
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||||
|
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||||
|
uint8_t b = ctx.previousRow[i];
|
||||||
|
ctx.currentRow[i] += (a + b) / 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_FILTER_PAETH:
|
||||||
|
for (uint32_t i = 0; i < ctx.rawRowBytes; i++) {
|
||||||
|
uint8_t a = (i >= static_cast<uint32_t>(bpp)) ? ctx.currentRow[i - bpp] : 0;
|
||||||
|
uint8_t b = ctx.previousRow[i];
|
||||||
|
uint8_t c = (i >= static_cast<uint32_t>(bpp)) ? ctx.previousRow[i - bpp] : 0;
|
||||||
|
ctx.currentRow[i] += paethPredictor(a, b, c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOG_ERR("PNG", "Unknown filter type: %d", filterType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-convert an entire scanline to grayscale.
|
||||||
|
// Branches once on colorType/bitDepth, then runs a tight loop for the whole row.
|
||||||
|
static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) {
|
||||||
|
const uint8_t* src = ctx.currentRow;
|
||||||
|
const uint32_t w = ctx.width;
|
||||||
|
|
||||||
|
switch (ctx.colorType) {
|
||||||
|
case PNG_COLOR_GRAYSCALE:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
memcpy(grayRow, src, w);
|
||||||
|
} else if (ctx.bitDepth == 16) {
|
||||||
|
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
|
||||||
|
} else {
|
||||||
|
const int ppb = 8 / ctx.bitDepth;
|
||||||
|
const uint8_t mask = (1 << ctx.bitDepth) - 1;
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
|
||||||
|
grayRow[x] = (src[x / ppb] >> shift & mask) * 255 / mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_COLOR_RGB:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
// Fast path: most common EPUB cover format
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
const uint8_t* p = src + x * 3;
|
||||||
|
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
grayRow[x] = (src[x * 6] * 25 + src[x * 6 + 2] * 50 + src[x * 6 + 4] * 25) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_COLOR_PALETTE: {
|
||||||
|
const int ppb = 8 / ctx.bitDepth;
|
||||||
|
const uint8_t mask = (1 << ctx.bitDepth) - 1;
|
||||||
|
const uint8_t* pal = ctx.palette;
|
||||||
|
const int palSize = ctx.paletteSize;
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
int shift = (ppb - 1 - (x % ppb)) * ctx.bitDepth;
|
||||||
|
uint8_t idx = (src[x / ppb] >> shift) & mask;
|
||||||
|
if (idx >= palSize) idx = 0;
|
||||||
|
grayRow[x] = (pal[idx * 3] * 25 + pal[idx * 3 + 1] * 50 + pal[idx * 3 + 2] * 25) / 100;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PNG_COLOR_GRAYSCALE_ALPHA:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 2];
|
||||||
|
} else {
|
||||||
|
for (uint32_t x = 0; x < w; x++) grayRow[x] = src[x * 4];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PNG_COLOR_RGBA:
|
||||||
|
if (ctx.bitDepth == 8) {
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
const uint8_t* p = src + x * 4;
|
||||||
|
grayRow[x] = (p[0] * 25 + p[1] * 50 + p[2] * 25) / 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (uint32_t x = 0; x < w; x++) {
|
||||||
|
grayRow[x] = (src[x * 8] * 25 + src[x * 8 + 2] * 50 + src[x * 8 + 4] * 25) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
memset(grayRow, 128, w);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||||
|
bool oneBit, bool crop) {
|
||||||
|
LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||||
|
|
||||||
|
// Verify PNG signature
|
||||||
|
uint8_t sig[8];
|
||||||
|
if (pngFile.read(sig, 8) != 8 || memcmp(sig, PNG_SIGNATURE, 8) != 0) {
|
||||||
|
LOG_ERR("PNG", "Invalid PNG signature");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read IHDR chunk
|
||||||
|
uint32_t ihdrLen;
|
||||||
|
if (!readBE32(pngFile, ihdrLen)) return false;
|
||||||
|
|
||||||
|
uint8_t ihdrType[4];
|
||||||
|
if (pngFile.read(ihdrType, 4) != 4 || memcmp(ihdrType, "IHDR", 4) != 0) {
|
||||||
|
LOG_ERR("PNG", "Missing IHDR chunk");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t width, height;
|
||||||
|
if (!readBE32(pngFile, width) || !readBE32(pngFile, height)) return false;
|
||||||
|
|
||||||
|
uint8_t ihdrRest[5];
|
||||||
|
if (pngFile.read(ihdrRest, 5) != 5) return false;
|
||||||
|
|
||||||
|
uint8_t bitDepth = ihdrRest[0];
|
||||||
|
uint8_t colorType = ihdrRest[1];
|
||||||
|
uint8_t compression = ihdrRest[2];
|
||||||
|
uint8_t filter = ihdrRest[3];
|
||||||
|
uint8_t interlace = ihdrRest[4];
|
||||||
|
|
||||||
|
// Skip IHDR CRC
|
||||||
|
pngFile.seekCur(4);
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "Image: %ux%u, depth=%u, color=%u, interlace=%u", width, height, bitDepth, colorType, interlace);
|
||||||
|
|
||||||
|
if (compression != 0 || filter != 0) {
|
||||||
|
LOG_ERR("PNG", "Unsupported compression/filter method");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interlace != 0) {
|
||||||
|
LOG_ERR("PNG", "Interlaced PNGs not supported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety limits
|
||||||
|
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||||
|
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
||||||
|
|
||||||
|
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT || width == 0 || height == 0) {
|
||||||
|
LOG_ERR("PNG", "Image too large or zero (%ux%u)", width, height);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bytes per pixel and raw row bytes
|
||||||
|
uint8_t bytesPerPixel;
|
||||||
|
uint32_t rawRowBytes;
|
||||||
|
|
||||||
|
switch (colorType) {
|
||||||
|
case PNG_COLOR_GRAYSCALE:
|
||||||
|
if (bitDepth == 16) {
|
||||||
|
bytesPerPixel = 2;
|
||||||
|
rawRowBytes = width * 2;
|
||||||
|
} else if (bitDepth == 8) {
|
||||||
|
bytesPerPixel = 1;
|
||||||
|
rawRowBytes = width;
|
||||||
|
} else {
|
||||||
|
// Sub-byte: 1, 2, or 4 bits
|
||||||
|
bytesPerPixel = 1;
|
||||||
|
rawRowBytes = (width * bitDepth + 7) / 8;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_RGB:
|
||||||
|
bytesPerPixel = (bitDepth == 16) ? 6 : 3;
|
||||||
|
rawRowBytes = width * bytesPerPixel;
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_PALETTE:
|
||||||
|
bytesPerPixel = 1;
|
||||||
|
rawRowBytes = (width * bitDepth + 7) / 8;
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_GRAYSCALE_ALPHA:
|
||||||
|
bytesPerPixel = (bitDepth == 16) ? 4 : 2;
|
||||||
|
rawRowBytes = width * bytesPerPixel;
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_RGBA:
|
||||||
|
bytesPerPixel = (bitDepth == 16) ? 8 : 4;
|
||||||
|
rawRowBytes = width * bytesPerPixel;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG_ERR("PNG", "Unsupported color type: %d", colorType);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate raw row bytes won't cause memory issues
|
||||||
|
if (rawRowBytes > 16384) {
|
||||||
|
LOG_ERR("PNG", "Row too large: %u bytes", rawRowBytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize decode context
|
||||||
|
PngDecodeContext ctx = {.file = pngFile,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.bitDepth = bitDepth,
|
||||||
|
.colorType = colorType,
|
||||||
|
.bytesPerPixel = bytesPerPixel,
|
||||||
|
.rawRowBytes = rawRowBytes,
|
||||||
|
.currentRow = nullptr,
|
||||||
|
.previousRow = nullptr,
|
||||||
|
.zstream = {},
|
||||||
|
.zstreamInitialized = false,
|
||||||
|
.chunkBytesRemaining = 0,
|
||||||
|
.idatFinished = false,
|
||||||
|
.readBuf = {},
|
||||||
|
.palette = {},
|
||||||
|
.paletteSize = 0};
|
||||||
|
|
||||||
|
// Allocate scanline buffers
|
||||||
|
ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes));
|
||||||
|
ctx.previousRow = static_cast<uint8_t*>(calloc(rawRowBytes, 1));
|
||||||
|
if (!ctx.currentRow || !ctx.previousRow) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate scanline buffers (%u bytes each)", rawRowBytes);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan for PLTE chunk (palette) and first IDAT chunk
|
||||||
|
// We need to read chunks until we find IDAT, collecting PLTE along the way
|
||||||
|
bool foundIdat = false;
|
||||||
|
while (!foundIdat) {
|
||||||
|
uint32_t chunkLen;
|
||||||
|
if (!readBE32(pngFile, chunkLen)) break;
|
||||||
|
|
||||||
|
uint8_t chunkType[4];
|
||||||
|
if (pngFile.read(chunkType, 4) != 4) break;
|
||||||
|
|
||||||
|
if (memcmp(chunkType, "PLTE", 4) == 0) {
|
||||||
|
int entries = chunkLen / 3;
|
||||||
|
if (entries > 256) entries = 256;
|
||||||
|
ctx.paletteSize = entries;
|
||||||
|
size_t palBytes = entries * 3;
|
||||||
|
pngFile.read(ctx.palette, palBytes);
|
||||||
|
// Skip any remaining palette data
|
||||||
|
if (chunkLen > palBytes) pngFile.seekCur(chunkLen - palBytes);
|
||||||
|
pngFile.seekCur(4); // CRC
|
||||||
|
} else if (memcmp(chunkType, "IDAT", 4) == 0) {
|
||||||
|
ctx.chunkBytesRemaining = chunkLen;
|
||||||
|
foundIdat = true;
|
||||||
|
} else if (memcmp(chunkType, "IEND", 4) == 0) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Skip unknown chunk
|
||||||
|
pngFile.seekCur(chunkLen + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundIdat) {
|
||||||
|
LOG_ERR("PNG", "No IDAT chunk found");
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize zlib decompression
|
||||||
|
memset(&ctx.zstream, 0, sizeof(ctx.zstream));
|
||||||
|
if (mz_inflateInit(&ctx.zstream) != MZ_OK) {
|
||||||
|
LOG_ERR("PNG", "Failed to initialize zlib");
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.zstreamInitialized = true;
|
||||||
|
|
||||||
|
// Calculate output dimensions (same logic as JpegToBmpConverter)
|
||||||
|
int outWidth = width;
|
||||||
|
int outHeight = height;
|
||||||
|
uint32_t scaleX_fp = 65536;
|
||||||
|
uint32_t scaleY_fp = 65536;
|
||||||
|
bool needsScaling = false;
|
||||||
|
|
||||||
|
if (targetWidth > 0 && targetHeight > 0 &&
|
||||||
|
(static_cast<int>(width) > targetWidth || static_cast<int>(height) > targetHeight)) {
|
||||||
|
const float scaleToFitWidth = static_cast<float>(targetWidth) / width;
|
||||||
|
const float scaleToFitHeight = static_cast<float>(targetHeight) / height;
|
||||||
|
float scale = 1.0;
|
||||||
|
if (crop) {
|
||||||
|
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
|
} else {
|
||||||
|
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
outWidth = static_cast<int>(width * scale);
|
||||||
|
outHeight = static_cast<int>(height * scale);
|
||||||
|
if (outWidth < 1) outWidth = 1;
|
||||||
|
if (outHeight < 1) outHeight = 1;
|
||||||
|
|
||||||
|
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth;
|
||||||
|
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight;
|
||||||
|
needsScaling = true;
|
||||||
|
|
||||||
|
LOG_DBG("PNG", "Pre-scaling %ux%u -> %dx%d (fit to %dx%d)", width, height, outWidth, outHeight, targetWidth,
|
||||||
|
targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write BMP header
|
||||||
|
int bytesPerRow;
|
||||||
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
|
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
|
||||||
|
bytesPerRow = (outWidth + 3) / 4 * 4;
|
||||||
|
} else if (oneBit) {
|
||||||
|
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
|
||||||
|
bytesPerRow = (outWidth + 31) / 32 * 4;
|
||||||
|
} else {
|
||||||
|
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
|
||||||
|
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate BMP row buffer
|
||||||
|
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
|
if (!rowBuffer) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate row buffer");
|
||||||
|
mz_inflateEnd(&ctx.zstream);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ditherers (same as JpegToBmpConverter)
|
||||||
|
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
||||||
|
FloydSteinbergDitherer* fsDitherer = nullptr;
|
||||||
|
Atkinson1BitDitherer* atkinson1BitDitherer = nullptr;
|
||||||
|
|
||||||
|
if (oneBit) {
|
||||||
|
atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth);
|
||||||
|
} else if (!USE_8BIT_OUTPUT) {
|
||||||
|
if (USE_ATKINSON) {
|
||||||
|
atkinsonDitherer = new AtkinsonDitherer(outWidth);
|
||||||
|
} else if (USE_FLOYD_STEINBERG) {
|
||||||
|
fsDitherer = new FloydSteinbergDitherer(outWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scaling accumulators
|
||||||
|
uint32_t* rowAccum = nullptr;
|
||||||
|
uint16_t* rowCount = nullptr;
|
||||||
|
int currentOutY = 0;
|
||||||
|
uint32_t nextOutY_srcStart = 0;
|
||||||
|
|
||||||
|
if (needsScaling) {
|
||||||
|
rowAccum = new uint32_t[outWidth]();
|
||||||
|
rowCount = new uint16_t[outWidth]();
|
||||||
|
nextOutY_srcStart = scaleY_fp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate grayscale row buffer - batch-convert each scanline to avoid
|
||||||
|
// per-pixel getPixelGray() switch overhead in the hot loops
|
||||||
|
auto* grayRow = static_cast<uint8_t*>(malloc(width));
|
||||||
|
if (!grayRow) {
|
||||||
|
LOG_ERR("PNG", "Failed to allocate grayscale row buffer");
|
||||||
|
delete[] rowAccum;
|
||||||
|
delete[] rowCount;
|
||||||
|
delete atkinsonDitherer;
|
||||||
|
delete fsDitherer;
|
||||||
|
delete atkinson1BitDitherer;
|
||||||
|
free(rowBuffer);
|
||||||
|
mz_inflateEnd(&ctx.zstream);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
// Process each scanline
|
||||||
|
for (uint32_t y = 0; y < height; y++) {
|
||||||
|
// Decode one scanline
|
||||||
|
if (!decodeScanline(ctx)) {
|
||||||
|
LOG_ERR("PNG", "Failed to decode scanline %u", y);
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch-convert entire scanline to grayscale (one branch, tight loop)
|
||||||
|
convertScanlineToGray(ctx, grayRow);
|
||||||
|
|
||||||
|
if (!needsScaling) {
|
||||||
|
// Direct output (no scaling)
|
||||||
|
memset(rowBuffer, 0, bytesPerRow);
|
||||||
|
|
||||||
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
rowBuffer[x] = adjustPixel(grayRow[x]);
|
||||||
|
}
|
||||||
|
} else if (oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t bit =
|
||||||
|
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(grayRow[x], x) : quantize1bit(grayRow[x], x, y);
|
||||||
|
const int byteIndex = x / 8;
|
||||||
|
const int bitOffset = 7 - (x % 8);
|
||||||
|
rowBuffer[byteIndex] |= (bit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = adjustPixel(grayRow[x]);
|
||||||
|
uint8_t twoBit;
|
||||||
|
if (atkinsonDitherer) {
|
||||||
|
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||||
|
} else if (fsDitherer) {
|
||||||
|
twoBit = fsDitherer->processPixel(gray, x);
|
||||||
|
} else {
|
||||||
|
twoBit = quantize(gray, x, y);
|
||||||
|
}
|
||||||
|
const int byteIndex = (x * 2) / 8;
|
||||||
|
const int bitOffset = 6 - ((x * 2) % 8);
|
||||||
|
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinsonDitherer)
|
||||||
|
atkinsonDitherer->nextRow();
|
||||||
|
else if (fsDitherer)
|
||||||
|
fsDitherer->nextRow();
|
||||||
|
}
|
||||||
|
bmpOut.write(rowBuffer, bytesPerRow);
|
||||||
|
} else {
|
||||||
|
// Area-averaging scaling (same as JpegToBmpConverter)
|
||||||
|
for (int outX = 0; outX < outWidth; outX++) {
|
||||||
|
const int srcXStart = (static_cast<uint32_t>(outX) * scaleX_fp) >> 16;
|
||||||
|
const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16;
|
||||||
|
|
||||||
|
int sum = 0;
|
||||||
|
int count = 0;
|
||||||
|
for (int srcX = srcXStart; srcX < srcXEnd && srcX < static_cast<int>(width); srcX++) {
|
||||||
|
sum += grayRow[srcX];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0 && srcXStart < static_cast<int>(width)) {
|
||||||
|
sum = grayRow[srcXStart];
|
||||||
|
count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rowAccum[outX] += sum;
|
||||||
|
rowCount[outX] += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've crossed into the next output row
|
||||||
|
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
|
||||||
|
|
||||||
|
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
||||||
|
memset(rowBuffer, 0, bytesPerRow);
|
||||||
|
|
||||||
|
if (USE_8BIT_OUTPUT && !oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||||
|
rowBuffer[x] = adjustPixel(gray);
|
||||||
|
}
|
||||||
|
} else if (oneBit) {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||||
|
const uint8_t bit =
|
||||||
|
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, currentOutY);
|
||||||
|
const int byteIndex = x / 8;
|
||||||
|
const int bitOffset = 7 - (x % 8);
|
||||||
|
rowBuffer[byteIndex] |= (bit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < outWidth; x++) {
|
||||||
|
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
|
||||||
|
uint8_t twoBit;
|
||||||
|
if (atkinsonDitherer) {
|
||||||
|
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||||
|
} else if (fsDitherer) {
|
||||||
|
twoBit = fsDitherer->processPixel(gray, x);
|
||||||
|
} else {
|
||||||
|
twoBit = quantize(gray, x, currentOutY);
|
||||||
|
}
|
||||||
|
const int byteIndex = (x * 2) / 8;
|
||||||
|
const int bitOffset = 6 - ((x * 2) % 8);
|
||||||
|
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||||
|
}
|
||||||
|
if (atkinsonDitherer)
|
||||||
|
atkinsonDitherer->nextRow();
|
||||||
|
else if (fsDitherer)
|
||||||
|
fsDitherer->nextRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
bmpOut.write(rowBuffer, bytesPerRow);
|
||||||
|
currentOutY++;
|
||||||
|
|
||||||
|
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
|
||||||
|
memset(rowCount, 0, outWidth * sizeof(uint16_t));
|
||||||
|
|
||||||
|
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap current/previous row buffers
|
||||||
|
uint8_t* temp = ctx.previousRow;
|
||||||
|
ctx.previousRow = ctx.currentRow;
|
||||||
|
ctx.currentRow = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
free(grayRow);
|
||||||
|
delete[] rowAccum;
|
||||||
|
delete[] rowCount;
|
||||||
|
delete atkinsonDitherer;
|
||||||
|
delete fsDitherer;
|
||||||
|
delete atkinson1BitDitherer;
|
||||||
|
free(rowBuffer);
|
||||||
|
mz_inflateEnd(&ctx.zstream);
|
||||||
|
free(ctx.currentRow);
|
||||||
|
free(ctx.previousRow);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
LOG_DBG("PNG", "Successfully converted PNG to BMP");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop) {
|
||||||
|
return pngFileToBmpStreamInternal(pngFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||||
|
int targetMaxHeight) {
|
||||||
|
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth,
|
||||||
|
int targetMaxHeight) {
|
||||||
|
return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);
|
||||||
|
}
|
||||||
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
14
lib/PngToBmpConverter/PngToBmpConverter.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class FsFile;
|
||||||
|
class Print;
|
||||||
|
|
||||||
|
class PngToBmpConverter {
|
||||||
|
static bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit,
|
||||||
|
bool crop = true);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static bool pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop = true);
|
||||||
|
static bool pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||||
|
static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight);
|
||||||
|
};
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <esp_sleep.h>
|
|
||||||
|
|
||||||
void HalGPIO::begin() {
|
void HalGPIO::begin() {
|
||||||
inputMgr.begin();
|
inputMgr.begin();
|
||||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
pinMode(BAT_GPIO0, INPUT);
|
|
||||||
pinMode(UART0_RXD, INPUT);
|
pinMode(UART0_RXD, INPUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,23 +21,6 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
|||||||
|
|
||||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||||
|
|
||||||
void HalGPIO::startDeepSleep() {
|
|
||||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
|
||||||
while (inputMgr.isPressed(BTN_POWER)) {
|
|
||||||
delay(50);
|
|
||||||
inputMgr.update();
|
|
||||||
}
|
|
||||||
// Arm the wakeup trigger *after* the button is released
|
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
|
||||||
// Enter Deep Sleep
|
|
||||||
esp_deep_sleep_start();
|
|
||||||
}
|
|
||||||
|
|
||||||
int HalGPIO::getBatteryPercentage() const {
|
|
||||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
|
||||||
return battery.readPercentage();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HalGPIO::isUsbConnected() const {
|
bool HalGPIO::isUsbConnected() const {
|
||||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||||
return digitalRead(UART0_RXD) == HIGH;
|
return digitalRead(UART0_RXD) == HIGH;
|
||||||
|
|||||||
@@ -38,12 +38,6 @@ class HalGPIO {
|
|||||||
bool wasAnyReleased() const;
|
bool wasAnyReleased() const;
|
||||||
unsigned long getHeldTime() const;
|
unsigned long getHeldTime() const;
|
||||||
|
|
||||||
// Setup wake up GPIO and enter deep sleep
|
|
||||||
void startDeepSleep();
|
|
||||||
|
|
||||||
// Get battery percentage (range 0-100)
|
|
||||||
int getBatteryPercentage() const;
|
|
||||||
|
|
||||||
// Check if USB is connected
|
// Check if USB is connected
|
||||||
bool isUsbConnected() const;
|
bool isUsbConnected() const;
|
||||||
|
|
||||||
|
|||||||
95
lib/hal/HalPowerManager.cpp
Normal file
95
lib/hal/HalPowerManager.cpp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#include "HalPowerManager.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp_sleep.h>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "HalGPIO.h"
|
||||||
|
|
||||||
|
HalPowerManager powerManager; // Singleton instance
|
||||||
|
|
||||||
|
void HalPowerManager::begin() {
|
||||||
|
pinMode(BAT_GPIO0, INPUT);
|
||||||
|
normalFreq = getCpuFrequencyMhz();
|
||||||
|
modeMutex = xSemaphoreCreateMutex();
|
||||||
|
assert(modeMutex != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalPowerManager::setPowerSaving(bool enabled) {
|
||||||
|
if (normalFreq <= 0) {
|
||||||
|
return; // invalid state
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wifiMode = WiFi.getMode();
|
||||||
|
if (wifiMode != WIFI_MODE_NULL) {
|
||||||
|
// Wifi is active, force disabling power saving
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We don't use mutex here to avoid too much overhead,
|
||||||
|
// it's not very important if we read a slightly stale value for currentLockMode
|
||||||
|
const LockMode mode = currentLockMode;
|
||||||
|
|
||||||
|
if (mode == None && enabled && !isLowPower) {
|
||||||
|
LOG_DBG("PWR", "Going to low-power mode");
|
||||||
|
if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) {
|
||||||
|
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", LOW_POWER_FREQ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLowPower = true;
|
||||||
|
|
||||||
|
} else if ((!enabled || mode != None) && isLowPower) {
|
||||||
|
LOG_DBG("PWR", "Restoring normal CPU frequency");
|
||||||
|
if (!setCpuFrequencyMhz(normalFreq)) {
|
||||||
|
LOG_DBG("PWR", "Failed to set CPU frequency = %d MHz", normalFreq);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLowPower = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, no change needed
|
||||||
|
}
|
||||||
|
|
||||||
|
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||||
|
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||||
|
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||||
|
delay(50);
|
||||||
|
gpio.update();
|
||||||
|
}
|
||||||
|
// Arm the wakeup trigger *after* the button is released
|
||||||
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
// Enter Deep Sleep
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
int HalPowerManager::getBatteryPercentage() const {
|
||||||
|
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||||
|
return battery.readPercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
HalPowerManager::Lock::Lock() {
|
||||||
|
xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY);
|
||||||
|
// Current limitation: only one lock at a time
|
||||||
|
if (powerManager.currentLockMode != None) {
|
||||||
|
LOG_ERR("PWR", "Lock already held, ignore");
|
||||||
|
valid = false;
|
||||||
|
} else {
|
||||||
|
powerManager.currentLockMode = NormalSpeed;
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
xSemaphoreGive(powerManager.modeMutex);
|
||||||
|
if (valid) {
|
||||||
|
// Immediately restore normal CPU frequency if currently in low-power mode
|
||||||
|
powerManager.setPowerSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HalPowerManager::Lock::~Lock() {
|
||||||
|
xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY);
|
||||||
|
if (valid) {
|
||||||
|
powerManager.currentLockMode = None;
|
||||||
|
}
|
||||||
|
xSemaphoreGive(powerManager.modeMutex);
|
||||||
|
}
|
||||||
56
lib/hal/HalPowerManager.h
Normal file
56
lib/hal/HalPowerManager.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <InputManager.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "HalGPIO.h"
|
||||||
|
|
||||||
|
class HalPowerManager;
|
||||||
|
extern HalPowerManager powerManager; // Singleton
|
||||||
|
|
||||||
|
class HalPowerManager {
|
||||||
|
int normalFreq = 0; // MHz
|
||||||
|
bool isLowPower = false;
|
||||||
|
|
||||||
|
enum LockMode { None, NormalSpeed };
|
||||||
|
LockMode currentLockMode = None;
|
||||||
|
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||||
|
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// Control CPU frequency for power saving
|
||||||
|
void setPowerSaving(bool enabled);
|
||||||
|
|
||||||
|
// Setup wake up GPIO and enter deep sleep
|
||||||
|
// Should be called inside main loop() to handle the currentLockMode
|
||||||
|
void startDeepSleep(HalGPIO& gpio) const;
|
||||||
|
|
||||||
|
// Get battery percentage (range 0-100)
|
||||||
|
int getBatteryPercentage() const;
|
||||||
|
|
||||||
|
// RAII helper class to manage power saving locks
|
||||||
|
// Usage: create an instance of Lock in a scope to disable power saving, for example when running a task that needs
|
||||||
|
// full performance. When the Lock instance is destroyed (goes out of scope), power saving will be re-enabled.
|
||||||
|
class Lock {
|
||||||
|
friend class HalPowerManager;
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Lock();
|
||||||
|
~Lock();
|
||||||
|
|
||||||
|
// Non-copyable and non-movable
|
||||||
|
Lock(const Lock&) = delete;
|
||||||
|
Lock& operator=(const Lock&) = delete;
|
||||||
|
Lock(Lock&&) = delete;
|
||||||
|
Lock& operator=(Lock&&) = delete;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -30,6 +31,9 @@ build_flags =
|
|||||||
-std=gnu++2a
|
-std=gnu++2a
|
||||||
# Enable UTF-8 long file names in SdFat
|
# Enable UTF-8 long file names in SdFat
|
||||||
-DUSE_UTF8_LONG_NAMES=1
|
-DUSE_UTF8_LONG_NAMES=1
|
||||||
|
# Increase PNG scanline buffer to support up to 800px wide images
|
||||||
|
# Default is (320*4+1)*2=2562, we need more for larger images
|
||||||
|
-DPNG_MAX_BUFFERED_PIXELS=6402
|
||||||
|
|
||||||
build_unflags =
|
build_unflags =
|
||||||
-std=gnu++11
|
-std=gnu++11
|
||||||
@@ -41,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 =
|
||||||
@@ -50,6 +55,7 @@ lib_deps =
|
|||||||
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
|
||||||
bblanchon/ArduinoJson @ 7.4.2
|
bblanchon/ArduinoJson @ 7.4.2
|
||||||
ricmoo/QRCode @ 0.0.1
|
ricmoo/QRCode @ 0.0.1
|
||||||
|
bitbank2/PNGdec @ ^1.0.0
|
||||||
links2004/WebSockets @ 2.7.3
|
links2004/WebSockets @ 2.7.3
|
||||||
|
|
||||||
[env:default]
|
[env:default]
|
||||||
|
|||||||
620
scripts/gen_i18n.py
Executable file
620
scripts/gen_i18n.py
Executable 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
|
||||||
@@ -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}),
|
||||||
}};
|
}};
|
||||||
|
|||||||
700
scripts/generate_test_epub.py
Normal file
700
scripts/generate_test_epub.py
Normal file
@@ -0,0 +1,700 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate test EPUBs for image rendering verification.
|
||||||
|
|
||||||
|
Creates EPUBs with annotated JPEG and PNG images to verify:
|
||||||
|
- Grayscale rendering (4 levels)
|
||||||
|
- Image scaling
|
||||||
|
- Image centering
|
||||||
|
- Cache performance
|
||||||
|
- Page serialization
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
except ImportError:
|
||||||
|
print("Please install Pillow: pip install Pillow")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
OUTPUT_DIR = Path(__file__).parent.parent / "test" / "epubs"
|
||||||
|
SCREEN_WIDTH = 480
|
||||||
|
SCREEN_HEIGHT = 800
|
||||||
|
|
||||||
|
def get_font(size=20):
|
||||||
|
"""Get a font, falling back to default if needed."""
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", size)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype("/usr/share/fonts/TTF/DejaVuSans.ttf", size)
|
||||||
|
except:
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
def draw_text_centered(draw, y, text, font, fill=0):
|
||||||
|
"""Draw centered text at given y position."""
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
x = (draw.im.size[0] - text_width) // 2
|
||||||
|
draw.text((x, y), text, font=font, fill=fill)
|
||||||
|
|
||||||
|
def draw_text_wrapped(draw, x, y, text, font, max_width, fill=0):
|
||||||
|
"""Draw text with word wrapping."""
|
||||||
|
words = text.split()
|
||||||
|
lines = []
|
||||||
|
current_line = []
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
test_line = ' '.join(current_line + [word])
|
||||||
|
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||||
|
if bbox[2] - bbox[0] <= max_width:
|
||||||
|
current_line.append(word)
|
||||||
|
else:
|
||||||
|
if current_line:
|
||||||
|
lines.append(' '.join(current_line))
|
||||||
|
current_line = [word]
|
||||||
|
if current_line:
|
||||||
|
lines.append(' '.join(current_line))
|
||||||
|
|
||||||
|
line_height = font.size + 4 if hasattr(font, 'size') else 20
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
draw.text((x, y + i * line_height), line, font=font, fill=fill)
|
||||||
|
|
||||||
|
return len(lines) * line_height
|
||||||
|
|
||||||
|
def create_grayscale_test_image(filename, is_png=True):
|
||||||
|
"""
|
||||||
|
Create image with 4 grayscale squares to verify 4-level rendering.
|
||||||
|
"""
|
||||||
|
width, height = 400, 600
|
||||||
|
img = Image.new('L', (width, height), 255)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(16)
|
||||||
|
font_small = get_font(14)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
draw_text_centered(draw, 10, "GRAYSCALE TEST", font, fill=0)
|
||||||
|
draw_text_centered(draw, 35, "Verify 4 distinct gray levels", font_small, fill=64)
|
||||||
|
|
||||||
|
# Draw 4 grayscale squares
|
||||||
|
square_size = 70
|
||||||
|
start_y = 65
|
||||||
|
gap = 10
|
||||||
|
|
||||||
|
# Gray levels chosen to avoid Bayer dithering threshold boundaries (±40 dither offset)
|
||||||
|
# Thresholds at 64, 128, 192 - use values in the middle of each band for solid output
|
||||||
|
# Safe zones: 0-23 (black), 88-103 (dark gray), 152-167 (light gray), 232-255 (white)
|
||||||
|
levels = [
|
||||||
|
(0, "Level 0: BLACK"),
|
||||||
|
(96, "Level 1: DARK GRAY"),
|
||||||
|
(160, "Level 2: LIGHT GRAY"),
|
||||||
|
(255, "Level 3: WHITE"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (gray_value, label) in enumerate(levels):
|
||||||
|
y = start_y + i * (square_size + gap + 22)
|
||||||
|
x = (width - square_size) // 2
|
||||||
|
|
||||||
|
# Draw square with border
|
||||||
|
draw.rectangle([x-2, y-2, x + square_size + 2, y + square_size + 2], fill=0)
|
||||||
|
draw.rectangle([x, y, x + square_size, y + square_size], fill=gray_value)
|
||||||
|
|
||||||
|
# Label below square
|
||||||
|
bbox = draw.textbbox((0, 0), label, font=font_small)
|
||||||
|
label_width = bbox[2] - bbox[0]
|
||||||
|
draw.text(((width - label_width) // 2, y + square_size + 5), label, font=font_small, fill=0)
|
||||||
|
|
||||||
|
# Instructions at bottom (well below the last square)
|
||||||
|
y = height - 70
|
||||||
|
draw_text_centered(draw, y, "PASS: 4 distinct shades visible", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 20, "FAIL: Only black/white or", font_small, fill=64)
|
||||||
|
draw_text_centered(draw, y + 38, "muddy/indistinct grays", font_small, fill=64)
|
||||||
|
|
||||||
|
# Save
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_centering_test_image(filename, is_png=True):
|
||||||
|
"""
|
||||||
|
Create image with border markers to verify centering.
|
||||||
|
"""
|
||||||
|
width, height = 350, 400
|
||||||
|
img = Image.new('L', (width, height), 255)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(16)
|
||||||
|
font_small = get_font(14)
|
||||||
|
|
||||||
|
# Draw border
|
||||||
|
draw.rectangle([0, 0, width-1, height-1], outline=0, width=3)
|
||||||
|
|
||||||
|
# Corner markers
|
||||||
|
marker_size = 20
|
||||||
|
for x, y in [(0, 0), (width-marker_size, 0), (0, height-marker_size), (width-marker_size, height-marker_size)]:
|
||||||
|
draw.rectangle([x, y, x+marker_size, y+marker_size], fill=0)
|
||||||
|
|
||||||
|
# Center cross
|
||||||
|
cx, cy = width // 2, height // 2
|
||||||
|
draw.line([cx - 30, cy, cx + 30, cy], fill=0, width=2)
|
||||||
|
draw.line([cx, cy - 30, cx, cy + 30], fill=0, width=2)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
draw_text_centered(draw, 40, "CENTERING TEST", font, fill=0)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
y = 80
|
||||||
|
draw_text_centered(draw, y, "Image should be centered", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 20, "horizontally on screen", font_small, fill=0)
|
||||||
|
|
||||||
|
y = 150
|
||||||
|
draw_text_centered(draw, y, "Check:", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 25, "- Equal margins left & right", font_small, fill=64)
|
||||||
|
draw_text_centered(draw, y + 45, "- All 4 corners visible", font_small, fill=64)
|
||||||
|
draw_text_centered(draw, y + 65, "- Border is complete rectangle", font_small, fill=64)
|
||||||
|
|
||||||
|
# Pass/fail
|
||||||
|
y = height - 80
|
||||||
|
draw_text_centered(draw, y, "PASS: Centered, all corners visible", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 20, "FAIL: Off-center or cropped", font_small, fill=64)
|
||||||
|
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_scaling_test_image(filename, is_png=True):
|
||||||
|
"""
|
||||||
|
Create large image to verify scaling works.
|
||||||
|
"""
|
||||||
|
# Make image larger than screen but within decoder limits (max 2048x1536)
|
||||||
|
width, height = 1200, 1500
|
||||||
|
img = Image.new('L', (width, height), 240)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(48)
|
||||||
|
font_medium = get_font(32)
|
||||||
|
font_small = get_font(24)
|
||||||
|
|
||||||
|
# Border
|
||||||
|
draw.rectangle([0, 0, width-1, height-1], outline=0, width=8)
|
||||||
|
draw.rectangle([20, 20, width-21, height-21], outline=128, width=4)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
draw_text_centered(draw, 60, "SCALING TEST", font, fill=0)
|
||||||
|
draw_text_centered(draw, 130, f"Original: {width}x{height} (larger than screen)", font_medium, fill=64)
|
||||||
|
|
||||||
|
# Grid pattern to verify scaling quality
|
||||||
|
grid_start_y = 220
|
||||||
|
grid_size = 400
|
||||||
|
cell_size = 50
|
||||||
|
|
||||||
|
draw_text_centered(draw, grid_start_y - 40, "Grid pattern (check for artifacts):", font_small, fill=0)
|
||||||
|
|
||||||
|
grid_x = (width - grid_size) // 2
|
||||||
|
for row in range(grid_size // cell_size):
|
||||||
|
for col in range(grid_size // cell_size):
|
||||||
|
x = grid_x + col * cell_size
|
||||||
|
y = grid_start_y + row * cell_size
|
||||||
|
if (row + col) % 2 == 0:
|
||||||
|
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
||||||
|
else:
|
||||||
|
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200)
|
||||||
|
|
||||||
|
# Size indicator bars
|
||||||
|
y = grid_start_y + grid_size + 60
|
||||||
|
draw_text_centered(draw, y, "Width markers (should fit on screen):", font_small, fill=0)
|
||||||
|
|
||||||
|
bar_y = y + 40
|
||||||
|
# Full width bar
|
||||||
|
draw.rectangle([50, bar_y, width - 50, bar_y + 30], fill=0)
|
||||||
|
draw.text((60, bar_y + 5), "FULL WIDTH", font=font_small, fill=255)
|
||||||
|
|
||||||
|
# Half width bar
|
||||||
|
bar_y += 60
|
||||||
|
half_start = width // 4
|
||||||
|
draw.rectangle([half_start, bar_y, width - half_start, bar_y + 30], fill=85)
|
||||||
|
draw.text((half_start + 10, bar_y + 5), "HALF WIDTH", font=font_small, fill=255)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
y = height - 350
|
||||||
|
draw_text_centered(draw, y, "VERIFICATION:", font_medium, fill=0)
|
||||||
|
y += 50
|
||||||
|
instructions = [
|
||||||
|
"1. Image fits within screen bounds",
|
||||||
|
"2. All borders visible (not cropped)",
|
||||||
|
"3. Grid pattern clear (no moire)",
|
||||||
|
"4. Text readable after scaling",
|
||||||
|
"5. Aspect ratio preserved (not stretched)",
|
||||||
|
]
|
||||||
|
for i, text in enumerate(instructions):
|
||||||
|
draw_text_centered(draw, y + i * 35, text, font_small, fill=64)
|
||||||
|
|
||||||
|
y = height - 100
|
||||||
|
draw_text_centered(draw, y, "PASS: Scaled down, readable, complete", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 30, "FAIL: Cropped, distorted, or unreadable", font_small, fill=64)
|
||||||
|
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_wide_scaling_test_image(filename, is_png=True):
|
||||||
|
"""
|
||||||
|
Create wide image (1807x736) to test scaling with specific dimensions
|
||||||
|
that can trigger cache dimension mismatches due to floating-point rounding.
|
||||||
|
"""
|
||||||
|
width, height = 1807, 736
|
||||||
|
img = Image.new('L', (width, height), 240)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(48)
|
||||||
|
font_medium = get_font(32)
|
||||||
|
font_small = get_font(24)
|
||||||
|
|
||||||
|
# Border
|
||||||
|
draw.rectangle([0, 0, width-1, height-1], outline=0, width=6)
|
||||||
|
draw.rectangle([15, 15, width-16, height-16], outline=128, width=3)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
draw_text_centered(draw, 40, "WIDE SCALING TEST", font, fill=0)
|
||||||
|
draw_text_centered(draw, 100, f"Original: {width}x{height} (tests rounding edge case)", font_medium, fill=64)
|
||||||
|
|
||||||
|
# Grid pattern to verify scaling quality
|
||||||
|
grid_start_x = 100
|
||||||
|
grid_start_y = 180
|
||||||
|
grid_width = 600
|
||||||
|
grid_height = 300
|
||||||
|
cell_size = 50
|
||||||
|
|
||||||
|
draw.text((grid_start_x, grid_start_y - 35), "Grid pattern (check for artifacts):", font=font_small, fill=0)
|
||||||
|
|
||||||
|
for row in range(grid_height // cell_size):
|
||||||
|
for col in range(grid_width // cell_size):
|
||||||
|
x = grid_start_x + col * cell_size
|
||||||
|
y = grid_start_y + row * cell_size
|
||||||
|
if (row + col) % 2 == 0:
|
||||||
|
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=0)
|
||||||
|
else:
|
||||||
|
draw.rectangle([x, y, x + cell_size, y + cell_size], fill=200)
|
||||||
|
|
||||||
|
# Verification section on the right
|
||||||
|
text_x = 800
|
||||||
|
text_y = 180
|
||||||
|
draw.text((text_x, text_y), "VERIFICATION:", font=font_medium, fill=0)
|
||||||
|
text_y += 50
|
||||||
|
instructions = [
|
||||||
|
"1. Image fits within screen",
|
||||||
|
"2. All borders visible",
|
||||||
|
"3. Grid pattern clear",
|
||||||
|
"4. Text readable",
|
||||||
|
"5. No double-decode in log",
|
||||||
|
]
|
||||||
|
for i, text in enumerate(instructions):
|
||||||
|
draw.text((text_x, text_y + i * 35), text, font=font_small, fill=64)
|
||||||
|
|
||||||
|
# Dimension info
|
||||||
|
draw.text((text_x, 450), f"Dimensions: {width}x{height}", font=font_small, fill=0)
|
||||||
|
draw.text((text_x, 485), "Tests cache dimension matching", font=font_small, fill=64)
|
||||||
|
|
||||||
|
# Pass/fail at bottom
|
||||||
|
y = height - 80
|
||||||
|
draw_text_centered(draw, y, "PASS: Single decode, cached correctly", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 30, "FAIL: Cache mismatch, multiple decodes", font_small, fill=64)
|
||||||
|
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_cache_test_image(filename, page_num, is_png=True):
|
||||||
|
"""
|
||||||
|
Create image for cache performance testing.
|
||||||
|
"""
|
||||||
|
width, height = 400, 300
|
||||||
|
img = Image.new('L', (width, height), 255)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(18)
|
||||||
|
font_small = get_font(14)
|
||||||
|
font_large = get_font(36)
|
||||||
|
|
||||||
|
# Border
|
||||||
|
draw.rectangle([0, 0, width-1, height-1], outline=0, width=2)
|
||||||
|
|
||||||
|
# Page number prominent
|
||||||
|
draw_text_centered(draw, 30, f"CACHE TEST PAGE {page_num}", font, fill=0)
|
||||||
|
draw_text_centered(draw, 80, f"#{page_num}", font_large, fill=0)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
y = 140
|
||||||
|
draw_text_centered(draw, y, "Navigate away then return", font_small, fill=64)
|
||||||
|
draw_text_centered(draw, y + 25, "Second load should be faster", font_small, fill=64)
|
||||||
|
|
||||||
|
y = 220
|
||||||
|
draw_text_centered(draw, y, "PASS: Faster reload from cache", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 20, "FAIL: Same slow decode each time", font_small, fill=64)
|
||||||
|
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_gradient_test_image(filename, is_png=True):
|
||||||
|
"""
|
||||||
|
Create horizontal gradient to test grayscale banding.
|
||||||
|
"""
|
||||||
|
width, height = 400, 500
|
||||||
|
img = Image.new('L', (width, height), 255)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(16)
|
||||||
|
font_small = get_font(14)
|
||||||
|
|
||||||
|
draw_text_centered(draw, 10, "GRADIENT TEST", font, fill=0)
|
||||||
|
draw_text_centered(draw, 35, "Smooth gradient → 4 bands expected", font_small, fill=64)
|
||||||
|
|
||||||
|
# Horizontal gradient
|
||||||
|
gradient_y = 70
|
||||||
|
gradient_height = 100
|
||||||
|
for x in range(width):
|
||||||
|
gray = int(255 * x / width)
|
||||||
|
draw.line([(x, gradient_y), (x, gradient_y + gradient_height)], fill=gray)
|
||||||
|
|
||||||
|
# Border around gradient
|
||||||
|
draw.rectangle([0, gradient_y-1, width-1, gradient_y + gradient_height + 1], outline=0, width=1)
|
||||||
|
|
||||||
|
# Labels
|
||||||
|
y = gradient_y + gradient_height + 10
|
||||||
|
draw.text((5, y), "BLACK", font=font_small, fill=0)
|
||||||
|
draw.text((width - 50, y), "WHITE", font=font_small, fill=0)
|
||||||
|
|
||||||
|
# 4-step gradient (what it should look like)
|
||||||
|
y = 220
|
||||||
|
draw_text_centered(draw, y, "Expected result (4 distinct bands):", font_small, fill=0)
|
||||||
|
|
||||||
|
band_y = y + 25
|
||||||
|
band_height = 60
|
||||||
|
band_width = width // 4
|
||||||
|
for i, gray in enumerate([0, 85, 170, 255]):
|
||||||
|
x = i * band_width
|
||||||
|
draw.rectangle([x, band_y, x + band_width, band_y + band_height], fill=gray)
|
||||||
|
draw.rectangle([0, band_y-1, width-1, band_y + band_height + 1], outline=0, width=1)
|
||||||
|
|
||||||
|
# Vertical gradient
|
||||||
|
y = 340
|
||||||
|
draw_text_centered(draw, y, "Vertical gradient:", font_small, fill=0)
|
||||||
|
|
||||||
|
vgrad_y = y + 25
|
||||||
|
vgrad_height = 80
|
||||||
|
for row in range(vgrad_height):
|
||||||
|
gray = int(255 * row / vgrad_height)
|
||||||
|
draw.line([(50, vgrad_y + row), (width - 50, vgrad_y + row)], fill=gray)
|
||||||
|
draw.rectangle([49, vgrad_y-1, width-49, vgrad_y + vgrad_height + 1], outline=0, width=1)
|
||||||
|
|
||||||
|
# Pass/fail
|
||||||
|
y = height - 50
|
||||||
|
draw_text_centered(draw, y, "PASS: Clear 4-band quantization", font_small, fill=0)
|
||||||
|
draw_text_centered(draw, y + 20, "FAIL: Binary/noisy dithering", font_small, fill=64)
|
||||||
|
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_format_test_image(filename, format_name, is_png=True):
|
||||||
|
"""
|
||||||
|
Create simple image to verify format support.
|
||||||
|
"""
|
||||||
|
width, height = 350, 250
|
||||||
|
img = Image.new('L', (width, height), 255)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
font = get_font(20)
|
||||||
|
font_large = get_font(36)
|
||||||
|
font_small = get_font(14)
|
||||||
|
|
||||||
|
# Border
|
||||||
|
draw.rectangle([0, 0, width-1, height-1], outline=0, width=3)
|
||||||
|
|
||||||
|
# Format name
|
||||||
|
draw_text_centered(draw, 30, f"{format_name} FORMAT TEST", font, fill=0)
|
||||||
|
draw_text_centered(draw, 80, format_name, font_large, fill=0)
|
||||||
|
|
||||||
|
# Checkmark area
|
||||||
|
y = 140
|
||||||
|
draw_text_centered(draw, y, "If you can read this,", font_small, fill=64)
|
||||||
|
draw_text_centered(draw, y + 20, f"{format_name} decoding works!", font_small, fill=64)
|
||||||
|
|
||||||
|
y = height - 40
|
||||||
|
draw_text_centered(draw, y, f"PASS: {format_name} image visible", font_small, fill=0)
|
||||||
|
|
||||||
|
if is_png:
|
||||||
|
img.save(filename, 'PNG')
|
||||||
|
else:
|
||||||
|
img.save(filename, 'JPEG', quality=95)
|
||||||
|
|
||||||
|
def create_epub(epub_path, title, chapters):
|
||||||
|
"""
|
||||||
|
Create an EPUB file with the given chapters.
|
||||||
|
|
||||||
|
chapters: list of (chapter_title, html_content, images)
|
||||||
|
images: list of (image_filename, image_data)
|
||||||
|
"""
|
||||||
|
with zipfile.ZipFile(epub_path, 'w', zipfile.ZIP_DEFLATED) as epub:
|
||||||
|
# mimetype (must be first, uncompressed)
|
||||||
|
epub.writestr('mimetype', 'application/epub+zip', compress_type=zipfile.ZIP_STORED)
|
||||||
|
|
||||||
|
# Container
|
||||||
|
container_xml = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||||
|
<rootfiles>
|
||||||
|
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
||||||
|
</rootfiles>
|
||||||
|
</container>'''
|
||||||
|
epub.writestr('META-INF/container.xml', container_xml)
|
||||||
|
|
||||||
|
# Collect all images and chapters
|
||||||
|
manifest_items = []
|
||||||
|
spine_items = []
|
||||||
|
|
||||||
|
# Add chapters and images
|
||||||
|
for i, (chapter_title, html_content, images) in enumerate(chapters):
|
||||||
|
chapter_id = f'chapter{i+1}'
|
||||||
|
chapter_file = f'chapter{i+1}.xhtml'
|
||||||
|
|
||||||
|
# Add images for this chapter
|
||||||
|
for img_filename, img_data in images:
|
||||||
|
media_type = 'image/png' if img_filename.endswith('.png') else 'image/jpeg'
|
||||||
|
manifest_items.append(f' <item id="{img_filename.replace(".", "_")}" href="images/{img_filename}" media-type="{media_type}"/>')
|
||||||
|
epub.writestr(f'OEBPS/images/{img_filename}', img_data)
|
||||||
|
|
||||||
|
# Add chapter
|
||||||
|
manifest_items.append(f' <item id="{chapter_id}" href="{chapter_file}" media-type="application/xhtml+xml"/>')
|
||||||
|
spine_items.append(f' <itemref idref="{chapter_id}"/>')
|
||||||
|
epub.writestr(f'OEBPS/{chapter_file}', html_content)
|
||||||
|
|
||||||
|
# content.opf
|
||||||
|
content_opf = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<dc:identifier id="uid">test-epub-{title.lower().replace(" ", "-")}</dc:identifier>
|
||||||
|
<dc:title>{title}</dc:title>
|
||||||
|
<dc:language>en</dc:language>
|
||||||
|
</metadata>
|
||||||
|
<manifest>
|
||||||
|
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
|
||||||
|
{chr(10).join(manifest_items)}
|
||||||
|
</manifest>
|
||||||
|
<spine>
|
||||||
|
{chr(10).join(spine_items)}
|
||||||
|
</spine>
|
||||||
|
</package>'''
|
||||||
|
epub.writestr('OEBPS/content.opf', content_opf)
|
||||||
|
|
||||||
|
# Navigation document
|
||||||
|
nav_items = '\n'.join([f' <li><a href="chapter{i+1}.xhtml">{chapters[i][0]}</a></li>'
|
||||||
|
for i in range(len(chapters))])
|
||||||
|
nav_xhtml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
|
||||||
|
<head><title>Navigation</title></head>
|
||||||
|
<body>
|
||||||
|
<nav epub:type="toc">
|
||||||
|
<h1>Contents</h1>
|
||||||
|
<ol>
|
||||||
|
{nav_items}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
epub.writestr('OEBPS/nav.xhtml', nav_xhtml)
|
||||||
|
|
||||||
|
def make_chapter(title, body_content):
|
||||||
|
"""Create XHTML chapter content."""
|
||||||
|
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head><title>{title}</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
{body_content}
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
def main():
|
||||||
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Temp directory for images
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
tmpdir = Path(tmpdir)
|
||||||
|
|
||||||
|
print("Generating test images...")
|
||||||
|
|
||||||
|
# Generate all test images
|
||||||
|
images = {}
|
||||||
|
|
||||||
|
# JPEG tests
|
||||||
|
create_grayscale_test_image(tmpdir / 'grayscale_test.jpg', is_png=False)
|
||||||
|
create_centering_test_image(tmpdir / 'centering_test.jpg', is_png=False)
|
||||||
|
create_scaling_test_image(tmpdir / 'scaling_test.jpg', is_png=False)
|
||||||
|
create_wide_scaling_test_image(tmpdir / 'wide_scaling_test.jpg', is_png=False)
|
||||||
|
create_gradient_test_image(tmpdir / 'gradient_test.jpg', is_png=False)
|
||||||
|
create_format_test_image(tmpdir / 'jpeg_format.jpg', 'JPEG', is_png=False)
|
||||||
|
create_cache_test_image(tmpdir / 'cache_test_1.jpg', 1, is_png=False)
|
||||||
|
create_cache_test_image(tmpdir / 'cache_test_2.jpg', 2, is_png=False)
|
||||||
|
|
||||||
|
# PNG tests
|
||||||
|
create_grayscale_test_image(tmpdir / 'grayscale_test.png', is_png=True)
|
||||||
|
create_centering_test_image(tmpdir / 'centering_test.png', is_png=True)
|
||||||
|
create_scaling_test_image(tmpdir / 'scaling_test.png', is_png=True)
|
||||||
|
create_wide_scaling_test_image(tmpdir / 'wide_scaling_test.png', is_png=True)
|
||||||
|
create_gradient_test_image(tmpdir / 'gradient_test.png', is_png=True)
|
||||||
|
create_format_test_image(tmpdir / 'png_format.png', 'PNG', is_png=True)
|
||||||
|
create_cache_test_image(tmpdir / 'cache_test_1.png', 1, is_png=True)
|
||||||
|
create_cache_test_image(tmpdir / 'cache_test_2.png', 2, is_png=True)
|
||||||
|
|
||||||
|
# Read all images
|
||||||
|
for img_file in tmpdir.glob('*.*'):
|
||||||
|
images[img_file.name] = img_file.read_bytes()
|
||||||
|
|
||||||
|
print("Creating JPEG test EPUB...")
|
||||||
|
jpeg_chapters = [
|
||||||
|
("Introduction", make_chapter("JPEG Image Tests", """
|
||||||
|
<p>This EPUB tests JPEG image rendering.</p>
|
||||||
|
<p>Navigate through chapters to verify each test case.</p>
|
||||||
|
<p><strong>Test Plan:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Grayscale rendering (4 levels)</li>
|
||||||
|
<li>Image centering</li>
|
||||||
|
<li>Large image scaling</li>
|
||||||
|
<li>Cache performance</li>
|
||||||
|
</ul>
|
||||||
|
"""), []),
|
||||||
|
("1. JPEG Format", make_chapter("JPEG Format Test", """
|
||||||
|
<p>Basic JPEG decoding test.</p>
|
||||||
|
<img src="images/jpeg_format.jpg" alt="JPEG format test"/>
|
||||||
|
<p>If the image above is visible, JPEG decoding works.</p>
|
||||||
|
"""), [('jpeg_format.jpg', images['jpeg_format.jpg'])]),
|
||||||
|
("2. Grayscale", make_chapter("Grayscale Test", """
|
||||||
|
<p>Verify 4 distinct gray levels are visible.</p>
|
||||||
|
<img src="images/grayscale_test.jpg" alt="Grayscale test"/>
|
||||||
|
"""), [('grayscale_test.jpg', images['grayscale_test.jpg'])]),
|
||||||
|
("3. Gradient", make_chapter("Gradient Test", """
|
||||||
|
<p>Verify gradient quantizes to 4 bands.</p>
|
||||||
|
<img src="images/gradient_test.jpg" alt="Gradient test"/>
|
||||||
|
"""), [('gradient_test.jpg', images['gradient_test.jpg'])]),
|
||||||
|
("4. Centering", make_chapter("Centering Test", """
|
||||||
|
<p>Verify image is centered horizontally.</p>
|
||||||
|
<img src="images/centering_test.jpg" alt="Centering test"/>
|
||||||
|
"""), [('centering_test.jpg', images['centering_test.jpg'])]),
|
||||||
|
("5. Scaling", make_chapter("Scaling Test", """
|
||||||
|
<p>This image is 1200x1500 pixels - larger than the screen.</p>
|
||||||
|
<p>It should be scaled down to fit.</p>
|
||||||
|
<img src="images/scaling_test.jpg" alt="Scaling test"/>
|
||||||
|
"""), [('scaling_test.jpg', images['scaling_test.jpg'])]),
|
||||||
|
("6. Wide Scaling", make_chapter("Wide Scaling Test", """
|
||||||
|
<p>This image is 1807x736 pixels - a wide landscape format.</p>
|
||||||
|
<p>Tests scaling with dimensions that can cause cache mismatches.</p>
|
||||||
|
<img src="images/wide_scaling_test.jpg" alt="Wide scaling test"/>
|
||||||
|
"""), [('wide_scaling_test.jpg', images['wide_scaling_test.jpg'])]),
|
||||||
|
("7. Cache Test A", make_chapter("Cache Test - Page A", """
|
||||||
|
<p>First cache test page. Note the load time.</p>
|
||||||
|
<img src="images/cache_test_1.jpg" alt="Cache test 1"/>
|
||||||
|
<p>Navigate to next page, then come back.</p>
|
||||||
|
"""), [('cache_test_1.jpg', images['cache_test_1.jpg'])]),
|
||||||
|
("8. Cache Test B", make_chapter("Cache Test - Page B", """
|
||||||
|
<p>Second cache test page.</p>
|
||||||
|
<img src="images/cache_test_2.jpg" alt="Cache test 2"/>
|
||||||
|
<p>Navigate back to Page A - it should load faster from cache.</p>
|
||||||
|
"""), [('cache_test_2.jpg', images['cache_test_2.jpg'])]),
|
||||||
|
]
|
||||||
|
|
||||||
|
create_epub(OUTPUT_DIR / 'test_jpeg_images.epub', 'JPEG Image Tests', jpeg_chapters)
|
||||||
|
|
||||||
|
print("Creating PNG test EPUB...")
|
||||||
|
png_chapters = [
|
||||||
|
("Introduction", make_chapter("PNG Image Tests", """
|
||||||
|
<p>This EPUB tests PNG image rendering.</p>
|
||||||
|
<p>Navigate through chapters to verify each test case.</p>
|
||||||
|
<p><strong>Test Plan:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>PNG decoding (no crash)</li>
|
||||||
|
<li>Grayscale rendering (4 levels)</li>
|
||||||
|
<li>Image centering</li>
|
||||||
|
<li>Large image scaling</li>
|
||||||
|
</ul>
|
||||||
|
"""), []),
|
||||||
|
("1. PNG Format", make_chapter("PNG Format Test", """
|
||||||
|
<p>Basic PNG decoding test.</p>
|
||||||
|
<img src="images/png_format.png" alt="PNG format test"/>
|
||||||
|
<p>If the image above is visible and no crash occurred, PNG decoding works.</p>
|
||||||
|
"""), [('png_format.png', images['png_format.png'])]),
|
||||||
|
("2. Grayscale", make_chapter("Grayscale Test", """
|
||||||
|
<p>Verify 4 distinct gray levels are visible.</p>
|
||||||
|
<img src="images/grayscale_test.png" alt="Grayscale test"/>
|
||||||
|
"""), [('grayscale_test.png', images['grayscale_test.png'])]),
|
||||||
|
("3. Gradient", make_chapter("Gradient Test", """
|
||||||
|
<p>Verify gradient quantizes to 4 bands.</p>
|
||||||
|
<img src="images/gradient_test.png" alt="Gradient test"/>
|
||||||
|
"""), [('gradient_test.png', images['gradient_test.png'])]),
|
||||||
|
("4. Centering", make_chapter("Centering Test", """
|
||||||
|
<p>Verify image is centered horizontally.</p>
|
||||||
|
<img src="images/centering_test.png" alt="Centering test"/>
|
||||||
|
"""), [('centering_test.png', images['centering_test.png'])]),
|
||||||
|
("5. Scaling", make_chapter("Scaling Test", """
|
||||||
|
<p>This image is 1200x1500 pixels - larger than the screen.</p>
|
||||||
|
<p>It should be scaled down to fit.</p>
|
||||||
|
<img src="images/scaling_test.png" alt="Scaling test"/>
|
||||||
|
"""), [('scaling_test.png', images['scaling_test.png'])]),
|
||||||
|
("6. Wide Scaling", make_chapter("Wide Scaling Test", """
|
||||||
|
<p>This image is 1807x736 pixels - a wide landscape format.</p>
|
||||||
|
<p>Tests scaling with dimensions that can cause cache mismatches.</p>
|
||||||
|
<img src="images/wide_scaling_test.png" alt="Wide scaling test"/>
|
||||||
|
"""), [('wide_scaling_test.png', images['wide_scaling_test.png'])]),
|
||||||
|
("7. Cache Test A", make_chapter("Cache Test - Page A", """
|
||||||
|
<p>First cache test page. Note the load time.</p>
|
||||||
|
<img src="images/cache_test_1.png" alt="Cache test 1"/>
|
||||||
|
<p>Navigate to next page, then come back.</p>
|
||||||
|
"""), [('cache_test_1.png', images['cache_test_1.png'])]),
|
||||||
|
("8. Cache Test B", make_chapter("Cache Test - Page B", """
|
||||||
|
<p>Second cache test page.</p>
|
||||||
|
<img src="images/cache_test_2.png" alt="Cache test 2"/>
|
||||||
|
<p>Navigate back to Page A - it should load faster from cache.</p>
|
||||||
|
"""), [('cache_test_2.png', images['cache_test_2.png'])]),
|
||||||
|
]
|
||||||
|
|
||||||
|
create_epub(OUTPUT_DIR / 'test_png_images.epub', 'PNG Image Tests', png_chapters)
|
||||||
|
|
||||||
|
print("Creating mixed format test EPUB...")
|
||||||
|
mixed_chapters = [
|
||||||
|
("Introduction", make_chapter("Mixed Image Format Tests", """
|
||||||
|
<p>This EPUB contains both JPEG and PNG images.</p>
|
||||||
|
<p>Tests format detection and mixed rendering.</p>
|
||||||
|
"""), []),
|
||||||
|
("1. JPEG Image", make_chapter("JPEG in Mixed EPUB", """
|
||||||
|
<p>This is a JPEG image:</p>
|
||||||
|
<img src="images/jpeg_format.jpg" alt="JPEG"/>
|
||||||
|
"""), [('jpeg_format.jpg', images['jpeg_format.jpg'])]),
|
||||||
|
("2. PNG Image", make_chapter("PNG in Mixed EPUB", """
|
||||||
|
<p>This is a PNG image:</p>
|
||||||
|
<img src="images/png_format.png" alt="PNG"/>
|
||||||
|
"""), [('png_format.png', images['png_format.png'])]),
|
||||||
|
("3. Both Formats", make_chapter("Both Formats on One Page", """
|
||||||
|
<p>JPEG image:</p>
|
||||||
|
<img src="images/grayscale_test.jpg" alt="JPEG grayscale"/>
|
||||||
|
<p>PNG image:</p>
|
||||||
|
<img src="images/grayscale_test.png" alt="PNG grayscale"/>
|
||||||
|
<p>Both should render with proper grayscale.</p>
|
||||||
|
"""), [('grayscale_test.jpg', images['grayscale_test.jpg']),
|
||||||
|
('grayscale_test.png', images['grayscale_test.png'])]),
|
||||||
|
]
|
||||||
|
|
||||||
|
create_epub(OUTPUT_DIR / 'test_mixed_images.epub', 'Mixed Format Tests', mixed_chapters)
|
||||||
|
|
||||||
|
print(f"\nTest EPUBs created in: {OUTPUT_DIR}")
|
||||||
|
print("Files:")
|
||||||
|
for f in OUTPUT_DIR.glob('*.epub'):
|
||||||
|
print(f" - {f.name}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
24
scripts/update_hypenation.sh
Executable file
24
scripts/update_hypenation.sh
Executable 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
|
||||||
@@ -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 = 30;
|
|
||||||
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,68 @@ 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);
|
||||||
|
// 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,39 +148,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);
|
|
||||||
// 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");
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -182,6 +185,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();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@@ -12,90 +14,110 @@
|
|||||||
inline std::vector<SettingInfo> getSettingsList() {
|
inline std::vector<SettingInfo> getSettingsList() {
|
||||||
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(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
|
||||||
|
{StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
|
||||||
|
"sleepScreenCoverFilter", 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::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"},
|
SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,
|
||||||
"fontFamily", "Reader"),
|
{StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily",
|
||||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}, "fontSize",
|
StrId::STR_CAT_READER),
|
||||||
"Reader"),
|
SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize,
|
||||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}, "lineSpacing",
|
{StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize",
|
||||||
"Reader"),
|
StrId::STR_CAT_READER),
|
||||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", "Reader"),
|
SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing,
|
||||||
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
|
{StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER),
|
||||||
{"Justify", "Left", "Center", "Right", "Book's Style"}, "paragraphAlignment", "Reader"),
|
SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin",
|
||||||
SettingInfo::Toggle("Book's Embedded Style", &CrossPointSettings::embeddedStyle, "embeddedStyle", "Reader"),
|
StrId::STR_CAT_READER),
|
||||||
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", "Reader"),
|
SettingInfo::Enum(StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment,
|
||||||
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
|
{StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT,
|
||||||
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}, "orientation", "Reader"),
|
StrId::STR_BOOK_S_STYLE},
|
||||||
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing,
|
"paragraphAlignment", StrId::STR_CAT_READER),
|
||||||
"extraParagraphSpacing", "Reader"),
|
SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
|
||||||
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing, "textAntiAliasing", "Reader"),
|
StrId::STR_CAT_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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
61
src/activities/Activity.cpp
Normal file
61
src/activities/Activity.cpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#include "Activity.h"
|
||||||
|
|
||||||
|
#include <HalPowerManager.h>
|
||||||
|
|
||||||
|
void Activity::renderTaskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<Activity*>(param);
|
||||||
|
self->renderTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Activity::renderTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
{
|
||||||
|
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||||
|
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); }
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
#include "ActivityWithSubactivity.h"
|
#include "ActivityWithSubactivity.h"
|
||||||
|
|
||||||
|
#include <HalPowerManager.h>
|
||||||
|
|
||||||
|
void ActivityWithSubactivity::renderTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||||
|
{
|
||||||
|
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||||
|
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 +39,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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <Txt.h>
|
#include <Txt.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
|
|
||||||
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):
|
||||||
@@ -110,8 +111,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) {
|
||||||
|
|||||||
@@ -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, 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);
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
@@ -19,11 +20,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()) {
|
||||||
@@ -74,7 +70,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
// Try to generate thumbnail image for Continue Reading card
|
// Try to generate thumbnail image for Continue Reading card
|
||||||
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()));
|
||||||
bool success = epub.generateThumbBmp(coverHeight);
|
bool success = epub.generateThumbBmp(coverHeight);
|
||||||
@@ -83,7 +79,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
book.coverBmpPath = "";
|
book.coverBmpPath = "";
|
||||||
}
|
}
|
||||||
coverRendered = false;
|
coverRendered = false;
|
||||||
updateRequired = true;
|
requestUpdate();
|
||||||
} else if (StringUtils::checkFileExtension(book.path, ".xtch") ||
|
} else if (StringUtils::checkFileExtension(book.path, ".xtch") ||
|
||||||
StringUtils::checkFileExtension(book.path, ".xtc")) {
|
StringUtils::checkFileExtension(book.path, ".xtc")) {
|
||||||
// Handle XTC file
|
// Handle XTC file
|
||||||
@@ -92,7 +88,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
// Try to generate thumbnail image for Continue Reading card
|
// Try to generate thumbnail image for Continue Reading card
|
||||||
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()));
|
||||||
bool success = xtc.generateThumbBmp(coverHeight);
|
bool success = xtc.generateThumbBmp(coverHeight);
|
||||||
@@ -101,7 +97,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
|||||||
book.coverBmpPath = "";
|
book.coverBmpPath = "";
|
||||||
}
|
}
|
||||||
coverRendered = false;
|
coverRendered = false;
|
||||||
updateRequired = true;
|
requestUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +112,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;
|
||||||
|
|
||||||
@@ -127,28 +121,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();
|
||||||
}
|
}
|
||||||
@@ -200,12 +178,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)) {
|
||||||
@@ -234,19 +212,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();
|
||||||
@@ -261,10 +227,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(
|
||||||
@@ -275,14 +242,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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1);
|
||||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName);
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str());
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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), 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();
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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 "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@@ -57,11 +58,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();
|
||||||
|
|
||||||
@@ -73,8 +69,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;
|
||||||
@@ -108,14 +102,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() {
|
||||||
@@ -124,14 +111,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();
|
||||||
@@ -146,7 +125,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
|
||||||
@@ -186,8 +165,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;
|
||||||
@@ -201,7 +178,6 @@ void EpubReaderActivity::loop() {
|
|||||||
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||||
SETTINGS.orientation, [this](const uint8_t orientation) { onReaderMenuBack(orientation); },
|
SETTINGS.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
|
||||||
@@ -238,7 +214,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,18 +222,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,25 +243,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +272,7 @@ void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
|
|||||||
// Apply the user-selected orientation when the menu is dismissed.
|
// Apply the user-selected orientation when the menu is dismissed.
|
||||||
// This ensures the menu can be navigated without immediately rotating the screen.
|
// This ensures the menu can be navigated without immediately rotating the screen.
|
||||||
applyOrientation(orientation);
|
applyOrientation(orientation);
|
||||||
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
|
||||||
@@ -349,13 +328,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) {
|
||||||
@@ -367,8 +347,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();
|
||||||
|
|
||||||
@@ -377,7 +355,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) {
|
||||||
@@ -386,7 +364,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)) {
|
||||||
@@ -395,10 +373,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: {
|
||||||
@@ -409,7 +386,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,
|
||||||
@@ -417,14 +393,13 @@ 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::GO_HOME: {
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||||
@@ -433,31 +408,31 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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();
|
||||||
@@ -476,7 +451,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
}
|
}
|
||||||
pendingSubactivityExit = true;
|
pendingSubactivityExit = true;
|
||||||
}));
|
}));
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -490,39 +464,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;
|
||||||
}
|
}
|
||||||
@@ -539,7 +502,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;
|
||||||
}
|
}
|
||||||
@@ -578,7 +541,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,
|
||||||
@@ -623,7 +586,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;
|
||||||
@@ -631,7 +594,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;
|
||||||
@@ -643,7 +606,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);
|
||||||
@@ -672,9 +637,13 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
|
|||||||
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
|
||||||
const int orientedMarginRight, const int orientedMarginBottom,
|
const int orientedMarginRight, const int orientedMarginBottom,
|
||||||
const int orientedMarginLeft) {
|
const int orientedMarginLeft) {
|
||||||
|
// Force full refresh for pages with images when anti-aliasing is on,
|
||||||
|
// as grayscale tones require half refresh to display correctly
|
||||||
|
bool forceFullRefresh = page->hasImages() && SETTINGS.textAntiAliasing;
|
||||||
|
|
||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (forceFullRefresh || pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
||||||
} else {
|
} else {
|
||||||
@@ -772,8 +741,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) {
|
||||||
@@ -794,8 +763,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;
|
||||||
|
|||||||
@@ -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 "EpubReaderMenuActivity.h"
|
#include "EpubReaderMenuActivity.h"
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
@@ -11,8 +8,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;
|
||||||
@@ -23,16 +18,12 @@ 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
|
||||||
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;
|
||||||
@@ -53,4 +44,5 @@ 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +56,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();
|
||||||
@@ -113,9 +85,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
|
||||||
@@ -131,18 +104,18 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -32,35 +30,33 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fixed menu layout (order matters for up/down navigation).
|
// Fixed menu layout (order matters for up/down navigation).
|
||||||
const std::vector<MenuItem> menuItems = {
|
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER},
|
||||||
{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, {MenuAction::ROTATE_SCREEN, "Reading Orientation"},
|
{MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION},
|
||||||
{MenuAction::GO_TO_PERCENT, "Go to %"}, {MenuAction::GO_HOME, "Go Home"},
|
{MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT},
|
||||||
{MenuAction::SYNC, "Sync Progress"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}};
|
{MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON},
|
||||||
|
{MenuAction::SYNC, StrId::STR_SYNC_PROGRESS},
|
||||||
|
{MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}};
|
||||||
|
|
||||||
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};
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
int totalPages = 0;
|
int totalPages = 0;
|
||||||
int bookProgressPercent = 0;
|
int bookProgressPercent = 0;
|
||||||
|
|
||||||
const std::function<void(uint8_t)> onBack;
|
const std::function<void(uint8_t)> onBack;
|
||||||
const std::function<void(MenuAction)> onAction;
|
const std::function<void(MenuAction)> onAction;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
void renderScreen();
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
const auto labels = mappedInput.mapLabels("Back", "Select", "", "");
|
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();
|
||||||
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)) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user