mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Compare commits
54 Commits
d4026e0f33
...
d35770732e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35770732e | ||
|
|
2cd569bb1f | ||
|
|
bb05b5ae6d | ||
|
|
1803cdc389 | ||
|
|
fea92fd235 | ||
|
|
7254d46401 | ||
|
|
93a94ea838 | ||
|
|
999e60b75e | ||
|
|
5e9e53cc13 | ||
|
|
bd28d9d648 | ||
|
|
f559f408bb | ||
|
|
fce43feea1 | ||
|
|
092fcafd19 | ||
|
|
667e4d954c | ||
|
|
738f79c61c | ||
|
|
e3c2b04cab | ||
|
|
49598aec3e | ||
|
|
8162cf831e | ||
|
|
9b7a6a4eed | ||
|
|
968c65a695 | ||
|
|
73f47423e7 | ||
|
|
40b5420e11 | ||
|
|
dccd200b86 | ||
|
|
1fab1f9ec5 | ||
|
|
ad2bea2122 | ||
|
|
84d08684a2 | ||
|
|
7a5c1e8e0e | ||
|
|
d54f3c5143 | ||
|
|
374f1a1106 | ||
|
|
da4d3b5ea5 | ||
|
|
172916afd4 | ||
|
|
ebcd813ff6 | ||
|
|
712c566664 | ||
|
|
5894ae5afe | ||
|
|
8c1c80787a | ||
|
|
140fcb9db5 | ||
|
|
e0b6b9b28a | ||
|
|
83315b6179 | ||
|
|
8e0d2bece2 | ||
|
|
4848a77e1b | ||
|
|
49190cca6d | ||
|
|
e9c2fe1c87 | ||
|
|
dd1741bf0b | ||
|
|
51c5c3c0aa | ||
|
|
5e24895f6d | ||
|
|
e2ca0e94ca | ||
|
|
a4b9a43ca1 | ||
|
|
c73fca26f5 | ||
|
|
dfd7b615dc | ||
|
|
aca6dceaa8 | ||
|
|
6ca75c4653 | ||
|
|
1b9c8ab545 | ||
|
|
bf6cf83577 | ||
|
|
3a761b18af |
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,3 +7,5 @@ lib/EpdFont/fontsrc
|
||||
.vs
|
||||
build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
|
||||
@ -41,6 +41,8 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
- [ ] Full UTF support
|
||||
- [x] Screen rotation
|
||||
|
||||
Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages).
|
||||
|
||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||
|
||||
## Installing
|
||||
|
||||
@ -81,6 +81,18 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con
|
||||
> [!TIP]
|
||||
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
|
||||
|
||||
### 3.4.1 Calibre Wireless Transfers
|
||||
|
||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||
|
||||
1. Install the plugin in Calibre:
|
||||
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||
- Download the zip file.
|
||||
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||
2. On the device: File Transfer → Connect to Calibre → Join a network.
|
||||
3. Make sure your computer is on the same WiFi network.
|
||||
4. In Calibre, click "Send to device" to transfer books.
|
||||
|
||||
### 3.5 Settings
|
||||
|
||||
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
||||
@ -93,6 +105,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
||||
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
|
||||
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
|
||||
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
|
||||
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
|
||||
- **Status Bar**: Configure the status bar displayed while reading:
|
||||
- "None" - No status bar
|
||||
- "No Progress" - Show status bar without reading progress
|
||||
@ -132,7 +148,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
|
||||
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
|
||||
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
|
||||
- **Calibre Settings**: Set up integration for accessing a Calibre web library or connecting to Calibre as a wireless device.
|
||||
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
|
||||
- **Check for updates**: Check for firmware updates over WiFi.
|
||||
|
||||
### 3.6 Sleep Screen
|
||||
@ -178,6 +194,15 @@ This feature can be disabled in **[Settings](#35-settings)** to help avoid chang
|
||||
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
|
||||
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
|
||||
|
||||
### Supported Languages
|
||||
|
||||
CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages:
|
||||
|
||||
* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others.
|
||||
* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others.
|
||||
|
||||
What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi.
|
||||
|
||||
---
|
||||
|
||||
## 5. Chapter Selection Screen
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_bold
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_bold 12 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_bolditalic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_bolditalic 12 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_italic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_italic 12 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_12_regular
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_12_regular 12 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_bold
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_bold 14 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_bolditalic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_bolditalic 14 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_italic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_italic 14 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_14_regular
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_14_regular 14 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_bold
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_bold 16 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_bolditalic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_bolditalic 16 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_italic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_italic 16 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_16_regular
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_16_regular 16 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_bold
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_bold 18 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_bolditalic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_bolditalic 18 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_italic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_italic 18 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: bookerly_18_regular
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py bookerly_18_regular 18 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_12_bold
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_bold 12 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_12_bolditalic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_bolditalic 12 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_12_italic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_italic 12 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_12_regular
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_12_regular 12 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_14_bold
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_bold 14 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_14_bolditalic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_bolditalic 14 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_14_italic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_italic 14 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_14_regular
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_14_regular 14 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_16_bold
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_bold 16 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_16_bolditalic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_bolditalic 16 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_16_italic
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_italic 16 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_16_regular
|
||||
* size: 16
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_18_bold
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_bold 18 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_18_bolditalic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_bolditalic 18 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_18_italic
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_italic 18 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_18_regular
|
||||
* size: 18
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py notosans_18_regular 18 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: notosans_8_regular
|
||||
* size: 8
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_bold
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_bold 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_bolditalic
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_bolditalic 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_italic
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_italic 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_10_regular
|
||||
* size: 10
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_10_regular 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_bold
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_bold 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_bolditalic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_bolditalic 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_italic
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_italic 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_12_regular
|
||||
* size: 12
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_12_regular 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_bold
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_bold 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_bolditalic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_bolditalic 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_italic
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_italic 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_14_regular
|
||||
* size: 14
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_14_regular 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_bold
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_bold 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_bolditalic
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_bolditalic 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_italic
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_italic 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: opendyslexic_8_regular
|
||||
* size: 8
|
||||
* mode: 2-bit
|
||||
* Command used: fontconvert.py opendyslexic_8_regular 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: ubuntu_10_bold
|
||||
* size: 10
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_10_bold 10 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: ubuntu_10_regular
|
||||
* size: 10
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_10_regular 10 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: ubuntu_12_bold
|
||||
* size: 12
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_12_bold 12 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* name: ubuntu_12_regular
|
||||
* size: 12
|
||||
* mode: 1-bit
|
||||
* Command used: fontconvert.py ubuntu_12_regular 12 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
@ -270,9 +270,17 @@ for index, glyph in enumerate(all_glyphs):
|
||||
glyph_data.extend([b for b in packed])
|
||||
glyph_props.append(props)
|
||||
|
||||
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */")
|
||||
print("#pragma once")
|
||||
print("#include \"EpdFontData.h\"\n")
|
||||
print(f"""/**
|
||||
* generated by fontconvert.py
|
||||
* name: {font_name}
|
||||
* size: {size}
|
||||
* mode: {'2-bit' if is2Bit else '1-bit'}
|
||||
* Command used: {' '.join(sys.argv)}
|
||||
*/
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
""")
|
||||
|
||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||
for c in chunks(glyph_data, 16):
|
||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||
|
||||
@ -226,6 +226,8 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||
setupCacheDir();
|
||||
|
||||
const uint32_t indexingStart = millis();
|
||||
|
||||
// Begin building cache - stream entries to disk immediately
|
||||
if (!bookMetadataCache->beginWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||
@ -233,6 +235,7 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
}
|
||||
|
||||
// OPF Pass
|
||||
const uint32_t opfStart = millis();
|
||||
BookMetadataCache::BookMetadata bookMetadata;
|
||||
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
||||
@ -246,8 +249,10 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart);
|
||||
|
||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||
const uint32_t tocStart = millis();
|
||||
if (!bookMetadataCache->beginTocPass()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||
return false;
|
||||
@ -276,6 +281,7 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart);
|
||||
|
||||
// Close the cache files
|
||||
if (!bookMetadataCache->endWrite()) {
|
||||
@ -284,10 +290,13 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
}
|
||||
|
||||
// Build final book.bin
|
||||
const uint32_t buildStart = millis();
|
||||
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||
return false;
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart);
|
||||
Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart);
|
||||
|
||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||
|
||||
@ -40,7 +40,6 @@ bool BookMetadataCache::endContentOpfPass() {
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||
|
||||
// Open spine file for reading
|
||||
if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
@ -48,12 +47,41 @@ bool BookMetadataCache::beginTocPass() {
|
||||
spineFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
spineHrefIndex.clear();
|
||||
spineHrefIndex.reserve(spineCount);
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
SpineHrefIndexEntry idx;
|
||||
idx.hrefHash = fnvHash64(entry.href);
|
||||
idx.hrefLen = static_cast<uint16_t>(entry.href.size());
|
||||
idx.spineIndex = static_cast<int16_t>(i);
|
||||
spineHrefIndex.push_back(idx);
|
||||
}
|
||||
std::sort(spineHrefIndex.begin(), spineHrefIndex.end(),
|
||||
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||
});
|
||||
spineFile.seek(0);
|
||||
useSpineHrefIndex = true;
|
||||
Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount);
|
||||
} else {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endTocPass() {
|
||||
tocFile.close();
|
||||
spineFile.close();
|
||||
|
||||
spineHrefIndex.clear();
|
||||
spineHrefIndex.shrink_to_fit();
|
||||
useSpineHrefIndex = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -124,6 +152,18 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
// LUTs complete
|
||||
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||
|
||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||
std::vector<int16_t> spineToTocIndex(spineCount, -1);
|
||||
tocFile.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZipFile zip(epubPath);
|
||||
// Pre-open zip file to speed up size calculations
|
||||
if (!zip.open()) {
|
||||
@ -133,31 +173,56 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
tocFile.close();
|
||||
return false;
|
||||
}
|
||||
// TODO: For large ZIPs loading the all localHeaderOffsets will crash.
|
||||
// However not having them loaded is extremely slow. Need a better solution here.
|
||||
// Perhaps only a cache of spine items or a better way to speedup lookups?
|
||||
if (!zip.loadAllFileStatSlims()) {
|
||||
Serial.printf("[%lu] [BMC] Could not load zip local header offsets for size calculations\n", millis());
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
zip.close();
|
||||
return false;
|
||||
// NOTE: We intentionally skip calling loadAllFileStatSlims() here.
|
||||
// For large EPUBs (2000+ chapters), pre-loading all ZIP central directory entries
|
||||
// into memory causes OOM crashes on ESP32-C3's limited ~380KB RAM.
|
||||
// Instead, for large books we use a one-pass batch lookup that scans the ZIP
|
||||
// central directory once and matches against spine targets using hash comparison.
|
||||
// This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion.
|
||||
// See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134
|
||||
|
||||
std::vector<uint32_t> spineSizes;
|
||||
bool useBatchSizes = false;
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount);
|
||||
|
||||
std::vector<ZipFile::SizeTarget> targets;
|
||||
targets.reserve(spineCount);
|
||||
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
std::string path = FsHelpers::normalisePath(entry.href);
|
||||
|
||||
ZipFile::SizeTarget t;
|
||||
t.hash = ZipFile::fnvHash64(path.c_str(), path.size());
|
||||
t.len = static_cast<uint16_t>(path.size());
|
||||
t.index = static_cast<uint16_t>(i);
|
||||
targets.push_back(t);
|
||||
}
|
||||
|
||||
std::sort(targets.begin(), targets.end(), [](const ZipFile::SizeTarget& a, const ZipFile::SizeTarget& b) {
|
||||
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||
});
|
||||
|
||||
spineSizes.resize(spineCount, 0);
|
||||
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||
Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), matched, spineCount);
|
||||
|
||||
targets.clear();
|
||||
targets.shrink_to_fit();
|
||||
|
||||
useBatchSizes = true;
|
||||
}
|
||||
|
||||
uint32_t cumSize = 0;
|
||||
spineFile.seek(0);
|
||||
int lastSpineTocIndex = -1;
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
|
||||
tocFile.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
if (tocEntry.spineIndex == i) {
|
||||
spineEntry.tocIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
spineEntry.tocIndex = spineToTocIndex[i];
|
||||
|
||||
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||
// Logging here is for debugging
|
||||
@ -169,15 +234,24 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
}
|
||||
lastSpineTocIndex = spineEntry.tocIndex;
|
||||
|
||||
// Calculate size for cumulative size
|
||||
size_t itemSize = 0;
|
||||
if (useBatchSizes) {
|
||||
itemSize = spineSizes[i];
|
||||
if (itemSize == 0) {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
} else {
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
@ -248,21 +322,38 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
return;
|
||||
}
|
||||
|
||||
int spineIndex = -1;
|
||||
// find spine index
|
||||
// TODO: This lookup is slow as need to scan through all items each time. We can't hold it all in memory due to size.
|
||||
// But perhaps we can load just the hrefs in a vector/list to do an index lookup?
|
||||
int16_t spineIndex = -1;
|
||||
|
||||
if (useSpineHrefIndex) {
|
||||
uint64_t targetHash = fnvHash64(href);
|
||||
uint16_t targetLen = static_cast<uint16_t>(href.size());
|
||||
|
||||
auto it =
|
||||
std::lower_bound(spineHrefIndex.begin(), spineHrefIndex.end(), SpineHrefIndexEntry{targetHash, targetLen, 0},
|
||||
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||
});
|
||||
|
||||
while (it != spineHrefIndex.end() && it->hrefHash == targetHash && it->hrefLen == targetLen) {
|
||||
spineIndex = it->spineIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
}
|
||||
} else {
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
if (spineEntry.href == href) {
|
||||
spineIndex = i;
|
||||
spineIndex = static_cast<int16_t>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (spineIndex == -1) {
|
||||
Serial.printf("[%lu] [BMC] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const TocEntry entry(title, href, anchor, level, spineIndex);
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class BookMetadataCache {
|
||||
public:
|
||||
@ -53,6 +55,27 @@ class BookMetadataCache {
|
||||
FsFile spineFile;
|
||||
FsFile tocFile;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
uint64_t hrefHash; // FNV-1a 64-bit hash
|
||||
uint16_t hrefLen; // length for collision reduction
|
||||
int16_t spineIndex;
|
||||
};
|
||||
std::vector<SpineHrefIndexEntry> spineHrefIndex;
|
||||
bool useSpineHrefIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a 64-bit hash function
|
||||
static uint64_t fnvHash64(const std::string& s) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (char c : s) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint32_t writeSpineEntry(FsFile& file, const SpineEntry& entry) const;
|
||||
uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const;
|
||||
SpineEntry readSpineEntry(FsFile& file) const;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "HyphenationCommon.h"
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
|
||||
@ -16,14 +17,16 @@ LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 4>;
|
||||
using EntryArray = std::array<LanguageEntry, 5>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
{"german", "de", &germanHyphenator},
|
||||
{"russian", "ru", &russianHyphenator}}};
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
{"spanish", "es", &spanishHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
|
||||
734
lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h
Normal file
734
lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h
Normal file
@ -0,0 +1,734 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "../SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t es_trie_data[] = {
|
||||
0x00, 0x00, 0x34, 0xFC, 0x01, 0x04, 0x16, 0x02, 0x0E, 0x0C, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x22, 0x0F, 0x2C, 0x0F,
|
||||
0x22, 0x0D, 0x2C, 0x0D, 0x0B, 0x16, 0x0B, 0x20, 0x15, 0x16, 0x15, 0x0C, 0x02, 0x0C, 0x17, 0x0E, 0x04, 0x2C, 0x05,
|
||||
0x04, 0x0D, 0x04, 0x21, 0x04, 0x18, 0x0D, 0x04, 0x17, 0x04, 0x0D, 0x17, 0x04, 0x0E, 0x0D, 0x04, 0x0D, 0x21, 0x04,
|
||||
0x0D, 0x21, 0x21, 0x0F, 0x0E, 0x0F, 0x0E, 0x0D, 0x0F, 0x0E, 0x17, 0x33, 0x33, 0x0C, 0x33, 0x16, 0x29, 0x29, 0x0C,
|
||||
0x29, 0x16, 0x21, 0x0C, 0x21, 0x0E, 0x34, 0x0D, 0x3E, 0x36, 0x0D, 0x3F, 0x2B, 0x16, 0x0D, 0x3D, 0x3D, 0x0C, 0x3D,
|
||||
0x16, 0x1F, 0x1F, 0x16, 0x2A, 0x2C, 0x0D, 0x0E, 0x0E, 0x21, 0x1F, 0x0C, 0x2A, 0x0D, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C,
|
||||
0x2A, 0x0B, 0x16, 0x37, 0x20, 0x0C, 0x20, 0x16, 0x35, 0x24, 0x47, 0x47, 0x0C, 0x47, 0x16, 0x20, 0x0B, 0x20, 0x0D,
|
||||
0x0C, 0x20, 0x0D, 0x16, 0x20, 0x20, 0x03, 0x17, 0x0E, 0x0D, 0x23, 0x0E, 0x17, 0x17, 0x17, 0x21, 0x16, 0x0D, 0x18,
|
||||
0x48, 0x49, 0x16, 0x0C, 0x0C, 0x16, 0x0C, 0x16, 0x2D, 0x2B, 0x0E, 0x0D, 0x2B, 0x0E, 0x17, 0x17, 0x2B, 0x34, 0x0B,
|
||||
0x34, 0x0B, 0x0C, 0x34, 0x0B, 0x16, 0x21, 0x20, 0x0D, 0x21, 0x0E, 0x17, 0x20, 0x0D, 0x04, 0x0F, 0x19, 0x0C, 0x0D,
|
||||
0x2E, 0x0F, 0x0E, 0x21, 0x17, 0x0E, 0x2D, 0x0E, 0x2B, 0x0E, 0x22, 0x17, 0x17, 0x0E, 0x22, 0x0D, 0x0E, 0x38, 0x19,
|
||||
0x18, 0x03, 0x0C, 0x22, 0x0B, 0x0E, 0x22, 0x0B, 0x18, 0x40, 0x2A, 0x0C, 0x0C, 0x2A, 0x0C, 0x16, 0x18, 0x0D, 0x0C,
|
||||
0x18, 0x0D, 0x0E, 0x2B, 0x21, 0x2B, 0x17, 0x2A, 0x16, 0x02, 0x33, 0x02, 0x33, 0x0C, 0x02, 0x33, 0x16, 0x35, 0x0E,
|
||||
0x04, 0x0C, 0x20, 0x0C, 0x0C, 0x20, 0x0C, 0x16, 0x2B, 0x0E, 0x0E, 0x2B, 0x0E, 0x18, 0x04, 0x0D, 0x0E, 0x0D, 0x19,
|
||||
0x0E, 0x41, 0x10, 0x2A, 0x20, 0x04, 0x0C, 0x0D, 0x03, 0x0E, 0x16, 0x0D, 0x0E, 0x18, 0x0F, 0x05, 0x0E, 0x07, 0x0E,
|
||||
0xA0, 0x00, 0x51, 0xA0, 0x00, 0x71, 0xA0, 0x00, 0xC3, 0xA3, 0x00, 0x71, 0x74, 0x6E, 0x7A, 0xFD, 0xFD, 0xFD, 0xA1,
|
||||
0x00, 0x71, 0x74, 0xF4, 0xA1, 0x00, 0x71, 0x6E, 0xEF, 0xA3, 0x00, 0x71, 0x74, 0x73, 0x6E, 0xEA, 0xEA, 0xEA, 0xA2,
|
||||
0x00, 0x71, 0x7A, 0x73, 0xE1, 0xE1, 0xA0, 0x00, 0xA2, 0xB6, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xD1, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
||||
0x01, 0xD2, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x05, 0xB1, 0xA0, 0x05, 0xC2, 0xA0, 0x05,
|
||||
0xE2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x05,
|
||||
0x81, 0xA0, 0x06, 0x72, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x05, 0x81, 0x6F, 0x61, 0xFA, 0xFD, 0xAE, 0x06,
|
||||
0x31, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x70, 0x71, 0x73, 0x74, 0x76, 0x7A, 0xED, 0xED, 0xF9, 0xED,
|
||||
0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0x21, 0x6E, 0xE1, 0xA0, 0x06, 0x01, 0xA0, 0x06, 0x92,
|
||||
0xA0, 0x06, 0x12, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x51, 0x21, 0x61,
|
||||
0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6F, 0xFD, 0x28, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0x6C, 0xDA, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xE3, 0xFD, 0x44, 0x75, 0x62, 0x65, 0x6F, 0xFF, 0x65, 0xFF,
|
||||
0x91, 0xFF, 0xC6, 0xFF, 0xEF, 0xA0, 0x04, 0x41, 0xA0, 0x04, 0x52, 0xA0, 0x04, 0x72, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xF5, 0x21, 0x61, 0xF1, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x69, 0x75,
|
||||
0xFE, 0xC5, 0xFE, 0xC8, 0xFE, 0xCE, 0xFE, 0xC8, 0xFE, 0xD7, 0xFE, 0xDC, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE,
|
||||
0xDC, 0xFE, 0xC8, 0xFE, 0xE1, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xEA, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8,
|
||||
0xFE, 0xC8, 0xFE, 0xF4, 0xFE, 0xF4, 0xFF, 0xC7, 0xFF, 0xFD, 0x41, 0x6C, 0xFF, 0x45, 0x21, 0x61, 0xFC, 0x21, 0x75,
|
||||
0xFD, 0x41, 0x72, 0xFF, 0x3B, 0x22, 0x6E, 0x75, 0xF9, 0xFC, 0x41, 0x78, 0xFF, 0x32, 0x41, 0x78, 0xFF, 0x34, 0x21,
|
||||
0xB3, 0xFC, 0x41, 0x6E, 0xFF, 0x27, 0xA0, 0x01, 0x52, 0x21, 0x64, 0xFD, 0xA0, 0x06, 0x43, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x65, 0xFA, 0x23, 0x6E, 0x70, 0x76, 0xF4, 0xFA, 0xFD, 0x21, 0x74, 0xEA, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFA, 0x21,
|
||||
0x69, 0xED, 0x21, 0x6C, 0xFD, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xEA, 0xF4, 0xF7, 0xFD, 0x21, 0x6E, 0xF7, 0x25, 0x61,
|
||||
0x6F, 0xC3, 0x75, 0x65, 0xBB, 0xC0, 0xC8, 0xCB, 0xFD, 0xA1, 0x00, 0x61, 0x69, 0xF5, 0xA0, 0x07, 0xB1, 0x21, 0x62,
|
||||
0xFD, 0xA0, 0x00, 0xF1, 0x21, 0x68, 0xFD, 0x22, 0x69, 0x6F, 0xFA, 0xFA, 0x21, 0x74, 0xF5, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x24, 0x63, 0x73, 0x72, 0x74, 0xEF, 0xF2, 0xFD, 0xEC, 0xA2, 0x06, 0x01, 0x69, 0x65, 0xE0, 0xF7, 0xA0,
|
||||
0x02, 0x91, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFA, 0x21, 0x65, 0xFD, 0x22, 0x65, 0x72, 0xF7, 0xFD, 0x21, 0x6E, 0xEF,
|
||||
0x41, 0x6C, 0xFE, 0x6F, 0x22, 0x65, 0x75, 0xF9, 0xFC, 0x21, 0x74, 0xE3, 0x21, 0x73, 0xFD, 0x21, 0xB3, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x41, 0x63, 0xFE, 0x5A, 0x21, 0x73, 0xFC, 0x22, 0x65, 0x69, 0xFD, 0xF9, 0x21, 0x64, 0xCB, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xD3, 0x21, 0x69, 0xFD, 0x21, 0x6F, 0xB9, 0x21, 0x74, 0xFD,
|
||||
0xA7, 0x07, 0x62, 0x63, 0x67, 0x70, 0x6C, 0x72, 0x78, 0x75, 0xBF, 0xCB, 0xD9, 0xE3, 0xF1, 0xF7, 0xFD, 0x42, 0x63,
|
||||
0x74, 0xFF, 0xA2, 0xFF, 0xA2, 0x41, 0x63, 0xFF, 0x9B, 0x22, 0x69, 0x75, 0xF5, 0xFC, 0x41, 0x69, 0xFF, 0x92, 0x21,
|
||||
0x63, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0xA1, 0xFE, 0x0B, 0x21, 0xC3, 0xFC, 0x41, 0x73, 0xFF, 0x81, 0x21, 0x69, 0xFC,
|
||||
0xA4, 0x07, 0x62, 0x64, 0x66, 0x74, 0x78, 0xE3, 0xEF, 0xF6, 0xFD, 0x41, 0x75, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41,
|
||||
0x6F, 0xFD, 0xEB, 0xA2, 0x07, 0x62, 0x6D, 0x74, 0xF9, 0xFC, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32,
|
||||
0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x43, 0x65, 0xC3, 0x74, 0xFF, 0xFA, 0xFF, 0xFD, 0xFF, 0x2A, 0x41, 0x6E, 0xFF,
|
||||
0x20, 0x21, 0xAD, 0xFC, 0x23, 0x65, 0x69, 0xC3, 0xF9, 0xF9, 0xFD, 0x21, 0x64, 0xF9, 0xA3, 0x05, 0x02, 0x6B, 0x70,
|
||||
0x72, 0xD9, 0xE5, 0xFD, 0xA0, 0x07, 0x62, 0xA0, 0x07, 0xA1, 0x21, 0x6C, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x07, 0x82,
|
||||
0x67, 0xFD, 0xA0, 0x07, 0x82, 0xC1, 0x07, 0x82, 0x70, 0xFE, 0xFD, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF2, 0xF7,
|
||||
0xF7, 0xFA, 0xF7, 0xA0, 0x01, 0xA1, 0x21, 0x62, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x48, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0x6E, 0xFE, 0xF2, 0xFF, 0x46, 0xFF, 0x7F, 0xFF, 0x95, 0xFF, 0xC6, 0xFF, 0xCF, 0xFF, 0xE9,
|
||||
0xFF, 0xFD, 0xA0, 0x0A, 0x01, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0xA2, 0x00, 0x61, 0x6F, 0x61, 0xDE, 0xFD, 0xA0,
|
||||
0x08, 0x12, 0xA0, 0x08, 0x33, 0xC2, 0x07, 0x82, 0x6D, 0x6E, 0xFD, 0x4D, 0xFD, 0x4D, 0xA0, 0x0B, 0x45, 0x23, 0xA1,
|
||||
0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6, 0xF9, 0x21, 0x73, 0xF7, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA1, 0x07, 0x82, 0x6E, 0xFD, 0xA0, 0x08, 0x63, 0xA0,
|
||||
0x08, 0x92, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFA, 0xFD, 0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xFF, 0xB9, 0xFF, 0xBC, 0xFF, 0xBF, 0xFF, 0xEA, 0xFF, 0x70, 0xFF, 0x70, 0xFF, 0xF5, 0x42, 0x73, 0x75,
|
||||
0xFF, 0xEA, 0xFF, 0x96, 0xA0, 0x09, 0x91, 0x21, 0x68, 0xFD, 0xA1, 0x09, 0x81, 0x63, 0xFD, 0x21, 0x6F, 0xFB, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0x61, 0x65, 0x69, 0xE2, 0xFD, 0xA0, 0x00, 0x61, 0xA0,
|
||||
0x0C, 0xC3, 0x21, 0x75, 0xFD, 0xA0, 0x04, 0x91, 0x21, 0x74, 0xFD, 0x22, 0x64, 0x73, 0xF7, 0xFD, 0x22, 0x6E, 0x73,
|
||||
0xF5, 0xF5, 0x21, 0x6F, 0xFB, 0x22, 0x74, 0x7A, 0xED, 0xED, 0x21, 0x6E, 0xFB, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0x43, 0x63, 0x65, 0x6E, 0xFF, 0xEF, 0xFD, 0x20, 0xFF, 0xFD, 0x21, 0x6E, 0xD8, 0x23, 0x65, 0x61, 0x69, 0xD8, 0xF3,
|
||||
0xFD, 0x21, 0x6C, 0xF9, 0x41, 0x69, 0xFD, 0x1D, 0xA0, 0x04, 0xA2, 0xA0, 0x04, 0xC2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xB3, 0xEF, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xF5, 0x22, 0x6C, 0x6F, 0xDC, 0xF1, 0xA2, 0x00, 0x61, 0x61, 0x69, 0xD4, 0xFB, 0xA0, 0x0D, 0x43, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62, 0xF4, 0x21, 0xA1, 0xFD, 0x22, 0xC3, 0x61, 0xFD, 0xFA, 0x23,
|
||||
0x66, 0x6D, 0x72, 0xEF, 0xF2, 0xFB, 0xA0, 0x0D, 0x73, 0x21, 0x62, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA0,
|
||||
0x0D, 0x42, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x70, 0xFD, 0x22, 0xA1, 0xB3, 0xF1, 0xFD, 0x21, 0x70, 0xEF,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x6D, 0xE3, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xFA,
|
||||
0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFE, 0x28, 0x21, 0x73, 0xFC, 0x21, 0x6C, 0xCB, 0x22, 0x69,
|
||||
0x65, 0xFA, 0xFD, 0x45, 0x61, 0xC3, 0x65, 0x69, 0x68, 0xFF, 0xB0, 0xFF, 0xCF, 0xFF, 0xDD, 0xFF, 0xEE, 0xFF, 0xFB,
|
||||
0x21, 0x6E, 0xF0, 0xA0, 0x06, 0xD2, 0x41, 0x69, 0xFB, 0xE3, 0xA0, 0x0E, 0x92, 0x21, 0x65, 0xFD, 0xC3, 0x0D, 0xB3,
|
||||
0x63, 0x72, 0x6A, 0xFF, 0xF6, 0xFB, 0xD9, 0xFF, 0xFD, 0x21, 0x6F, 0xEE, 0x21, 0xAD, 0xFD, 0x43, 0x67, 0x69, 0xC3,
|
||||
0xFB, 0xC7, 0xFF, 0xE8, 0xFF, 0xFD, 0xA0, 0x06, 0xB2, 0xA1, 0x0E, 0x92, 0x61, 0xDB, 0x22, 0x63, 0x72, 0xF8, 0xFB,
|
||||
0x21, 0x65, 0xFB, 0x41, 0x72, 0xFB, 0xAD, 0xA1, 0x0E, 0x92, 0x73, 0xCA, 0x23, 0x65, 0x61, 0x69, 0xC5, 0xFB, 0xC5,
|
||||
0x21, 0x61, 0xBE, 0xC6, 0x0D, 0xB3, 0x72, 0x6C, 0x61, 0x6D, 0x74, 0x6F, 0xFF, 0xD3, 0xFF, 0xEA, 0xFF, 0xED, 0xFF,
|
||||
0xF6, 0xFF, 0xFD, 0xFB, 0x9A, 0xA0, 0x05, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0xC3, 0x05, 0x81, 0x64, 0x65,
|
||||
0x75, 0xFC, 0x8E, 0xFF, 0x9D, 0xFF, 0xFD, 0xC1, 0x0E, 0x92, 0x6F, 0xFF, 0x91, 0x43, 0xA1, 0xA9, 0xB3, 0xFF, 0x8B,
|
||||
0xFF, 0x8B, 0xFF, 0x8B, 0x45, 0x61, 0x65, 0x6C, 0x6F, 0xC3, 0xFF, 0x81, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, 0x81, 0xFF,
|
||||
0xF6, 0x42, 0x61, 0x6F, 0xFF, 0x71, 0xFF, 0x71, 0x41, 0x72, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41, 0x72, 0xFF, 0x63,
|
||||
0x21, 0x65, 0xFC, 0x41, 0x61, 0xFB, 0x3B, 0x42, 0x6D, 0x74, 0xFB, 0x37, 0xFF, 0xFC, 0xC7, 0x0D, 0xB3, 0x6E, 0x67,
|
||||
0x6C, 0x7A, 0x6D, 0x63, 0x73, 0xFF, 0xB4, 0xFF, 0x63, 0xFF, 0xD0, 0xFF, 0xE0, 0xFF, 0xEB, 0xFF, 0xF2, 0xFF, 0xF9,
|
||||
0x41, 0x65, 0xFF, 0x5B, 0x41, 0x73, 0xFF, 0x8F, 0x21, 0x65, 0xFC, 0xA2, 0x0D, 0xB3, 0x70, 0x72, 0xF5, 0xFD, 0x42,
|
||||
0x61, 0x65, 0xFF, 0x27, 0xFF, 0x39, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x20, 0xFF, 0x95, 0xFF, 0x20, 0xFF, 0x20,
|
||||
0xA2, 0x0D, 0xB3, 0x72, 0x6C, 0xEC, 0xF3, 0xA0, 0x0D, 0xE3, 0xC1, 0x0D, 0xE3, 0x6E, 0xFA, 0xE8, 0xA0, 0x0E, 0x72,
|
||||
0xA1, 0x05, 0x81, 0x69, 0xFD, 0xA1, 0x0D, 0xE3, 0x6E, 0xFB, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA, 0xEA, 0xED,
|
||||
0xFB, 0xEA, 0x41, 0x76, 0xFF, 0x0D, 0x41, 0x6D, 0xFF, 0x09, 0x22, 0x65, 0x6F, 0xF8, 0xFC, 0x48, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xD7, 0xFE, 0xE4, 0xFF, 0x23, 0xFF, 0x8D, 0xFF, 0xB0, 0xFF, 0xCB, 0xFF, 0xE8,
|
||||
0xFF, 0xFB, 0x21, 0x74, 0xE7, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFB, 0xB0, 0x21, 0xBA, 0xFC, 0x22, 0x75, 0xC3, 0xF9,
|
||||
0xFD, 0xA0, 0x01, 0x11, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x21, 0x64, 0xFB, 0xA2,
|
||||
0x04, 0xA2, 0x63, 0x72, 0xEA, 0xFD, 0x21, 0x6C, 0xE8, 0x21, 0x75, 0xFD, 0x21, 0x62, 0xFD, 0xA1, 0x04, 0xC2, 0x6D,
|
||||
0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD, 0xE3, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFD, 0x94, 0xFD, 0xD0, 0xFD, 0xD0, 0xFD, 0xD0, 0xFF, 0xDB, 0xFD, 0xD0, 0xFF,
|
||||
0xF0, 0x22, 0xA1, 0xAD, 0xB4, 0xB4, 0x24, 0x61, 0xC3, 0x65, 0x69, 0xAF, 0xFB, 0xAF, 0xAF, 0x21, 0x62, 0xF7, 0xC2,
|
||||
0x01, 0x11, 0x61, 0x6F, 0xFF, 0xA3, 0xFF, 0xA3, 0x21, 0x62, 0xF7, 0x21, 0xAD, 0xFD, 0xA2, 0x04, 0x91, 0x69, 0xC3,
|
||||
0xEE, 0xFD, 0x41, 0x74, 0xFA, 0x1F, 0x21, 0x72, 0xFC, 0x21, 0x6F, 0xFD, 0xA1, 0x0D, 0xB2, 0x62, 0xFD, 0x41, 0x72,
|
||||
0xFE, 0x63, 0x21, 0x61, 0xFC, 0xA1, 0x0D, 0xB2, 0x74, 0xFD, 0xA0, 0x0D, 0xB2, 0xA0, 0x0E, 0xB2, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xDE, 0xEA,
|
||||
0xEF, 0xEF, 0xEF, 0xF5, 0x42, 0x65, 0x6F, 0xFF, 0x88, 0xFF, 0xF1, 0xC3, 0x00, 0x61, 0x61, 0x6F, 0x72, 0xFD, 0xF4,
|
||||
0xFF, 0x3C, 0xFF, 0xF9, 0x41, 0x72, 0xFB, 0x6B, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xFB, 0x4A, 0x42, 0x6F, 0xC3, 0xFB,
|
||||
0x46, 0xFF, 0xFC, 0x43, 0x72, 0x69, 0x73, 0xFB, 0x3C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42, 0x73, 0x74, 0xFB, 0x32, 0xFB,
|
||||
0x32, 0x41, 0xAD, 0xFB, 0x48, 0x22, 0x69, 0xC3, 0xF5, 0xFC, 0x21, 0x6D, 0xFB, 0x41, 0x6D, 0xFB, 0x1F, 0x21, 0x72,
|
||||
0xFC, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x41, 0x76, 0xFB, 0x10, 0x21, 0xA1, 0xFC, 0xA0, 0x04, 0xE2,
|
||||
0x21, 0x70, 0xFD, 0x23, 0x61, 0xC3, 0x75, 0xF3, 0xF7, 0xFD, 0x21, 0x72, 0xF9, 0x41, 0x69, 0xFB, 0x5E, 0x21, 0x64,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x41, 0xB1, 0xFA, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xBA, 0xFD, 0x23, 0x6F, 0x75, 0xC3, 0xF3,
|
||||
0xFA, 0xFD, 0x41, 0x73, 0xFF, 0x42, 0x21, 0xBA, 0xFC, 0x42, 0x75, 0xC3, 0xFA, 0xF7, 0xFF, 0xFD, 0x41, 0x67, 0xFA,
|
||||
0xD3, 0x41, 0x7A, 0xFE, 0x14, 0x41, 0x6A, 0xFA, 0xC8, 0x23, 0xA9, 0xAD, 0xB3, 0xF4, 0xF8, 0xFC, 0x42, 0x61, 0xC3,
|
||||
0xF9, 0x40, 0xFB, 0x35, 0x42, 0x6D, 0x74, 0xF9, 0x39, 0xF9, 0x39, 0x43, 0x7A, 0x6D, 0x73, 0xFF, 0xF2, 0xFA, 0xAF,
|
||||
0xFF, 0xF9, 0x45, 0x65, 0xC3, 0x69, 0x6F, 0x71, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xF6, 0xFF, 0xDD, 0xFA, 0xA5, 0x41,
|
||||
0xAD, 0xFF, 0x76, 0x42, 0x69, 0xC3, 0xFF, 0x72, 0xFF, 0xFC, 0x43, 0xA1, 0xA9, 0xB3, 0xFA, 0x8A, 0xFA, 0x8A, 0xFA,
|
||||
0x8A, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFA, 0x80, 0xFF, 0xF6, 0xFA, 0x80, 0xFA, 0x80, 0x41, 0x65, 0xFA, 0xD8, 0x21,
|
||||
0x72, 0xFC, 0x42, 0x6E, 0x74, 0xFA, 0xA1, 0xFA, 0x6C, 0xA0, 0x00, 0x40, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||
0xA9, 0xAD, 0xFA, 0x94, 0xFF, 0xFD, 0x22, 0x65, 0xC3, 0xE9, 0xF9, 0x22, 0x61, 0x72, 0xE1, 0xFB, 0x41, 0x67, 0xFF,
|
||||
0x42, 0x41, 0xBA, 0xFF, 0x28, 0x42, 0x75, 0xC3, 0xFF, 0x24, 0xFF, 0xFC, 0xCE, 0x07, 0x62, 0x62, 0x64, 0x66, 0x67,
|
||||
0x63, 0x6A, 0x6C, 0x6E, 0x6D, 0x70, 0x65, 0x71, 0x7A, 0x73, 0xFF, 0x00, 0xFF, 0x1A, 0xFF, 0x27, 0xFF, 0x40, 0xFF,
|
||||
0x57, 0xFF, 0x65, 0xFF, 0x97, 0xFF, 0xAB, 0xFF, 0xBC, 0xFF, 0xEC, 0xFF, 0xF1, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0xF9,
|
||||
0xA0, 0x05, 0x02, 0x49, 0x6F, 0x63, 0x69, 0x66, 0x67, 0x76, 0x61, 0x73, 0x74, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x71,
|
||||
0xFA, 0x0C, 0xFA, 0x0C, 0xFA, 0x0C, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x0C, 0x41, 0x64, 0xF8, 0x73, 0x21, 0x6E, 0xFC,
|
||||
0x21, 0x69, 0xFD, 0xC3, 0x07, 0x62, 0x6E, 0x6D, 0x76, 0xFF, 0xDA, 0xFE, 0xDD, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0xF7,
|
||||
0x21, 0x65, 0xFC, 0x41, 0x61, 0xF9, 0xD3, 0x22, 0x69, 0x67, 0xF9, 0xFC, 0xC4, 0x07, 0x62, 0x62, 0x72, 0x63, 0x6A,
|
||||
0xFE, 0xC1, 0xFF, 0xFB, 0xF9, 0xCA, 0xFA, 0x73, 0x42, 0xA1, 0xB3, 0xF9, 0xBB, 0xF9, 0xBB, 0x43, 0x61, 0xC3, 0x6F,
|
||||
0xF9, 0xB4, 0xFF, 0xF9, 0xF9, 0xB4, 0x42, 0x63, 0x71, 0xFF, 0xF6, 0xF9, 0xAA, 0x42, 0x63, 0x71, 0xFF, 0xD0, 0xF9,
|
||||
0xA3, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xEF, 0xFD, 0xC1, 0x05, 0x81, 0x74, 0xFC, 0x34, 0x41, 0x74, 0xFC, 0x2E,
|
||||
0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x42, 0xA9, 0xB3, 0xF8, 0x05, 0xF8, 0x05, 0x48, 0x72, 0x61, 0x73,
|
||||
0x6D, 0x65, 0xC3, 0x64, 0x66, 0xF7, 0xFE, 0xF7, 0xFE, 0xF7, 0xFE, 0xFF, 0x16, 0xF7, 0xFE, 0xFF, 0xF9, 0xF7, 0xFE,
|
||||
0xF9, 0x7B, 0xC1, 0x05, 0x81, 0x72, 0xF7, 0xE5, 0x42, 0xAD, 0xA1, 0xFF, 0xFA, 0xF7, 0xDF, 0x43, 0x69, 0xC3, 0x74,
|
||||
0xFF, 0xDA, 0xFF, 0xF9, 0xF9, 0x55, 0x41, 0xA1, 0xF9, 0x4E, 0x42, 0x61, 0xC3, 0xF9, 0x4A, 0xFF, 0xFC, 0x41, 0x7A,
|
||||
0xF9, 0x40, 0x21, 0xAD, 0xFC, 0x22, 0x69, 0xC3, 0xF9, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x69, 0xFD, 0xC5, 0x07, 0x62,
|
||||
0x62, 0x6D, 0x6E, 0x73, 0x74, 0xFF, 0x95, 0xFF, 0xA7, 0xFF, 0xD9, 0xFF, 0xE7, 0xFF, 0xFD, 0x43, 0x61, 0x65, 0x6F,
|
||||
0xF9, 0x1C, 0xF9, 0x1C, 0xF9, 0x1C, 0xC2, 0x07, 0x82, 0x62, 0x6D, 0xF9, 0x15, 0xFF, 0xF6, 0x45, 0xA1, 0xA9, 0xAD,
|
||||
0xB3, 0xBA, 0xFF, 0xF7, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9, 0xF0, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3,
|
||||
0xFE, 0xBD, 0xFE, 0xEA, 0xFF, 0x13, 0xFF, 0x2F, 0xFF, 0xCB, 0xFF, 0xF0, 0xA1, 0x00, 0x61, 0x65, 0xED, 0x43, 0x6E,
|
||||
0x72, 0x73, 0xFE, 0xD2, 0xF8, 0xE1, 0xF8, 0xE1, 0xA0, 0x0C, 0x12, 0xA1, 0x0C, 0x12, 0x72, 0xFD, 0x21, 0x6E, 0xF8,
|
||||
0x21, 0xB3, 0xFD, 0x23, 0x61, 0x6F, 0xC3, 0xF2, 0xF5, 0xFD, 0x41, 0x70, 0xF7, 0x45, 0x41, 0x6E, 0xF7, 0x41, 0x21,
|
||||
0x65, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x74, 0xEC, 0xFD, 0x41, 0xA9, 0xF8,
|
||||
0xBA, 0x21, 0x65, 0xD6, 0x21, 0x69, 0xFD, 0xC7, 0x0F, 0x93, 0x65, 0x6C, 0x64, 0x6E, 0x72, 0xC3, 0x6D, 0xFF, 0xBE,
|
||||
0xF9, 0x7B, 0xFF, 0xD6, 0xFF, 0xF1, 0xF8, 0x9F, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0xA1, 0xFC, 0xEB, 0x21, 0xC3, 0xFC,
|
||||
0x42, 0x70, 0x74, 0xF8, 0xA9, 0xF7, 0x03, 0x22, 0x75, 0x65, 0xF6, 0xF9, 0x41, 0xA1, 0xF8, 0x74, 0x44, 0x61, 0xC3,
|
||||
0x65, 0x6F, 0xF8, 0x70, 0xFF, 0xFC, 0xF8, 0x70, 0xF8, 0x70, 0x21, 0x74, 0xF3, 0x41, 0x65, 0xF6, 0xE3, 0x21, 0x75,
|
||||
0xFC, 0x21, 0x6C, 0xFD, 0x41, 0x61, 0xFA, 0xF6, 0x41, 0x6E, 0xFC, 0xB6, 0x21, 0x65, 0xFC, 0x21, 0x6D, 0xFD, 0x41,
|
||||
0x65, 0xF8, 0x4B, 0x23, 0x63, 0x69, 0x74, 0xEE, 0xF9, 0xFC, 0x41, 0x69, 0xF8, 0x66, 0x21, 0x6D, 0xFC, 0x21, 0xB3,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0xC6, 0x0F, 0x93, 0x63, 0x73, 0x66, 0x6C, 0x72, 0x74, 0xFF, 0xB7, 0xFF, 0xCD, 0xFF, 0xD7,
|
||||
0xFF, 0xEC, 0xFB, 0x06, 0xFF, 0xFD, 0x41, 0x75, 0xFC, 0x7F, 0x21, 0x63, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x6D, 0xFF,
|
||||
0x57, 0x21, 0x65, 0xFC, 0x21, 0x6C, 0xAA, 0x21, 0x70, 0xFD, 0x41, 0x74, 0xFF, 0x4A, 0x41, 0x65, 0xF8, 0x29, 0x41,
|
||||
0x6D, 0xF6, 0x7F, 0x21, 0xAD, 0xFC, 0x41, 0x75, 0xF8, 0x1E, 0x44, 0x61, 0x69, 0xC3, 0x72, 0xF8, 0x1A, 0xFF, 0xF5,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0x22, 0x70, 0x74, 0xE4, 0xF3, 0xA5, 0x0F, 0x93, 0x6A, 0x6C, 0x6D, 0x6E, 0x73, 0xCB, 0xD2,
|
||||
0xD8, 0xDB, 0xFB, 0x41, 0x69, 0xFC, 0x36, 0x21, 0x70, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x63, 0xFA,
|
||||
0x65, 0x21, 0x69, 0xFC, 0x41, 0xAD, 0xF7, 0xCF, 0x42, 0x69, 0xC3, 0xF7, 0xCB, 0xFF, 0xFC, 0x21, 0x64, 0xF9, 0xA3,
|
||||
0x0F, 0x93, 0x63, 0x66, 0x72, 0xE8, 0xEF, 0xFD, 0x41, 0x62, 0xFA, 0xEF, 0xA1, 0x0F, 0x93, 0x72, 0xFC, 0x42, 0xA9,
|
||||
0xB3, 0xF7, 0x9E, 0xF7, 0x9E, 0x42, 0x61, 0xC3, 0xF7, 0x97, 0xFF, 0xF9, 0x21, 0x74, 0xF9, 0x41, 0x74, 0xFF, 0x50,
|
||||
0xA2, 0x0F, 0xC3, 0x73, 0x72, 0xF9, 0xFC, 0xA0, 0x0F, 0xC3, 0xC3, 0x0F, 0xC3, 0x6E, 0x6D, 0x72, 0xFD, 0x8F, 0xFA,
|
||||
0x1F, 0xF7, 0x7F, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA, 0xF1, 0xF4, 0xF1, 0xF1, 0x41, 0x79, 0xF8, 0x11, 0x21,
|
||||
0x61, 0xFC, 0x48, 0x69, 0x68, 0x61, 0x65, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xC2, 0xF8, 0x91, 0xFF, 0x31, 0xFF, 0x82,
|
||||
0xFF, 0xB1, 0xFF, 0xBE, 0xFF, 0xEE, 0xFF, 0xFD, 0x41, 0x74, 0xF8, 0x78, 0x21, 0x73, 0xFC, 0x41, 0x73, 0xF8, 0x71,
|
||||
0x21, 0x65, 0xFC, 0x22, 0x65, 0x6F, 0xF6, 0xFD, 0x22, 0x62, 0x72, 0xD4, 0xFB, 0x41, 0x73, 0xFD, 0x21, 0x21, 0x61,
|
||||
0xFC, 0x41, 0x61, 0xFD, 0x39, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62,
|
||||
0xFD, 0xA3, 0x00, 0x61, 0x75, 0x6F, 0x65, 0xE1, 0xEA, 0xFD, 0x41, 0x70, 0xF6, 0x09, 0x21, 0x6D, 0xFC, 0x41, 0x6A,
|
||||
0xF6, 0x02, 0xA0, 0x05, 0x52, 0x21, 0x74, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0x22, 0x62, 0x6C, 0xF0, 0xFD,
|
||||
0x22, 0x69, 0x6F, 0xE8, 0xFB, 0x21, 0x65, 0xFB, 0x21, 0x6C, 0xFD, 0xC1, 0x0E, 0xB2, 0x67, 0xF9, 0x8A, 0x41, 0x67,
|
||||
0xF5, 0x63, 0xA1, 0x0E, 0xB2, 0x65, 0xFC, 0x41, 0xB1, 0xF9, 0x7B, 0xA1, 0x0E, 0xB2, 0xC3, 0xFC, 0x43, 0xA1, 0xA9,
|
||||
0xB3, 0xF5, 0x51, 0xF5, 0x51, 0xF5, 0x51, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xF5, 0x47, 0xFF, 0xF6, 0xF5, 0x47, 0xF5,
|
||||
0x47, 0x21, 0x74, 0xF3, 0xC2, 0x0E, 0xB2, 0x64, 0x6E, 0xFA, 0x38, 0xFF, 0xFD, 0xA0, 0x10, 0xD2, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF9, 0x3A, 0xFB,
|
||||
0x1F, 0xFF, 0xB7, 0xFF, 0xC1, 0xFF, 0xCA, 0xFF, 0xE9, 0xFF, 0xF5, 0x21, 0x73, 0xEA, 0x41, 0x78, 0xF8, 0x7E, 0x21,
|
||||
0xB3, 0xFC, 0x21, 0xC3, 0xFD, 0x22, 0x61, 0x69, 0xF3, 0xFD, 0xC2, 0x00, 0x61, 0x65, 0x72, 0xFF, 0x8C, 0xFF, 0xFB,
|
||||
0x42, 0x61, 0x6F, 0xF6, 0x48, 0xF6, 0x48, 0x21, 0x65, 0xF9, 0x21, 0x6D, 0xFD, 0x41, 0x65, 0xF6, 0x3B, 0x21, 0x65,
|
||||
0xFC, 0x21, 0x6D, 0xFD, 0x22, 0x75, 0x65, 0xF3, 0xFD, 0x41, 0x65, 0xF5, 0x60, 0x21, 0x72, 0xFC, 0x41, 0x6F, 0xF5,
|
||||
0x59, 0x21, 0x72, 0xFC, 0x41, 0x72, 0xFB, 0x39, 0x21, 0x67, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x68,
|
||||
0xFB, 0x2C, 0x21, 0x6F, 0xFC, 0x21, 0x63, 0xFD, 0x41, 0x69, 0xF6, 0x72, 0x21, 0x6E, 0xFC, 0x41, 0x72, 0xF6, 0x6B,
|
||||
0x23, 0x6C, 0x6D, 0x65, 0xF2, 0xF9, 0xFC, 0x41, 0xB3, 0xFC, 0x0A, 0x42, 0x6F, 0xC3, 0xFC, 0x06, 0xFF, 0xFC, 0x21,
|
||||
0x73, 0xF9, 0xA0, 0x05, 0x22, 0x21, 0x65, 0xFD, 0x42, 0x6A, 0x6E, 0xFF, 0xFD, 0xF9, 0x78, 0x21, 0x6F, 0xF9, 0x42,
|
||||
0x65, 0x69, 0xFF, 0xFD, 0xF5, 0x0B, 0x24, 0x65, 0x61, 0x69, 0x74, 0xBC, 0xD4, 0xE6, 0xF9, 0x41, 0x74, 0xFF, 0xA2,
|
||||
0xC4, 0x02, 0xB1, 0x62, 0x63, 0x6E, 0x74, 0xFF, 0x9B, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFC, 0x41, 0x74, 0xF6, 0xD3,
|
||||
0x21, 0x73, 0xFC, 0xA1, 0x01, 0x82, 0x65, 0xFD, 0x42, 0x6F, 0xC3, 0xF8, 0xA2, 0xFA, 0x85, 0x41, 0x69, 0xF5, 0xE2,
|
||||
0x21, 0x65, 0xFC, 0x41, 0xA9, 0xFD, 0x00, 0x42, 0x65, 0xC3, 0xFC, 0xFC, 0xFF, 0xFC, 0x41, 0x62, 0xF5, 0xB3, 0xA4,
|
||||
0x09, 0xA3, 0x6D, 0x63, 0x6A, 0x72, 0xE3, 0xEE, 0xF5, 0xFC, 0x41, 0xAD, 0xFA, 0xC6, 0x42, 0x69, 0xC3, 0xFA, 0xC2,
|
||||
0xFF, 0xFC, 0xA1, 0x09, 0xA3, 0x6D, 0xF9, 0xA0, 0x09, 0xA3, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFC, 0x2F, 0xFE, 0xC3,
|
||||
0xF4, 0x14, 0xF4, 0x14, 0xA1, 0x09, 0xA3, 0x6A, 0xF3, 0x43, 0x61, 0xC3, 0x65, 0xF4, 0x02, 0xF5, 0xF7, 0xF4, 0x02,
|
||||
0x21, 0x72, 0xF6, 0x21, 0x65, 0xFD, 0xA1, 0x09, 0xA3, 0x6D, 0xFD, 0xA0, 0x09, 0xD3, 0xC1, 0x09, 0xD3, 0x6A, 0xF6,
|
||||
0x40, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF7, 0xF7, 0xF7, 0xFA, 0xF7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xC3, 0xFF, 0x85, 0xFF, 0xA7, 0xFF, 0xBD, 0xFF, 0xC2, 0xFF, 0xD2, 0xFF, 0xE7, 0xFF, 0xF5, 0x41, 0x73, 0xF6, 0x3B,
|
||||
0x42, 0x6C, 0x75, 0xF6, 0x37, 0xFF, 0xFC, 0x41, 0x6C, 0xF6, 0x30, 0x41, 0x72, 0xFF, 0x59, 0x41, 0x6D, 0xF6, 0x28,
|
||||
0x44, 0xA1, 0xAD, 0xB3, 0xBA, 0xFF, 0xF4, 0xF6, 0x27, 0xFF, 0xF8, 0xFF, 0xFC, 0xC5, 0x01, 0x82, 0x61, 0xC3, 0x69,
|
||||
0x6F, 0x75, 0xFF, 0xE0, 0xFF, 0xF3, 0xF6, 0x1A, 0xFF, 0xEB, 0xFF, 0xEF, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF,
|
||||
0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFF, 0xDE,
|
||||
0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0xF0, 0x42, 0x6E, 0x78, 0xFF, 0x8E, 0xFF, 0xEA,
|
||||
0xA0, 0x01, 0x82, 0x41, 0x72, 0xF5, 0x3F, 0x41, 0x72, 0xF5, 0x0B, 0x22, 0x61, 0x6F, 0xF8, 0xFC, 0x42, 0x6E, 0x70,
|
||||
0xF4, 0xEA, 0xF4, 0xEA, 0x21, 0x65, 0xF9, 0x41, 0x70, 0xF4, 0xE0, 0x22, 0x61, 0x6F, 0xFC, 0xFC, 0x41, 0x61, 0xFA,
|
||||
0xE0, 0x21, 0x75, 0xFC, 0x41, 0x6D, 0xFF, 0x00, 0x21, 0xA1, 0xFC, 0x41, 0x65, 0xF4, 0xBD, 0x22, 0xC3, 0x69, 0xF9,
|
||||
0xFC, 0x41, 0x69, 0xFB, 0x63, 0x21, 0x6C, 0xFC, 0x42, 0x6D, 0x63, 0xF4, 0x9C, 0xF3, 0x1F, 0x22, 0x61, 0x69, 0xF6,
|
||||
0xF9, 0x41, 0x61, 0xFE, 0xDD, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xF4, 0x89, 0x42, 0x61, 0x69, 0xFB, 0x45, 0xF4, 0xEA,
|
||||
0x43, 0x63, 0x68, 0x6E, 0xF4, 0xEC, 0xFF, 0xD2, 0xF4, 0xFD, 0x21, 0x65, 0xF6, 0x24, 0x61, 0x65, 0x6C, 0x72, 0xE5,
|
||||
0xE8, 0xEC, 0xFD, 0x41, 0x63, 0xF4, 0x85, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xF4, 0x72, 0x21, 0xC3, 0xFC, 0x41, 0x67,
|
||||
0xF4, 0x5A, 0x21, 0x75, 0xFC, 0x22, 0x6D, 0x72, 0xF6, 0xFD, 0x41, 0x69, 0xF4, 0x6E, 0x41, 0x62, 0xF2, 0xCD, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x76, 0xFD, 0x21, 0x6F, 0xFD, 0xCC, 0x09, 0xA3, 0x62, 0x63, 0x64, 0x67, 0x6C, 0x6E, 0x70, 0x66,
|
||||
0x72, 0x73, 0x74, 0x6D, 0xFF, 0x6B, 0xFF, 0x77, 0xFF, 0x7E, 0xFF, 0x87, 0xFF, 0x95, 0xFF, 0xA8, 0xFF, 0xCC, 0xFF,
|
||||
0xD9, 0xFF, 0xEA, 0xFF, 0xEF, 0xFA, 0x67, 0xFF, 0xFD, 0xC1, 0x02, 0x91, 0x69, 0xF4, 0x16, 0x21, 0x63, 0xFA, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x64, 0xF4, 0x78, 0x22, 0x75, 0x65, 0xFC, 0xAC, 0x41, 0x6F, 0xFA, 0x27, 0x42, 0x63, 0x61, 0xFF,
|
||||
0xFC, 0xF8, 0x70, 0x41, 0x76, 0xF2, 0x79, 0x21, 0xAD, 0xFC, 0x42, 0x69, 0xC3, 0xF4, 0x24, 0xFF, 0xFD, 0x21, 0x75,
|
||||
0xF9, 0xC2, 0x02, 0x91, 0x61, 0x68, 0xFF, 0x7D, 0xFA, 0x12, 0xC6, 0x09, 0xA3, 0x66, 0x6C, 0x6E, 0x71, 0x78, 0x76,
|
||||
0xFF, 0xCF, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xF7, 0xFE, 0x17, 0x42, 0x6F, 0x61, 0xF2, 0x4A, 0xF2, 0x4A,
|
||||
0x21, 0x75, 0xF9, 0x41, 0x6C, 0xFF, 0x2D, 0x21, 0x61, 0xFC, 0x21, 0x75, 0xFD, 0xC3, 0x09, 0xA3, 0x63, 0x67, 0x6E,
|
||||
0xFF, 0xF3, 0xFF, 0xFD, 0xF3, 0xB3, 0x41, 0x73, 0xFB, 0x5F, 0x44, 0x74, 0x61, 0xC3, 0x65, 0xF3, 0xA3, 0xF2, 0x26,
|
||||
0xF4, 0x1B, 0xF2, 0x26, 0x43, 0x6F, 0x61, 0x6C, 0xF2, 0x19, 0xF2, 0x19, 0xFF, 0xF3, 0x42, 0x63, 0x74, 0xF2, 0x0F,
|
||||
0xF2, 0x0F, 0x21, 0x6E, 0xF9, 0x22, 0x75, 0x65, 0xEC, 0xFD, 0x41, 0x73, 0xF2, 0x00, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0x41, 0x6F, 0xF8, 0x25, 0xA4, 0x09, 0xA3, 0x62, 0x63, 0x66, 0x70, 0xC8, 0xED, 0xF9, 0xFC, 0x41, 0x7A, 0xF1,
|
||||
0xE7, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x09, 0xA3, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x2B,
|
||||
0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3, 0x6E, 0xFD, 0x41, 0x74, 0xF4, 0x1F, 0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3, 0x64,
|
||||
0xFD, 0x41, 0x69, 0xF4, 0x16, 0xA1, 0x09, 0xD3, 0x74, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xE6, 0xFF,
|
||||
0xF2, 0xFD, 0xC7, 0xFD, 0xC7, 0xFF, 0xFB, 0xA0, 0x0C, 0x13, 0x21, 0x67, 0xFD, 0x22, 0x63, 0x74, 0xFA, 0xFA, 0x21,
|
||||
0x70, 0xF5, 0x22, 0x70, 0x6D, 0xF8, 0xFD, 0xA2, 0x05, 0x22, 0x6F, 0x75, 0xF0, 0xFB, 0xA0, 0x0A, 0x92, 0xA0, 0x0A,
|
||||
0xB3, 0xA0, 0x0B, 0x13, 0x23, 0xA1, 0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6,
|
||||
0xF9, 0xA1, 0x0A, 0xB3, 0x73, 0xF7, 0xA0, 0x0A, 0xE3, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0x28, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xD4, 0xD7, 0xED, 0xD7, 0xD7, 0xD7, 0xF5, 0x21,
|
||||
0x72, 0xEF, 0x21, 0x65, 0xFD, 0x48, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x74, 0xFD, 0xE7, 0xFE, 0x87, 0xFE,
|
||||
0xE8, 0xFF, 0x11, 0xFF, 0x55, 0xFF, 0x6D, 0xFF, 0x93, 0xFF, 0xFD, 0x21, 0x6E, 0xE7, 0x58, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x68, 0x61, 0x65,
|
||||
0x69, 0xF2, 0x79, 0xF3, 0xD1, 0xF4, 0x53, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0xC4,
|
||||
0xF4, 0x5A, 0xF7, 0x4E, 0xF4, 0x5A, 0xF9, 0xC2, 0xFB, 0x92, 0xFC, 0x33, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4,
|
||||
0x5A, 0xF4, 0x5A, 0xFC, 0x53, 0xFC, 0xC1, 0xFD, 0xC4, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x16, 0xA0, 0x0D, 0x22, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xC3, 0x00, 0x71, 0x2E, 0x69, 0x65, 0xF0, 0x3F, 0xFF, 0xF3, 0xFF, 0xFD, 0xC3, 0x00,
|
||||
0x71, 0x2E, 0x7A, 0x73, 0xF0, 0x33, 0xF0, 0x39, 0xF0, 0x39, 0xC1, 0x00, 0x71, 0x2E, 0xF0, 0x27, 0xD6, 0x00, 0x81,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xF0, 0x21, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24,
|
||||
0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0,
|
||||
0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0x41, 0x74, 0xF0, 0x69, 0x21, 0x70, 0xFC, 0xD7, 0x00, 0x91,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x65, 0xEF, 0xD5, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0,
|
||||
0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01,
|
||||
0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xFF, 0xFD, 0x42, 0x6F, 0x70, 0xF3, 0xA8, 0xFF, 0xB1,
|
||||
0x41, 0x6E, 0xFB, 0x50, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x69, 0x6F, 0xEF, 0x82, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF,
|
||||
0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE,
|
||||
0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xFF,
|
||||
0xF5, 0xFF, 0xFC, 0x41, 0x74, 0xF1, 0xF3, 0x21, 0x70, 0xFC, 0x41, 0x6F, 0xFC, 0x90, 0x21, 0x6C, 0xFC, 0x41, 0x74,
|
||||
0xF0, 0x5B, 0x41, 0x6D, 0xFA, 0xEF, 0x41, 0x61, 0xEF, 0x9F, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x25, 0x6D, 0x75,
|
||||
0x72, 0x73, 0x6E, 0xE4, 0xEB, 0xEE, 0xF2, 0xFD, 0xA0, 0x02, 0x32, 0x21, 0x61, 0xFD, 0x41, 0x2E, 0xEF, 0x06, 0xA1,
|
||||
0x02, 0x32, 0x73, 0xFC, 0x22, 0x6F, 0x61, 0xF1, 0xFB, 0x41, 0x64, 0xEF, 0x88, 0x23, 0x63, 0x67, 0x72, 0xEB, 0xF7,
|
||||
0xFC, 0x21, 0x6F, 0xE1, 0x41, 0x75, 0xEF, 0x68, 0x21, 0x72, 0xFC, 0x42, 0x64, 0x73, 0xFF, 0xFD, 0xF2, 0xE9, 0x22,
|
||||
0x6C, 0x61, 0xEF, 0xF9, 0x41, 0x6C, 0xEF, 0x64, 0x21, 0x61, 0xFC, 0xA0, 0x07, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0xA1, 0x04, 0x72, 0x72, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xEF, 0xD7, 0xEF, 0xD7, 0xEF,
|
||||
0xD7, 0xEF, 0xD7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEF, 0xC1, 0xEF, 0xC4, 0xEF, 0xC4, 0xEF, 0xC4,
|
||||
0xEF, 0xC4, 0xEF, 0xC4, 0xFF, 0xF0, 0x21, 0x69, 0xEA, 0x21, 0x74, 0xFD, 0x22, 0x66, 0x6E, 0xC3, 0xFD, 0x42, 0x68,
|
||||
0x6F, 0xF2, 0x5F, 0xEF, 0xB4, 0x21, 0x6E, 0xF9, 0xA0, 0x06, 0xF3, 0xA0, 0x07, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF3, 0x4F, 0xF3, 0x26,
|
||||
0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xF5, 0x21, 0x72, 0xE7, 0x21, 0x65, 0xFD, 0x41,
|
||||
0x6C, 0xFA, 0x21, 0x41, 0x6F, 0xF2, 0x6E, 0x24, 0x61, 0x62, 0x63, 0x74, 0xC5, 0xF5, 0xF8, 0xFC, 0x41, 0x6F, 0xEF,
|
||||
0x25, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0xDC, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x68, 0x6C, 0x72, 0x6F, 0x61, 0x75, 0x65, 0x69, 0xC3, 0xEE, 0x30, 0xEE, 0x33, 0xEE, 0x39, 0xEE,
|
||||
0x33, 0xEE, 0x42, 0xEE, 0x47, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x47, 0xFD, 0xF1, 0xEE, 0x4C, 0xEE, 0x33, 0xEE, 0x33,
|
||||
0xFD, 0xFD, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xFE, 0x09, 0xFE, 0x0F, 0xFE, 0x5B, 0xFE, 0xAE, 0xFF,
|
||||
0x19, 0xFF, 0x3C, 0xFF, 0x54, 0xFF, 0x9A, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x74, 0xF2, 0xB2, 0x21, 0x6E, 0xFC, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF1,
|
||||
0x95, 0xF1, 0xD1, 0xF1, 0xD1, 0xFF, 0xFB, 0xF1, 0xD1, 0xF1, 0xD1, 0xF1, 0xD7, 0x21, 0x61, 0xEA, 0xA0, 0x07, 0xC1,
|
||||
0xA0, 0x07, 0xD2, 0xA0, 0x07, 0xF2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x74, 0xFD,
|
||||
0x42, 0x61, 0x6F, 0xFF, 0xFD, 0xEE, 0xA8, 0x21, 0x6D, 0xF9, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0xA0, 0x09, 0x32, 0x21, 0x61, 0xFD, 0x22, 0x72, 0x6C, 0xF7, 0xFD, 0xA0, 0x02, 0x12, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xEF, 0x4A, 0x22, 0x68, 0x6C, 0xF9, 0xFC, 0x22, 0x61, 0x65, 0xE0, 0xE0, 0x21,
|
||||
0x68, 0xFB, 0x21, 0x74, 0xE3, 0x22, 0x63, 0x72, 0xFA, 0xFD, 0x23, 0xB3, 0xA1, 0xA9, 0xD6, 0xEB, 0xFB, 0x21, 0x6A,
|
||||
0xC0, 0x21, 0x6C, 0xBD, 0x21, 0x74, 0xBA, 0x21, 0x6E, 0xFD, 0x22, 0x6C, 0x65, 0xF7, 0xFD, 0xA0, 0x02, 0x11, 0x21,
|
||||
0x6A, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x69, 0xFF, 0xA6, 0x21, 0x6E, 0xFC, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0x9C,
|
||||
0x21, 0x61, 0xFC, 0x46, 0x64, 0x65, 0x69, 0x74, 0x67, 0x6E, 0xFF, 0x98, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xEC, 0xFF,
|
||||
0xF6, 0xFF, 0xFD, 0x42, 0x63, 0x7A, 0xFF, 0x82, 0xFF, 0x82, 0x41, 0x6E, 0xFF, 0x7B, 0x21, 0x65, 0xFC, 0x22, 0x65,
|
||||
0x69, 0xF2, 0xFD, 0x21, 0x64, 0xFB, 0x41, 0x67, 0xFF, 0x6C, 0x21, 0x69, 0xFC, 0x41, 0x72, 0xFF, 0x65, 0x21, 0x74,
|
||||
0xFC, 0x23, 0x65, 0x6C, 0x73, 0xEF, 0xF6, 0xFD, 0xA0, 0x09, 0x12, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFF, 0x51, 0x21,
|
||||
0xBA, 0xFC, 0x23, 0x61, 0x75, 0xC3, 0xF6, 0xF9, 0xFD, 0x21, 0x6F, 0xDE, 0xC2, 0x09, 0x12, 0x63, 0x64, 0xFF, 0x91,
|
||||
0xFF, 0x91, 0x23, 0xA1, 0xA9, 0xB3, 0xE0, 0xE0, 0xE0, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x6C, 0xFF, 0xF0, 0xFF, 0xF9,
|
||||
0xFF, 0xD9, 0xFF, 0xD9, 0xFF, 0x81, 0x41, 0x69, 0xFF, 0x84, 0x21, 0x72, 0xFC, 0x41, 0x65, 0xFF, 0x6A, 0x21, 0x63,
|
||||
0xFC, 0x42, 0xA1, 0xA9, 0xFF, 0x12, 0xFF, 0x12, 0x43, 0x61, 0xC3, 0x69, 0xFF, 0x0B, 0xFF, 0xF9, 0xFF, 0x0B, 0x41,
|
||||
0xA9, 0xFF, 0x01, 0x42, 0x65, 0xC3, 0xFE, 0xFD, 0xFF, 0xFC, 0x41, 0x67, 0xFF, 0x0A, 0x21, 0x65, 0xFC, 0x4B, 0x72,
|
||||
0x62, 0x63, 0x64, 0x6C, 0x70, 0x6E, 0x76, 0x78, 0x79, 0x73, 0xFF, 0x5A, 0xFF, 0x91, 0xFF, 0xA5, 0xFF, 0xAC, 0xFF,
|
||||
0xBF, 0xFF, 0xD3, 0xFF, 0xDA, 0xFF, 0xE4, 0xFF, 0x49, 0xFF, 0xF2, 0xFF, 0xFD, 0xA0, 0x08, 0xC3, 0x21, 0x64, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x72, 0xF7, 0x22, 0x72, 0x6F, 0xFA, 0xFD, 0x22, 0x7A, 0x63, 0xEF, 0xEF, 0x21, 0x69, 0xFB,
|
||||
0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0xA1, 0xA9, 0xB3, 0xDE, 0xDE, 0xDE, 0x22, 0x61, 0xC3,
|
||||
0xD7, 0xF9, 0x23, 0x61, 0x65, 0x6F, 0xD2, 0xD2, 0xD2, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xF1, 0xFD, 0xA0, 0x08,
|
||||
0xF2, 0x21, 0x73, 0xC0, 0x22, 0x61, 0x69, 0xFA, 0xFD, 0x21, 0x75, 0xFB, 0x21, 0xAD, 0xC6, 0x22, 0x69, 0xC3, 0xC3,
|
||||
0xFD, 0x42, 0x76, 0x6E, 0xFF, 0xAD, 0xFF, 0xFB, 0x42, 0x61, 0x69, 0xED, 0xDD, 0xFF, 0xF9, 0x41, 0x6C, 0xFE, 0x80,
|
||||
0x42, 0x72, 0x65, 0xFE, 0x7C, 0xFF, 0xFC, 0x21, 0x67, 0xF9, 0x41, 0x76, 0xFF, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x73,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x6C, 0xFF, 0x7E, 0x21, 0x6C, 0xFC, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x41, 0x72, 0xFE, 0x52, 0x43, 0x61, 0x65, 0x74, 0xF1, 0xB9, 0xF1, 0xB9, 0xFF, 0xFC, 0x41,
|
||||
0x73, 0xFF, 0xA0, 0x21, 0x65, 0xFC, 0x41, 0x6E, 0xFF, 0x5C, 0x21, 0x75, 0xFC, 0x21, 0xB3, 0xF9, 0x22, 0xC3, 0x6F,
|
||||
0xFD, 0xF6, 0x4D, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x79, 0x7A, 0xFF, 0x59, 0xFF,
|
||||
0x6C, 0xFF, 0x85, 0xFF, 0x95, 0xFE, 0x37, 0xFF, 0xA7, 0xFF, 0xB9, 0xFF, 0xCC, 0xFF, 0xD9, 0xFF, 0xE0, 0xFF, 0xEE,
|
||||
0xFF, 0xF5, 0xFF, 0xFB, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x25, 0xFF, 0x47, 0xFF, 0x25, 0xFF, 0x25, 0x21, 0x6A,
|
||||
0xF3, 0x41, 0x6A, 0xFF, 0x43, 0x21, 0xA9, 0xFC, 0x41, 0xB1, 0xFD, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xA9, 0xFD, 0x22,
|
||||
0x65, 0xC3, 0xFA, 0xFD, 0x23, 0x65, 0xC3, 0x70, 0xE7, 0xEE, 0xFB, 0x41, 0x6E, 0xFD, 0xD9, 0x21, 0xA9, 0xFC, 0x22,
|
||||
0x65, 0xC3, 0xF9, 0xFD, 0x21, 0x72, 0xFB, 0x21, 0x66, 0xFD, 0xC6, 0x02, 0x11, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73,
|
||||
0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0x46, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73,
|
||||
0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0x21, 0xA1, 0xED, 0xC6, 0x09, 0x12, 0x6E,
|
||||
0x72, 0x62, 0x64, 0x6D, 0x73, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0x42, 0xA1,
|
||||
0xB3, 0xFF, 0xEB, 0xFE, 0x1C, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFE, 0x15, 0xFE, 0x35, 0xFE, 0x15, 0xFE, 0x15, 0x44,
|
||||
0x6F, 0x61, 0xC3, 0x68, 0xFE, 0x08, 0xFF, 0xD7, 0xFF, 0xEC, 0xFF, 0xF3, 0x41, 0xA9, 0xFE, 0x85, 0x42, 0x65, 0xC3,
|
||||
0xFE, 0x81, 0xFF, 0xFC, 0xA1, 0x05, 0x92, 0x75, 0xF9, 0x41, 0x66, 0xFD, 0x42, 0x42, 0x63, 0x71, 0xFD, 0x3E, 0xFD,
|
||||
0x3E, 0x22, 0x69, 0x75, 0xF5, 0xF9, 0x41, 0x62, 0xFD, 0xCD, 0x21, 0x6D, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x7A, 0xFD,
|
||||
0x28, 0x42, 0x63, 0x6E, 0xFD, 0x75, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0x21, 0x72, 0xFD, 0x44, 0x61, 0x65, 0x69, 0x75,
|
||||
0xFD, 0x17, 0xFF, 0xFD, 0xFD, 0x9C, 0xFD, 0x7B, 0x41, 0x69, 0xFD, 0x4D, 0x41, 0x69, 0xFD, 0x8B, 0x43, 0x62, 0x63,
|
||||
0x6C, 0xFF, 0xF8, 0xFD, 0x5C, 0xFF, 0xFC, 0x41, 0x73, 0xFC, 0xF8, 0x41, 0x63, 0xFC, 0xF4, 0x22, 0x65, 0x75, 0xF8,
|
||||
0xFC, 0x43, 0x61, 0x69, 0x72, 0xFF, 0xE9, 0xFD, 0x4F, 0xFF, 0xFB, 0x23, 0x63, 0x70, 0x74, 0xB6, 0xCA, 0xF6, 0x42,
|
||||
0x63, 0x74, 0xFC, 0xF1, 0xFC, 0xEE, 0x4A, 0x6D, 0x6E, 0x6F, 0x61, 0xC3, 0x63, 0x71, 0x64, 0x73, 0x72, 0xFF, 0x07,
|
||||
0xFF, 0x1D, 0xFD, 0x24, 0xFF, 0x20, 0xFF, 0x48, 0xFF, 0x74, 0xFF, 0x8C, 0xFF, 0x9C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42,
|
||||
0x72, 0x6F, 0xFD, 0x05, 0xFC, 0xF7, 0x42, 0x61, 0x6F, 0xFC, 0xFE, 0xFC, 0xFE, 0x22, 0x65, 0x69, 0xF2, 0xF9, 0x41,
|
||||
0x74, 0xFC, 0xF2, 0x21, 0x72, 0xFC, 0x41, 0xA1, 0xFC, 0xDD, 0x42, 0x61, 0xC3, 0xFC, 0xD9, 0xFF, 0xFC, 0x42, 0x6E,
|
||||
0x75, 0xFC, 0xE0, 0xFF, 0xF9, 0x41, 0xB3, 0xFD, 0x0D, 0x42, 0x6F, 0xC3, 0xFD, 0x09, 0xFF, 0xFC, 0x21, 0x69, 0xF9,
|
||||
0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x42, 0x67, 0x6E, 0xFF, 0x6E, 0xFC, 0x74, 0x41, 0x65, 0xFF, 0x75, 0x42, 0x6F,
|
||||
0x72, 0xFC, 0xEE, 0xFF, 0xFC, 0x22, 0x61, 0x70, 0xEE, 0xF9, 0x41, 0x72, 0xFD, 0x0C, 0x41, 0x73, 0xFC, 0x9F, 0x21,
|
||||
0x75, 0xFC, 0x44, 0x65, 0x6C, 0x6F, 0x72, 0xFC, 0x9B, 0xFF, 0x4C, 0xFF, 0xF5, 0xFF, 0xFD, 0x42, 0x63, 0x74, 0xFC,
|
||||
0xEE, 0xFC, 0xEE, 0x21, 0x6E, 0xF9, 0x41, 0x63, 0xFC, 0x8C, 0x41, 0x72, 0xFC, 0x7D, 0xC1, 0x05, 0x92, 0x61, 0xFC,
|
||||
0x97, 0x41, 0x72, 0xFC, 0x91, 0x24, 0x65, 0x61, 0x6C, 0x6F, 0xEE, 0xF2, 0xF6, 0xFC, 0x41, 0x62, 0xFC, 0x20, 0x21,
|
||||
0x69, 0xFC, 0x41, 0x63, 0xFC, 0x5F, 0x41, 0x61, 0xFC, 0x58, 0x22, 0x65, 0x74, 0xF8, 0xFC, 0x42, 0x67, 0x72, 0xFD,
|
||||
0xCE, 0xFC, 0x20, 0x41, 0x78, 0xFC, 0x05, 0x23, 0x65, 0x6F, 0x75, 0xF5, 0xFC, 0xE1, 0x41, 0x65, 0xFC, 0x95, 0x47,
|
||||
0x63, 0x65, 0x66, 0x68, 0x73, 0x74, 0x76, 0xFF, 0xA4, 0xFF, 0xB8, 0xFF, 0xCD, 0xFF, 0xDA, 0xFF, 0xE5, 0xFF, 0xF5,
|
||||
0xFF, 0xFC, 0x41, 0x6E, 0xFC, 0x31, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x47, 0x64, 0x65, 0x67, 0x6C, 0x6D, 0x6E,
|
||||
0x73, 0xFF, 0x30, 0xFF, 0x39, 0xFF, 0x47, 0xFF, 0x5F, 0xFF, 0x74, 0xFF, 0xE0, 0xFF, 0xFD, 0x43, 0x72, 0x73, 0x6E,
|
||||
0xFC, 0x69, 0xFC, 0x69, 0xFC, 0x69, 0x21, 0x61, 0xF6, 0x41, 0x6C, 0xFC, 0x04, 0x21, 0x6C, 0xFC, 0x41, 0x61, 0xFD,
|
||||
0xE7, 0x21, 0x74, 0xFC, 0xA0, 0x09, 0x53, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0x22, 0x73, 0x69, 0xF5, 0xFB, 0xC1, 0x05,
|
||||
0x92, 0x61, 0xFB, 0x98, 0x43, 0x6E, 0x72, 0x73, 0xFB, 0x92, 0xFF, 0xFA, 0xFB, 0x92, 0x43, 0x6E, 0x72, 0x73, 0xFB,
|
||||
0x88, 0xFB, 0x88, 0xFB, 0x88, 0x42, 0xA9, 0xB3, 0xFF, 0xF6, 0xFB, 0x7E, 0x45, 0x72, 0x64, 0x65, 0xC3, 0x6D, 0xFB,
|
||||
0x77, 0xFB, 0x77, 0xFF, 0xE5, 0xFF, 0xF9, 0xFB, 0x77, 0x42, 0x6E, 0x73, 0xFB, 0x67, 0xFB, 0x67, 0x42, 0xA1, 0xAD,
|
||||
0xFB, 0x60, 0xFF, 0xC8, 0x45, 0x69, 0x61, 0x65, 0x6F, 0xC3, 0xFF, 0xE2, 0xFF, 0xF2, 0xFB, 0x59, 0xFB, 0x59, 0xFF,
|
||||
0xF9, 0x41, 0xA1, 0xFB, 0x49, 0x42, 0x61, 0xC3, 0xFB, 0x45, 0xFF, 0xFC, 0x21, 0xB1, 0xF9, 0x41, 0x62, 0xFB, 0x9C,
|
||||
0x47, 0x64, 0x65, 0x62, 0x73, 0x6E, 0xC3, 0x72, 0xFF, 0x81, 0xFF, 0x88, 0xFF, 0x9A, 0xFF, 0x8F, 0xFF, 0xDE, 0xFF,
|
||||
0xF9, 0xFF, 0xFC, 0x46, 0xC3, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xFB, 0x5A, 0xFC, 0x32, 0xFD, 0x07, 0xFE, 0x4E, 0xFF,
|
||||
0x4B, 0xFF, 0xEA, 0x41, 0x69, 0xFB, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x45, 0x63, 0x6E, 0x72, 0x73, 0x69,
|
||||
0xFA, 0xCE, 0xF4, 0xA7, 0xFB, 0x01, 0xFF, 0xE3, 0xFF, 0xFD, 0xA0, 0x11, 0x72, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x22, 0x65, 0xC3, 0xFA, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x65, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72, 0x65, 0x69,
|
||||
0xE8, 0x5B, 0xE8, 0x5E, 0xE8, 0x64, 0xE8, 0x5E, 0xE8, 0x6D, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8,
|
||||
0x5E, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x77, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x80, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E,
|
||||
0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x8A, 0xFF, 0xDC, 0xFF, 0xFD, 0x42, 0x6D, 0x72, 0xF4, 0x38, 0xF3, 0xDE, 0x41, 0x69,
|
||||
0xF3, 0xD3, 0x43, 0x6C, 0x73, 0x74, 0xF9, 0xB2, 0xFF, 0xFC, 0xF9, 0xB2, 0x42, 0x6E, 0x74, 0xF9, 0xA8, 0xF9, 0xA8,
|
||||
0x41, 0x69, 0xEB, 0x9B, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x65, 0x69, 0x6F, 0x61, 0xE7, 0xDE, 0xE7, 0xE1, 0xE7, 0xE1,
|
||||
0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7,
|
||||
0xE1, 0xE7, 0xE1, 0xF7, 0xB7, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE8, 0x0D, 0xE8, 0x0D,
|
||||
0xFF, 0xCE, 0xFF, 0xD9, 0xFF, 0xE3, 0xFF, 0xFD, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0xE7, 0x8D, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7,
|
||||
0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xF7, 0x41, 0xA0, 0x08, 0xB1, 0x21, 0x2E, 0xFD, 0x49, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x2E,
|
||||
0x73, 0xE8, 0x4E, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x57, 0xFF, 0xFA, 0xFF, 0xFD,
|
||||
0x22, 0x2E, 0x73, 0xDE, 0xE1, 0x21, 0x61, 0xFB, 0x21, 0xAD, 0xFD, 0x23, 0x6F, 0x61, 0xC3, 0xD9, 0xF5, 0xFD, 0x21,
|
||||
0x66, 0xF9, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0xE7, 0x0E, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A,
|
||||
0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7,
|
||||
0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xFF, 0xFD, 0x41, 0x73,
|
||||
0xFF, 0x84, 0x42, 0x2E, 0x65, 0xFF, 0x7D, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x42, 0x6F, 0x61, 0xFF, 0x95, 0xFF, 0xFD,
|
||||
0x21, 0x6E, 0xF9, 0x41, 0x72, 0xF9, 0x23, 0x42, 0x65, 0x72, 0xFF, 0xFC, 0xE7, 0x37, 0x21, 0x74, 0xF9, 0x42, 0x6C,
|
||||
0x73, 0xF8, 0x4D, 0xFF, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE7, 0x64, 0xE7, 0x67, 0xE7, 0x67,
|
||||
0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x6D, 0x41, 0x6E, 0xF8, 0xFB, 0x21, 0x6F, 0xFC, 0x22, 0x6F, 0x72, 0xE3,
|
||||
0xFD, 0x41, 0x63, 0xE7, 0x04, 0x21, 0x65, 0xFC, 0x41, 0x61, 0xEA, 0x8B, 0x22, 0x6E, 0x67, 0xF9, 0xFC, 0x41, 0x64,
|
||||
0xF7, 0x46, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x6F, 0x61, 0x65, 0x69, 0x75,
|
||||
0xE6, 0x5D, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6,
|
||||
0x60, 0xF6, 0x36, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60,
|
||||
0xE6, 0x60, 0xFE, 0xD0, 0xFF, 0x4F, 0xFF, 0xAC, 0xFF, 0xBD, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x2E, 0xE6,
|
||||
0x0C, 0x42, 0x2E, 0x73, 0xE6, 0x08, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x6C, 0xFB, 0x42, 0x6F, 0x61,
|
||||
0xE6, 0xD5, 0xE6, 0xD5, 0x21, 0x6E, 0xF9, 0x21, 0x61, 0xFD, 0x22, 0x65, 0x6D, 0xF0, 0xFD, 0x41, 0x65, 0xFE, 0x9F,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFA, 0x22, 0x6C, 0x69, 0xFA, 0xFD, 0x42, 0x6C,
|
||||
0x62, 0xF7, 0x7C, 0xFF, 0xFB, 0x42, 0x63, 0x6F, 0xE6, 0x55, 0xE6, 0xEB, 0x21, 0x69, 0xF9, 0x41, 0x2E, 0xE8, 0xAA,
|
||||
0x42, 0x2E, 0x73, 0xE8, 0xA6, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x47, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xE9, 0x79, 0xE9, 0xB5, 0xE9, 0xB5, 0xE9, 0xB5, 0xFF, 0xFB, 0xE9, 0xB5, 0xE9, 0xBB, 0x43,
|
||||
0x61, 0x69, 0x6F, 0xF5, 0xB9, 0xFF, 0xEA, 0xE9, 0xB0, 0x42, 0x61, 0x74, 0xF5, 0xAF, 0xE6, 0xBD, 0x41, 0x72, 0xE6,
|
||||
0x11, 0x21, 0x65, 0xFC, 0x46, 0x63, 0x6C, 0x6D, 0x70, 0x74, 0x78, 0xF1, 0xA5, 0xFF, 0xBC, 0xFF, 0xE8, 0xFF, 0xF2,
|
||||
0xFF, 0xFD, 0xFF, 0x0D, 0xA0, 0x0A, 0x13, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD,
|
||||
0xA1, 0x06, 0xF3, 0x63, 0xFD, 0x21, 0x69, 0xEC, 0x21, 0x6D, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD,
|
||||
0x21, 0x61, 0xDE, 0x21, 0x69, 0xFD, 0xA2, 0x06, 0xF3, 0x6E, 0x78, 0xF5, 0xFD, 0xA0, 0x0A, 0x43, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x07, 0x23, 0x6E, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF6, 0xA6,
|
||||
0xF6, 0xA6, 0xF6, 0xA6, 0xFF, 0xFB, 0xF6, 0xA6, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE9, 0xF3,
|
||||
0xE9, 0xCA, 0xF6, 0x93, 0xF6, 0x93, 0xFF, 0xBF, 0xFF, 0xD8, 0xF6, 0x93, 0xFF, 0xF0, 0x21, 0x72, 0xE7, 0x42, 0x65,
|
||||
0x6F, 0xFF, 0xFD, 0xE9, 0x19, 0x43, 0x64, 0x70, 0x73, 0xF0, 0xC5, 0xFF, 0xF9, 0xF1, 0x1F, 0x42, 0x6F, 0x65, 0xE9,
|
||||
0x08, 0xF0, 0xB7, 0x42, 0x6C, 0x6D, 0xF6, 0x93, 0xFF, 0xF9, 0x5B, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0x61, 0x65, 0x69, 0x6F,
|
||||
0xE4, 0xDF, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4,
|
||||
0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2,
|
||||
0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xFE, 0xF6, 0xFF, 0x10, 0xFF, 0x62, 0xFF, 0xE8, 0xFF, 0xF9, 0xD6, 0x00, 0x41,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xE4, 0x8D, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90,
|
||||
0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4,
|
||||
0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0x41, 0x6C, 0xF5, 0xF5, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72,
|
||||
0x69, 0xE4, 0x44, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47,
|
||||
0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4,
|
||||
0x47, 0xE4, 0x47, 0xE4, 0x73, 0xE4, 0x73, 0xFF, 0xFC, 0xD6, 0x00, 0x81, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xE3, 0xFC, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3,
|
||||
0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0x41, 0x75, 0xF3, 0x6B, 0x41, 0x66, 0xEF, 0x7D, 0xA0, 0x0D, 0x02, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0xA1, 0xFD, 0x44, 0x6E, 0x70, 0x74, 0xC3, 0xFF, 0xED, 0xF5, 0x4D, 0xF5, 0x4D, 0xFF, 0xFD,
|
||||
0x41, 0x61, 0xFC, 0x4E, 0x21, 0xAD, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C,
|
||||
0x65, 0x69, 0x6F, 0xE3, 0x86, 0xE3, 0x89, 0xE3, 0x8F, 0xE3, 0x89, 0xE3, 0x98, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0x89,
|
||||
0xE3, 0x89, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0xA2, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0xAB, 0xE3, 0x89, 0xE3,
|
||||
0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xFF, 0x8A, 0xFF, 0xCF, 0xFF, 0xE6, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xE3,
|
||||
0x38, 0xF4, 0x32, 0x21, 0x65, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x62, 0xFD, 0x41, 0x2E, 0xE4, 0x07, 0x21, 0x65, 0xFC,
|
||||
0x21, 0x74, 0xFD, 0x48, 0x6C, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xAB, 0xE6, 0xEC, 0xE7, 0x28, 0xE7,
|
||||
0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x2E, 0x21, 0x61, 0xE7, 0x41, 0x6E, 0xE3, 0x8F, 0x21, 0x61, 0xFC,
|
||||
0x47, 0x6F, 0x61, 0x6E, 0x67, 0x6C, 0x73, 0x74, 0xF3, 0xF5, 0xFF, 0xD0, 0xFF, 0xDA, 0xFF, 0xF6, 0xFF, 0xFD, 0xF4,
|
||||
0xA8, 0xFC, 0x8B, 0xA0, 0x05, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA0, 0x02, 0xB2, 0xCC,
|
||||
0x01, 0xA1, 0x68, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x70, 0x71, 0x73, 0x74, 0x76, 0xFF, 0xFD, 0xE4, 0xE9, 0xE4,
|
||||
0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9,
|
||||
0x41, 0x69, 0xE6, 0xCA, 0x44, 0x6E, 0x63, 0x6C, 0x78, 0xFF, 0xCF, 0xEE, 0x79, 0xFF, 0xD5, 0xFF, 0xFC, 0x41, 0x72,
|
||||
0xE8, 0xA2, 0x21, 0x61, 0xFC, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x74, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE6, 0x54, 0xFF, 0xFB, 0xE6, 0x90, 0xE6, 0x90,
|
||||
0xE6, 0x90, 0xE6, 0x90, 0xE6, 0x96, 0x21, 0x69, 0xEA, 0x41, 0x69, 0xE3, 0x9F, 0x44, 0x63, 0x6C, 0x6E, 0x72, 0xEE,
|
||||
0x37, 0xFF, 0xD2, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xE6, 0x62, 0x21, 0x6C, 0xFC, 0x43, 0x6E, 0x72, 0x74, 0xF4,
|
||||
0x02, 0xFE, 0xA2, 0xF4, 0x02, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x61, 0x69, 0x75, 0x6F, 0xE2, 0x4B, 0xE2,
|
||||
0x4E, 0xE2, 0x54, 0xE2, 0x4E, 0xE2, 0x5D, 0xE2, 0x62, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x62,
|
||||
0xF2, 0x24, 0xE2, 0x67, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x70, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2,
|
||||
0x4E, 0xE2, 0x4E, 0xFF, 0x50, 0xFF, 0xA0, 0xFF, 0xE2, 0xFF, 0xF3, 0xFF, 0xF6, 0xA0, 0x0B, 0x95, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0xC3, 0x00, 0x71, 0x7A, 0x73, 0x65, 0xE1, 0xF1, 0xE1, 0xF1, 0xFF, 0xFD, 0x41,
|
||||
0x74, 0xED, 0xA2, 0x42, 0x2E, 0x72, 0xE1, 0xDE, 0xFF, 0xFC, 0x43, 0x6D, 0x6E, 0x72, 0xF3, 0x81, 0xF3, 0x81, 0xF1,
|
||||
0x88, 0x45, 0x63, 0x66, 0x6F, 0x74, 0x75, 0xED, 0x98, 0xED, 0x98, 0xFB, 0x31, 0xF3, 0x77, 0xF2, 0xA5, 0xD9, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x6F, 0x61, 0x65, 0xE1, 0xBA, 0xE1, 0xBD, 0xE1, 0xC3, 0xE1, 0xBD, 0xE1, 0xCC, 0xE1, 0xD1,
|
||||
0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xD1, 0xE1, 0xBD, 0xE1, 0xD6, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1,
|
||||
0xBD, 0xFF, 0xCF, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xFF, 0xDF, 0xFF, 0xE6, 0xFF, 0xF0,
|
||||
0xC1, 0x0D, 0x22, 0x6F, 0xE2, 0x8F, 0x42, 0x63, 0x71, 0xFF, 0xFA, 0xF1, 0x1E, 0xC2, 0x00, 0x71, 0x2E, 0x69, 0xE1,
|
||||
0x5F, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x2E, 0x65, 0xE1, 0x56, 0xED, 0x24, 0x41, 0x74, 0xFE, 0xB9, 0x21, 0x63, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x41, 0x72, 0xE5, 0x49, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x75, 0xE1, 0x3F, 0xE1, 0x6B,
|
||||
0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1,
|
||||
0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B,
|
||||
0xE1, 0x6B, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x70, 0xE2, 0xB2, 0x42, 0x6D, 0x74, 0xFF, 0xFC, 0xEC, 0xBA, 0xD7, 0x00,
|
||||
0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x6F, 0xE0, 0xE9, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15,
|
||||
0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1,
|
||||
0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xFF, 0xF9, 0x42, 0x61, 0x6F, 0xF1, 0x95, 0xF1,
|
||||
0x95, 0x21, 0x74, 0xF9, 0x41, 0x61, 0xF4, 0x4F, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0xA0, 0x10, 0x92, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x10, 0xB2, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x23, 0x65, 0x61, 0x70, 0xE2, 0xEE, 0xFD, 0x44, 0x64, 0x72, 0x6E, 0x74, 0xF1, 0x7E, 0xFF, 0xF9,
|
||||
0xF1, 0x42, 0xF9, 0xFB, 0x41, 0x6E, 0xEB, 0x6F, 0x21, 0x6F, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x65,
|
||||
0xEC, 0x1B, 0xA0, 0x06, 0x31, 0x41, 0xB1, 0xE1, 0xF2, 0x21, 0xC3, 0xFC, 0x22, 0x2E, 0x65, 0xF6, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x73, 0xFB, 0x41, 0x61, 0xE6, 0x3D, 0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x41,
|
||||
0x6F, 0xE6, 0x2E, 0xA1, 0x04, 0xC2, 0x73, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xE4, 0x2E, 0xE4, 0x2E, 0xFF,
|
||||
0xFB, 0xE4, 0x2E, 0xE4, 0x2E, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xDF, 0xE4, 0x1B, 0xE4, 0x1B,
|
||||
0xFF, 0xD3, 0xE4, 0x1B, 0xFF, 0xE2, 0xFF, 0xF0, 0x21, 0x61, 0xEA, 0x23, 0x6E, 0x6C, 0x72, 0xA4, 0xA7, 0xFD, 0x41,
|
||||
0x7A, 0xEB, 0xBB, 0x43, 0x63, 0x65, 0x72, 0xF1, 0x9A, 0xFF, 0xFC, 0xF1, 0x9A, 0x42, 0x71, 0x63, 0xE5, 0xE7, 0xFF,
|
||||
0xAA, 0x41, 0x65, 0xFF, 0xA3, 0x42, 0x64, 0x74, 0xFD, 0x3A, 0xFF, 0xFC, 0xA2, 0x04, 0xA2, 0x72, 0x6E, 0xEE, 0xF9,
|
||||
0x41, 0x65, 0xFD, 0x36, 0x21, 0x69, 0xFC, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0xC1, 0x04, 0xA2, 0x72, 0xE1, 0x66, 0x41,
|
||||
0x71, 0xE5, 0xBC, 0xA1, 0x04, 0xC2, 0x72, 0xFC, 0x41, 0x65, 0xE5, 0xB3, 0x21, 0x74, 0xFC, 0xA1, 0x04, 0xC2, 0x73,
|
||||
0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xEF, 0xFF, 0xFB, 0xE3, 0xB0, 0xE3, 0xB0, 0xE3, 0xB0, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0x61, 0xFF, 0xC2, 0xE3, 0x9D, 0xE3, 0x9D, 0xFF, 0xD0, 0xFF, 0xD5, 0xFF,
|
||||
0xF0, 0x21, 0x69, 0xEA, 0x41, 0x6F, 0xEA, 0x8B, 0xA1, 0x04, 0x52, 0x72, 0xFC, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xE0, 0x80, 0xE0, 0x83, 0xFF, 0xFB, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x89, 0x21, 0x61, 0xEA,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x72, 0xFC, 0x7C, 0x21, 0x70, 0xFC, 0x41, 0x64, 0xFC, 0x75, 0x22, 0x6D, 0x6E, 0xF9, 0xFC,
|
||||
0xA0, 0x0E, 0x13, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x0E, 0x43, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xD8, 0x22, 0x6C, 0x73, 0xFA, 0xFD, 0x41, 0x2E, 0xE1, 0x38, 0x42, 0x2E,
|
||||
0x73, 0xE1, 0x34, 0xFF, 0xFC, 0x42, 0x61, 0x73, 0xFF, 0xF9, 0xE1, 0xD0, 0x24, 0x69, 0x6F, 0x65, 0x74, 0xC9, 0xD7,
|
||||
0xE9, 0xF9, 0x43, 0x6C, 0x72, 0x73, 0xFF, 0x8D, 0xFF, 0xB2, 0xFF, 0xF7, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x75,
|
||||
0x65, 0x61, 0x69, 0x6F, 0xDF, 0x00, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF,
|
||||
0x03, 0xDF, 0x03, 0xDF, 0x03, 0xEE, 0xD9, 0xDF, 0x03, 0xDF, 0x03, 0xFD, 0xA1, 0xFD, 0xAA, 0xDF, 0x03, 0xDF, 0x03,
|
||||
0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xFD, 0xC1, 0xFE, 0x17, 0xFE, 0x66, 0xFE, 0x95, 0xFF, 0x08, 0xFF, 0x13, 0xFF,
|
||||
0xF6, 0x42, 0x6D, 0x72, 0xDF, 0x3C, 0xEA, 0x76, 0x42, 0x65, 0x69, 0xFC, 0xC6, 0xFF, 0xF9, 0xD7, 0x00, 0x41, 0x2E,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x75, 0xDE, 0x9E, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1,
|
||||
0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE,
|
||||
0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x6E, 0x61, 0xDE, 0x5C, 0xEE,
|
||||
0xD0, 0x41, 0xA1, 0xF0, 0xDB, 0x43, 0x61, 0xC3, 0x65, 0xF0, 0xD7, 0xFF, 0xFC, 0xF0, 0xD7, 0x21, 0x69, 0xF6, 0x21,
|
||||
0x63, 0xFD, 0xA0, 0x0A, 0x72, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69,
|
||||
0xC3, 0xEE, 0xFD, 0x21, 0x6E, 0xFB, 0x42, 0x65, 0x72, 0xE2, 0x3D, 0xE9, 0xEC, 0x22, 0x69, 0x74, 0xF6, 0xF9, 0xA0,
|
||||
0x0B, 0xB1, 0x23, 0xA1, 0xA9, 0xAD, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0xC3, 0x65, 0x6F, 0xF6, 0xF9, 0xF6, 0xF6, 0x43,
|
||||
0x64, 0x6E, 0x72, 0xF5, 0xFA, 0xED, 0xB7, 0xFF, 0xF7, 0x41, 0x6D, 0xEF, 0xA6, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72,
|
||||
0x65, 0x61, 0x6F, 0xDD, 0xF5, 0xDD, 0xF8, 0xDD, 0xFE, 0xDD, 0xF8, 0xDE, 0x07, 0xDE, 0x0C, 0xDD, 0xF8, 0xDD, 0xF8,
|
||||
0xDD, 0xF8, 0xDD, 0xF8, 0xFF, 0x9F, 0xDD, 0xF8, 0xDE, 0x11, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x1A, 0xDD, 0xF8, 0xDD,
|
||||
0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x24, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xFC, 0xC4, 0x00, 0x71, 0x74,
|
||||
0x73, 0x6E, 0x61, 0xDD, 0xAD, 0xDD, 0xAD, 0xDD, 0xAD, 0xEE, 0x21, 0xA0, 0x00, 0xD1, 0x21, 0x2E, 0xFD, 0x22, 0x2E,
|
||||
0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x02, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x2E, 0x65, 0xEC, 0xFD, 0x21, 0x6C,
|
||||
0xFB, 0x22, 0x2E, 0x73, 0xEF, 0xF2, 0x21, 0x6E, 0xED, 0x21, 0xB3, 0xFD, 0x21, 0x65, 0xEA, 0x21, 0x6E, 0xFD, 0x23,
|
||||
0x61, 0xC3, 0x6F, 0xEF, 0xF7, 0xFD, 0x21, 0x6C, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xC9, 0x23, 0x2E, 0x61, 0x65,
|
||||
0xC3, 0xC9, 0xFD, 0x21, 0x72, 0xF9, 0xC6, 0x00, 0x71, 0x7A, 0x73, 0x65, 0x61, 0x69, 0x6F, 0xDD, 0x57, 0xDD, 0x57,
|
||||
0xFF, 0xBF, 0xFF, 0xD2, 0xFF, 0xF0, 0xFF, 0xFD, 0x41, 0x74, 0xDF, 0xF2, 0x21, 0x63, 0xFC, 0x41, 0x76, 0xDE, 0x67,
|
||||
0x44, 0x6E, 0x2E, 0x73, 0x6C, 0xFF, 0xF9, 0xF5, 0xEC, 0xF5, 0xEF, 0xFF, 0xFC, 0x41, 0x65, 0xFA, 0x22, 0x41, 0x76,
|
||||
0xE8, 0xEA, 0xA0, 0x0E, 0xD2, 0xA0, 0x0E, 0xF3, 0xA0, 0x0F, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21,
|
||||
0x6F, 0xF1, 0x21, 0x64, 0xFD, 0x44, 0x6C, 0x6D, 0x72, 0x75, 0xFF, 0xCF, 0xFA, 0x44, 0xFF, 0xD3, 0xFF, 0xFD, 0xA0,
|
||||
0x0F, 0x52, 0xA1, 0x0F, 0x52, 0x73, 0xFD, 0x21, 0x61, 0xFB, 0xA1, 0x04, 0x52, 0x73, 0xFD, 0x47, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xDD, 0xE5, 0xFF, 0xFB, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xEE, 0x21,
|
||||
0x65, 0xEA, 0x21, 0x72, 0xFD, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF4, 0xB1, 0xA0, 0x0F, 0xF3, 0xA1, 0x06, 0xF3, 0x72,
|
||||
0xFD, 0x41, 0x72, 0xF9, 0xC6, 0xA1, 0x06, 0xF3, 0x6F, 0xFC, 0xA0, 0x10, 0x23, 0x41, 0x2E, 0xF7, 0x64, 0x42, 0x2E,
|
||||
0x73, 0xF7, 0x60, 0xFF, 0xFC, 0x21, 0x74, 0xF9, 0x21, 0x69, 0xFD, 0xA2, 0x07, 0x23, 0x72, 0x76, 0xEC, 0xFD, 0x45,
|
||||
0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xF9, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03, 0x48, 0x72, 0x68, 0x61,
|
||||
0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE1, 0x50, 0xE1, 0x27, 0xFF, 0xC7, 0xED, 0xF0, 0xFF, 0xD0, 0xED, 0xF0, 0xED, 0xF0,
|
||||
0xFF, 0xF0, 0x21, 0x72, 0xE7, 0xC7, 0x07, 0xB1, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xDD, 0x6A, 0xDD, 0x6D,
|
||||
0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x73, 0x21, 0x61, 0xE8, 0x22, 0x65, 0x72, 0xE2, 0xFD, 0xA0,
|
||||
0x11, 0x43, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x70, 0x64, 0x72, 0xE0, 0xFD, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7A, 0x61, 0x65, 0x6F, 0x75, 0xDC, 0x19, 0xDC, 0x1C, 0xDC, 0x22, 0xDC, 0x1C, 0xDC, 0x2B, 0xDC, 0x30, 0xDC, 0x1C,
|
||||
0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x30, 0xDC, 0x1C, 0xFE, 0x72, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE,
|
||||
0xC8, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE, 0xE8, 0xFF, 0x26, 0xFF, 0x5F, 0xFF, 0xF9,
|
||||
0x41, 0x65, 0xE1, 0x23, 0x41, 0x6E, 0xDF, 0x92, 0x21, 0x69, 0xFC, 0x22, 0x74, 0x64, 0xF5, 0xFD, 0x41, 0x6C, 0xDF,
|
||||
0x86, 0x21, 0x65, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x62, 0xDF, 0x7C, 0x21, 0x6F, 0xFC, 0x41, 0x72, 0xDF, 0x75, 0x21,
|
||||
0x61, 0xFC, 0x43, 0x63, 0x70, 0x74, 0xFF, 0xF6, 0xDF, 0x6E, 0xFF, 0xFD, 0x41, 0xA1, 0xDF, 0x8F, 0x21, 0xC3, 0xFC,
|
||||
0x21, 0x6C, 0xFD, 0x24, 0x6E, 0x62, 0x6C, 0x74, 0xCF, 0xDB, 0xEC, 0xFD, 0x21, 0xA1, 0xBF, 0x21, 0xC3, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x2E, 0xE4, 0xB3, 0x42, 0x2E, 0x73, 0xE4, 0xAF, 0xFF, 0xFC, 0x22, 0x6F, 0x61,
|
||||
0xF9, 0xF9, 0x21, 0x72, 0xFB, 0x23, 0x61, 0x6F, 0x65, 0xD8, 0xEA, 0xFD, 0x41, 0x73, 0xDE, 0x49, 0x21, 0x61, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x64, 0xE0, 0x00, 0x41, 0x6C, 0xDF, 0xFC, 0x41, 0x69, 0xE9, 0x65, 0x42,
|
||||
0x63, 0x74, 0xDF, 0xF7, 0xFF, 0xFC, 0xA4, 0x0E, 0xB2, 0x6D, 0x6E, 0x74, 0x63, 0xEA, 0xED, 0xF1, 0xF9, 0x41, 0xBA,
|
||||
0xE4, 0x87, 0x41, 0x75, 0xDF, 0xE5, 0xA2, 0x0E, 0xB2, 0xC3, 0x78, 0xF8, 0xFC, 0x41, 0x6E, 0xDF, 0xDA, 0x21, 0x61,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0xB3, 0xF0, 0x22, 0x6F, 0xC3, 0xED, 0xFD, 0x21,
|
||||
0x69, 0xFB, 0x41, 0x2E, 0xDB, 0x9E, 0x42, 0x2E, 0x73, 0xDB, 0x9A, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x41,
|
||||
0xAD, 0xDF, 0xAF, 0x43, 0x69, 0xC3, 0x65, 0xDF, 0xAB, 0xFF, 0xFC, 0xDF, 0xAB, 0x41, 0xA1, 0xDF, 0xA1, 0x43, 0x61,
|
||||
0xC3, 0x6F, 0xDF, 0x9D, 0xFF, 0xFC, 0xDF, 0x9D, 0x41, 0x61, 0xE4, 0x31, 0x21, 0x76, 0xFC, 0x41, 0x74, 0xDD, 0x80,
|
||||
0x41, 0x69, 0xDF, 0x88, 0xA1, 0x0E, 0x92, 0x72, 0xFC, 0x41, 0x76, 0xDF, 0x7F, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x69,
|
||||
0xDF, 0x7B, 0xDF, 0xF0, 0xDF, 0x7B, 0xFF, 0xF7, 0xFF, 0xFC, 0xC8, 0x0E, 0xB2, 0x62, 0x63, 0x64, 0x67, 0x6A, 0x6C,
|
||||
0x73, 0x74, 0xFF, 0x9E, 0xFF, 0xA9, 0xFF, 0xB7, 0xFF, 0xC0, 0xFF, 0xCE, 0xFF, 0xDC, 0xFF, 0xDF, 0xFF, 0xF0, 0x41,
|
||||
0x65, 0xDF, 0x49, 0x41, 0x69, 0xDD, 0x81, 0x21, 0x63, 0xFC, 0x21, 0x61, 0xFD, 0xA2, 0x0E, 0xB2, 0x63, 0x72, 0xF2,
|
||||
0xFD, 0xC3, 0x0E, 0xB2, 0x72, 0x62, 0x73, 0xDF, 0x34, 0xE1, 0xB9, 0xE0, 0xFB, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0xDF, 0x28, 0xFF, 0x3B, 0xFF, 0x4E, 0xFF, 0xC4, 0xFF, 0xED, 0xFF, 0xF4, 0xE5, 0xE3, 0x21, 0x73, 0xEA,
|
||||
0x42, 0x73, 0x6E, 0xFE, 0xFB, 0xFF, 0xFD, 0x41, 0x70, 0xE6, 0x22, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x6F,
|
||||
0xDA, 0x54, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA,
|
||||
0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80,
|
||||
0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xFF, 0xF5, 0xFF, 0xFC, 0x41, 0x68, 0xEC, 0xA2, 0x21, 0x63, 0xFC, 0xC2, 0x01,
|
||||
0xF2, 0x2E, 0x73, 0xDA, 0x02, 0xFF, 0xFD, 0xC1, 0x01, 0xF2, 0x2E, 0xD9, 0xF9, 0xA0, 0x01, 0xF2, 0x42, 0x61, 0x72,
|
||||
0xF6, 0xB8, 0xDB, 0x22, 0x41, 0x65, 0xDE, 0x04, 0x42, 0x61, 0x6D, 0xDE, 0x00, 0xE5, 0xAF, 0x44, 0x74, 0x6C, 0x63,
|
||||
0x72, 0xFF, 0xEE, 0xFF, 0xF5, 0xEA, 0x58, 0xFF, 0xF9, 0x41, 0x75, 0xEC, 0x56, 0x41, 0x6F, 0xEC, 0x52, 0x22, 0x71,
|
||||
0x63, 0xF8, 0xFC, 0x21, 0x6F, 0xFB, 0x41, 0x6C, 0xEA, 0x9C, 0x41, 0x70, 0xEB, 0x6A, 0x41, 0x62, 0xE5, 0x83, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x63, 0xDA, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0xDC,
|
||||
0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x74, 0x76, 0x77, 0x79,
|
||||
0x72, 0x7A, 0x73, 0x6C, 0x78, 0x65, 0x69, 0x61, 0x6F, 0x75, 0xC3, 0xD9, 0xA2, 0xD9, 0xA5, 0xD9, 0xAB, 0xD9, 0xA5,
|
||||
0xD9, 0xB4, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xBE, 0xD9, 0xA5, 0xD9,
|
||||
0xC7, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xFF, 0x4E, 0xFF, 0xA0, 0xFF, 0xA9, 0xFF, 0xAF, 0xFF, 0xAF, 0xFF, 0xC4,
|
||||
0xFF, 0xDE, 0xFF, 0xE1, 0xFF, 0xE5, 0xFF, 0xED, 0xFF, 0xFD, 0x42, 0x63, 0x64, 0xFF, 0x62, 0xF8, 0xFA, 0xD7, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x6C, 0x72, 0x69, 0xD9, 0x44, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47,
|
||||
0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9,
|
||||
0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x73, 0xD9, 0x73, 0xFF, 0xF9, 0x41, 0x73, 0xFE, 0xF3, 0xD7, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x61, 0xD8, 0xF8, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB,
|
||||
0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8,
|
||||
0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xFF, 0xFC, 0x42, 0x6E, 0x72, 0xEA, 0x5D, 0xEA,
|
||||
0x5D, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x69, 0xD8, 0xA9, 0xD8, 0xAC, 0xD8, 0xB2, 0xD8, 0xAC, 0xD8, 0xBB,
|
||||
0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xC5, 0xD8, 0xAC, 0xD8,
|
||||
0xAC, 0xD8, 0xAC, 0xD8, 0xCE, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xFF, 0xF9, 0xF4, 0x61,
|
||||
0xD6, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xD8, 0x5E, 0xD8, 0x61, 0xD8, 0x67, 0xD8, 0x61, 0xD8, 0x70, 0xD8, 0x75, 0xD8,
|
||||
0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x75, 0xD8, 0x61, 0xD8, 0x7A, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61,
|
||||
0xD8, 0x83, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0x41, 0x6F, 0xF1, 0x80, 0xD7, 0x00, 0x41,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x6F, 0xD8, 0x15, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8,
|
||||
0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18,
|
||||
0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xFF, 0xFC, 0xC1, 0x00, 0x41, 0x2E, 0xD7, 0xCD, 0x41,
|
||||
0x73, 0xE8, 0xC1, 0xA0, 0x02, 0x82, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4,
|
||||
0x80, 0xF4, 0x80, 0xFF, 0xFB, 0x21, 0x6C, 0xF6, 0x21, 0x65, 0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4, 0x70, 0xF4, 0x70,
|
||||
0xF4, 0x70, 0x21, 0x6C, 0xF6, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0xA0, 0x02, 0xD2, 0x21, 0x2E,
|
||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x22, 0x6F, 0x61, 0xFB, 0xFB, 0x21, 0x63, 0xFB, 0x21, 0x69, 0xFD, 0x25, 0x6D,
|
||||
0x74, 0x73, 0x6E, 0x72, 0xD1, 0xD1, 0xE1, 0xE7, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xE5, 0xE5, 0xE5, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x73, 0xFD, 0x25, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xB9, 0xB9, 0xC9, 0xCF, 0xFD, 0x46, 0x73, 0x69, 0x2E, 0x64,
|
||||
0x6F, 0x72, 0xD7, 0x59, 0xFF, 0x92, 0xD7, 0x59, 0xFF, 0xDD, 0xFF, 0xC1, 0xFF, 0xF5, 0x41, 0x73, 0xFF, 0x86, 0x21,
|
||||
0x6F, 0xFC, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0x3F, 0xE8, 0x39, 0xFF, 0xFD, 0xFF, 0x78, 0xE8, 0x39, 0xA0,
|
||||
0x02, 0xA3, 0x21, 0x2E, 0xFD, 0x23, 0x2E, 0x73, 0x69, 0xFA, 0xFD, 0xE3, 0x42, 0x6F, 0x61, 0xF3, 0xEA, 0xF3, 0xEA,
|
||||
0x21, 0x63, 0xF9, 0x43, 0x65, 0x61, 0x69, 0xFF, 0xEF, 0xF3, 0xE0, 0xFF, 0xFD, 0x41, 0x6F, 0xF3, 0xD6, 0x22, 0x74,
|
||||
0x6D, 0xF2, 0xFC, 0x41, 0x73, 0xFF, 0x76, 0x21, 0x65, 0xFC, 0x21, 0x6F, 0xF9, 0x41, 0x65, 0xFF, 0x6F, 0x21, 0x6C,
|
||||
0xFC, 0x47, 0x61, 0x2E, 0x73, 0x74, 0x6D, 0x64, 0x62, 0xFF, 0xB5, 0xD6, 0xF4, 0xFF, 0xEA, 0xFF, 0xF3, 0xFF, 0xF6,
|
||||
0xFF, 0x6D, 0xFF, 0xFD, 0x21, 0x6D, 0xE0, 0x42, 0x2E, 0x65, 0xD6, 0xDB, 0xFF, 0xFD, 0x21, 0x61, 0xF6, 0xA0, 0x02,
|
||||
0xA2, 0x42, 0x2E, 0x73, 0xFF, 0xFD, 0xFF, 0xA2, 0x42, 0x2E, 0x73, 0xFF, 0x98, 0xFF, 0x9B, 0x23, 0x65, 0x6F, 0x61,
|
||||
0xF2, 0xF2, 0xF9, 0x21, 0x6C, 0xF9, 0x21, 0x65, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xEC, 0xEC, 0xEC, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0x47, 0x61, 0x65, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xFF, 0xC2,
|
||||
0xFF, 0xC2, 0xFF, 0xEA, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xFD, 0xFF, 0x08, 0x44, 0x6D, 0x74, 0x73, 0x6E, 0xFE, 0xDF,
|
||||
0xFE, 0xDF, 0xFE, 0xEF, 0xFE, 0xF5, 0x41, 0x6F, 0xFE, 0xB6, 0x43, 0x6F, 0x61, 0x65, 0xF3, 0x41, 0xF3, 0x41, 0xF3,
|
||||
0x41, 0x42, 0x2E, 0x6C, 0xD6, 0x6F, 0xFF, 0xF6, 0x21, 0x65, 0xF9, 0x41, 0x65, 0xE7, 0x5F, 0x44, 0x2E, 0x6D, 0x6C,
|
||||
0x6E, 0xD6, 0x61, 0xFF, 0xFC, 0xFF, 0xE8, 0xFF, 0xE4, 0x21, 0x65, 0xF3, 0x46, 0x6C, 0x6E, 0x6F, 0x6D, 0x74, 0x73,
|
||||
0xFE, 0xA9, 0xFF, 0xD4, 0xFE, 0x8A, 0xFF, 0xE9, 0xFF, 0xFD, 0xFF, 0xFD, 0x21, 0x6F, 0xED, 0x21, 0x64, 0xFD, 0x47,
|
||||
0x73, 0x69, 0x62, 0x72, 0x64, 0x6F, 0x6E, 0xFF, 0x5D, 0xFE, 0x71, 0xFF, 0x64, 0xFF, 0x98, 0xFF, 0xAE, 0xFE, 0xA0,
|
||||
0xFF, 0xFD, 0x41, 0x67, 0xFE, 0x9B, 0x21, 0x6F, 0xFC, 0x41, 0x63, 0xD6, 0x1E, 0x21, 0x69, 0xFC, 0x41, 0x65, 0xFF,
|
||||
0x06, 0x21, 0x74, 0xFC, 0x45, 0x2E, 0x6C, 0x74, 0x6E, 0x73, 0xD6, 0x0D, 0xFF, 0xEF, 0xFF, 0xF6, 0xE7, 0x07, 0xFF,
|
||||
0xFD, 0x45, 0xB1, 0xA9, 0xAD, 0xA1, 0xB3, 0xFE, 0x30, 0xFE, 0xA4, 0xFF, 0x09, 0xFF, 0xC5, 0xFF, 0xF0, 0xA0, 0x01,
|
||||
0x72, 0xA0, 0x01, 0x92, 0x21, 0xB3, 0xFD, 0x22, 0x75, 0xC3, 0xF7, 0xFD, 0x21, 0x65, 0xF2, 0xA0, 0x02, 0x62, 0x21,
|
||||
0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x65, 0xF5, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xE6, 0xE9, 0xFD, 0x22, 0x6F, 0x61, 0xE5, 0xF9, 0x21, 0x63, 0xFB, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0x61, 0xD4, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67,
|
||||
0xFD, 0x41, 0x67, 0xE1, 0x68, 0x23, 0xC3, 0x6F, 0x69, 0xED, 0xF9, 0xFC, 0xA0, 0x00, 0xC2, 0x21, 0x2E, 0xFD, 0x22,
|
||||
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x65, 0xF8, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73,
|
||||
0x6D, 0xE9, 0xEC, 0xFD, 0x44, 0x2E, 0x6F, 0x61, 0x74, 0xD5, 0x78, 0xFF, 0xE8, 0xFF, 0xF9, 0xF5, 0x24, 0x42, 0x6F,
|
||||
0x61, 0xD9, 0x83, 0xD9, 0x83, 0x21, 0x74, 0xF9, 0x41, 0x6E, 0xF2, 0xAF, 0x43, 0x63, 0x74, 0x65, 0xE7, 0x07, 0xE7,
|
||||
0x07, 0xFD, 0x93, 0x41, 0x74, 0xE6, 0xFD, 0x41, 0x69, 0xE5, 0x70, 0x41, 0x61, 0xD6, 0xF0, 0x21, 0xAD, 0xFC, 0x21,
|
||||
0xC3, 0xFD, 0xA1, 0x04, 0xA2, 0x70, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xD9, 0x07, 0xD9, 0x43,
|
||||
0xFF, 0xFB, 0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x49, 0x21, 0x6F, 0xEA, 0x22, 0x6E, 0x74, 0xD4, 0xFD, 0xA0,
|
||||
0x00, 0x91, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x0F, 0x72, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD,
|
||||
0x22, 0x6F, 0x61, 0xFB, 0xFB, 0xA0, 0x03, 0x32, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x10, 0xF3,
|
||||
0x21, 0x2E, 0xFD, 0x23, 0x61, 0x2E, 0x73, 0xF5, 0xFA, 0xFD, 0x21, 0x73, 0xEB, 0x22, 0x2E, 0x65, 0xE5, 0xFD, 0x21,
|
||||
0x6C, 0xFB, 0x22, 0x65, 0x61, 0xEE, 0xFD, 0x22, 0x63, 0x64, 0xD3, 0xFB, 0x4D, 0x65, 0x61, 0x2E, 0x78, 0x6C, 0x73,
|
||||
0x63, 0x6D, 0x6E, 0x70, 0x72, 0x6F, 0x69, 0xFE, 0xF1, 0xFE, 0xF6, 0xD4, 0xD5, 0xFF, 0x04, 0xFF, 0x3B, 0xFF, 0x60,
|
||||
0xFF, 0x74, 0xFF, 0x77, 0xFF, 0x7B, 0xFF, 0x85, 0xFF, 0xB5, 0xFF, 0xC0, 0xFF, 0xFB, 0x42, 0x65, 0xC3, 0xFE, 0xC0,
|
||||
0xFE, 0xC6, 0xA0, 0x03, 0x23, 0x21, 0x2E, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x43, 0x2E,
|
||||
0x73, 0x69, 0xD4, 0x97, 0xE5, 0x91, 0xFC, 0xD0, 0x21, 0x65, 0xF6, 0x41, 0x73, 0xFE, 0xB1, 0x44, 0x2E, 0x73, 0x69,
|
||||
0x6E, 0xFE, 0xAA, 0xFE, 0xAD, 0xFF, 0xFC, 0xFE, 0xAD, 0x43, 0x2E, 0x74, 0x65, 0xD4, 0x79, 0xFF, 0xEC, 0xFF, 0xF3,
|
||||
0xA0, 0x0C, 0xF1, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x11, 0x22, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0x6F, 0x69, 0x65, 0xC7, 0xEB, 0xFD, 0x42,
|
||||
0x6F, 0x72, 0xD4, 0x4A, 0xE0, 0x14, 0x45, 0x2E, 0x64, 0x66, 0x67, 0x74, 0xD4, 0x43, 0xFF, 0xF9, 0xF1, 0x94, 0xE5,
|
||||
0xEC, 0xFA, 0x5A, 0x41, 0x65, 0xFC, 0x6C, 0x42, 0x6E, 0x73, 0xFE, 0x56, 0xFE, 0x56, 0x41, 0x6F, 0xFF, 0x9E, 0x41,
|
||||
0x73, 0xFE, 0x48, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFE, 0x44, 0xFE, 0x47, 0xFF, 0xF8, 0xFF, 0xFC, 0xFE, 0x47,
|
||||
0x42, 0x61, 0x73, 0xFF, 0xF0, 0xFE, 0x37, 0x43, 0x2E, 0x73, 0x69, 0xFE, 0x2D, 0xFE, 0x30, 0xFF, 0x7F, 0x43, 0x73,
|
||||
0x2E, 0x6E, 0xFE, 0x26, 0xFE, 0x23, 0xFE, 0x26, 0x23, 0xAD, 0xA9, 0xA1, 0xE5, 0xEC, 0xF6, 0x45, 0x6D, 0x2E, 0x73,
|
||||
0x69, 0x6E, 0xFF, 0xC6, 0xFE, 0x12, 0xFE, 0x15, 0xFF, 0x64, 0xFE, 0x15, 0xA0, 0x03, 0x22, 0x21, 0x2E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x41, 0x65, 0xFF, 0x32, 0x42, 0x2E, 0x73, 0xFF, 0x2B, 0xFF, 0x2E, 0x23, 0x65, 0x61, 0x6F, 0xF9, 0xF9,
|
||||
0xF9, 0x41, 0x73, 0xFF, 0x20, 0x21, 0x6F, 0xFC, 0x41, 0x68, 0xD7, 0xC2, 0xC2, 0x00, 0xD1, 0x2E, 0x73, 0xFD, 0xDC,
|
||||
0xFD, 0xDF, 0x22, 0x6F, 0x61, 0xF7, 0xF7, 0x4C, 0x6F, 0xC3, 0x65, 0x61, 0x2E, 0x6D, 0x74, 0x6C, 0x73, 0x6E, 0x63,
|
||||
0x69, 0xFF, 0x7B, 0xFF, 0xB5, 0xFF, 0xBC, 0xFF, 0x24, 0xD3, 0xAA, 0xFF, 0xD2, 0xFF, 0xD5, 0xFF, 0xE0, 0xFF, 0xD5,
|
||||
0xFF, 0xEB, 0xFF, 0xEE, 0xFF, 0xFB, 0x41, 0x61, 0xFE, 0xFF, 0x43, 0x65, 0x6F, 0x61, 0xF0, 0x49, 0xF0, 0x49, 0xFB,
|
||||
0xBA, 0x43, 0x2E, 0x61, 0x65, 0xFD, 0x9B, 0xFD, 0xA1, 0xFE, 0xED, 0x43, 0x2E, 0x73, 0x72, 0xFD, 0x91, 0xFD, 0x94,
|
||||
0xFF, 0xF6, 0x48, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x6F, 0x61, 0x65, 0xD3, 0x63, 0xFC, 0xFE, 0xFC, 0xFE, 0xFF, 0xE2,
|
||||
0xFC, 0xE6, 0xFF, 0xF6, 0xFD, 0x8D, 0xE3, 0xDD, 0xA0, 0x05, 0x41, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
||||
0xFD, 0x41, 0x6E, 0xFD, 0x65, 0x21, 0xB3, 0xFC, 0x41, 0x65, 0xFE, 0xAD, 0x21, 0x6E, 0xFC, 0x22, 0xC3, 0x6F, 0xF6,
|
||||
0xFD, 0x43, 0x74, 0x61, 0x69, 0xE4, 0xD8, 0xFF, 0xEA, 0xFF, 0xFB, 0x41, 0x72, 0xE4, 0xCE, 0x41, 0x64, 0xFE, 0xBA,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x72, 0x69, 0xE4,
|
||||
0xB7, 0xFF, 0xFD, 0x41, 0x69, 0xE2, 0xB7, 0x41, 0x74, 0xED, 0x7B, 0x43, 0x64, 0x73, 0x74, 0xEA, 0xF2, 0xFF, 0xFC,
|
||||
0xE4, 0xA8, 0x41, 0x73, 0xEC, 0xE8, 0x42, 0x2E, 0x65, 0xD2, 0xF0, 0xFF, 0xFC, 0xA0, 0x08, 0xF1, 0x21, 0x2E, 0xFD,
|
||||
0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x73, 0xFD, 0x21, 0xAD, 0xFD, 0x53, 0x61, 0x69, 0x73, 0x2E,
|
||||
0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0x6F, 0x63, 0x65, 0x66, 0x67, 0x70, 0x75, 0x6C, 0xC3, 0xFE, 0x25, 0xFE, 0x38,
|
||||
0xFE, 0x59, 0xD2, 0xD2, 0xFE, 0x81, 0xFE, 0x8F, 0xFE, 0x9F, 0xFF, 0x28, 0xFF, 0x4D, 0xFF, 0x6F, 0xFB, 0x0B, 0xFF,
|
||||
0xA7, 0xFF, 0xB1, 0xFF, 0xC8, 0xFF, 0xB1, 0xFF, 0xCF, 0xFF, 0xD7, 0xFF, 0xE5, 0xFF, 0xFD, 0xA0, 0x01, 0xB2, 0x21,
|
||||
0xA1, 0xFD, 0x43, 0xC3, 0x65, 0x73, 0xFF, 0xFD, 0xD2, 0xF0, 0xE3, 0x8C, 0x41, 0x65, 0xEB, 0xDA, 0x21, 0x6C, 0xFC,
|
||||
0x41, 0x65, 0xE4, 0xF6, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x43, 0x2E, 0x63, 0x74, 0xD2, 0x77, 0xFF, 0xF3, 0xFF,
|
||||
0xFD, 0x41, 0x61, 0xD2, 0x6D, 0x21, 0x63, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x6F, 0xD5, 0x4C, 0x43, 0x6F, 0x62, 0x69,
|
||||
0xFD, 0xD5, 0xFF, 0xF9, 0xFF, 0xFC, 0xA0, 0x01, 0xB1, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x65, 0xFD, 0x43,
|
||||
0x65, 0x6F, 0x72, 0xEC, 0xC5, 0xD6, 0x64, 0xDE, 0x0C, 0x45, 0x2E, 0x68, 0x64, 0x65, 0x74, 0xD2, 0x3F, 0xFF, 0xF3,
|
||||
0xE3, 0xEC, 0xEB, 0xCF, 0xFF, 0xF6, 0xA0, 0x04, 0x13, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x42, 0x2E, 0x65, 0xFC, 0x44, 0xFF, 0xFD, 0xA0, 0x03, 0xC2, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x61, 0xED, 0x22, 0x61, 0x65, 0xEA, 0xEA, 0x46, 0x73, 0x2E, 0x6E, 0x69, 0x62, 0x72, 0xFF, 0xE8, 0xFC, 0x2C, 0xFC,
|
||||
0x2F, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFB, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFC, 0x19, 0xFC, 0x1C, 0xFD, 0xCD,
|
||||
0xFD, 0x6B, 0xFC, 0x1C, 0x42, 0x73, 0x61, 0xFC, 0x0C, 0xFF, 0xF0, 0x43, 0xA9, 0xA1, 0xAD, 0xFD, 0xD5, 0xFF, 0xD6,
|
||||
0xFF, 0xF9, 0xA0, 0x02, 0xF3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6D, 0xFD, 0x43, 0x65,
|
||||
0x61, 0x6F, 0xEE, 0x8D, 0xEE, 0x8D, 0xEE, 0x8D, 0x43, 0x2E, 0x73, 0x69, 0xFF, 0xA2, 0xFF, 0xA5, 0xFF, 0xA8, 0x21,
|
||||
0x65, 0xF6, 0xA0, 0x03, 0xE3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x24, 0x2E, 0x73, 0x69, 0x6E, 0xF7, 0xFA, 0xFD,
|
||||
0xFA, 0x43, 0x2E, 0x74, 0x65, 0xFF, 0x83, 0xFF, 0xEB, 0xFF, 0xF7, 0x21, 0x6F, 0xEA, 0x41, 0x65, 0xFF, 0x7C, 0x21,
|
||||
0x6E, 0xE0, 0x21, 0x73, 0xDA, 0x25, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0xDA, 0xF3, 0xFD, 0xDA, 0x22, 0x61, 0x73,
|
||||
0xF5, 0xCF, 0x23, 0x2E, 0x73, 0x69, 0xC7, 0xCA, 0xCD, 0x23, 0x73, 0x2E, 0x6E, 0xC3, 0xC0, 0xC3, 0x23, 0xAD, 0xA9,
|
||||
0xA1, 0xED, 0xF2, 0xF9, 0x45, 0x6D, 0x2E, 0x73, 0x69, 0x6E, 0xFF, 0xCE, 0xFF, 0xB2, 0xFF, 0xB5, 0xFF, 0xB8, 0xFF,
|
||||
0xB5, 0x44, 0x6F, 0xC3, 0x65, 0x61, 0xFF, 0xC5, 0xFF, 0xE9, 0xFF, 0xF0, 0xFF, 0xAB, 0xA0, 0x10, 0x54, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xEE, 0xF1,
|
||||
0xFD, 0x21, 0x65, 0xF9, 0x42, 0x61, 0x6C, 0xFF, 0x82, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFF, 0x72, 0xFF, 0x75, 0x43,
|
||||
0x2E, 0x61, 0x65, 0xFF, 0x6B, 0xFF, 0xF9, 0xFF, 0x71, 0x43, 0x2E, 0x73, 0x72, 0xFF, 0x61, 0xFF, 0x64, 0xFF, 0xF6,
|
||||
0x43, 0x2E, 0x6F, 0x61, 0xFE, 0xEC, 0xFF, 0xF6, 0xFF, 0xE5, 0x47, 0x73, 0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0xFF,
|
||||
0x5F, 0xFF, 0x69, 0xFE, 0xE5, 0xFF, 0x6C, 0xFF, 0xAB, 0xFF, 0xD4, 0xFF, 0xF6, 0x42, 0x2E, 0x65, 0xFB, 0x09, 0xFC,
|
||||
0x5B, 0x21, 0x64, 0xF9, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x45, 0x2E, 0x65, 0x61, 0x6D, 0x69, 0xFA, 0xF9, 0xFC,
|
||||
0x4B, 0xFA, 0xFF, 0xFB, 0x10, 0xFF, 0xFD, 0x21, 0x72, 0xF0, 0x21, 0x6F, 0xFD, 0x4B, 0xC3, 0x65, 0x2E, 0x6D, 0x74,
|
||||
0x6C, 0x73, 0x6E, 0x6F, 0x61, 0x69, 0xFE, 0xE1, 0xFE, 0xF7, 0xD0, 0xBF, 0xFA, 0x5A, 0xFA, 0x5A, 0xFE, 0xFA, 0xFA,
|
||||
0x5A, 0xFA, 0x42, 0xFC, 0x35, 0xFF, 0xC4, 0xFF, 0xFD, 0x46, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x72, 0xD0, 0x9D, 0xFA,
|
||||
0x38, 0xFA, 0x38, 0xFD, 0x1C, 0xFA, 0x20, 0xFA, 0xCC, 0x41, 0x61, 0xE1, 0x84, 0x21, 0x6C, 0xFC, 0x21, 0x64, 0xFD,
|
||||
0x41, 0x6F, 0xFB, 0x7E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x2E, 0xE3,
|
||||
0x46, 0x42, 0x2E, 0x73, 0xE3, 0x42, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x69, 0xFB, 0x23, 0x64, 0x6D,
|
||||
0x63, 0xD7, 0xEA, 0xFD, 0xA0, 0x00, 0x81, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA1, 0xFD,
|
||||
0x42, 0x6F, 0x72, 0xD4, 0x62, 0xDC, 0x11, 0xA0, 0x11, 0x92, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x44, 0x61, 0x6F, 0x74, 0x75, 0xE0, 0xA2,
|
||||
0xE9, 0x8F, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x6E, 0xE1, 0xC8, 0x42, 0x63, 0x72, 0xE1, 0xC4, 0xE1, 0xC4, 0x42, 0x6D,
|
||||
0x72, 0xD9, 0x69, 0xD4, 0xC3, 0x41, 0x67, 0xD9, 0xBC, 0x42, 0x61, 0x6F, 0xD0, 0x9B, 0xD0, 0x9B, 0x43, 0x61, 0x65,
|
||||
0x6F, 0xD0, 0x94, 0xD0, 0x94, 0xD0, 0x94, 0x21, 0x69, 0xF6, 0x44, 0x61, 0x69, 0x65, 0x6F, 0xD0, 0x87, 0xD0, 0x87,
|
||||
0xD0, 0x87, 0xD0, 0x87, 0x44, 0x6A, 0x67, 0x6C, 0x6D, 0xFF, 0xDF, 0xD9, 0x97, 0xFF, 0xF0, 0xFF, 0xF3, 0x44, 0xA1,
|
||||
0xA9, 0xB3, 0xAD, 0xFF, 0xC7, 0xFF, 0xCE, 0xD8, 0x5C, 0xFF, 0xF3, 0x41, 0x72, 0xDC, 0x2A, 0x21, 0x65, 0xFC, 0x41,
|
||||
0x6D, 0xE2, 0x99, 0x21, 0x75, 0xFC, 0x41, 0x69, 0xD1, 0xE0, 0x44, 0x63, 0x67, 0x6C, 0x6D, 0xFF, 0xF2, 0xD9, 0x83,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xD2, 0x2C, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x41, 0x69, 0xD7, 0xE1, 0x21,
|
||||
0x75, 0xFC, 0x43, 0x63, 0x67, 0x71, 0xD1, 0xB0, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x61, 0xC3, 0x6F, 0xD1, 0xA3, 0xD9,
|
||||
0x2F, 0xD1, 0xA3, 0x41, 0xAD, 0xD1, 0x99, 0x43, 0x65, 0x69, 0xC3, 0xD1, 0x95, 0xD1, 0x95, 0xFF, 0xFC, 0x42, 0x69,
|
||||
0x61, 0xD7, 0x0B, 0xD1, 0x8E, 0x44, 0xA1, 0xAD, 0xA9, 0xB3, 0xD1, 0x84, 0xD1, 0x84, 0xD1, 0x84, 0xD1, 0x84, 0x45,
|
||||
0x61, 0xC3, 0x69, 0x65, 0x6F, 0xD1, 0x77, 0xFF, 0xF3, 0xD1, 0x77, 0xD1, 0x77, 0xD1, 0x77, 0x41, 0x6F, 0xD1, 0xE6,
|
||||
0x25, 0x6A, 0x67, 0x6C, 0x6D, 0x74, 0xC0, 0xCE, 0xD8, 0xEC, 0xFC, 0x41, 0xB3, 0xD1, 0x58, 0x21, 0xC3, 0xFC, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x72, 0xFF, 0x7F, 0x41, 0xA9, 0xD1, 0x4D, 0x43, 0x63, 0x71, 0x73, 0xD1, 0x46, 0xD1, 0x46, 0xD6,
|
||||
0x27, 0x22, 0xC3, 0x69, 0xF2, 0xF6, 0x41, 0x6D, 0xD1, 0xA5, 0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF9, 0xFD, 0x41,
|
||||
0x71, 0xD1, 0x2B, 0x21, 0x73, 0xFC, 0x41, 0x61, 0xD1, 0x35, 0x21, 0x6C, 0xFC, 0x47, 0x62, 0x6E, 0x63, 0x74, 0x67,
|
||||
0x65, 0x70, 0xFF, 0xCC, 0xD8, 0xD5, 0xFF, 0xCF, 0xFF, 0xE1, 0xFF, 0xED, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x72, 0x74,
|
||||
0x63, 0xD1, 0x07, 0xD1, 0x07, 0xD1, 0x07, 0x21, 0x61, 0xF6, 0x42, 0x62, 0x64, 0xD8, 0xB2, 0xFF, 0xFD, 0x41, 0x72,
|
||||
0xD0, 0x12, 0xA0, 0x0D, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x48, 0xC3, 0x61, 0x65, 0x69,
|
||||
0x6F, 0x75, 0x74, 0x70, 0xFE, 0xF9, 0xFF, 0x18, 0xFF, 0x36, 0xFF, 0x80, 0xFF, 0xC6, 0xFF, 0xE9, 0xFF, 0xF0, 0xFF,
|
||||
0xFD, 0x41, 0x72, 0xFA, 0x54, 0x21, 0x74, 0xFC, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x65, 0xC3, 0xFA, 0xFD,
|
||||
0x4F, 0x6F, 0x73, 0x2E, 0x6D, 0x6E, 0x72, 0x64, 0x65, 0x61, 0xC3, 0x63, 0x74, 0x75, 0x78, 0x6C, 0xFC, 0x13, 0xFC,
|
||||
0x2E, 0xCE, 0xA5, 0xFC, 0x46, 0xFC, 0x66, 0xFD, 0xE6, 0xFE, 0x08, 0xFE, 0x22, 0xFE, 0x48, 0xFE, 0x5B, 0xFE, 0x7D,
|
||||
0xFE, 0x8A, 0xFE, 0x8E, 0xFF, 0xD5, 0xFF, 0xFB, 0x43, 0x2E, 0x73, 0x6D, 0xF8, 0x9B, 0xF8, 0x9E, 0xFA, 0x4F, 0xA0,
|
||||
0x03, 0x53, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x84, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xF0, 0xF0, 0xFB, 0x22, 0x2E, 0x6C, 0xE3, 0xF9, 0x21, 0x65, 0xFB, 0x23, 0x65,
|
||||
0x61, 0x6F, 0xE1, 0xE1, 0xE1, 0x23, 0x65, 0x6F, 0x61, 0xDA, 0xDA, 0xDA, 0x21, 0x6C, 0xF9, 0x24, 0x6D, 0x74, 0x6C,
|
||||
0x65, 0xEC, 0xEC, 0xEF, 0xFD, 0x22, 0x2E, 0x6C, 0xC1, 0xED, 0x21, 0x73, 0xFB, 0x21, 0x6F, 0xFD, 0x23, 0x73, 0x6E,
|
||||
0x6F, 0xEC, 0xFD, 0xFA, 0x21, 0x6F, 0xF9, 0x43, 0x73, 0x69, 0x6D, 0xF8, 0x40, 0xF9, 0x8F, 0xFF, 0xFD, 0x21, 0xA1,
|
||||
0xF6, 0x43, 0x6F, 0x61, 0xC3, 0xF8, 0x33, 0xFF, 0x95, 0xFF, 0xFD, 0x41, 0x69, 0xF9, 0x78, 0x21, 0x74, 0xFC, 0x41,
|
||||
0x73, 0xF8, 0xEC, 0x42, 0x2E, 0x65, 0xF8, 0xE5, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x43, 0x69, 0x61, 0x65, 0xFF, 0xEF,
|
||||
0xFF, 0xFD, 0xF8, 0x1C, 0x41, 0x61, 0xEA, 0xAB, 0x42, 0x6F, 0x69, 0xCE, 0x5D, 0xCE, 0xBE, 0x21, 0x6D, 0xF9, 0x41,
|
||||
0x69, 0xCE, 0xB4, 0x21, 0x6D, 0xFC, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x44, 0x6D, 0x74, 0x6C, 0x6F,
|
||||
0xF6, 0xB8, 0xFF, 0xE3, 0xFF, 0xFB, 0xE7, 0x2D, 0xC3, 0x02, 0x91, 0x61, 0xC3, 0x65, 0xCF, 0xCC, 0xD7, 0x58, 0xCF,
|
||||
0xCC, 0x21, 0x69, 0xF4, 0x21, 0x63, 0xFD, 0xC1, 0x05, 0x81, 0x61, 0xCE, 0x3D, 0x21, 0x69, 0xFA, 0x21, 0x63, 0xFD,
|
||||
0x21, 0xAD, 0xFD, 0xA0, 0x02, 0xB1, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x23, 0x62, 0x65, 0xC3,
|
||||
0xF4, 0xFA, 0xFD, 0x21, 0x62, 0xED, 0x21, 0x6C, 0xEA, 0x21, 0x6D, 0xE7, 0x21, 0x69, 0xE7, 0x21, 0x70, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0x25, 0xAD, 0xA1, 0xA9, 0xBA, 0xB3, 0xEE, 0xF1, 0xE1, 0xF4, 0xFD, 0x22, 0x74, 0x69, 0xD0, 0xD0, 0x21,
|
||||
0x6E, 0xCE, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x72, 0xF5, 0xFD, 0x25, 0x69, 0xC3, 0x61, 0x65, 0x75, 0xCC, 0xE5, 0xD6,
|
||||
0xFB, 0xD9, 0x41, 0x75, 0xEA, 0x4B, 0xA0, 0x0B, 0xE3, 0x22, 0x75, 0x74, 0xFD, 0xFD, 0x22, 0x73, 0x64, 0xFB, 0xF8,
|
||||
0xA0, 0x0C, 0x63, 0x21, 0x72, 0xFD, 0xA0, 0x0C, 0x93, 0x21, 0x2E, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xEF, 0xF7, 0xFD,
|
||||
0x41, 0x73, 0xEA, 0x44, 0x21, 0xA9, 0xFC, 0xA0, 0x0A, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0xA0, 0x0C, 0x42, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x65, 0xFD, 0x24, 0x69, 0xC3, 0x65,
|
||||
0x72, 0xD7, 0xE2, 0xEE, 0xFD, 0x21, 0x72, 0xF7, 0x42, 0x65, 0x72, 0xFF, 0xFD, 0xCE, 0x2D, 0x41, 0x72, 0xFC, 0xB4,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x72, 0xCD, 0xC6, 0x21, 0x65, 0xFC, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0xC3, 0x68, 0x66, 0x6D, 0x74, 0x6F, 0x61, 0x64, 0x67, 0xFF, 0x2D,
|
||||
0xFF, 0x3C, 0xFF, 0x7F, 0xFD, 0xF7, 0xFF, 0x8A, 0xFF, 0xDC, 0xE9, 0x9F, 0xE9, 0x9F, 0xFF, 0xED, 0xFF, 0xFD, 0x41,
|
||||
0x65, 0xD8, 0x86, 0x43, 0x6E, 0x2E, 0x73, 0xD8, 0x7E, 0xF7, 0x21, 0xF7, 0x24, 0x42, 0x6F, 0x61, 0xFF, 0xF6, 0xF7,
|
||||
0x1D, 0x42, 0x2E, 0x73, 0xF8, 0xC5, 0xF8, 0xC8, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x41, 0x2E, 0xEE, 0x81, 0x21, 0x73,
|
||||
0xFC, 0x21, 0x65, 0xFD, 0x44, 0x6E, 0x72, 0x2E, 0x73, 0xFF, 0xF1, 0xFF, 0xFD, 0xF7, 0x72, 0xF7, 0x75, 0x41, 0x61,
|
||||
0xDE, 0x29, 0x41, 0x72, 0xF8, 0xA1, 0x42, 0x2E, 0x73, 0xF7, 0x5D, 0xF7, 0x60, 0x4A, 0x67, 0x64, 0x73, 0x6E, 0x62,
|
||||
0x63, 0x61, 0x74, 0x65, 0x6F, 0xFE, 0x65, 0xFE, 0x84, 0xFE, 0xAB, 0xFF, 0x9A, 0xFF, 0xB9, 0xFF, 0xC7, 0xFF, 0xE4,
|
||||
0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xF9, 0x41, 0x69, 0xFB, 0xFC, 0x21, 0x72, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x74, 0xFD,
|
||||
0x68, 0xA0, 0x11, 0xB2, 0x42, 0x64, 0x74, 0xFF, 0xFD, 0xFC, 0x01, 0x21, 0x69, 0xF9, 0x21, 0x73, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x74, 0x6C, 0x6E, 0xDD, 0xE0, 0xFD, 0x5C, 0x62,
|
||||
0x2E, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0xC3, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xCD, 0x5C, 0xDB, 0x8C, 0xDD, 0xF1, 0xE3, 0xC6, 0xE4, 0x43, 0xE5,
|
||||
0xC4, 0xE7, 0x42, 0xE7, 0x94, 0xE7, 0xDD, 0xE8, 0x9B, 0xE9, 0xD6, 0xEA, 0x67, 0xED, 0x21, 0xED, 0x83, 0xEE, 0x2C,
|
||||
0xF0, 0x08, 0xF2, 0x7F, 0xF2, 0xDD, 0xF3, 0x29, 0xF3, 0x78, 0xF3, 0xC3, 0xF4, 0x0C, 0xF6, 0x24, 0xF7, 0x4C, 0xF9,
|
||||
0x4F, 0xFD, 0x7C, 0xFF, 0xB0, 0xFF, 0xF9,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns es_patterns = {
|
||||
es_trie_data,
|
||||
sizeof(es_trie_data),
|
||||
};
|
||||
@ -40,6 +40,23 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
||||
return false;
|
||||
}
|
||||
|
||||
// flush the contents of partWordBuffer to currentTextBlock
|
||||
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
// determine font style
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (boldUntilDepth < depth) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (italicUntilDepth < depth) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
// flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
||||
partWordBufferIndex = 0;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
||||
if (currentTextBlock) {
|
||||
@ -67,39 +84,43 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
if (strcmp(name, "table") == 0) {
|
||||
// Add placeholder text
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
if (self->currentTextBlock) {
|
||||
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
|
||||
}
|
||||
|
||||
// Skip table contents
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for a element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||
|
||||
// Skip table contents (skip until parent as we pre-advanced depth above)
|
||||
self->skipUntilDepth = self->depth - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
// TODO: Start processing image tags
|
||||
std::string alt;
|
||||
std::string alt = "[Image]";
|
||||
if (atts != nullptr) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "alt") == 0) {
|
||||
if (strlen(atts[i + 1]) > 0) {
|
||||
alt = "[Image: " + std::string(atts[i + 1]) + "]";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for a element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
|
||||
} else {
|
||||
// Skip for now
|
||||
self->skipUntilDepth = self->depth;
|
||||
self->depth += 1;
|
||||
// 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)) {
|
||||
// start skip
|
||||
@ -123,21 +144,43 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
if (strcmp(name, "br") == 0) {
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||
} else {
|
||||
self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment);
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment));
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
}
|
||||
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
self->depth += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Unprocessed tag, just increasing depth and continue forward
|
||||
self->depth += 1;
|
||||
}
|
||||
|
||||
@ -149,22 +192,11 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
return;
|
||||
}
|
||||
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (self->boldUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (isWhitespace(s[i])) {
|
||||
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle);
|
||||
self->partWordBufferIndex = 0;
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
// Skip the whitespace char
|
||||
continue;
|
||||
@ -186,9 +218,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
|
||||
// If we're about to run out of space, then cut the word off and start a new one
|
||||
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle);
|
||||
self->partWordBufferIndex = 0;
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
@ -216,21 +246,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
// text styling needs to be overhauled to fix it.
|
||||
const bool shouldBreakText =
|
||||
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
||||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
|
||||
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
strcmp(name, "table") == 0 || matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||
|
||||
if (shouldBreakText) {
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (self->boldUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (self->italicUntilDepth < self->depth) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
}
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||
self->currentTextBlock->addWord(self->partWordBuffer, fontStyle);
|
||||
self->partWordBufferIndex = 0;
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ class ChapterHtmlSlimParser {
|
||||
bool hyphenationEnabled;
|
||||
|
||||
void startNewTextBlock(TextBlock::Style style);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
// XML callbacks
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
|
||||
@ -38,6 +38,9 @@ ContentOpfParser::~ContentOpfParser() {
|
||||
if (SdMan.exists((cachePath + itemCacheFile).c_str())) {
|
||||
SdMan.remove((cachePath + itemCacheFile).c_str());
|
||||
}
|
||||
itemIndex.clear();
|
||||
itemIndex.shrink_to_fit();
|
||||
useItemIndex = false;
|
||||
}
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
@ -129,6 +132,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
||||
millis());
|
||||
}
|
||||
|
||||
// Sort item index for binary search if we have enough items
|
||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
self->useItemIndex = true;
|
||||
Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -180,6 +192,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
}
|
||||
}
|
||||
|
||||
// Record index entry for fast lookup later
|
||||
if (self->tempItemStore) {
|
||||
ItemIndexEntry entry;
|
||||
entry.idHash = fnvHash(itemId);
|
||||
entry.idLen = static_cast<uint16_t>(itemId.size());
|
||||
entry.fileOffset = static_cast<uint32_t>(self->tempItemStore.position());
|
||||
self->itemIndex.push_back(entry);
|
||||
}
|
||||
|
||||
// Write items down to SD card
|
||||
serialization::writeString(self->tempItemStore, itemId);
|
||||
serialization::writeString(self->tempItemStore, href);
|
||||
@ -215,21 +236,52 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "idref") == 0) {
|
||||
const std::string idref = atts[i + 1];
|
||||
// Resolve the idref to href using items map
|
||||
std::string href;
|
||||
bool found = false;
|
||||
|
||||
if (self->useItemIndex) {
|
||||
// Fast path: binary search
|
||||
uint32_t targetHash = fnvHash(idref);
|
||||
uint16_t targetLen = static_cast<uint16_t>(idref.size());
|
||||
|
||||
auto it = std::lower_bound(self->itemIndex.begin(), self->itemIndex.end(),
|
||||
ItemIndexEntry{targetHash, targetLen, 0},
|
||||
[](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
|
||||
// Check for match (may need to check a few due to hash collisions)
|
||||
while (it != self->itemIndex.end() && it->idHash == targetHash) {
|
||||
self->tempItemStore.seek(it->fileOffset);
|
||||
std::string itemId;
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
if (itemId == idref) {
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
std::string href;
|
||||
while (self->tempItemStore.available()) {
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
if (itemId == idref) {
|
||||
self->cache->createSpineEntry(href);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found && self->cache) {
|
||||
self->cache->createSpineEntry(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub.h"
|
||||
#include "expat.h"
|
||||
|
||||
@ -28,6 +31,27 @@ class ContentOpfParser final : public Print {
|
||||
FsFile tempItemStore;
|
||||
std::string coverItemId;
|
||||
|
||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
||||
struct ItemIndexEntry {
|
||||
uint32_t idHash; // FNV-1a hash of itemId
|
||||
uint16_t idLen; // length for collision reduction
|
||||
uint32_t fileOffset; // offset in .items.bin
|
||||
};
|
||||
std::vector<ItemIndexEntry> itemIndex;
|
||||
bool useItemIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a hash function
|
||||
static uint32_t fnvHash(const std::string& s) {
|
||||
uint32_t hash = 2166136261u;
|
||||
for (char c : s) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
@ -3,43 +3,88 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "BitmapHelpers.h"
|
||||
|
||||
// ============================================================================
|
||||
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
|
||||
// ============================================================================
|
||||
// Note: For cover images, dithering is done in JpegToBmpConverter.cpp
|
||||
// This file handles BMP reading - use simple quantization to avoid double-dithering
|
||||
constexpr bool USE_ATKINSON = true; // Use Atkinson dithering instead of Floyd-Steinberg
|
||||
// IMAGE PROCESSING OPTIONS
|
||||
// ============================================================================
|
||||
constexpr bool USE_ATKINSON = true;
|
||||
|
||||
Bitmap::~Bitmap() {
|
||||
delete[] errorCurRow;
|
||||
delete[] errorNextRow;
|
||||
|
||||
delete atkinsonDitherer;
|
||||
delete fsDitherer;
|
||||
}
|
||||
|
||||
uint16_t Bitmap::readLE16(FsFile& f) {
|
||||
const int c0 = f.read();
|
||||
const int c1 = f.read();
|
||||
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
|
||||
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
|
||||
return static_cast<uint16_t>(b0) | (static_cast<uint16_t>(b1) << 8);
|
||||
// ===================================
|
||||
// IO Helpers
|
||||
// ===================================
|
||||
|
||||
int Bitmap::readByte() const {
|
||||
if (file && *file) {
|
||||
return file->read();
|
||||
} else if (memoryBuffer) {
|
||||
if (bufferPos < memorySize) {
|
||||
return memoryBuffer[bufferPos++];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint32_t Bitmap::readLE32(FsFile& f) {
|
||||
const int c0 = f.read();
|
||||
const int c1 = f.read();
|
||||
const int c2 = f.read();
|
||||
const int c3 = f.read();
|
||||
size_t Bitmap::readBytes(void* buf, size_t count) const {
|
||||
if (file && *file) {
|
||||
return file->read(buf, count);
|
||||
} else if (memoryBuffer) {
|
||||
size_t available = memorySize - bufferPos;
|
||||
if (count > available) count = available;
|
||||
memcpy(buf, memoryBuffer + bufferPos, count);
|
||||
bufferPos += count;
|
||||
return count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
|
||||
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
|
||||
const auto b2 = static_cast<uint8_t>(c2 < 0 ? 0 : c2);
|
||||
const auto b3 = static_cast<uint8_t>(c3 < 0 ? 0 : c3);
|
||||
bool Bitmap::seekSet(uint32_t pos) const {
|
||||
if (file && *file) {
|
||||
return file->seek(pos);
|
||||
} else if (memoryBuffer) {
|
||||
if (pos <= memorySize) {
|
||||
bufferPos = pos;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return static_cast<uint32_t>(b0) | (static_cast<uint32_t>(b1) << 8) | (static_cast<uint32_t>(b2) << 16) |
|
||||
(static_cast<uint32_t>(b3) << 24);
|
||||
bool Bitmap::seekCur(int32_t offset) const {
|
||||
if (file && *file) {
|
||||
return file->seekCur(offset);
|
||||
} else if (memoryBuffer) {
|
||||
if (bufferPos + offset <= memorySize) {
|
||||
bufferPos += offset;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t Bitmap::readLE16() {
|
||||
const int c0 = readByte();
|
||||
const int c1 = readByte();
|
||||
return static_cast<uint16_t>(c0 & 0xFF) | (static_cast<uint16_t>(c1 & 0xFF) << 8);
|
||||
}
|
||||
|
||||
uint32_t Bitmap::readLE32() {
|
||||
const int c0 = readByte();
|
||||
const int c1 = readByte();
|
||||
const int c2 = readByte();
|
||||
const int c3 = readByte();
|
||||
return static_cast<uint32_t>(c0 & 0xFF) | (static_cast<uint32_t>(c1 & 0xFF) << 8) |
|
||||
(static_cast<uint32_t>(c2 & 0xFF) << 16) | (static_cast<uint32_t>(c3 & 0xFF) << 24);
|
||||
}
|
||||
|
||||
const char* Bitmap::errorToString(BmpReaderError err) {
|
||||
@ -51,27 +96,25 @@ const char* Bitmap::errorToString(BmpReaderError err) {
|
||||
case BmpReaderError::SeekStartFailed:
|
||||
return "SeekStartFailed";
|
||||
case BmpReaderError::NotBMP:
|
||||
return "NotBMP (missing 'BM')";
|
||||
return "NotBMP";
|
||||
case BmpReaderError::DIBTooSmall:
|
||||
return "DIBTooSmall (<40 bytes)";
|
||||
return "DIBTooSmall";
|
||||
case BmpReaderError::BadPlanes:
|
||||
return "BadPlanes (!= 1)";
|
||||
return "BadPlanes";
|
||||
case BmpReaderError::UnsupportedBpp:
|
||||
return "UnsupportedBpp (expected 1, 2, 8, 24, or 32)";
|
||||
return "UnsupportedBpp";
|
||||
case BmpReaderError::UnsupportedCompression:
|
||||
return "UnsupportedCompression (expected BI_RGB or BI_BITFIELDS for 32bpp)";
|
||||
return "UnsupportedCompression";
|
||||
case BmpReaderError::BadDimensions:
|
||||
return "BadDimensions";
|
||||
case BmpReaderError::ImageTooLarge:
|
||||
return "ImageTooLarge (max 2048x3072)";
|
||||
return "ImageTooLarge";
|
||||
case BmpReaderError::PaletteTooLarge:
|
||||
return "PaletteTooLarge";
|
||||
|
||||
case BmpReaderError::SeekPixelDataFailed:
|
||||
return "SeekPixelDataFailed";
|
||||
case BmpReaderError::BufferTooSmall:
|
||||
return "BufferTooSmall";
|
||||
|
||||
case BmpReaderError::OomRowBuffer:
|
||||
return "OomRowBuffer";
|
||||
case BmpReaderError::ShortReadRow:
|
||||
@ -81,67 +124,85 @@ const char* Bitmap::errorToString(BmpReaderError err) {
|
||||
}
|
||||
|
||||
BmpReaderError Bitmap::parseHeaders() {
|
||||
if (!file) return BmpReaderError::FileInvalid;
|
||||
if (!file.seek(0)) return BmpReaderError::SeekStartFailed;
|
||||
if (!file && !memoryBuffer) return BmpReaderError::FileInvalid;
|
||||
if (!seekSet(0)) return BmpReaderError::SeekStartFailed;
|
||||
|
||||
// --- BMP FILE HEADER ---
|
||||
const uint16_t bfType = readLE16(file);
|
||||
const uint16_t bfType = readLE16();
|
||||
if (bfType != 0x4D42) return BmpReaderError::NotBMP;
|
||||
|
||||
file.seekCur(8);
|
||||
bfOffBits = readLE32(file);
|
||||
seekCur(8);
|
||||
bfOffBits = readLE32();
|
||||
|
||||
// --- DIB HEADER ---
|
||||
const uint32_t biSize = readLE32(file);
|
||||
const uint32_t biSize = readLE32();
|
||||
if (biSize < 40) return BmpReaderError::DIBTooSmall;
|
||||
|
||||
width = static_cast<int32_t>(readLE32(file));
|
||||
const auto rawHeight = static_cast<int32_t>(readLE32(file));
|
||||
width = static_cast<int32_t>(readLE32());
|
||||
const auto rawHeight = static_cast<int32_t>(readLE32());
|
||||
topDown = rawHeight < 0;
|
||||
height = topDown ? -rawHeight : rawHeight;
|
||||
|
||||
const uint16_t planes = readLE16(file);
|
||||
bpp = readLE16(file);
|
||||
const uint32_t comp = readLE32(file);
|
||||
const uint16_t planes = readLE16();
|
||||
bpp = readLE16();
|
||||
const uint32_t comp = readLE32();
|
||||
const bool validBpp = bpp == 1 || bpp == 2 || bpp == 8 || bpp == 24 || bpp == 32;
|
||||
|
||||
if (planes != 1) return BmpReaderError::BadPlanes;
|
||||
if (!validBpp) return BmpReaderError::UnsupportedBpp;
|
||||
// Allow BI_RGB (0) for all, and BI_BITFIELDS (3) for 32bpp which is common for BGRA masks.
|
||||
if (!(comp == 0 || (bpp == 32 && comp == 3))) return BmpReaderError::UnsupportedCompression;
|
||||
|
||||
file.seekCur(12); // biSizeImage, biXPelsPerMeter, biYPelsPerMeter
|
||||
const uint32_t colorsUsed = readLE32(file);
|
||||
seekCur(12);
|
||||
const uint32_t colorsUsed = readLE32();
|
||||
if (colorsUsed > 256u) return BmpReaderError::PaletteTooLarge;
|
||||
file.seekCur(4); // biClrImportant
|
||||
seekCur(4);
|
||||
|
||||
// Robustness Fix: Skip extended header bytes (V4/V5)
|
||||
if (biSize > 40) {
|
||||
seekCur(biSize - 40);
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0) return BmpReaderError::BadDimensions;
|
||||
|
||||
// Safety limits to prevent memory issues on ESP32
|
||||
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
||||
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT) {
|
||||
return BmpReaderError::ImageTooLarge;
|
||||
}
|
||||
|
||||
// Pre-calculate Row Bytes to avoid doing this every row
|
||||
rowBytes = (width * bpp + 31) / 32 * 4;
|
||||
|
||||
// Initialize safe default palette
|
||||
if (bpp == 1) {
|
||||
// For 1-bit, default to Black(0) and White(1)
|
||||
paletteLum[0] = 0;
|
||||
paletteLum[1] = 255;
|
||||
} else if (bpp <= 8) {
|
||||
int maxIdx = (1 << bpp) - 1;
|
||||
for (int i = 0; i <= maxIdx; i++) {
|
||||
paletteLum[i] = (i * 255) / maxIdx;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < 256; i++) paletteLum[i] = static_cast<uint8_t>(i);
|
||||
if (colorsUsed > 0) {
|
||||
for (uint32_t i = 0; i < colorsUsed; i++) {
|
||||
}
|
||||
|
||||
// If indexed color (<=8bpp), we MUST load the palette.
|
||||
// The palette is located AFTER the DIB header.
|
||||
if (bpp <= 8) {
|
||||
// Explicit seek to palette start
|
||||
if (!seekSet(14 + biSize)) return BmpReaderError::SeekStartFailed;
|
||||
|
||||
uint32_t colorsToRead = colorsUsed;
|
||||
if (colorsToRead == 0) colorsToRead = 1 << bpp;
|
||||
if (colorsToRead > 256) colorsToRead = 256;
|
||||
|
||||
for (uint32_t i = 0; i < colorsToRead; i++) {
|
||||
uint8_t rgb[4];
|
||||
file.read(rgb, 4); // Read B, G, R, Reserved in one go
|
||||
if (readBytes(rgb, 4) != 4) break;
|
||||
paletteLum[i] = (77u * rgb[2] + 150u * rgb[1] + 29u * rgb[0]) >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (!file.seek(bfOffBits)) {
|
||||
return BmpReaderError::SeekPixelDataFailed;
|
||||
}
|
||||
if (!seekSet(bfOffBits)) return BmpReaderError::SeekPixelDataFailed;
|
||||
|
||||
// Create ditherer if enabled (only for 2-bit output)
|
||||
// Use OUTPUT dimensions for dithering (after prescaling)
|
||||
if (bpp > 2 && dithering) {
|
||||
if (USE_ATKINSON) {
|
||||
atkinsonDitherer = new AtkinsonDitherer(width);
|
||||
@ -153,31 +214,25 @@ BmpReaderError Bitmap::parseHeaders() {
|
||||
return BmpReaderError::Ok;
|
||||
}
|
||||
|
||||
// packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white
|
||||
BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
|
||||
// Note: rowBuffer should be pre-allocated by the caller to size 'rowBytes'
|
||||
if (file.read(rowBuffer, rowBytes) != rowBytes) return BmpReaderError::ShortReadRow;
|
||||
if (readBytes(rowBuffer, rowBytes) != (size_t)rowBytes) return BmpReaderError::ShortReadRow;
|
||||
|
||||
prevRowY += 1;
|
||||
|
||||
uint8_t* outPtr = data;
|
||||
uint8_t currentOutByte = 0;
|
||||
int bitShift = 6;
|
||||
int currentX = 0;
|
||||
|
||||
// Helper lambda to pack 2bpp color into the output stream
|
||||
auto packPixel = [&](const uint8_t lum) {
|
||||
uint8_t color;
|
||||
if (atkinsonDitherer) {
|
||||
color = atkinsonDitherer->processPixel(adjustPixel(lum), currentX);
|
||||
color = atkinsonDitherer->processPixel(lum, currentX);
|
||||
} else if (fsDitherer) {
|
||||
color = fsDitherer->processPixel(adjustPixel(lum), currentX);
|
||||
color = fsDitherer->processPixel(lum, currentX);
|
||||
} else {
|
||||
if (bpp > 2) {
|
||||
// Simple quantization or noise dithering
|
||||
color = quantize(adjustPixel(lum), currentX, prevRowY);
|
||||
} else {
|
||||
// do not quantize 2bpp image
|
||||
color = static_cast<uint8_t>(lum >> 6);
|
||||
}
|
||||
}
|
||||
@ -192,13 +247,18 @@ BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
|
||||
currentX++;
|
||||
};
|
||||
|
||||
uint8_t lum;
|
||||
|
||||
switch (bpp) {
|
||||
case 32: {
|
||||
const uint8_t* p = rowBuffer;
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t lum; // Declare lum here
|
||||
// Handle Alpha channel (byte 3). If transparent (<128), treat as White.
|
||||
// This fixes 32-bit icons appearing as black squares on white backgrounds.
|
||||
if (p[3] < 128) {
|
||||
lum = 255;
|
||||
} else {
|
||||
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||
}
|
||||
packPixel(lum);
|
||||
p += 4;
|
||||
}
|
||||
@ -207,32 +267,27 @@ BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
|
||||
case 24: {
|
||||
const uint8_t* p = rowBuffer;
|
||||
for (int x = 0; x < width; x++) {
|
||||
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||
packPixel(lum);
|
||||
p += 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
for (int x = 0; x < width; x++) {
|
||||
packPixel(paletteLum[rowBuffer[x]]);
|
||||
}
|
||||
for (int x = 0; x < width; x++) packPixel(paletteLum[rowBuffer[x]]);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
for (int x = 0; x < width; x++) {
|
||||
lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
|
||||
uint8_t lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
|
||||
packPixel(lum);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
for (int x = 0; x < width; x++) {
|
||||
// Get palette index (0 or 1) from bit at position x
|
||||
const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0;
|
||||
// Use palette lookup for proper black/white mapping
|
||||
lum = paletteLum[palIndex];
|
||||
packPixel(lum);
|
||||
packPixel(paletteLum[palIndex]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -245,20 +300,13 @@ BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
|
||||
else if (fsDitherer)
|
||||
fsDitherer->nextRow();
|
||||
|
||||
// Flush remaining bits if width is not a multiple of 4
|
||||
if (bitShift != 6) *outPtr = currentOutByte;
|
||||
|
||||
return BmpReaderError::Ok;
|
||||
}
|
||||
|
||||
BmpReaderError Bitmap::rewindToData() const {
|
||||
if (!file.seek(bfOffBits)) {
|
||||
return BmpReaderError::SeekPixelDataFailed;
|
||||
}
|
||||
|
||||
// Reset dithering when rewinding
|
||||
if (!seekSet(bfOffBits)) return BmpReaderError::SeekPixelDataFailed;
|
||||
if (fsDitherer) fsDitherer->reset();
|
||||
if (atkinsonDitherer) atkinsonDitherer->reset();
|
||||
|
||||
return BmpReaderError::Ok;
|
||||
}
|
||||
|
||||
@ -32,11 +32,16 @@ class Bitmap {
|
||||
public:
|
||||
static const char* errorToString(BmpReaderError err);
|
||||
|
||||
explicit Bitmap(FsFile& file, bool dithering = false) : file(file), dithering(dithering) {}
|
||||
explicit Bitmap(FsFile& file, bool dithering = false) : file(&file), dithering(dithering) {}
|
||||
explicit Bitmap(const uint8_t* buffer, size_t size, bool dithering = false)
|
||||
: file(nullptr), memoryBuffer(buffer), memorySize(size), dithering(dithering) {}
|
||||
|
||||
~Bitmap();
|
||||
BmpReaderError parseHeaders();
|
||||
BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) const;
|
||||
BmpReaderError rewindToData() const;
|
||||
|
||||
// Getters
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
bool isTopDown() const { return topDown; }
|
||||
@ -46,10 +51,21 @@ class Bitmap {
|
||||
uint16_t getBpp() const { return bpp; }
|
||||
|
||||
private:
|
||||
static uint16_t readLE16(FsFile& f);
|
||||
static uint32_t readLE32(FsFile& f);
|
||||
// Internal IO helpers
|
||||
int readByte() const;
|
||||
size_t readBytes(void* buf, size_t count) const;
|
||||
bool seekSet(uint32_t pos) const;
|
||||
bool seekCur(int32_t offset) const; // Only needed for skip?
|
||||
|
||||
uint16_t readLE16();
|
||||
uint32_t readLE32();
|
||||
|
||||
// Source (one is valid)
|
||||
FsFile* file = nullptr;
|
||||
const uint8_t* memoryBuffer = nullptr;
|
||||
size_t memorySize = 0;
|
||||
mutable size_t bufferPos = 0;
|
||||
|
||||
FsFile& file;
|
||||
bool dithering = false;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <EInkDisplay.h>
|
||||
#include <EpdFontFamily.h>
|
||||
#include <HalDisplay.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
@ -14,18 +14,20 @@ class GfxRenderer {
|
||||
// Logical screen orientation from the perspective of callers
|
||||
enum Orientation {
|
||||
Portrait, // 480x800 logical coordinates (current default)
|
||||
LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||
LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap
|
||||
// top/bottom)
|
||||
PortraitInverted, // 480x800 logical coordinates, inverted
|
||||
LandscapeCounterClockwise // 800x480 logical coordinates, native panel orientation
|
||||
LandscapeCounterClockwise // 800x480 logical coordinates, native panel
|
||||
// orientation
|
||||
};
|
||||
|
||||
private:
|
||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
|
||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE,
|
||||
"BW buffer chunking does not line up with display buffer size");
|
||||
|
||||
EInkDisplay& einkDisplay;
|
||||
HalDisplay& display;
|
||||
RenderMode renderMode;
|
||||
Orientation orientation;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
@ -36,7 +38,7 @@ class GfxRenderer {
|
||||
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
|
||||
|
||||
public:
|
||||
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
||||
explicit GfxRenderer(HalDisplay& halDisplay) : display(halDisplay), renderMode(BW), orientation(Portrait) {}
|
||||
~GfxRenderer() { freeBwBufferChunks(); }
|
||||
|
||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
||||
@ -47,14 +49,15 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
|
||||
// Orientation control (affects logical width/height and coordinate transforms)
|
||||
// Orientation control (affects logical width/height and coordinate
|
||||
// transforms)
|
||||
void setOrientation(const Orientation o) { orientation = o; }
|
||||
Orientation getOrientation() const { return orientation; }
|
||||
|
||||
// Screen ops
|
||||
int getScreenWidth() const;
|
||||
int getScreenHeight() const;
|
||||
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
|
||||
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
||||
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
||||
void displayWindow(int x, int y, int width, int height) const;
|
||||
void invertScreen() const;
|
||||
@ -62,15 +65,30 @@ class GfxRenderer {
|
||||
|
||||
// Drawing
|
||||
void drawPixel(int x, int y, bool state = true) const;
|
||||
bool readPixel(int x, int y) const; // Returns true if pixel is black
|
||||
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void fillRectDithered(int x, int y, int width, int height, uint8_t grayLevel) const;
|
||||
void drawRoundedRect(int x, int y, int width, int height, int radius, bool state = true) const;
|
||||
void fillRoundedRect(int x, int y, int width, int height, int radius, bool state = true) const;
|
||||
void fillRoundedRectDithered(int x, int y, int width, int height, int radius, uint8_t grayLevel) const;
|
||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||
float cropY = 0) const;
|
||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||
void drawTransparentBitmap(const Bitmap& bitmap, int x, int y, int w, int h) const;
|
||||
void drawRoundedBitmap(const Bitmap& bitmap, int x, int y, int w, int h, int radius) const;
|
||||
void draw2BitImage(const uint8_t data[], int x, int y, int w, int h) const;
|
||||
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||
|
||||
// Region caching - copies a rectangular region to/from a buffer
|
||||
// Returns allocated buffer on success, nullptr on failure. Caller owns the
|
||||
// memory.
|
||||
uint8_t* captureRegion(int x, int y, int width, int height, size_t* outSize) const;
|
||||
// Restores a previously captured region. Buffer must match dimensions.
|
||||
void restoreRegion(const uint8_t* buffer, int x, int y, int width, int height) const;
|
||||
|
||||
// Text
|
||||
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
|
||||
|
||||
@ -33,10 +33,10 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat
|
||||
|
||||
size_t KOReaderDocumentId::getOffset(int i) {
|
||||
// Offset = 1024 << (2*i)
|
||||
// For i = -1: 1024 >> 2 = 256
|
||||
// For i = -1: KOReader uses a value of 0
|
||||
// For i >= 0: 1024 << (2*i)
|
||||
if (i < 0) {
|
||||
return CHUNK_SIZE >> (-2 * i);
|
||||
return 0;
|
||||
}
|
||||
return CHUNK_SIZE << (2 * i);
|
||||
}
|
||||
|
||||
422
lib/ThemeEngine/include/BasicElements.h
Normal file
422
lib/ThemeEngine/include/BasicElements.h
Normal file
@ -0,0 +1,422 @@
|
||||
#pragma once
|
||||
|
||||
#include <Bitmap.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "ThemeContext.h"
|
||||
#include "ThemeTypes.h"
|
||||
#include "UIElement.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// Safe integer parsing (no exceptions)
|
||||
inline int parseIntSafe(const std::string& s, int defaultVal = 0) {
|
||||
if (s.empty()) return defaultVal;
|
||||
char* end;
|
||||
long val = strtol(s.c_str(), &end, 10);
|
||||
return (end != s.c_str()) ? static_cast<int>(val) : defaultVal;
|
||||
}
|
||||
|
||||
// Safe float parsing (no exceptions)
|
||||
inline float parseFloatSafe(const std::string& s, float defaultVal = 0.0f) {
|
||||
if (s.empty()) return defaultVal;
|
||||
char* end;
|
||||
float val = strtof(s.c_str(), &end);
|
||||
return (end != s.c_str()) ? val : defaultVal;
|
||||
}
|
||||
|
||||
// --- Container ---
|
||||
class Container : public UIElement {
|
||||
protected:
|
||||
std::vector<UIElement*> children;
|
||||
Expression bgColorExpr;
|
||||
bool hasBg = false;
|
||||
bool border = false;
|
||||
Expression borderExpr; // Dynamic border based on expression
|
||||
int padding = 0; // Inner padding for children
|
||||
int borderRadius = 0; // Corner radius (for future rounded rect support)
|
||||
|
||||
public:
|
||||
explicit Container(const std::string& id) : UIElement(id), bgColorExpr(Expression::parse("0xFF")) {}
|
||||
virtual ~Container() = default;
|
||||
|
||||
Container* asContainer() override { return this; }
|
||||
|
||||
ElementType getType() const override { return ElementType::Container; }
|
||||
const char* getTypeName() const override { return "Container"; }
|
||||
|
||||
void addChild(UIElement* child) { children.push_back(child); }
|
||||
|
||||
void clearChildren() { children.clear(); }
|
||||
|
||||
const std::vector<UIElement*>& getChildren() const { return children; }
|
||||
|
||||
void setBackgroundColorExpr(const std::string& expr) {
|
||||
bgColorExpr = Expression::parse(expr);
|
||||
hasBg = true;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setBorder(bool enable) {
|
||||
border = enable;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setBorderExpr(const std::string& expr) {
|
||||
borderExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
bool hasBorderExpr() const { return !borderExpr.empty(); }
|
||||
|
||||
void setPadding(int p) {
|
||||
padding = p;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
int getPadding() const { return padding; }
|
||||
|
||||
void setBorderRadius(int r) {
|
||||
borderRadius = r;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
int getBorderRadius() const { return borderRadius; }
|
||||
|
||||
void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
|
||||
UIElement::layout(context, parentX, parentY, parentW, parentH);
|
||||
// Children are laid out with padding offset
|
||||
int childX = absX + padding;
|
||||
int childY = absY + padding;
|
||||
int childW = absW - 2 * padding;
|
||||
int childH = absH - 2 * padding;
|
||||
for (auto child : children) {
|
||||
child->layout(context, childX, childY, childW, childH);
|
||||
}
|
||||
}
|
||||
|
||||
void markDirty() override {
|
||||
UIElement::markDirty();
|
||||
for (auto child : children) {
|
||||
child->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
|
||||
};
|
||||
|
||||
// --- Rectangle ---
|
||||
class Rectangle : public UIElement {
|
||||
bool fill = false;
|
||||
Expression fillExpr; // Dynamic fill based on expression
|
||||
Expression colorExpr;
|
||||
int borderRadius = 0;
|
||||
|
||||
public:
|
||||
explicit Rectangle(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {}
|
||||
ElementType getType() const override { return ElementType::Rectangle; }
|
||||
const char* getTypeName() const override { return "Rectangle"; }
|
||||
|
||||
void setFill(bool f) {
|
||||
fill = f;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setFillExpr(const std::string& expr) {
|
||||
fillExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setColorExpr(const std::string& c) {
|
||||
colorExpr = Expression::parse(c);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setBorderRadius(int r) {
|
||||
borderRadius = r;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
|
||||
};
|
||||
|
||||
// --- Label ---
|
||||
class Label : public UIElement {
|
||||
public:
|
||||
enum class Alignment { Left, Center, Right };
|
||||
|
||||
private:
|
||||
Expression textExpr;
|
||||
int fontId = 0;
|
||||
Alignment alignment = Alignment::Left;
|
||||
Expression colorExpr;
|
||||
int maxLines = 1; // For multi-line support
|
||||
bool ellipsis = true; // Truncate with ... if too long
|
||||
|
||||
public:
|
||||
explicit Label(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {}
|
||||
ElementType getType() const override { return ElementType::Label; }
|
||||
const char* getTypeName() const override { return "Label"; }
|
||||
|
||||
void setText(const std::string& expr) {
|
||||
textExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setFont(int fid) {
|
||||
fontId = fid;
|
||||
markDirty();
|
||||
}
|
||||
void setAlignment(Alignment a) {
|
||||
alignment = a;
|
||||
markDirty();
|
||||
}
|
||||
void setCentered(bool c) {
|
||||
alignment = c ? Alignment::Center : Alignment::Left;
|
||||
markDirty();
|
||||
}
|
||||
void setColorExpr(const std::string& c) {
|
||||
colorExpr = Expression::parse(c);
|
||||
markDirty();
|
||||
}
|
||||
void setMaxLines(int lines) {
|
||||
maxLines = lines;
|
||||
markDirty();
|
||||
}
|
||||
void setEllipsis(bool e) {
|
||||
ellipsis = e;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
|
||||
};
|
||||
|
||||
// --- BitmapElement ---
|
||||
class BitmapElement : public UIElement {
|
||||
Expression srcExpr;
|
||||
bool scaleToFit = true;
|
||||
bool preserveAspect = true;
|
||||
int borderRadius = 0;
|
||||
|
||||
public:
|
||||
explicit BitmapElement(const std::string& id) : UIElement(id) {
|
||||
cacheable = true; // Bitmaps benefit from caching
|
||||
}
|
||||
ElementType getType() const override { return ElementType::Bitmap; }
|
||||
const char* getTypeName() const override { return "Bitmap"; }
|
||||
|
||||
void setSrc(const std::string& src) {
|
||||
srcExpr = Expression::parse(src);
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
void setScaleToFit(bool scale) {
|
||||
scaleToFit = scale;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
void setPreserveAspect(bool preserve) {
|
||||
preserveAspect = preserve;
|
||||
invalidateCache();
|
||||
}
|
||||
|
||||
void setBorderRadius(int r) {
|
||||
borderRadius = r;
|
||||
// Radius doesn't affect cache key unless we baked it in (we don't currently),
|
||||
// but we should redraw.
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
|
||||
};
|
||||
|
||||
// --- ProgressBar ---
|
||||
class ProgressBar : public UIElement {
|
||||
Expression valueExpr; // Current value (0-100 or 0-max)
|
||||
Expression maxExpr; // Max value (default 100)
|
||||
Expression fgColorExpr; // Foreground color
|
||||
Expression bgColorExpr; // Background color
|
||||
bool showBorder = true;
|
||||
int borderWidth = 1;
|
||||
|
||||
public:
|
||||
explicit ProgressBar(const std::string& id)
|
||||
: UIElement(id),
|
||||
valueExpr(Expression::parse("0")),
|
||||
maxExpr(Expression::parse("100")),
|
||||
fgColorExpr(Expression::parse("0x00")), // Black fill
|
||||
bgColorExpr(Expression::parse("0xFF")) // White background
|
||||
{}
|
||||
|
||||
ElementType getType() const override { return ElementType::ProgressBar; }
|
||||
const char* getTypeName() const override { return "ProgressBar"; }
|
||||
|
||||
void setValue(const std::string& expr) {
|
||||
valueExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setMax(const std::string& expr) {
|
||||
maxExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setFgColor(const std::string& expr) {
|
||||
fgColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setBgColor(const std::string& expr) {
|
||||
bgColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setShowBorder(bool show) {
|
||||
showBorder = show;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
std::string valStr = context.evaluatestring(valueExpr);
|
||||
std::string maxStr = context.evaluatestring(maxExpr);
|
||||
|
||||
int value = parseIntSafe(valStr, 0);
|
||||
int maxVal = parseIntSafe(maxStr, 100);
|
||||
if (maxVal <= 0) maxVal = 100;
|
||||
|
||||
float ratio = static_cast<float>(value) / static_cast<float>(maxVal);
|
||||
if (ratio < 0) ratio = 0;
|
||||
if (ratio > 1) ratio = 1;
|
||||
|
||||
// Draw background
|
||||
std::string bgStr = context.evaluatestring(bgColorExpr);
|
||||
uint8_t bgColor = Color::parse(bgStr).value;
|
||||
renderer.fillRect(absX, absY, absW, absH, bgColor == 0x00);
|
||||
|
||||
// Draw filled portion
|
||||
int fillWidth = static_cast<int>(absW * ratio);
|
||||
if (fillWidth > 0) {
|
||||
std::string fgStr = context.evaluatestring(fgColorExpr);
|
||||
uint8_t fgColor = Color::parse(fgStr).value;
|
||||
renderer.fillRect(absX, absY, fillWidth, absH, fgColor == 0x00);
|
||||
}
|
||||
|
||||
// Draw border
|
||||
if (showBorder) {
|
||||
renderer.drawRect(absX, absY, absW, absH, true);
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Divider (horizontal or vertical line) ---
|
||||
class Divider : public UIElement {
|
||||
Expression colorExpr;
|
||||
bool horizontal = true;
|
||||
int thickness = 1;
|
||||
|
||||
public:
|
||||
explicit Divider(const std::string& id) : UIElement(id), colorExpr(Expression::parse("0x00")) {}
|
||||
|
||||
ElementType getType() const override { return ElementType::Divider; }
|
||||
const char* getTypeName() const override { return "Divider"; }
|
||||
|
||||
void setColorExpr(const std::string& expr) {
|
||||
colorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setHorizontal(bool h) {
|
||||
horizontal = h;
|
||||
markDirty();
|
||||
}
|
||||
void setThickness(int t) {
|
||||
thickness = t;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
std::string colStr = context.evaluatestring(colorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
bool black = (color == 0x00);
|
||||
|
||||
if (horizontal) {
|
||||
for (int i = 0; i < thickness && i < absH; i++) {
|
||||
renderer.drawLine(absX, absY + i, absX + absW - 1, absY + i, black);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < thickness && i < absW; i++) {
|
||||
renderer.drawLine(absX + i, absY, absX + i, absY + absH - 1, black);
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
// --- BatteryIcon ---
|
||||
class BatteryIcon : public UIElement {
|
||||
Expression valueExpr;
|
||||
Expression colorExpr;
|
||||
|
||||
public:
|
||||
explicit BatteryIcon(const std::string& id)
|
||||
: UIElement(id), valueExpr(Expression::parse("0")), colorExpr(Expression::parse("0x00")) {
|
||||
// Black by default
|
||||
}
|
||||
|
||||
ElementType getType() const override { return ElementType::BatteryIcon; }
|
||||
const char* getTypeName() const override { return "BatteryIcon"; }
|
||||
|
||||
void setValue(const std::string& expr) {
|
||||
valueExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setColor(const std::string& expr) {
|
||||
colorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
std::string valStr = context.evaluatestring(valueExpr);
|
||||
int percentage = parseIntSafe(valStr, 0);
|
||||
|
||||
std::string colStr = context.evaluatestring(colorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
bool black = (color == 0x00);
|
||||
|
||||
constexpr int batteryWidth = 15;
|
||||
constexpr int batteryHeight = 12;
|
||||
|
||||
int x = absX;
|
||||
int y = absY;
|
||||
|
||||
if (absW > batteryWidth) x += (absW - batteryWidth) / 2;
|
||||
if (absH > batteryHeight) y += (absH - batteryHeight) / 2;
|
||||
|
||||
renderer.drawLine(x + 1, y, x + batteryWidth - 3, y, black);
|
||||
renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1, black);
|
||||
renderer.drawLine(x, y + 1, x, y + batteryHeight - 2, black);
|
||||
renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2, black);
|
||||
|
||||
renderer.drawPixel(x + batteryWidth - 1, y + 3, black);
|
||||
renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4, black);
|
||||
renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5, black);
|
||||
|
||||
if (percentage > 0) {
|
||||
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
|
||||
if (filledWidth > batteryWidth - 5) {
|
||||
filledWidth = batteryWidth - 5;
|
||||
}
|
||||
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4, black);
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
303
lib/ThemeEngine/include/DefaultTheme.h
Normal file
303
lib/ThemeEngine/include/DefaultTheme.h
Normal file
@ -0,0 +1,303 @@
|
||||
#pragma once
|
||||
|
||||
// Default theme - matches the original CrossPoint Reader look
|
||||
// This is embedded in the firmware as a fallback
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// Use static function for C++14 ODR compatibility
|
||||
static const char* getDefaultThemeIni() {
|
||||
static const char* theme = R"INI(
|
||||
; ============================================
|
||||
; DEFAULT THEME - Original CrossPoint Reader
|
||||
; ============================================
|
||||
; Screen: 480x800
|
||||
; Layout: Centered book card + vertical menu list
|
||||
|
||||
[Global]
|
||||
FontUI12 = UI_12
|
||||
FontUI10 = UI_10
|
||||
NavBookCount = 1
|
||||
|
||||
; ============================================
|
||||
; HOME SCREEN
|
||||
; ============================================
|
||||
|
||||
[Home]
|
||||
Type = Container
|
||||
X = 0
|
||||
Y = 0
|
||||
Width = 480
|
||||
Height = 800
|
||||
BgColor = white
|
||||
|
||||
; --- Battery (top right) ---
|
||||
[BatteryWrapper]
|
||||
Parent = Home
|
||||
Type = Container
|
||||
X = 400
|
||||
Y = 10
|
||||
Width = 80
|
||||
Height = 20
|
||||
|
||||
[BatteryIcon]
|
||||
Parent = BatteryWrapper
|
||||
Type = BatteryIcon
|
||||
X = 0
|
||||
Y = 5
|
||||
Width = 15
|
||||
Height = 20
|
||||
Value = {BatteryPercent}
|
||||
Color = black
|
||||
|
||||
[BatteryText]
|
||||
Parent = BatteryWrapper
|
||||
Type = Label
|
||||
Font = Small
|
||||
Text = {BatteryPercent}%
|
||||
X = 22
|
||||
Y = 0
|
||||
Width = 50
|
||||
Height = 20
|
||||
Align = Left
|
||||
Visible = {ShowBatteryPercent}
|
||||
|
||||
; --- Book Card (centered) ---
|
||||
; Original: 240x400 at (120, 30)
|
||||
[BookCard]
|
||||
Parent = Home
|
||||
Type = Container
|
||||
X = 120
|
||||
Y = 30
|
||||
Width = 240
|
||||
Height = 400
|
||||
Border = true
|
||||
BgColor = {IsBookSelected ? "black" : "white"}
|
||||
Visible = {HasBook}
|
||||
|
||||
; Bookmark ribbon decoration (when no cover)
|
||||
[BookmarkRibbon]
|
||||
Parent = BookCard
|
||||
Type = Container
|
||||
X = 200
|
||||
Y = 5
|
||||
Width = 30
|
||||
Height = 60
|
||||
BgColor = {IsBookSelected ? "white" : "black"}
|
||||
Visible = {!HasCover}
|
||||
|
||||
[BookmarkNotch]
|
||||
Parent = BookmarkRibbon
|
||||
Type = Container
|
||||
X = 10
|
||||
Y = 45
|
||||
Width = 10
|
||||
Height = 15
|
||||
BgColor = {IsBookSelected ? "black" : "white"}
|
||||
|
||||
; Title centered in card
|
||||
[BookCover]
|
||||
Parent = BookCard
|
||||
Type = Bitmap
|
||||
X = 0
|
||||
Y = 0
|
||||
Width = 240
|
||||
Height = 400
|
||||
Src = {BookCoverPath}
|
||||
ScaleToFit = true
|
||||
PreserveAspect = true
|
||||
Visible = {HasCover}
|
||||
|
||||
; White box for text overlay
|
||||
[InfoBox]
|
||||
Parent = BookCard
|
||||
Type = Container
|
||||
X = 20
|
||||
Y = 120
|
||||
Width = 200
|
||||
Height = 150
|
||||
BgColor = white
|
||||
Border = true
|
||||
|
||||
[BookTitle]
|
||||
Parent = InfoBox
|
||||
Type = Label
|
||||
Font = UI_12
|
||||
Text = {BookTitle}
|
||||
X = 10
|
||||
Y = 10
|
||||
Width = 180
|
||||
Height = 80
|
||||
Color = black
|
||||
Align = center
|
||||
Ellipsis = true
|
||||
MaxLines = 3
|
||||
|
||||
[BookAuthor]
|
||||
Parent = InfoBox
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = {BookAuthor}
|
||||
X = 10
|
||||
Y = 100
|
||||
Width = 180
|
||||
Height = 40
|
||||
Color = black
|
||||
Align = center
|
||||
Ellipsis = true
|
||||
|
||||
; "Continue Reading" at bottom of card
|
||||
[ContinueLabel]
|
||||
Parent = BookCard
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = Continue Reading
|
||||
X = 20
|
||||
Y = 365
|
||||
Width = 200
|
||||
Height = 25
|
||||
Color = {IsBookSelected ? "white" : "black"}
|
||||
Align = center
|
||||
Visible = {HasBook}
|
||||
|
||||
; --- No Book Message ---
|
||||
[NoBookCard]
|
||||
Parent = Home
|
||||
Type = Container
|
||||
X = 120
|
||||
Y = 30
|
||||
Width = 240
|
||||
Height = 400
|
||||
Border = true
|
||||
Visible = {!HasBook}
|
||||
|
||||
[NoBookTitle]
|
||||
Parent = NoBookCard
|
||||
Type = Label
|
||||
Font = UI_12
|
||||
Text = No open book
|
||||
X = 20
|
||||
Y = 175
|
||||
Width = 200
|
||||
Height = 25
|
||||
Align = center
|
||||
|
||||
[NoBookSubtitle]
|
||||
Parent = NoBookCard
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = Start reading below
|
||||
X = 20
|
||||
Y = 205
|
||||
Width = 200
|
||||
Height = 25
|
||||
Align = center
|
||||
|
||||
; --- Menu List ---
|
||||
; Original: margin=20, tileWidth=440, tileHeight=45, spacing=8
|
||||
; menuStartY = 30 + 400 + 15 = 445
|
||||
[MenuList]
|
||||
Parent = Home
|
||||
Type = List
|
||||
Source = MainMenu
|
||||
ItemTemplate = MenuItem
|
||||
X = 20
|
||||
Y = 445
|
||||
Width = 440
|
||||
Height = 280
|
||||
Direction = Vertical
|
||||
ItemHeight = 45
|
||||
Spacing = 8
|
||||
|
||||
; --- Menu Item Template ---
|
||||
[MenuItem]
|
||||
Type = Container
|
||||
Width = 440
|
||||
Height = 45
|
||||
BgColor = {Item.Selected ? "black" : "white"}
|
||||
Border = true
|
||||
|
||||
[MenuItemLabel]
|
||||
Parent = MenuItem
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = {Item.Title}
|
||||
X = 0
|
||||
Y = 0
|
||||
Width = 440
|
||||
Height = 45
|
||||
Color = {Item.Selected ? "white" : "black"}
|
||||
Align = center
|
||||
|
||||
; --- Button Hints (bottom) ---
|
||||
; Original: 4 buttons at [25, 130, 245, 350], width=106, height=40
|
||||
; Y = pageHeight - 40 = 760
|
||||
|
||||
[HintBtn2]
|
||||
Parent = Home
|
||||
Type = Container
|
||||
X = 130
|
||||
Y = 760
|
||||
Width = 106
|
||||
Height = 40
|
||||
BgColor = white
|
||||
Border = true
|
||||
|
||||
[HintBtn2Label]
|
||||
Parent = HintBtn2
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = Confirm
|
||||
X = 0
|
||||
Y = 0
|
||||
Width = 106
|
||||
Height = 40
|
||||
Align = center
|
||||
|
||||
[HintBtn3]
|
||||
Parent = Home
|
||||
Type = Container
|
||||
X = 245
|
||||
Y = 760
|
||||
Width = 106
|
||||
Height = 40
|
||||
BgColor = white
|
||||
Border = true
|
||||
|
||||
[HintBtn3Label]
|
||||
Parent = HintBtn3
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = Up
|
||||
X = 0
|
||||
Y = 0
|
||||
Width = 106
|
||||
Height = 40
|
||||
Align = center
|
||||
|
||||
[HintBtn4]
|
||||
Parent = Home
|
||||
Type = Container
|
||||
X = 350
|
||||
Y = 760
|
||||
Width = 106
|
||||
Height = 40
|
||||
BgColor = white
|
||||
Border = true
|
||||
|
||||
[HintBtn4Label]
|
||||
Parent = HintBtn4
|
||||
Type = Label
|
||||
Font = UI_10
|
||||
Text = Down
|
||||
X = 0
|
||||
Y = 0
|
||||
Width = 106
|
||||
Height = 40
|
||||
Align = center
|
||||
|
||||
)INI";
|
||||
return theme;
|
||||
}
|
||||
|
||||
} // namespace ThemeEngine
|
||||
38
lib/ThemeEngine/include/IniParser.h
Normal file
38
lib/ThemeEngine/include/IniParser.h
Normal file
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Forward declaration for FS file or stream if needed,
|
||||
// but for now we'll take a string buffer or filename to keep it generic?
|
||||
// Or better, depend on FS.h to read files directly.
|
||||
|
||||
#ifdef FILE_READ
|
||||
#undef FILE_READ
|
||||
#endif
|
||||
#ifdef FILE_WRITE
|
||||
#undef FILE_WRITE
|
||||
#endif
|
||||
#include <FS.h>
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
struct IniSection {
|
||||
std::string name;
|
||||
std::map<std::string, std::string> properties;
|
||||
};
|
||||
|
||||
class IniParser {
|
||||
public:
|
||||
// Parse a stream (File, Serial, etc.)
|
||||
static std::map<std::string, std::map<std::string, std::string>> parse(Stream& stream);
|
||||
|
||||
// Parse a string buffer (useful for testing)
|
||||
static std::map<std::string, std::map<std::string, std::string>> parseString(const std::string& content);
|
||||
|
||||
private:
|
||||
static void trim(std::string& s);
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
721
lib/ThemeEngine/include/LayoutElements.h
Normal file
721
lib/ThemeEngine/include/LayoutElements.h
Normal file
@ -0,0 +1,721 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "BasicElements.h"
|
||||
#include "ThemeContext.h"
|
||||
#include "ThemeTypes.h"
|
||||
#include "UIElement.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// --- HStack: Horizontal Stack Layout ---
|
||||
// Children are arranged horizontally with optional spacing
|
||||
class HStack : public Container {
|
||||
public:
|
||||
enum class VAlign { Top, Center, Bottom };
|
||||
|
||||
private:
|
||||
int spacing = 0; // Gap between children
|
||||
int padding = 0; // Internal padding
|
||||
VAlign vAlign = VAlign::Top;
|
||||
|
||||
public:
|
||||
HStack(const std::string& id) : Container(id) {}
|
||||
ElementType getType() const override { return ElementType::HStack; }
|
||||
const char* getTypeName() const override { return "HStack"; }
|
||||
|
||||
void setSpacing(int s) {
|
||||
spacing = s;
|
||||
markDirty();
|
||||
}
|
||||
void setPadding(int p) {
|
||||
padding = p;
|
||||
markDirty();
|
||||
}
|
||||
void setVAlign(VAlign a) {
|
||||
vAlign = a;
|
||||
markDirty();
|
||||
}
|
||||
void setVAlignFromString(const std::string& s) {
|
||||
if (s == "center" || s == "Center") {
|
||||
vAlign = VAlign::Center;
|
||||
} else if (s == "bottom" || s == "Bottom") {
|
||||
vAlign = VAlign::Bottom;
|
||||
} else {
|
||||
vAlign = VAlign::Top;
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
|
||||
UIElement::layout(context, parentX, parentY, parentW, parentH);
|
||||
|
||||
int currentX = absX + padding;
|
||||
int availableH = absH - 2 * padding;
|
||||
int availableW = absW - 2 * padding;
|
||||
|
||||
for (auto child : children) {
|
||||
// Let child calculate its preferred size first
|
||||
// Pass large parent bounds to avoid clamping issues during size calculation
|
||||
child->layout(context, currentX, absY + padding, availableW, availableH);
|
||||
int childW = child->getAbsW();
|
||||
int childH = child->getAbsH();
|
||||
|
||||
// Extract child's own Y offset (from first layout pass)
|
||||
int childYOffset = child->getAbsY() - (absY + padding);
|
||||
|
||||
// Calculate base position based on vertical alignment
|
||||
int childY = absY + padding;
|
||||
if (childH < availableH) {
|
||||
switch (vAlign) {
|
||||
case VAlign::Center:
|
||||
childY = absY + padding + (availableH - childH) / 2;
|
||||
break;
|
||||
case VAlign::Bottom:
|
||||
childY = absY + padding + (availableH - childH);
|
||||
break;
|
||||
case VAlign::Top:
|
||||
default:
|
||||
childY = absY + padding;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add child's own Y offset to the calculated position
|
||||
childY += childYOffset;
|
||||
|
||||
// Only do second layout pass if position changed from first pass
|
||||
int firstPassY = child->getAbsY();
|
||||
if (childY != firstPassY) {
|
||||
child->layout(context, currentX, childY, childW, childH);
|
||||
}
|
||||
currentX += childW + spacing;
|
||||
availableW -= (childW + spacing);
|
||||
if (availableW < 0) availableW = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- VStack: Vertical Stack Layout ---
|
||||
// Children are arranged vertically with optional spacing
|
||||
class VStack : public Container {
|
||||
public:
|
||||
enum class HAlign { Left, Center, Right };
|
||||
|
||||
private:
|
||||
int spacing = 0;
|
||||
int padding = 0;
|
||||
HAlign hAlign = HAlign::Left;
|
||||
|
||||
public:
|
||||
VStack(const std::string& id) : Container(id) {}
|
||||
ElementType getType() const override { return ElementType::VStack; }
|
||||
const char* getTypeName() const override { return "VStack"; }
|
||||
|
||||
void setSpacing(int s) {
|
||||
spacing = s;
|
||||
markDirty();
|
||||
}
|
||||
void setPadding(int p) {
|
||||
padding = p;
|
||||
markDirty();
|
||||
}
|
||||
void setHAlign(HAlign a) {
|
||||
hAlign = a;
|
||||
markDirty();
|
||||
}
|
||||
void setHAlignFromString(const std::string& s) {
|
||||
if (s == "center" || s == "Center") {
|
||||
hAlign = HAlign::Center;
|
||||
} else if (s == "right" || s == "Right") {
|
||||
hAlign = HAlign::Right;
|
||||
} else {
|
||||
hAlign = HAlign::Left;
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
|
||||
UIElement::layout(context, parentX, parentY, parentW, parentH);
|
||||
|
||||
int currentY = absY + padding;
|
||||
int availableW = absW - 2 * padding;
|
||||
int availableH = absH - 2 * padding;
|
||||
|
||||
for (auto child : children) {
|
||||
// Pass large parent bounds to avoid clamping issues during size calculation
|
||||
child->layout(context, absX + padding, currentY, availableW, availableH);
|
||||
int childW = child->getAbsW();
|
||||
int childH = child->getAbsH();
|
||||
|
||||
// Extract child's own X offset (from first layout pass)
|
||||
int childXOffset = child->getAbsX() - (absX + padding);
|
||||
|
||||
// Calculate base position based on horizontal alignment
|
||||
int childX = absX + padding;
|
||||
if (childW < availableW) {
|
||||
switch (hAlign) {
|
||||
case HAlign::Center:
|
||||
childX = absX + padding + (availableW - childW) / 2;
|
||||
break;
|
||||
case HAlign::Right:
|
||||
childX = absX + padding + (availableW - childW);
|
||||
break;
|
||||
case HAlign::Left:
|
||||
default:
|
||||
childX = absX + padding;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add child's own X offset to the calculated position
|
||||
childX += childXOffset;
|
||||
|
||||
// Only do second layout pass if position changed from first pass
|
||||
int firstPassX = child->getAbsX();
|
||||
if (childX != firstPassX) {
|
||||
child->layout(context, childX, currentY, childW, childH);
|
||||
}
|
||||
currentY += childH + spacing;
|
||||
availableH -= (childH + spacing);
|
||||
if (availableH < 0) availableH = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Grid: Grid Layout ---
|
||||
// Children arranged in a grid with specified columns
|
||||
class Grid : public Container {
|
||||
int columns = 2;
|
||||
int rowSpacing = 10;
|
||||
int colSpacing = 10;
|
||||
int padding = 0;
|
||||
|
||||
public:
|
||||
Grid(const std::string& id) : Container(id) {}
|
||||
ElementType getType() const override { return ElementType::Grid; }
|
||||
const char* getTypeName() const override { return "Grid"; }
|
||||
|
||||
void setColumns(int c) {
|
||||
columns = c > 0 ? c : 1;
|
||||
markDirty();
|
||||
}
|
||||
void setRowSpacing(int s) {
|
||||
rowSpacing = s;
|
||||
markDirty();
|
||||
}
|
||||
void setColSpacing(int s) {
|
||||
colSpacing = s;
|
||||
markDirty();
|
||||
}
|
||||
void setPadding(int p) {
|
||||
padding = p;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
|
||||
UIElement::layout(context, parentX, parentY, parentW, parentH);
|
||||
|
||||
if (children.empty()) return;
|
||||
|
||||
// Guard against division by zero
|
||||
int cols = columns > 0 ? columns : 1;
|
||||
int availableW = absW - 2 * padding - (cols - 1) * colSpacing;
|
||||
int cellW = availableW / cols;
|
||||
int availableH = absH - 2 * padding;
|
||||
|
||||
int row = 0, col = 0;
|
||||
int currentY = absY + padding;
|
||||
int maxRowHeight = 0;
|
||||
|
||||
for (auto child : children) {
|
||||
int cellX = absX + padding + col * (cellW + colSpacing);
|
||||
|
||||
// Pass cell dimensions to avoid clamping issues
|
||||
child->layout(context, cellX, currentY, cellW, availableH);
|
||||
int childH = child->getAbsH();
|
||||
if (childH > maxRowHeight) maxRowHeight = childH;
|
||||
|
||||
col++;
|
||||
if (col >= cols) {
|
||||
col = 0;
|
||||
row++;
|
||||
currentY += maxRowHeight + rowSpacing;
|
||||
availableH -= (maxRowHeight + rowSpacing);
|
||||
if (availableH < 0) availableH = 0;
|
||||
maxRowHeight = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Badge: Small overlay text/indicator ---
|
||||
class Badge : public UIElement {
|
||||
Expression textExpr;
|
||||
Expression bgColorExpr;
|
||||
Expression fgColorExpr;
|
||||
int fontId = 0;
|
||||
int paddingH = 8; // Horizontal padding
|
||||
int paddingV = 4; // Vertical padding
|
||||
int borderRadius = 0;
|
||||
|
||||
public:
|
||||
Badge(const std::string& id) : UIElement(id) {
|
||||
bgColorExpr = Expression::parse("0x00"); // Black background
|
||||
fgColorExpr = Expression::parse("0xFF"); // White text
|
||||
}
|
||||
|
||||
ElementType getType() const override { return ElementType::Badge; }
|
||||
const char* getTypeName() const override { return "Badge"; }
|
||||
|
||||
void setText(const std::string& expr) {
|
||||
textExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setBgColor(const std::string& expr) {
|
||||
bgColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setFgColor(const std::string& expr) {
|
||||
fgColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setFont(int fid) {
|
||||
fontId = fid;
|
||||
markDirty();
|
||||
}
|
||||
void setPaddingH(int p) {
|
||||
paddingH = p;
|
||||
markDirty();
|
||||
}
|
||||
void setPaddingV(int p) {
|
||||
paddingV = p;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setBorderRadius(int r) {
|
||||
borderRadius = r;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
std::string text = context.evaluatestring(textExpr);
|
||||
if (text.empty()) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate badge size based on text content - always auto-sizes
|
||||
int textW = renderer.getTextWidth(fontId, text.c_str());
|
||||
int textH = renderer.getLineHeight(fontId);
|
||||
int badgeW = textW + 2 * paddingH;
|
||||
int badgeH = textH + 2 * paddingV;
|
||||
|
||||
// Badge always auto-sizes to content
|
||||
int drawW = badgeW;
|
||||
int drawH = badgeH;
|
||||
|
||||
// Position the badge within its container
|
||||
// If absW/absH are set, use them as bounding box for alignment
|
||||
int drawX = absX;
|
||||
int drawY = absY;
|
||||
|
||||
// Right-align badge within bounding box if width is specified
|
||||
if (absW > 0 && absW > drawW) {
|
||||
drawX = absX + absW - drawW;
|
||||
}
|
||||
// Vertically center badge within bounding box if height is specified
|
||||
if (absH > 0 && absH > drawH) {
|
||||
drawY = absY + (absH - drawH) / 2;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
std::string bgStr = context.evaluatestring(bgColorExpr);
|
||||
uint8_t bgColor = Color::parse(bgStr).value;
|
||||
if (borderRadius > 0) {
|
||||
if (bgColor == 0x00) {
|
||||
renderer.fillRoundedRect(drawX, drawY, drawW, drawH, borderRadius, true);
|
||||
} else if (bgColor >= 0xF0) {
|
||||
renderer.fillRoundedRect(drawX, drawY, drawW, drawH, borderRadius, false);
|
||||
} else {
|
||||
renderer.fillRoundedRectDithered(drawX, drawY, drawW, drawH, borderRadius, bgColor);
|
||||
}
|
||||
} else {
|
||||
renderer.fillRect(drawX, drawY, drawW, drawH, bgColor == 0x00);
|
||||
}
|
||||
|
||||
// Draw border for contrast (only if not black background)
|
||||
if (bgColor != 0x00) {
|
||||
if (borderRadius > 0) {
|
||||
renderer.drawRoundedRect(drawX, drawY, drawW, drawH, borderRadius, true);
|
||||
} else {
|
||||
renderer.drawRect(drawX, drawY, drawW, drawH, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text centered within the badge
|
||||
std::string fgStr = context.evaluatestring(fgColorExpr);
|
||||
uint8_t fgColor = Color::parse(fgStr).value;
|
||||
int textX = drawX + paddingH;
|
||||
int textY = drawY + paddingV;
|
||||
renderer.drawText(fontId, textX, textY, text.c_str(), fgColor == 0x00);
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Toggle: On/Off Switch ---
|
||||
// Fully themable toggle with track and knob
|
||||
// Supports rounded or square appearance based on BorderRadius
|
||||
class Toggle : public UIElement {
|
||||
Expression valueExpr; // Boolean expression for on/off state
|
||||
Expression onColorExpr; // Track color when ON
|
||||
Expression offColorExpr; // Track color when OFF
|
||||
Expression knobColorExpr; // Knob color (optional, defaults to opposite of track)
|
||||
int trackWidth = 44;
|
||||
int trackHeight = 24;
|
||||
int knobSize = 20;
|
||||
int borderRadius = 0; // 0 = square, >0 = rounded (use trackHeight/2 for pill shape)
|
||||
int knobRadius = 0; // Knob corner radius
|
||||
|
||||
public:
|
||||
Toggle(const std::string& id) : UIElement(id) {
|
||||
valueExpr = Expression::parse("false");
|
||||
onColorExpr = Expression::parse("0x00"); // Black when on
|
||||
offColorExpr = Expression::parse("0xCC"); // Light gray when off
|
||||
}
|
||||
|
||||
ElementType getType() const override { return ElementType::Toggle; }
|
||||
const char* getTypeName() const override { return "Toggle"; }
|
||||
|
||||
void setValue(const std::string& expr) {
|
||||
valueExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setOnColor(const std::string& expr) {
|
||||
onColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setOffColor(const std::string& expr) {
|
||||
offColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setKnobColor(const std::string& expr) {
|
||||
knobColorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setTrackWidth(int w) {
|
||||
trackWidth = w;
|
||||
markDirty();
|
||||
}
|
||||
void setTrackHeight(int h) {
|
||||
trackHeight = h;
|
||||
markDirty();
|
||||
}
|
||||
void setKnobSize(int s) {
|
||||
knobSize = s;
|
||||
markDirty();
|
||||
}
|
||||
void setBorderRadius(int r) {
|
||||
borderRadius = r;
|
||||
markDirty();
|
||||
}
|
||||
void setKnobRadius(int r) {
|
||||
knobRadius = r;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
// Evaluate the value - handle simple variable references directly
|
||||
bool isOn = false;
|
||||
std::string rawExpr = valueExpr.rawExpr;
|
||||
|
||||
// If it's a simple {variable} reference, resolve it directly
|
||||
if (rawExpr.size() > 2 && rawExpr.front() == '{' && rawExpr.back() == '}') {
|
||||
std::string varName = rawExpr.substr(1, rawExpr.size() - 2);
|
||||
// Trim whitespace
|
||||
size_t start = varName.find_first_not_of(" \t");
|
||||
size_t end = varName.find_last_not_of(" \t");
|
||||
if (start != std::string::npos) {
|
||||
varName = varName.substr(start, end - start + 1);
|
||||
}
|
||||
isOn = context.getAnyAsBool(varName, false);
|
||||
} else {
|
||||
isOn = context.evaluateBool(rawExpr);
|
||||
}
|
||||
|
||||
// Get track color based on state
|
||||
std::string colorStr = isOn ? context.evaluatestring(onColorExpr) : context.evaluatestring(offColorExpr);
|
||||
uint8_t trackColor = Color::parse(colorStr).value;
|
||||
|
||||
// Calculate track position (centered vertically in bounding box)
|
||||
int trackX = absX;
|
||||
int trackY = absY + (absH - trackHeight) / 2;
|
||||
|
||||
// Calculate effective border radius (capped at half height for pill shape)
|
||||
int effectiveRadius = borderRadius;
|
||||
if (effectiveRadius > trackHeight / 2) {
|
||||
effectiveRadius = trackHeight / 2;
|
||||
}
|
||||
|
||||
// Draw track
|
||||
if (effectiveRadius > 0) {
|
||||
// Rounded track
|
||||
if (trackColor == 0x00) {
|
||||
renderer.fillRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, true);
|
||||
} else if (trackColor >= 0xF0) {
|
||||
renderer.fillRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, false);
|
||||
renderer.drawRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, true);
|
||||
} else {
|
||||
renderer.fillRoundedRectDithered(trackX, trackY, trackWidth, trackHeight, effectiveRadius, trackColor);
|
||||
renderer.drawRoundedRect(trackX, trackY, trackWidth, trackHeight, effectiveRadius, true);
|
||||
}
|
||||
} else {
|
||||
// Square track
|
||||
if (trackColor == 0x00) {
|
||||
renderer.fillRect(trackX, trackY, trackWidth, trackHeight, true);
|
||||
} else if (trackColor >= 0xF0) {
|
||||
renderer.fillRect(trackX, trackY, trackWidth, trackHeight, false);
|
||||
renderer.drawRect(trackX, trackY, trackWidth, trackHeight, true);
|
||||
} else {
|
||||
renderer.fillRectDithered(trackX, trackY, trackWidth, trackHeight, trackColor);
|
||||
renderer.drawRect(trackX, trackY, trackWidth, trackHeight, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate knob position
|
||||
int knobMargin = (trackHeight - knobSize) / 2;
|
||||
int knobX = isOn ? (trackX + trackWidth - knobSize - knobMargin) : (trackX + knobMargin);
|
||||
int knobY = trackY + knobMargin;
|
||||
|
||||
// Determine knob color
|
||||
bool knobBlack;
|
||||
if (!knobColorExpr.empty()) {
|
||||
std::string knobStr = context.evaluatestring(knobColorExpr);
|
||||
uint8_t knobColor = Color::parse(knobStr).value;
|
||||
knobBlack = (knobColor == 0x00);
|
||||
} else {
|
||||
// Default: knob is opposite color of track
|
||||
knobBlack = (trackColor >= 0x80);
|
||||
}
|
||||
|
||||
// Calculate effective knob radius
|
||||
int effectiveKnobRadius = knobRadius;
|
||||
if (effectiveKnobRadius > knobSize / 2) {
|
||||
effectiveKnobRadius = knobSize / 2;
|
||||
}
|
||||
|
||||
// Draw knob
|
||||
if (effectiveKnobRadius > 0) {
|
||||
renderer.fillRoundedRect(knobX, knobY, knobSize, knobSize, effectiveKnobRadius, knobBlack);
|
||||
if (!knobBlack) {
|
||||
renderer.drawRoundedRect(knobX, knobY, knobSize, knobSize, effectiveKnobRadius, true);
|
||||
}
|
||||
} else {
|
||||
renderer.fillRect(knobX, knobY, knobSize, knobSize, knobBlack);
|
||||
if (!knobBlack) {
|
||||
renderer.drawRect(knobX, knobY, knobSize, knobSize, true);
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
// --- TabBar: Horizontal tab selection ---
|
||||
class TabBar : public Container {
|
||||
Expression selectedExpr; // Currently selected tab index or name
|
||||
int tabSpacing = 0;
|
||||
int padding = 0;
|
||||
int indicatorHeight = 3;
|
||||
bool showIndicator = true;
|
||||
|
||||
public:
|
||||
TabBar(const std::string& id) : Container(id) {}
|
||||
ElementType getType() const override { return ElementType::TabBar; }
|
||||
const char* getTypeName() const override { return "TabBar"; }
|
||||
|
||||
void setSelected(const std::string& expr) {
|
||||
selectedExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setTabSpacing(int s) {
|
||||
tabSpacing = s;
|
||||
markDirty();
|
||||
}
|
||||
void setPadding(int p) {
|
||||
padding = p;
|
||||
markDirty();
|
||||
}
|
||||
void setIndicatorHeight(int h) {
|
||||
indicatorHeight = h;
|
||||
markDirty();
|
||||
}
|
||||
void setShowIndicator(bool show) {
|
||||
showIndicator = show;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
|
||||
UIElement::layout(context, parentX, parentY, parentW, parentH);
|
||||
|
||||
if (children.empty()) return;
|
||||
|
||||
// Distribute tabs evenly
|
||||
int numTabs = children.size();
|
||||
int totalSpacing = (numTabs - 1) * tabSpacing;
|
||||
int availableW = absW - 2 * padding - totalSpacing;
|
||||
int tabW = availableW / numTabs;
|
||||
int currentX = absX + padding;
|
||||
|
||||
for (size_t i = 0; i < children.size(); i++) {
|
||||
children[i]->layout(context, currentX, absY, tabW, absH - indicatorHeight);
|
||||
currentX += tabW + tabSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
// Draw background if set
|
||||
if (hasBg) {
|
||||
std::string colStr = context.evaluatestring(bgColorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
renderer.fillRect(absX, absY, absW, absH, color == 0x00);
|
||||
}
|
||||
|
||||
// Draw children (tab labels)
|
||||
for (auto child : children) {
|
||||
child->draw(renderer, context);
|
||||
}
|
||||
|
||||
// Draw selection indicator
|
||||
if (showIndicator && !children.empty()) {
|
||||
std::string selStr = context.evaluatestring(selectedExpr);
|
||||
int selectedIdx = parseIntSafe(selStr, 0);
|
||||
|
||||
if (selectedIdx >= 0 && selectedIdx < static_cast<int>(children.size())) {
|
||||
UIElement* tab = children[selectedIdx];
|
||||
int indX = tab->getAbsX();
|
||||
int indY = absY + absH - indicatorHeight;
|
||||
int indW = tab->getAbsW();
|
||||
renderer.fillRect(indX, indY, indW, indicatorHeight, true);
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Icon: Small symbolic image ---
|
||||
// Can be a built-in icon name or a path to a BMP
|
||||
class Icon : public UIElement {
|
||||
Expression srcExpr; // Icon name or path
|
||||
Expression colorExpr;
|
||||
int iconSize = 24;
|
||||
|
||||
// Built-in icon names and their simple representations
|
||||
// In a real implementation, these would be actual bitmap data
|
||||
|
||||
public:
|
||||
Icon(const std::string& id) : UIElement(id) {
|
||||
colorExpr = Expression::parse("0x00"); // Black by default
|
||||
}
|
||||
|
||||
ElementType getType() const override { return ElementType::Icon; }
|
||||
const char* getTypeName() const override { return "Icon"; }
|
||||
|
||||
void setSrc(const std::string& expr) {
|
||||
srcExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setColorExpr(const std::string& expr) {
|
||||
colorExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setIconSize(int s) {
|
||||
iconSize = s;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
|
||||
};
|
||||
|
||||
// --- ScrollIndicator: Visual scroll position ---
|
||||
class ScrollIndicator : public UIElement {
|
||||
Expression positionExpr; // 0.0 to 1.0
|
||||
Expression totalExpr; // Total items
|
||||
Expression visibleExpr; // Visible items
|
||||
int trackWidth = 4;
|
||||
|
||||
public:
|
||||
ScrollIndicator(const std::string& id) : UIElement(id) {
|
||||
positionExpr = Expression::parse("0");
|
||||
totalExpr = Expression::parse("1");
|
||||
visibleExpr = Expression::parse("1");
|
||||
}
|
||||
|
||||
ElementType getType() const override { return ElementType::ScrollIndicator; }
|
||||
const char* getTypeName() const override { return "ScrollIndicator"; }
|
||||
|
||||
void setPosition(const std::string& expr) {
|
||||
positionExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setTotal(const std::string& expr) {
|
||||
totalExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setVisibleCount(const std::string& expr) {
|
||||
visibleExpr = Expression::parse(expr);
|
||||
markDirty();
|
||||
}
|
||||
void setTrackWidth(int w) {
|
||||
trackWidth = w;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
// Get values
|
||||
std::string posStr = context.evaluatestring(positionExpr);
|
||||
std::string totalStr = context.evaluatestring(totalExpr);
|
||||
std::string visStr = context.evaluatestring(visibleExpr);
|
||||
|
||||
float position = parseFloatSafe(posStr, 0.0f);
|
||||
int total = parseIntSafe(totalStr, 1);
|
||||
int visible = parseIntSafe(visStr, 1);
|
||||
|
||||
if (total <= visible) {
|
||||
// No need to show scrollbar
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw track
|
||||
int trackX = absX + (absW - trackWidth) / 2;
|
||||
renderer.drawRect(trackX, absY, trackWidth, absH, true);
|
||||
|
||||
// Calculate thumb size and position
|
||||
float ratio = static_cast<float>(visible) / static_cast<float>(total);
|
||||
int thumbH = static_cast<int>(absH * ratio);
|
||||
if (thumbH < 20) thumbH = 20; // Minimum thumb size
|
||||
|
||||
int maxScroll = total - visible;
|
||||
float scrollRatio = maxScroll > 0 ? position / maxScroll : 0;
|
||||
int thumbY = absY + static_cast<int>((absH - thumbH) * scrollRatio);
|
||||
|
||||
// Draw thumb
|
||||
renderer.fillRect(trackX, thumbY, trackWidth, thumbH, true);
|
||||
|
||||
markClean();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
144
lib/ThemeEngine/include/ListElement.h
Normal file
144
lib/ThemeEngine/include/ListElement.h
Normal file
@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "BasicElements.h"
|
||||
#include "UIElement.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// --- List ---
|
||||
// Supports vertical, horizontal, and grid layouts
|
||||
class List : public Container {
|
||||
public:
|
||||
enum class Direction { Vertical, Horizontal };
|
||||
enum class LayoutMode { List, Grid };
|
||||
|
||||
private:
|
||||
std::string source; // Data source name (e.g., "MainMenu", "FileList")
|
||||
std::string itemTemplateId; // ID of the template element
|
||||
int itemWidth = 0; // Explicit item width (0 = auto)
|
||||
int itemHeight = 0; // Explicit item height (0 = auto from template)
|
||||
int scrollOffset = 0; // Scroll position for long lists
|
||||
int visibleItems = -1; // Max visible items (-1 = auto)
|
||||
int spacing = 0; // Gap between items
|
||||
int columns = 1; // Number of columns (for grid mode)
|
||||
Direction direction = Direction::Vertical;
|
||||
LayoutMode layoutMode = LayoutMode::List;
|
||||
|
||||
// Template element reference (resolved after loading)
|
||||
UIElement* itemTemplate = nullptr;
|
||||
|
||||
public:
|
||||
List(const std::string& id) : Container(id) {}
|
||||
|
||||
ElementType getType() const override { return ElementType::List; }
|
||||
const char* getTypeName() const override { return "List"; }
|
||||
|
||||
void setSource(const std::string& s) {
|
||||
source = s;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
const std::string& getSource() const { return source; }
|
||||
|
||||
void setItemTemplateId(const std::string& id) {
|
||||
itemTemplateId = id;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setItemTemplate(UIElement* elem) {
|
||||
itemTemplate = elem;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
UIElement* getItemTemplate() const { return itemTemplate; }
|
||||
|
||||
void setItemWidth(int w) {
|
||||
itemWidth = w;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setItemHeight(int h) {
|
||||
itemHeight = h;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
int getItemHeight() const {
|
||||
if (itemHeight > 0) return itemHeight;
|
||||
if (itemTemplate) return itemTemplate->getAbsH() > 0 ? itemTemplate->getAbsH() : 45;
|
||||
return 45;
|
||||
}
|
||||
|
||||
int getItemWidth() const {
|
||||
if (itemWidth > 0) return itemWidth;
|
||||
if (itemTemplate) return itemTemplate->getAbsW() > 0 ? itemTemplate->getAbsW() : 100;
|
||||
return 100;
|
||||
}
|
||||
|
||||
void setScrollOffset(int offset) {
|
||||
scrollOffset = offset;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
int getScrollOffset() const { return scrollOffset; }
|
||||
|
||||
void setVisibleItems(int count) {
|
||||
visibleItems = count;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setSpacing(int s) {
|
||||
spacing = s;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setColumns(int c) {
|
||||
columns = c > 0 ? c : 1;
|
||||
if (columns > 1) layoutMode = LayoutMode::Grid;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setDirection(Direction d) {
|
||||
direction = d;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setDirectionFromString(const std::string& dir) {
|
||||
if (dir == "Horizontal" || dir == "horizontal" || dir == "row") {
|
||||
direction = Direction::Horizontal;
|
||||
} else {
|
||||
direction = Direction::Vertical;
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setLayoutMode(LayoutMode m) {
|
||||
layoutMode = m;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// Resolve template reference from element map
|
||||
void resolveTemplate(const std::map<std::string, UIElement*>& elements) {
|
||||
if (elements.count(itemTemplateId)) {
|
||||
itemTemplate = elements.at(itemTemplateId);
|
||||
}
|
||||
}
|
||||
|
||||
void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
|
||||
// Layout self first (bounds)
|
||||
UIElement::layout(context, parentX, parentY, parentW, parentH);
|
||||
|
||||
// Pre-layout the template once with list's dimensions to get item sizes
|
||||
// Pass absH so percentage heights in the template work correctly
|
||||
if (itemTemplate && itemHeight == 0) {
|
||||
itemTemplate->layout(context, absX, absY, absW, absH);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw is implemented in BasicElements.cpp
|
||||
void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
564
lib/ThemeEngine/include/ThemeContext.h
Normal file
564
lib/ThemeEngine/include/ThemeContext.h
Normal file
@ -0,0 +1,564 @@
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// Token types for expression parsing
|
||||
struct ExpressionToken {
|
||||
enum Type { LITERAL, VARIABLE };
|
||||
Type type;
|
||||
std::string value; // Literal text or variable name
|
||||
};
|
||||
|
||||
// Pre-parsed expression for efficient repeated evaluation
|
||||
struct Expression {
|
||||
std::vector<ExpressionToken> tokens;
|
||||
std::string rawExpr; // Original expression string for complex evaluation
|
||||
|
||||
bool empty() const { return tokens.empty() && rawExpr.empty(); }
|
||||
|
||||
static Expression parse(const std::string& str) {
|
||||
Expression expr;
|
||||
expr.rawExpr = str;
|
||||
|
||||
if (str.empty()) return expr;
|
||||
|
||||
size_t start = 0;
|
||||
while (start < str.length()) {
|
||||
size_t open = str.find('{', start);
|
||||
if (open == std::string::npos) {
|
||||
// Remaining literal
|
||||
expr.tokens.push_back({ExpressionToken::LITERAL, str.substr(start)});
|
||||
break;
|
||||
}
|
||||
|
||||
if (open > start) {
|
||||
// Literal before variable
|
||||
expr.tokens.push_back({ExpressionToken::LITERAL, str.substr(start, open - start)});
|
||||
}
|
||||
|
||||
size_t close = str.find('}', open);
|
||||
if (close == std::string::npos) {
|
||||
// Broken brace, treat as literal
|
||||
expr.tokens.push_back({ExpressionToken::LITERAL, str.substr(open)});
|
||||
break;
|
||||
}
|
||||
|
||||
// Variable
|
||||
expr.tokens.push_back({ExpressionToken::VARIABLE, str.substr(open + 1, close - open - 1)});
|
||||
start = close + 1;
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
};
|
||||
|
||||
class ThemeContext {
|
||||
private:
|
||||
std::map<std::string, std::string> strings;
|
||||
std::map<std::string, int> ints;
|
||||
std::map<std::string, bool> bools;
|
||||
|
||||
const ThemeContext* parent = nullptr;
|
||||
|
||||
// Helper to trim whitespace
|
||||
static std::string trim(const std::string& s) {
|
||||
size_t start = s.find_first_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) return "";
|
||||
size_t end = s.find_last_not_of(" \t\n\r");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
// Helper to check if string is a number
|
||||
static bool isNumber(const std::string& s) {
|
||||
if (s.empty()) return false;
|
||||
size_t start = (s[0] == '-') ? 1 : 0;
|
||||
for (size_t i = start; i < s.length(); i++) {
|
||||
if (!isdigit(s[i])) return false;
|
||||
}
|
||||
return start < s.length();
|
||||
}
|
||||
|
||||
// Helper to check if string is a hex number (0x..)
|
||||
static bool isHexNumber(const std::string& s) {
|
||||
if (s.size() < 3) return false;
|
||||
if (!(s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))) return false;
|
||||
for (size_t i = 2; i < s.length(); i++) {
|
||||
char c = s[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int parseInt(const std::string& s) {
|
||||
if (isHexNumber(s)) {
|
||||
return static_cast<int>(std::strtol(s.c_str(), nullptr, 16));
|
||||
}
|
||||
if (isNumber(s)) {
|
||||
return static_cast<int>(std::strtol(s.c_str(), nullptr, 10));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool coerceBool(const std::string& s) {
|
||||
std::string v = trim(s);
|
||||
if (v.empty()) return false;
|
||||
if (v == "true" || v == "1") return true;
|
||||
if (v == "false" || v == "0") return false;
|
||||
if (isHexNumber(v) || isNumber(v)) return parseInt(v) != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit ThemeContext(const ThemeContext* parent = nullptr) : parent(parent) {}
|
||||
|
||||
void setString(const std::string& key, const std::string& value) { strings[key] = value; }
|
||||
void setInt(const std::string& key, int value) { ints[key] = value; }
|
||||
void setBool(const std::string& key, bool value) { bools[key] = value; }
|
||||
|
||||
// Helper to populate list data efficiently
|
||||
void setListItem(const std::string& listName, int index, const std::string& property, const std::string& value) {
|
||||
strings[listName + "." + std::to_string(index) + "." + property] = value;
|
||||
}
|
||||
void setListItem(const std::string& listName, int index, const std::string& property, int value) {
|
||||
ints[listName + "." + std::to_string(index) + "." + property] = value;
|
||||
}
|
||||
void setListItem(const std::string& listName, int index, const std::string& property, bool value) {
|
||||
bools[listName + "." + std::to_string(index) + "." + property] = value;
|
||||
}
|
||||
void setListItem(const std::string& listName, int index, const std::string& property, const char* value) {
|
||||
strings[listName + "." + std::to_string(index) + "." + property] = value;
|
||||
}
|
||||
|
||||
std::string getString(const std::string& key, const std::string& defaultValue = "") const {
|
||||
auto it = strings.find(key);
|
||||
if (it != strings.end()) return it->second;
|
||||
if (parent) return parent->getString(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
int getInt(const std::string& key, int defaultValue = 0) const {
|
||||
auto it = ints.find(key);
|
||||
if (it != ints.end()) return it->second;
|
||||
if (parent) return parent->getInt(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool getBool(const std::string& key, bool defaultValue = false) const {
|
||||
auto it = bools.find(key);
|
||||
if (it != bools.end()) return it->second;
|
||||
if (parent) return parent->getBool(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool hasKey(const std::string& key) const {
|
||||
if (strings.count(key) || ints.count(key) || bools.count(key)) return true;
|
||||
if (parent) return parent->hasKey(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get any value as string
|
||||
std::string getAnyAsString(const std::string& key) const {
|
||||
// Check strings first
|
||||
auto sit = strings.find(key);
|
||||
if (sit != strings.end()) return sit->second;
|
||||
|
||||
// Check ints
|
||||
auto iit = ints.find(key);
|
||||
if (iit != ints.end()) return std::to_string(iit->second);
|
||||
|
||||
// Check bools
|
||||
auto bit = bools.find(key);
|
||||
if (bit != bools.end()) return bit->second ? "true" : "false";
|
||||
|
||||
// Check parent
|
||||
if (parent) return parent->getAnyAsString(key);
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool getAnyAsBool(const std::string& key, bool defaultValue = false) const {
|
||||
auto bit = bools.find(key);
|
||||
if (bit != bools.end()) return bit->second;
|
||||
|
||||
auto iit = ints.find(key);
|
||||
if (iit != ints.end()) return iit->second != 0;
|
||||
|
||||
auto sit = strings.find(key);
|
||||
if (sit != strings.end()) return coerceBool(sit->second);
|
||||
|
||||
if (parent) return parent->getAnyAsBool(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
int getAnyAsInt(const std::string& key, int defaultValue = 0) const {
|
||||
auto iit = ints.find(key);
|
||||
if (iit != ints.end()) return iit->second;
|
||||
|
||||
auto bit = bools.find(key);
|
||||
if (bit != bools.end()) return bit->second ? 1 : 0;
|
||||
|
||||
auto sit = strings.find(key);
|
||||
if (sit != strings.end()) return parseInt(sit->second);
|
||||
|
||||
if (parent) return parent->getAnyAsInt(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Evaluate a complex boolean expression
|
||||
// Supports: !, &&, ||, ==, !=, <, >, <=, >=, parentheses
|
||||
bool evaluateBool(const std::string& expression) const {
|
||||
std::string expr = trim(expression);
|
||||
if (expr.empty()) return false;
|
||||
|
||||
// Handle literal true/false
|
||||
if (expr == "true" || expr == "1") return true;
|
||||
if (expr == "false" || expr == "0") return false;
|
||||
|
||||
// Handle {var} wrapper
|
||||
if (expr.size() > 2 && expr.front() == '{' && expr.back() == '}') {
|
||||
expr = trim(expr.substr(1, expr.size() - 2));
|
||||
}
|
||||
|
||||
struct Token {
|
||||
enum Type { Identifier, Number, String, Op, LParen, RParen, End };
|
||||
Type type;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct Tokenizer {
|
||||
const std::string& s;
|
||||
size_t pos = 0;
|
||||
Token peeked{Token::End, ""};
|
||||
bool hasPeek = false;
|
||||
|
||||
explicit Tokenizer(const std::string& input) : s(input) {}
|
||||
|
||||
static std::string trimCopy(const std::string& in) {
|
||||
size_t start = in.find_first_not_of(" \t\n\r");
|
||||
if (start == std::string::npos) return "";
|
||||
size_t end = in.find_last_not_of(" \t\n\r");
|
||||
return in.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
void skipWs() {
|
||||
while (pos < s.size() && (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n' || s[pos] == '\r')) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
Token readToken() {
|
||||
skipWs();
|
||||
if (pos >= s.size()) return {Token::End, ""};
|
||||
char c = s[pos];
|
||||
|
||||
if (c == '(') {
|
||||
pos++;
|
||||
return {Token::LParen, "("};
|
||||
}
|
||||
if (c == ')') {
|
||||
pos++;
|
||||
return {Token::RParen, ")"};
|
||||
}
|
||||
|
||||
if (c == '{') {
|
||||
size_t end = s.find('}', pos + 1);
|
||||
std::string inner;
|
||||
if (end == std::string::npos) {
|
||||
inner = s.substr(pos + 1);
|
||||
pos = s.size();
|
||||
} else {
|
||||
inner = s.substr(pos + 1, end - pos - 1);
|
||||
pos = end + 1;
|
||||
}
|
||||
return {Token::Identifier, trimCopy(inner)};
|
||||
}
|
||||
|
||||
if (c == '"' || c == '\'') {
|
||||
char quote = c;
|
||||
pos++;
|
||||
std::string out;
|
||||
while (pos < s.size()) {
|
||||
char ch = s[pos++];
|
||||
if (ch == '\\' && pos < s.size()) {
|
||||
out.push_back(s[pos++]);
|
||||
continue;
|
||||
}
|
||||
if (ch == quote) break;
|
||||
out.push_back(ch);
|
||||
}
|
||||
return {Token::String, out};
|
||||
}
|
||||
|
||||
// Operators
|
||||
if (pos + 1 < s.size()) {
|
||||
std::string two = s.substr(pos, 2);
|
||||
if (two == "&&" || two == "||" || two == "==" || two == "!=" || two == "<=" || two == ">=") {
|
||||
pos += 2;
|
||||
return {Token::Op, two};
|
||||
}
|
||||
}
|
||||
if (c == '!' || c == '<' || c == '>') {
|
||||
pos++;
|
||||
return {Token::Op, std::string(1, c)};
|
||||
}
|
||||
|
||||
// Number (decimal or hex)
|
||||
if (isdigit(c) || (c == '-' && pos + 1 < s.size() && isdigit(s[pos + 1]))) {
|
||||
size_t start = pos;
|
||||
pos++;
|
||||
if (pos + 1 < s.size() && s[start] == '0' && (s[pos] == 'x' || s[pos] == 'X')) {
|
||||
pos++; // consume x
|
||||
while (pos < s.size() && isxdigit(s[pos])) pos++;
|
||||
} else {
|
||||
while (pos < s.size() && isdigit(s[pos])) pos++;
|
||||
}
|
||||
return {Token::Number, s.substr(start, pos - start)};
|
||||
}
|
||||
|
||||
// Identifier
|
||||
if (isalpha(c) || c == '_' || c == '.') {
|
||||
size_t start = pos;
|
||||
pos++;
|
||||
while (pos < s.size()) {
|
||||
char ch = s[pos];
|
||||
if (isalnum(ch) || ch == '_' || ch == '.') {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {Token::Identifier, s.substr(start, pos - start)};
|
||||
}
|
||||
|
||||
// Unknown char, skip
|
||||
pos++;
|
||||
return readToken();
|
||||
}
|
||||
|
||||
Token next() {
|
||||
if (hasPeek) {
|
||||
hasPeek = false;
|
||||
return peeked;
|
||||
}
|
||||
return readToken();
|
||||
}
|
||||
|
||||
Token peek() {
|
||||
if (!hasPeek) {
|
||||
peeked = readToken();
|
||||
hasPeek = true;
|
||||
}
|
||||
return peeked;
|
||||
}
|
||||
};
|
||||
|
||||
Tokenizer tz(expr);
|
||||
|
||||
std::function<bool()> parseOr;
|
||||
std::function<bool()> parseAnd;
|
||||
std::function<bool()> parseNot;
|
||||
std::function<bool()> parseComparison;
|
||||
std::function<std::string()> parseValue;
|
||||
|
||||
parseValue = [&]() -> std::string {
|
||||
Token t = tz.next();
|
||||
if (t.type == Token::LParen) {
|
||||
bool inner = parseOr();
|
||||
Token close = tz.next();
|
||||
if (close.type != Token::RParen) {
|
||||
// best-effort: no-op
|
||||
}
|
||||
return inner ? "true" : "false";
|
||||
}
|
||||
if (t.type == Token::String) {
|
||||
return "'" + t.text + "'";
|
||||
}
|
||||
if (t.type == Token::Number) {
|
||||
return t.text;
|
||||
}
|
||||
if (t.type == Token::Identifier) {
|
||||
return t.text;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
auto isComparisonOp = [](const Token& t) {
|
||||
if (t.type != Token::Op) return false;
|
||||
return t.text == "==" || t.text == "!=" || t.text == "<" || t.text == ">" || t.text == "<=" || t.text == ">=";
|
||||
};
|
||||
|
||||
parseComparison = [&]() -> bool {
|
||||
std::string left = parseValue();
|
||||
Token op = tz.peek();
|
||||
if (isComparisonOp(op)) {
|
||||
tz.next();
|
||||
std::string right = parseValue();
|
||||
int cmp = compareValues(left, right);
|
||||
if (op.text == "==") return cmp == 0;
|
||||
if (op.text == "!=") return cmp != 0;
|
||||
if (op.text == "<") return cmp < 0;
|
||||
if (op.text == ">") return cmp > 0;
|
||||
if (op.text == "<=") return cmp <= 0;
|
||||
if (op.text == ">=") return cmp >= 0;
|
||||
return false;
|
||||
}
|
||||
return coerceBool(resolveValue(left));
|
||||
};
|
||||
|
||||
parseNot = [&]() -> bool {
|
||||
Token t = tz.peek();
|
||||
if (t.type == Token::Op && t.text == "!") {
|
||||
tz.next();
|
||||
return !parseNot();
|
||||
}
|
||||
return parseComparison();
|
||||
};
|
||||
|
||||
parseAnd = [&]() -> bool {
|
||||
bool value = parseNot();
|
||||
while (true) {
|
||||
Token t = tz.peek();
|
||||
if (t.type == Token::Op && t.text == "&&") {
|
||||
tz.next();
|
||||
value = value && parseNot();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
parseOr = [&]() -> bool {
|
||||
bool value = parseAnd();
|
||||
while (true) {
|
||||
Token t = tz.peek();
|
||||
if (t.type == Token::Op && t.text == "||") {
|
||||
tz.next();
|
||||
value = value || parseAnd();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return parseOr();
|
||||
}
|
||||
|
||||
// Compare two values (handles variables, numbers, strings)
|
||||
int compareValues(const std::string& left, const std::string& right) const {
|
||||
std::string leftVal = resolveValue(left);
|
||||
std::string rightVal = resolveValue(right);
|
||||
|
||||
// Try numeric comparison
|
||||
if ((isNumber(leftVal) || isHexNumber(leftVal)) && (isNumber(rightVal) || isHexNumber(rightVal))) {
|
||||
int l = parseInt(leftVal);
|
||||
int r = parseInt(rightVal);
|
||||
return (l < r) ? -1 : (l > r) ? 1 : 0;
|
||||
}
|
||||
|
||||
// String comparison
|
||||
return leftVal.compare(rightVal);
|
||||
}
|
||||
|
||||
// Resolve a value (variable name -> value, or literal)
|
||||
std::string resolveValue(const std::string& val) const {
|
||||
std::string v = trim(val);
|
||||
|
||||
// Remove quotes for string literals
|
||||
if (v.size() >= 2 && v.front() == '"' && v.back() == '"') {
|
||||
return v.substr(1, v.size() - 2);
|
||||
}
|
||||
if (v.size() >= 2 && v.front() == '\'' && v.back() == '\'') {
|
||||
return v.substr(1, v.size() - 2);
|
||||
}
|
||||
|
||||
// If it's a number, return as-is
|
||||
if (isNumber(v)) return v;
|
||||
|
||||
// Check for hex color literals (0x00, 0xFF, etc.)
|
||||
if (isHexNumber(v)) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Check for known color names - return as-is
|
||||
if (v == "black" || v == "white" || v == "gray" || v == "grey") {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Check for boolean literals
|
||||
if (v == "true" || v == "false" || v == "1" || v == "0") {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Try to look up as variable
|
||||
std::string varName = v;
|
||||
if (varName.size() >= 2 && varName.front() == '{' && varName.back() == '}') {
|
||||
varName = trim(varName.substr(1, varName.size() - 2));
|
||||
}
|
||||
|
||||
if (hasKey(varName)) {
|
||||
return getAnyAsString(varName);
|
||||
}
|
||||
|
||||
// Return as literal if not found as variable
|
||||
return v;
|
||||
}
|
||||
|
||||
// Evaluate a string expression with variable substitution
|
||||
std::string evaluatestring(const Expression& expr) const {
|
||||
if (expr.empty()) return "";
|
||||
|
||||
std::string result;
|
||||
for (const auto& token : expr.tokens) {
|
||||
if (token.type == ExpressionToken::LITERAL) {
|
||||
result += token.value;
|
||||
} else {
|
||||
// Variable lookup - check for comparison expressions inside
|
||||
std::string varName = token.value;
|
||||
|
||||
// If the variable contains comparison operators, evaluate as condition
|
||||
if (varName.find("==") != std::string::npos || varName.find("!=") != std::string::npos ||
|
||||
varName.find("&&") != std::string::npos || varName.find("||") != std::string::npos) {
|
||||
result += evaluateBool(varName) ? "true" : "false";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle ternary: condition ? trueVal : falseVal
|
||||
size_t qPos = varName.find('?');
|
||||
if (qPos != std::string::npos) {
|
||||
size_t cPos = varName.find(':', qPos);
|
||||
if (cPos != std::string::npos) {
|
||||
std::string condition = trim(varName.substr(0, qPos));
|
||||
std::string trueVal = trim(varName.substr(qPos + 1, cPos - qPos - 1));
|
||||
std::string falseVal = trim(varName.substr(cPos + 1));
|
||||
|
||||
bool condResult = evaluateBool(condition);
|
||||
result += resolveValue(condResult ? trueVal : falseVal);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Normal variable lookup
|
||||
std::string strVal = getAnyAsString(varName);
|
||||
result += strVal;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Legacy method for backward compatibility
|
||||
std::string evaluateString(const std::string& expression) const {
|
||||
if (expression.empty()) return "";
|
||||
Expression expr = Expression::parse(expression);
|
||||
return evaluatestring(expr);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
155
lib/ThemeEngine/include/ThemeManager.h
Normal file
155
lib/ThemeEngine/include/ThemeManager.h
Normal file
@ -0,0 +1,155 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "BasicElements.h"
|
||||
#include "IniParser.h"
|
||||
#include "ThemeContext.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
struct ProcessedAsset {
|
||||
std::vector<uint8_t> data;
|
||||
int w, h;
|
||||
GfxRenderer::Orientation orientation;
|
||||
};
|
||||
|
||||
// Screen render cache - stores full screen state for quick restore
|
||||
struct ScreenCache {
|
||||
uint8_t* buffer = nullptr;
|
||||
size_t bufferSize = 0;
|
||||
std::string screenName;
|
||||
uint32_t contextHash = 0; // Hash of context data to detect changes
|
||||
bool valid = false;
|
||||
|
||||
ScreenCache() = default;
|
||||
~ScreenCache() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent double-free from copy
|
||||
ScreenCache(const ScreenCache&) = delete;
|
||||
ScreenCache& operator=(const ScreenCache&) = delete;
|
||||
|
||||
// Allow move
|
||||
ScreenCache(ScreenCache&& other) noexcept
|
||||
: buffer(other.buffer),
|
||||
bufferSize(other.bufferSize),
|
||||
screenName(std::move(other.screenName)),
|
||||
contextHash(other.contextHash),
|
||||
valid(other.valid) {
|
||||
other.buffer = nullptr;
|
||||
other.bufferSize = 0;
|
||||
other.valid = false;
|
||||
}
|
||||
|
||||
ScreenCache& operator=(ScreenCache&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (buffer) free(buffer);
|
||||
buffer = other.buffer;
|
||||
bufferSize = other.bufferSize;
|
||||
screenName = std::move(other.screenName);
|
||||
contextHash = other.contextHash;
|
||||
valid = other.valid;
|
||||
other.buffer = nullptr;
|
||||
other.bufferSize = 0;
|
||||
other.valid = false;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void invalidate() { valid = false; }
|
||||
};
|
||||
|
||||
class ThemeManager {
|
||||
private:
|
||||
std::map<std::string, UIElement*> elements; // All elements by ID
|
||||
std::string currentThemeName;
|
||||
int navBookCount = 1; // Number of navigable book slots (from theme [Global] section)
|
||||
std::map<std::string, int> fontMap;
|
||||
|
||||
// Screen-level caching for fast redraw
|
||||
std::map<std::string, ScreenCache> screenCaches;
|
||||
bool useCaching = true;
|
||||
|
||||
// Track which elements are data-dependent vs static
|
||||
std::map<std::string, bool> elementDependsOnData;
|
||||
|
||||
// Factory and property methods
|
||||
UIElement* createElement(const std::string& id, const std::string& type);
|
||||
void applyProperties(UIElement* elem, const std::map<std::string, std::string>& props);
|
||||
|
||||
public:
|
||||
static ThemeManager& get() {
|
||||
static ThemeManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Initialize defaults (fonts, etc.)
|
||||
void begin();
|
||||
|
||||
// Register a font ID mapping (e.g. "UI_12" -> 0)
|
||||
void registerFont(const std::string& name, int id);
|
||||
|
||||
// Theme loading
|
||||
void loadTheme(const std::string& themeName);
|
||||
void unloadTheme();
|
||||
|
||||
// Get current theme name
|
||||
const std::string& getCurrentTheme() const { return currentThemeName; }
|
||||
|
||||
// Get number of navigable book slots (from theme config, default 1)
|
||||
int getNavBookCount() const { return navBookCount; }
|
||||
|
||||
// Render a screen
|
||||
void renderScreen(const std::string& screenName, const GfxRenderer& renderer, const ThemeContext& context);
|
||||
|
||||
// Render with dirty tracking (only redraws changed regions)
|
||||
void renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer, const ThemeContext& context,
|
||||
const ThemeContext* prevContext = nullptr);
|
||||
|
||||
// Invalidate all caches (call when theme changes or screen switches)
|
||||
void invalidateAllCaches();
|
||||
|
||||
// Invalidate specific screen cache
|
||||
void invalidateScreenCache(const std::string& screenName);
|
||||
|
||||
// Enable/disable caching
|
||||
void setCachingEnabled(bool enabled) { useCaching = enabled; }
|
||||
bool isCachingEnabled() const { return useCaching; }
|
||||
|
||||
// Asset path resolution
|
||||
std::string getAssetPath(const std::string& assetName);
|
||||
|
||||
// Asset caching
|
||||
const std::vector<uint8_t>* getCachedAsset(const std::string& path);
|
||||
void cacheAsset(const std::string& path, std::vector<uint8_t>&& data);
|
||||
const ProcessedAsset* getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
|
||||
int targetW = 0, int targetH = 0);
|
||||
void cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, int targetW = 0, int targetH = 0);
|
||||
|
||||
// Clear asset caches (for memory management)
|
||||
void clearAssetCaches();
|
||||
|
||||
// Get element by ID (useful for direct manipulation)
|
||||
UIElement* getElement(const std::string& id) {
|
||||
auto it = elements.find(id);
|
||||
return it != elements.end() ? it->second : nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::vector<uint8_t>> assetCache;
|
||||
std::map<std::string, ProcessedAsset> processedCache;
|
||||
|
||||
// Compute a simple hash of context data for cache invalidation
|
||||
uint32_t computeContextHash(const ThemeContext& context, const std::string& screenName);
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
84
lib/ThemeEngine/include/ThemeTypes.h
Normal file
84
lib/ThemeEngine/include/ThemeTypes.h
Normal file
@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
enum class DimensionUnit { PIXELS, PERCENT, UNKNOWN };
|
||||
|
||||
struct Dimension {
|
||||
int value;
|
||||
DimensionUnit unit;
|
||||
|
||||
Dimension(int v, DimensionUnit u) : value(v), unit(u) {}
|
||||
Dimension() : value(0), unit(DimensionUnit::PIXELS) {}
|
||||
|
||||
static Dimension parse(const std::string& str) {
|
||||
if (str.empty()) return Dimension(0, DimensionUnit::PIXELS);
|
||||
|
||||
auto safeParseInt = [](const std::string& s) {
|
||||
char* end = nullptr;
|
||||
long v = std::strtol(s.c_str(), &end, 10);
|
||||
if (!end || end == s.c_str()) return 0;
|
||||
return static_cast<int>(v);
|
||||
};
|
||||
|
||||
if (str.back() == '%') {
|
||||
return Dimension(safeParseInt(str.substr(0, str.length() - 1)), DimensionUnit::PERCENT);
|
||||
}
|
||||
return Dimension(safeParseInt(str), DimensionUnit::PIXELS);
|
||||
}
|
||||
|
||||
int resolve(int parentSize) const {
|
||||
if (unit == DimensionUnit::PERCENT) {
|
||||
return (parentSize * value) / 100;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
struct Color {
|
||||
uint8_t value; // For E-Ink: 0 (Black) to 255 (White), or simplified palette
|
||||
|
||||
explicit Color(uint8_t v) : value(v) {}
|
||||
Color() : value(0) {}
|
||||
|
||||
static Color parse(const std::string& str) {
|
||||
if (str.empty()) return Color(0);
|
||||
if (str == "black") return Color(0x00);
|
||||
if (str == "white") return Color(0xFF);
|
||||
if (str == "gray" || str == "grey") return Color(0x80);
|
||||
if (str.size() > 2 && str.substr(0, 2) == "0x") {
|
||||
return Color((uint8_t)std::strtol(str.c_str(), nullptr, 16));
|
||||
}
|
||||
// Safe fallback using strtol (returns 0 on error, no exception)
|
||||
return Color((uint8_t)std::strtol(str.c_str(), nullptr, 10));
|
||||
}
|
||||
};
|
||||
|
||||
// Rect structure for dirty regions
|
||||
struct Rect {
|
||||
int x, y, w, h;
|
||||
|
||||
Rect() : x(0), y(0), w(0), h(0) {}
|
||||
Rect(int x, int y, int w, int h) : x(x), y(y), w(w), h(h) {}
|
||||
|
||||
bool isEmpty() const { return w <= 0 || h <= 0; }
|
||||
|
||||
bool intersects(const Rect& other) const {
|
||||
return !(x + w <= other.x || other.x + other.w <= x || y + h <= other.y || other.y + other.h <= y);
|
||||
}
|
||||
|
||||
Rect unite(const Rect& other) const {
|
||||
if (isEmpty()) return other;
|
||||
if (other.isEmpty()) return *this;
|
||||
int nx = std::min(x, other.x);
|
||||
int ny = std::min(y, other.y);
|
||||
int nx2 = std::max(x + w, other.x + other.w);
|
||||
int ny2 = std::max(y + h, other.y + other.h);
|
||||
return Rect(nx, ny, nx2 - nx, ny2 - ny);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
211
lib/ThemeEngine/include/UIElement.h
Normal file
211
lib/ThemeEngine/include/UIElement.h
Normal file
@ -0,0 +1,211 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ThemeContext.h"
|
||||
#include "ThemeTypes.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
class Container; // Forward declaration
|
||||
|
||||
class UIElement {
|
||||
public:
|
||||
int getAbsX() const { return absX; }
|
||||
int getAbsY() const { return absY; }
|
||||
int getAbsW() const { return absW; }
|
||||
int getAbsH() const { return absH; }
|
||||
const std::string& getId() const { return id; }
|
||||
|
||||
protected:
|
||||
std::string id;
|
||||
Dimension x, y, width, height;
|
||||
Expression visibleExpr;
|
||||
bool visibleExprIsStatic = true; // True if visibility doesn't depend on data
|
||||
|
||||
// Recomputed every layout pass
|
||||
int absX = 0, absY = 0, absW = 0, absH = 0;
|
||||
|
||||
// Layout caching - track last params to skip redundant layout
|
||||
int lastParentX = -1, lastParentY = -1, lastParentW = -1, lastParentH = -1;
|
||||
bool layoutValid = false;
|
||||
|
||||
// Caching support
|
||||
bool cacheable = false; // Set true for expensive elements like bitmaps
|
||||
bool cacheValid = false; // Is the cached render still valid?
|
||||
uint8_t* cachedRender = nullptr;
|
||||
size_t cachedRenderSize = 0;
|
||||
int cachedX = 0, cachedY = 0, cachedW = 0, cachedH = 0;
|
||||
|
||||
// Dirty tracking
|
||||
bool dirty = true; // Needs redraw
|
||||
|
||||
bool isVisible(const ThemeContext& context) const {
|
||||
if (visibleExpr.empty()) return true;
|
||||
return context.evaluateBool(visibleExpr.rawExpr);
|
||||
}
|
||||
|
||||
public:
|
||||
UIElement(const std::string& id) : id(id), visibleExpr(Expression::parse("true")) {}
|
||||
|
||||
virtual ~UIElement() {
|
||||
if (cachedRender) {
|
||||
free(cachedRender);
|
||||
cachedRender = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void setX(Dimension val) {
|
||||
x = val;
|
||||
markDirty();
|
||||
}
|
||||
void setY(Dimension val) {
|
||||
y = val;
|
||||
markDirty();
|
||||
}
|
||||
void setWidth(Dimension val) {
|
||||
width = val;
|
||||
markDirty();
|
||||
}
|
||||
void setHeight(Dimension val) {
|
||||
height = val;
|
||||
markDirty();
|
||||
}
|
||||
void setVisibleExpr(const std::string& expr) {
|
||||
visibleExpr = Expression::parse(expr);
|
||||
// Check if expression contains variables
|
||||
visibleExprIsStatic =
|
||||
(expr == "true" || expr == "false" || expr == "1" || expr == "0" || expr.find('{') == std::string::npos);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
void setCacheable(bool val) { cacheable = val; }
|
||||
bool isCacheable() const { return cacheable; }
|
||||
|
||||
virtual void markDirty() {
|
||||
dirty = true;
|
||||
cacheValid = false;
|
||||
layoutValid = false;
|
||||
}
|
||||
|
||||
void markClean() { dirty = false; }
|
||||
bool isDirty() const { return dirty; }
|
||||
|
||||
// Invalidate cache (called when dependent data changes)
|
||||
void invalidateCache() {
|
||||
cacheValid = false;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// Calculate absolute position based on parent
|
||||
virtual void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) {
|
||||
// Skip layout if params unchanged and layout is still valid
|
||||
if (layoutValid && parentX == lastParentX && parentY == lastParentY && parentW == lastParentW &&
|
||||
parentH == lastParentH) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastParentX = parentX;
|
||||
lastParentY = parentY;
|
||||
lastParentW = parentW;
|
||||
lastParentH = parentH;
|
||||
layoutValid = true;
|
||||
|
||||
int newX = parentX + x.resolve(parentW);
|
||||
int newY = parentY + y.resolve(parentH);
|
||||
int newW = width.resolve(parentW);
|
||||
int newH = height.resolve(parentH);
|
||||
|
||||
// Clamp to parent bounds
|
||||
if (newX >= parentX + parentW) newX = parentX + parentW - 1;
|
||||
if (newY >= parentY + parentH) newY = parentY + parentH - 1;
|
||||
|
||||
int maxX = parentX + parentW;
|
||||
int maxY = parentY + parentH;
|
||||
|
||||
if (newX + newW > maxX) newW = maxX - newX;
|
||||
if (newY + newH > maxY) newH = maxY - newY;
|
||||
|
||||
if (newW < 0) newW = 0;
|
||||
if (newH < 0) newH = 0;
|
||||
|
||||
// Check if position changed
|
||||
if (newX != absX || newY != absY || newW != absW || newH != absH) {
|
||||
absX = newX;
|
||||
absY = newY;
|
||||
absW = newW;
|
||||
absH = newH;
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
virtual Container* asContainer() { return nullptr; }
|
||||
|
||||
enum class ElementType {
|
||||
Base,
|
||||
Container,
|
||||
Rectangle,
|
||||
Label,
|
||||
Bitmap,
|
||||
List,
|
||||
ProgressBar,
|
||||
Divider,
|
||||
// Layout elements
|
||||
HStack,
|
||||
VStack,
|
||||
Grid,
|
||||
// Advanced elements
|
||||
Badge,
|
||||
Toggle,
|
||||
TabBar,
|
||||
Icon,
|
||||
BatteryIcon,
|
||||
ScrollIndicator
|
||||
};
|
||||
|
||||
virtual ElementType getType() const { return ElementType::Base; }
|
||||
virtual const char* getTypeName() const { return "UIElement"; }
|
||||
|
||||
int getLayoutHeight() const { return absH; }
|
||||
int getLayoutWidth() const { return absW; }
|
||||
|
||||
// Get bounding rect for this element
|
||||
Rect getBounds() const { return Rect(absX, absY, absW, absH); }
|
||||
|
||||
// Main draw method - handles caching automatically
|
||||
virtual void draw(const GfxRenderer& renderer, const ThemeContext& context) = 0;
|
||||
|
||||
protected:
|
||||
// Cache the rendered output
|
||||
bool cacheRender(const GfxRenderer& renderer) {
|
||||
if (cachedRender) {
|
||||
free(cachedRender);
|
||||
cachedRender = nullptr;
|
||||
}
|
||||
|
||||
cachedRender = renderer.captureRegion(absX, absY, absW, absH, &cachedRenderSize);
|
||||
if (cachedRender) {
|
||||
cachedX = absX;
|
||||
cachedY = absY;
|
||||
cachedW = absW;
|
||||
cachedH = absH;
|
||||
cacheValid = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restore from cache
|
||||
bool restoreFromCache(const GfxRenderer& renderer) const {
|
||||
if (!cacheValid || !cachedRender) return false;
|
||||
if (absX != cachedX || absY != cachedY || absW != cachedW || absH != cachedH) return false;
|
||||
|
||||
renderer.restoreRegion(cachedRender, absX, absY, absW, absH);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ThemeEngine
|
||||
500
lib/ThemeEngine/src/BasicElements.cpp
Normal file
500
lib/ThemeEngine/src/BasicElements.cpp
Normal file
@ -0,0 +1,500 @@
|
||||
#include "BasicElements.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "Bitmap.h"
|
||||
#include "ListElement.h"
|
||||
#include "ThemeManager.h"
|
||||
#include "ThemeTypes.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// --- Container ---
|
||||
void Container::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
if (hasBg) {
|
||||
std::string colStr = context.evaluatestring(bgColorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
// Use dithered fill for grayscale values, solid fill for black/white
|
||||
// Use rounded rect if borderRadius > 0
|
||||
if (color == 0x00) {
|
||||
if (borderRadius > 0) {
|
||||
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, true);
|
||||
} else {
|
||||
renderer.fillRect(absX, absY, absW, absH, true);
|
||||
}
|
||||
} else if (color >= 0xF0) {
|
||||
if (borderRadius > 0) {
|
||||
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, false);
|
||||
} else {
|
||||
renderer.fillRect(absX, absY, absW, absH, false);
|
||||
}
|
||||
} else {
|
||||
if (borderRadius > 0) {
|
||||
renderer.fillRoundedRectDithered(absX, absY, absW, absH, borderRadius, color);
|
||||
} else {
|
||||
renderer.fillRectDithered(absX, absY, absW, absH, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dynamic border expression
|
||||
bool drawBorder = border;
|
||||
if (hasBorderExpr()) {
|
||||
drawBorder = context.evaluateBool(borderExpr.rawExpr);
|
||||
}
|
||||
|
||||
if (drawBorder) {
|
||||
if (borderRadius > 0) {
|
||||
renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, true);
|
||||
} else {
|
||||
renderer.drawRect(absX, absY, absW, absH, true);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto child : children) {
|
||||
child->draw(renderer, context);
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
|
||||
// --- Rectangle ---
|
||||
void Rectangle::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
std::string colStr = context.evaluatestring(colorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
|
||||
bool shouldFill = fill;
|
||||
if (!fillExpr.empty()) {
|
||||
shouldFill = context.evaluateBool(fillExpr.rawExpr);
|
||||
}
|
||||
|
||||
if (shouldFill) {
|
||||
// Use dithered fill for grayscale values, solid fill for black/white
|
||||
// Use rounded rect if borderRadius > 0
|
||||
if (color == 0x00) {
|
||||
if (borderRadius > 0) {
|
||||
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, true);
|
||||
} else {
|
||||
renderer.fillRect(absX, absY, absW, absH, true);
|
||||
}
|
||||
} else if (color >= 0xF0) {
|
||||
if (borderRadius > 0) {
|
||||
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, false);
|
||||
} else {
|
||||
renderer.fillRect(absX, absY, absW, absH, false);
|
||||
}
|
||||
} else {
|
||||
if (borderRadius > 0) {
|
||||
renderer.fillRoundedRectDithered(absX, absY, absW, absH, borderRadius, color);
|
||||
} else {
|
||||
renderer.fillRectDithered(absX, absY, absW, absH, color);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Draw border
|
||||
bool black = (color == 0x00);
|
||||
if (borderRadius > 0) {
|
||||
renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, black);
|
||||
} else {
|
||||
renderer.drawRect(absX, absY, absW, absH, black);
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
|
||||
// --- Label ---
|
||||
void Label::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
if (!isVisible(context)) return;
|
||||
|
||||
std::string finalStr = context.evaluatestring(textExpr);
|
||||
|
||||
if (finalStr.empty()) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string colStr = context.evaluatestring(colorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
bool black = (color == 0x00);
|
||||
|
||||
int textWidth = renderer.getTextWidth(fontId, finalStr.c_str());
|
||||
int lineHeight = renderer.getLineHeight(fontId);
|
||||
|
||||
std::vector<std::string> lines;
|
||||
lines.reserve(maxLines); // Pre-allocate to avoid reallocations
|
||||
if (absW > 0 && textWidth > absW && maxLines > 1) {
|
||||
// Logic to wrap text
|
||||
std::string remaining = finalStr;
|
||||
while (!remaining.empty() && (int)lines.size() < maxLines) {
|
||||
// If it fits, add entire line
|
||||
if (renderer.getTextWidth(fontId, remaining.c_str()) <= absW) {
|
||||
lines.push_back(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
// Binary search for maximum characters that fit (O(log n) instead of O(n))
|
||||
int len = remaining.length();
|
||||
int lo = 1, hi = len;
|
||||
while (lo < hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (renderer.getTextWidth(fontId, remaining.substr(0, mid).c_str()) <= absW) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
int cut = lo;
|
||||
|
||||
// Find last space before cut
|
||||
if (cut < (int)remaining.length()) {
|
||||
int space = -1;
|
||||
for (int i = cut; i > 0; i--) {
|
||||
if (remaining[i] == ' ') {
|
||||
space = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (space != -1) cut = space;
|
||||
}
|
||||
|
||||
std::string line = remaining.substr(0, cut);
|
||||
|
||||
// If we're at the last allowed line but still have more text
|
||||
if ((int)lines.size() == maxLines - 1 && cut < (int)remaining.length()) {
|
||||
if (ellipsis) {
|
||||
line = renderer.truncatedText(fontId, remaining.c_str(), absW);
|
||||
}
|
||||
lines.push_back(line);
|
||||
break;
|
||||
}
|
||||
|
||||
lines.push_back(line);
|
||||
// Advance
|
||||
if (cut < (int)remaining.length()) {
|
||||
// Skip the space if check
|
||||
if (remaining[cut] == ' ') cut++;
|
||||
remaining = remaining.substr(cut);
|
||||
} else {
|
||||
remaining = "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single line handling (truncate if needed)
|
||||
if (ellipsis && textWidth > absW && absW > 0) {
|
||||
finalStr = renderer.truncatedText(fontId, finalStr.c_str(), absW);
|
||||
}
|
||||
lines.push_back(finalStr);
|
||||
}
|
||||
|
||||
// Draw lines
|
||||
int totalTextHeight = lines.size() * lineHeight;
|
||||
int startY = absY;
|
||||
|
||||
// Vertical centering
|
||||
if (absH > 0 && totalTextHeight < absH) {
|
||||
startY = absY + (absH - totalTextHeight) / 2;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < lines.size(); i++) {
|
||||
int lineWidth = renderer.getTextWidth(fontId, lines[i].c_str());
|
||||
int drawX = absX;
|
||||
|
||||
if (alignment == Alignment::Center && absW > 0) {
|
||||
drawX = absX + (absW - lineWidth) / 2;
|
||||
} else if (alignment == Alignment::Right && absW > 0) {
|
||||
drawX = absX + absW - lineWidth;
|
||||
}
|
||||
|
||||
renderer.drawText(fontId, drawX, startY + i * lineHeight, lines[i].c_str(), black);
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
|
||||
// --- BitmapElement ---
|
||||
void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
if (!isVisible(context)) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string path = context.evaluatestring(srcExpr);
|
||||
if (path.empty()) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.find('/') == std::string::npos || (path.length() > 0 && path[0] != '/')) {
|
||||
path = ThemeManager::get().getAssetPath(path);
|
||||
}
|
||||
|
||||
// Fast path: use cached 1-bit render
|
||||
const ProcessedAsset* processed = ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH);
|
||||
if (processed && processed->w == absW && processed->h == absH) {
|
||||
renderer.restoreRegion(processed->data.data(), absX, absY, absW, absH);
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to draw bitmap with centering and optional rounded corners
|
||||
auto drawBmp = [&](Bitmap& bmp) {
|
||||
int drawX = absX;
|
||||
int drawY = absY;
|
||||
if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2;
|
||||
if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2;
|
||||
if (borderRadius > 0) {
|
||||
renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius);
|
||||
} else {
|
||||
renderer.drawBitmap(bmp, drawX, drawY, absW, absH);
|
||||
}
|
||||
};
|
||||
|
||||
bool drawSuccess = false;
|
||||
|
||||
// Try RAM cache first
|
||||
const std::vector<uint8_t>* cachedData = ThemeManager::get().getCachedAsset(path);
|
||||
if (cachedData && !cachedData->empty()) {
|
||||
Bitmap bmp(cachedData->data(), cachedData->size());
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
drawBmp(bmp);
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: load from SD card
|
||||
if (!drawSuccess && path.length() > 0 && path[0] == '/') {
|
||||
FsFile file;
|
||||
if (SdMan.openFileForRead("HOME", path, file)) {
|
||||
size_t fileSize = file.size();
|
||||
if (fileSize > 0 && fileSize < 100000) {
|
||||
std::vector<uint8_t> fileData(fileSize);
|
||||
if (file.read(fileData.data(), fileSize) == fileSize) {
|
||||
ThemeManager::get().cacheAsset(path, std::move(fileData));
|
||||
const std::vector<uint8_t>* newCachedData = ThemeManager::get().getCachedAsset(path);
|
||||
if (newCachedData && !newCachedData->empty()) {
|
||||
Bitmap bmp(newCachedData->data(), newCachedData->size());
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
drawBmp(bmp);
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Bitmap bmp(file, true);
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
drawBmp(bmp);
|
||||
drawSuccess = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Cache rendered result for fast subsequent draws using captureRegion
|
||||
if (drawSuccess && absW * absH <= 40000) {
|
||||
size_t capturedSize = 0;
|
||||
uint8_t* captured = renderer.captureRegion(absX, absY, absW, absH, &capturedSize);
|
||||
if (captured && capturedSize > 0) {
|
||||
ProcessedAsset asset;
|
||||
asset.w = absW;
|
||||
asset.h = absH;
|
||||
asset.orientation = renderer.getOrientation();
|
||||
asset.data.assign(captured, captured + capturedSize);
|
||||
free(captured);
|
||||
ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH);
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
|
||||
// --- List ---
|
||||
void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
if (!isVisible(context)) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw background
|
||||
if (hasBg) {
|
||||
std::string colStr = context.evaluatestring(bgColorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
renderer.fillRect(absX, absY, absW, absH, color == 0x00);
|
||||
}
|
||||
if (border) {
|
||||
renderer.drawRect(absX, absY, absW, absH, true);
|
||||
}
|
||||
|
||||
if (!itemTemplate) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
int count = context.getInt(source + ".Count");
|
||||
if (count <= 0) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get item dimensions
|
||||
int itemW = getItemWidth();
|
||||
int itemH = getItemHeight();
|
||||
|
||||
// Pre-allocate string buffers to avoid repeated allocations
|
||||
std::string prefix;
|
||||
prefix.reserve(source.length() + 16);
|
||||
std::string key;
|
||||
key.reserve(source.length() + 32);
|
||||
char numBuf[12];
|
||||
|
||||
// Handle different layout modes
|
||||
if (direction == Direction::Horizontal || layoutMode == LayoutMode::Grid) {
|
||||
// Horizontal or Grid layout
|
||||
int col = 0;
|
||||
int row = 0;
|
||||
int currentX = absX;
|
||||
int currentY = absY;
|
||||
|
||||
// For grid, calculate item width based on columns only if not explicitly set
|
||||
if (layoutMode == LayoutMode::Grid && columns > 1 && itemWidth == 0) {
|
||||
int totalSpacing = (columns - 1) * spacing;
|
||||
itemW = (absW - totalSpacing) / columns;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
// Build prefix efficiently: "source.i."
|
||||
prefix.clear();
|
||||
prefix += source;
|
||||
prefix += '.';
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", i);
|
||||
prefix += numBuf;
|
||||
prefix += '.';
|
||||
|
||||
// Create item context with scoped variables
|
||||
ThemeContext itemContext(&context);
|
||||
|
||||
// Standard list item variables - include all properties for full flexibility
|
||||
std::string nameVal = context.getString(prefix + "Name");
|
||||
itemContext.setString("Item.Name", nameVal);
|
||||
itemContext.setString("Item.Title", context.getString(prefix + "Title"));
|
||||
itemContext.setString("Item.Value", context.getAnyAsString(prefix + "Value"));
|
||||
itemContext.setString("Item.Type", context.getString(prefix + "Type"));
|
||||
itemContext.setString("Item.ValueLabel", context.getString(prefix + "ValueLabel"));
|
||||
itemContext.setString("Item.BgColor", context.getString(prefix + "BgColor"));
|
||||
itemContext.setBool("Item.Selected", context.getBool(prefix + "Selected"));
|
||||
itemContext.setBool("Item.Value", context.getBool(prefix + "Value"));
|
||||
itemContext.setString("Item.Icon", context.getString(prefix + "Icon"));
|
||||
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
|
||||
itemContext.setString("Item.Progress", context.getString(prefix + "Progress"));
|
||||
|
||||
// Viewport check
|
||||
if (direction == Direction::Horizontal) {
|
||||
if (currentX + itemW < absX) {
|
||||
currentX += itemW + spacing;
|
||||
continue;
|
||||
}
|
||||
if (currentX > absX + absW) break;
|
||||
} else {
|
||||
// Grid mode
|
||||
if (currentY + itemH < absY) {
|
||||
col++;
|
||||
if (col >= columns) {
|
||||
col = 0;
|
||||
row++;
|
||||
currentY += itemH + spacing;
|
||||
}
|
||||
currentX = absX + col * (itemW + spacing);
|
||||
continue;
|
||||
}
|
||||
if (currentY > absY + absH) break;
|
||||
}
|
||||
itemContext.setInt("Item.Index", i);
|
||||
itemContext.setInt("Item.Count", count);
|
||||
// ValueIndex may not exist for all item types, so check first
|
||||
if (context.hasKey(prefix + "ValueIndex")) {
|
||||
itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex"));
|
||||
}
|
||||
|
||||
// Layout and draw
|
||||
itemTemplate->layout(itemContext, currentX, currentY, itemW, itemH);
|
||||
itemTemplate->draw(renderer, itemContext);
|
||||
|
||||
if (layoutMode == LayoutMode::Grid && columns > 1) {
|
||||
col++;
|
||||
if (col >= columns) {
|
||||
col = 0;
|
||||
row++;
|
||||
currentX = absX;
|
||||
currentY += itemH + spacing;
|
||||
} else {
|
||||
currentX += itemW + spacing;
|
||||
}
|
||||
} else {
|
||||
// Horizontal list
|
||||
currentX += itemW + spacing;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Vertical list (default)
|
||||
int currentY = absY;
|
||||
int viewportBottom = absY + absH;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
// Skip items above viewport
|
||||
if (currentY + itemH < absY) {
|
||||
currentY += itemH + spacing;
|
||||
continue;
|
||||
}
|
||||
// Stop if below viewport
|
||||
if (currentY > viewportBottom) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Build prefix efficiently: "source.i."
|
||||
prefix.clear();
|
||||
prefix += source;
|
||||
prefix += '.';
|
||||
snprintf(numBuf, sizeof(numBuf), "%d", i);
|
||||
prefix += numBuf;
|
||||
prefix += '.';
|
||||
|
||||
// Create item context with scoped variables
|
||||
ThemeContext itemContext(&context);
|
||||
|
||||
// Standard list item variables - include all properties for full flexibility
|
||||
std::string nameVal = context.getString(prefix + "Name");
|
||||
itemContext.setString("Item.Name", nameVal);
|
||||
itemContext.setString("Item.Title", context.getString(prefix + "Title"));
|
||||
itemContext.setString("Item.Value", context.getAnyAsString(prefix + "Value"));
|
||||
itemContext.setString("Item.Type", context.getString(prefix + "Type"));
|
||||
itemContext.setString("Item.ValueLabel", context.getString(prefix + "ValueLabel"));
|
||||
itemContext.setString("Item.BgColor", context.getString(prefix + "BgColor"));
|
||||
itemContext.setBool("Item.Selected", context.getBool(prefix + "Selected"));
|
||||
itemContext.setBool("Item.Value", context.getBool(prefix + "Value"));
|
||||
itemContext.setString("Item.Icon", context.getString(prefix + "Icon"));
|
||||
itemContext.setString("Item.Image", context.getString(prefix + "Image"));
|
||||
itemContext.setString("Item.Progress", context.getString(prefix + "Progress"));
|
||||
itemContext.setInt("Item.Index", i);
|
||||
itemContext.setInt("Item.Count", count);
|
||||
// ValueIndex may not exist for all item types, so check first
|
||||
if (context.hasKey(prefix + "ValueIndex")) {
|
||||
itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex"));
|
||||
}
|
||||
|
||||
// Layout and draw the template for this item
|
||||
itemTemplate->layout(itemContext, absX, currentY, absW, itemH);
|
||||
itemTemplate->draw(renderer, itemContext);
|
||||
|
||||
currentY += itemH + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
|
||||
} // namespace ThemeEngine
|
||||
99
lib/ThemeEngine/src/IniParser.cpp
Normal file
99
lib/ThemeEngine/src/IniParser.cpp
Normal file
@ -0,0 +1,99 @@
|
||||
#include "IniParser.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
void IniParser::trim(std::string& s) {
|
||||
if (s.empty()) return;
|
||||
|
||||
// Trim left
|
||||
size_t first = s.find_first_not_of(" \t\n\r");
|
||||
if (first == std::string::npos) {
|
||||
s.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Trim right
|
||||
size_t last = s.find_last_not_of(" \t\n\r");
|
||||
s = s.substr(first, (last - first + 1));
|
||||
}
|
||||
|
||||
std::map<std::string, std::map<std::string, std::string>> IniParser::parse(Stream& stream) {
|
||||
std::map<std::string, std::map<std::string, std::string>> sections;
|
||||
std::string currentSection;
|
||||
String line;
|
||||
|
||||
while (stream.available()) {
|
||||
line = stream.readStringUntil('\n');
|
||||
std::string sLine = line.c_str();
|
||||
trim(sLine);
|
||||
|
||||
if (sLine.empty() || sLine[0] == ';' || sLine[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sLine.front() == '[' && sLine.back() == ']') {
|
||||
currentSection = sLine.substr(1, sLine.size() - 2);
|
||||
trim(currentSection);
|
||||
} else {
|
||||
size_t eqPos = sLine.find('=');
|
||||
if (eqPos != std::string::npos) {
|
||||
std::string key = sLine.substr(0, eqPos);
|
||||
std::string value = sLine.substr(eqPos + 1);
|
||||
trim(key);
|
||||
trim(value);
|
||||
|
||||
// Remove quotes if present
|
||||
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
|
||||
value = value.substr(1, value.size() - 2);
|
||||
}
|
||||
|
||||
if (!currentSection.empty()) {
|
||||
sections[currentSection][key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
std::map<std::string, std::map<std::string, std::string>> IniParser::parseString(const std::string& content) {
|
||||
std::map<std::string, std::map<std::string, std::string>> sections;
|
||||
std::stringstream ss(content);
|
||||
std::string line;
|
||||
std::string currentSection;
|
||||
|
||||
while (std::getline(ss, line)) {
|
||||
trim(line);
|
||||
|
||||
if (line.empty() || line[0] == ';' || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.front() == '[' && line.back() == ']') {
|
||||
currentSection = line.substr(1, line.size() - 2);
|
||||
trim(currentSection);
|
||||
} else {
|
||||
size_t eqPos = line.find('=');
|
||||
if (eqPos != std::string::npos) {
|
||||
std::string key = line.substr(0, eqPos);
|
||||
std::string value = line.substr(eqPos + 1);
|
||||
trim(key);
|
||||
trim(value);
|
||||
|
||||
// Remove quotes if present
|
||||
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
|
||||
value = value.substr(1, value.size() - 2);
|
||||
}
|
||||
|
||||
if (!currentSection.empty()) {
|
||||
sections[currentSection][key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
} // namespace ThemeEngine
|
||||
194
lib/ThemeEngine/src/LayoutElements.cpp
Normal file
194
lib/ThemeEngine/src/LayoutElements.cpp
Normal file
@ -0,0 +1,194 @@
|
||||
#include "LayoutElements.h"
|
||||
|
||||
#include <Bitmap.h>
|
||||
|
||||
#include "ThemeManager.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
// Built-in icon drawing
|
||||
// These are simple geometric representations of common icons
|
||||
void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
|
||||
if (!isVisible(context)) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string iconName = context.evaluatestring(srcExpr);
|
||||
if (iconName.empty()) {
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string colStr = context.evaluatestring(colorExpr);
|
||||
uint8_t color = Color::parse(colStr).value;
|
||||
// Draw as black if color is dark (< 0x80), as white if light
|
||||
// This allows grayscale colors to render visibly
|
||||
bool black = (color < 0x80);
|
||||
|
||||
// iconSize determines the actual drawn icon size
|
||||
// absW/absH determine the bounding box for centering
|
||||
int drawSize = iconSize;
|
||||
int boundW = absW > 0 ? absW : iconSize;
|
||||
int boundH = absH > 0 ? absH : iconSize;
|
||||
|
||||
// Center the icon within its bounding box
|
||||
int iconX = absX + (boundW - drawSize) / 2;
|
||||
int iconY = absY + (boundH - drawSize) / 2;
|
||||
int w = drawSize;
|
||||
int h = drawSize;
|
||||
int cx = iconX + w / 2;
|
||||
int cy = iconY + h / 2;
|
||||
|
||||
// 1. Try to load as a theme asset (exact match or .bmp extension)
|
||||
std::string path = iconName;
|
||||
bool isPath = iconName.find('/') != std::string::npos || iconName.find('.') != std::string::npos;
|
||||
|
||||
std::string assetPath = path;
|
||||
if (!isPath) {
|
||||
assetPath = ThemeManager::get().getAssetPath(iconName + ".bmp");
|
||||
} else if (path[0] != '/') {
|
||||
assetPath = ThemeManager::get().getAssetPath(iconName);
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>* data = ThemeManager::get().getCachedAsset(assetPath);
|
||||
if (data && !data->empty()) {
|
||||
Bitmap bmp(data->data(), data->size());
|
||||
if (bmp.parseHeaders() == BmpReaderError::Ok) {
|
||||
renderer.drawTransparentBitmap(bmp, iconX, iconY, w, h);
|
||||
markClean();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Built-in icons (simple geometric shapes) as fallback
|
||||
// All icons use iconX, iconY, w, h, cx, cy for proper centering
|
||||
if (iconName == "heart" || iconName == "favorite") {
|
||||
// Simple heart shape approximation
|
||||
int s = w / 4;
|
||||
renderer.fillRect(cx - s, cy - s / 2, s * 2, s, black);
|
||||
renderer.fillRect(cx - s * 3 / 2, cy - s, s, s, black);
|
||||
renderer.fillRect(cx + s / 2, cy - s, s, s, black);
|
||||
// Bottom point
|
||||
for (int i = 0; i < s; i++) {
|
||||
renderer.drawLine(cx - s + i, cy + i, cx + s - i, cy + i, black);
|
||||
}
|
||||
} else if (iconName == "book" || iconName == "books") {
|
||||
// Book icon
|
||||
int bw = w * 2 / 3;
|
||||
int bh = h * 3 / 4;
|
||||
int bx = iconX + (w - bw) / 2;
|
||||
int by = iconY + (h - bh) / 2;
|
||||
renderer.drawRect(bx, by, bw, bh, black);
|
||||
renderer.drawLine(bx + bw / 3, by, bx + bw / 3, by + bh - 1, black);
|
||||
// Pages
|
||||
renderer.drawLine(bx + 2, by + bh / 4, bx + bw / 3 - 2, by + bh / 4, black);
|
||||
renderer.drawLine(bx + 2, by + bh / 2, bx + bw / 3 - 2, by + bh / 2, black);
|
||||
} else if (iconName == "folder" || iconName == "files") {
|
||||
// Folder icon
|
||||
int fw = w * 3 / 4;
|
||||
int fh = h * 2 / 3;
|
||||
int fx = iconX + (w - fw) / 2;
|
||||
int fy = iconY + (h - fh) / 2;
|
||||
// Tab
|
||||
renderer.fillRect(fx, fy, fw / 3, fh / 6, black);
|
||||
// Body
|
||||
renderer.drawRect(fx, fy + fh / 6, fw, fh - fh / 6, black);
|
||||
} else if (iconName == "settings" || iconName == "gear") {
|
||||
// Gear icon - simplified as circle with notches
|
||||
int r = w / 3;
|
||||
// Draw circle approximation
|
||||
renderer.drawRect(cx - r, cy - r, r * 2, r * 2, black);
|
||||
// Inner circle
|
||||
int ir = r / 2;
|
||||
renderer.drawRect(cx - ir, cy - ir, ir * 2, ir * 2, black);
|
||||
// Teeth
|
||||
int t = r / 3;
|
||||
renderer.fillRect(cx - t / 2, iconY, t, r - ir, black);
|
||||
renderer.fillRect(cx - t / 2, cy + r, t, r - ir, black);
|
||||
renderer.fillRect(iconX, cy - t / 2, r - ir, t, black);
|
||||
renderer.fillRect(cx + r, cy - t / 2, r - ir, t, black);
|
||||
} else if (iconName == "transfer" || iconName == "arrow" || iconName == "send") {
|
||||
// Arrow pointing right
|
||||
int aw = w / 2;
|
||||
int ah = h / 3;
|
||||
int ax = iconX + w / 4;
|
||||
int ay = cy - ah / 2;
|
||||
// Shaft
|
||||
renderer.fillRect(ax, ay, aw, ah, black);
|
||||
// Arrow head
|
||||
for (int i = 0; i < ah; i++) {
|
||||
renderer.drawLine(ax + aw, cy - ah + i, ax + aw + ah - i, cy, black);
|
||||
renderer.drawLine(ax + aw, cy + ah - i, ax + aw + ah - i, cy, black);
|
||||
}
|
||||
} else if (iconName == "library" || iconName == "device") {
|
||||
// Device/tablet icon
|
||||
int dw = w * 2 / 3;
|
||||
int dh = h * 3 / 4;
|
||||
int dx = iconX + (w - dw) / 2;
|
||||
int dy = iconY + (h - dh) / 2;
|
||||
renderer.drawRect(dx, dy, dw, dh, black);
|
||||
// Screen
|
||||
renderer.drawRect(dx + 2, dy + 2, dw - 4, dh - 8, black);
|
||||
// Home button
|
||||
renderer.fillRect(dx + dw / 2 - 2, dy + dh - 5, 4, 2, black);
|
||||
} else if (iconName == "battery") {
|
||||
// Battery icon
|
||||
int bw = w * 3 / 4;
|
||||
int bh = h / 2;
|
||||
int bx = iconX + (w - bw) / 2;
|
||||
int by = iconY + (h - bh) / 2;
|
||||
renderer.drawRect(bx, by, bw - 3, bh, black);
|
||||
renderer.fillRect(bx + bw - 3, by + bh / 4, 3, bh / 2, black);
|
||||
} else if (iconName == "check" || iconName == "checkmark") {
|
||||
// Checkmark - use iconX/iconY for proper centering
|
||||
int x1 = iconX + w / 4;
|
||||
int y1 = cy;
|
||||
int x2 = cx;
|
||||
int y2 = iconY + h * 3 / 4;
|
||||
int x3 = iconX + w * 3 / 4;
|
||||
int y3 = iconY + h / 4;
|
||||
renderer.drawLine(x1, y1, x2, y2, black);
|
||||
renderer.drawLine(x2, y2, x3, y3, black);
|
||||
// Thicken
|
||||
renderer.drawLine(x1, y1 + 1, x2, y2 + 1, black);
|
||||
renderer.drawLine(x2, y2 + 1, x3, y3 + 1, black);
|
||||
} else if (iconName == "back" || iconName == "left") {
|
||||
// Left arrow
|
||||
int s = w / 3;
|
||||
for (int i = 0; i < s; i++) {
|
||||
renderer.drawLine(cx - s + i, cy, cx, cy - s + i, black);
|
||||
renderer.drawLine(cx - s + i, cy, cx, cy + s - i, black);
|
||||
}
|
||||
} else if (iconName == "up") {
|
||||
// Up arrow
|
||||
int s = h / 3;
|
||||
for (int i = 0; i < s; i++) {
|
||||
renderer.drawLine(cx, cy - s + i, cx - s + i, cy, black);
|
||||
renderer.drawLine(cx, cy - s + i, cx + s - i, cy, black);
|
||||
}
|
||||
} else if (iconName == "down") {
|
||||
// Down arrow
|
||||
int s = h / 3;
|
||||
for (int i = 0; i < s; i++) {
|
||||
renderer.drawLine(cx, cy + s - i, cx - s + i, cy, black);
|
||||
renderer.drawLine(cx, cy + s - i, cx + s - i, cy, black);
|
||||
}
|
||||
} else if (iconName == "right") {
|
||||
// Right arrow
|
||||
int s = w / 3;
|
||||
for (int i = 0; i < s; i++) {
|
||||
renderer.drawLine(cx + s - i, cy, cx, cy - s + i, black);
|
||||
renderer.drawLine(cx + s - i, cy, cx, cy + s - i, black);
|
||||
}
|
||||
} else {
|
||||
// Unknown icon - draw placeholder
|
||||
renderer.drawRect(iconX, iconY, w, h, black);
|
||||
renderer.drawLine(iconX, iconY, iconX + w - 1, iconY + h - 1, black);
|
||||
renderer.drawLine(iconX + w - 1, iconY, iconX, iconY + h - 1, black);
|
||||
}
|
||||
|
||||
markClean();
|
||||
}
|
||||
|
||||
} // namespace ThemeEngine
|
||||
623
lib/ThemeEngine/src/ThemeManager.cpp
Normal file
623
lib/ThemeEngine/src/ThemeManager.cpp
Normal file
@ -0,0 +1,623 @@
|
||||
#include "ThemeManager.h"
|
||||
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "DefaultTheme.h"
|
||||
#include "LayoutElements.h"
|
||||
#include "ListElement.h"
|
||||
|
||||
namespace ThemeEngine {
|
||||
|
||||
void ThemeManager::begin() {}
|
||||
|
||||
void ThemeManager::registerFont(const std::string& name, int id) { fontMap[name] = id; }
|
||||
|
||||
std::string ThemeManager::getAssetPath(const std::string& assetName) {
|
||||
// Check if absolute path
|
||||
if (!assetName.empty() && assetName[0] == '/') return assetName;
|
||||
|
||||
// Otherwise relative to theme root
|
||||
std::string rootPath = "/themes/" + currentThemeName + "/" + assetName;
|
||||
if (SdMan.exists(rootPath.c_str())) return rootPath;
|
||||
|
||||
// Fallback to assets/ subfolder
|
||||
return "/themes/" + currentThemeName + "/assets/" + assetName;
|
||||
}
|
||||
|
||||
UIElement* ThemeManager::createElement(const std::string& id, const std::string& type) {
|
||||
// Basic elements
|
||||
if (type == "Container") return new Container(id);
|
||||
if (type == "Rectangle") return new Rectangle(id);
|
||||
if (type == "Label") return new Label(id);
|
||||
if (type == "Bitmap") return new BitmapElement(id);
|
||||
if (type == "List") return new List(id);
|
||||
if (type == "ProgressBar") return new ProgressBar(id);
|
||||
if (type == "Divider") return new Divider(id);
|
||||
|
||||
// Layout elements
|
||||
if (type == "HStack") return new HStack(id);
|
||||
if (type == "VStack") return new VStack(id);
|
||||
if (type == "Grid") return new Grid(id);
|
||||
|
||||
// Advanced elements
|
||||
if (type == "Badge") return new Badge(id);
|
||||
if (type == "Toggle") return new Toggle(id);
|
||||
if (type == "TabBar") return new TabBar(id);
|
||||
if (type == "Icon") return new Icon(id);
|
||||
if (type == "ScrollIndicator") return new ScrollIndicator(id);
|
||||
if (type == "BatteryIcon") return new BatteryIcon(id);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string, std::string>& props) {
|
||||
const auto elemType = elem->getType();
|
||||
|
||||
for (const auto& kv : props) {
|
||||
const std::string& key = kv.first;
|
||||
const std::string& val = kv.second;
|
||||
|
||||
// Common properties
|
||||
if (key == "X")
|
||||
elem->setX(Dimension::parse(val));
|
||||
else if (key == "Y")
|
||||
elem->setY(Dimension::parse(val));
|
||||
else if (key == "Width")
|
||||
elem->setWidth(Dimension::parse(val));
|
||||
else if (key == "Height")
|
||||
elem->setHeight(Dimension::parse(val));
|
||||
else if (key == "Visible")
|
||||
elem->setVisibleExpr(val);
|
||||
else if (key == "Cacheable")
|
||||
elem->setCacheable(val == "true" || val == "1");
|
||||
|
||||
// Rectangle properties
|
||||
else if (key == "Fill") {
|
||||
if (elemType == UIElement::ElementType::Rectangle) {
|
||||
auto rect = static_cast<Rectangle*>(elem);
|
||||
if (val.find('{') != std::string::npos) {
|
||||
rect->setFillExpr(val);
|
||||
} else {
|
||||
rect->setFill(val == "true" || val == "1");
|
||||
}
|
||||
}
|
||||
} else if (key == "Color") {
|
||||
if (elemType == UIElement::ElementType::Rectangle) {
|
||||
static_cast<Rectangle*>(elem)->setColorExpr(val);
|
||||
} else if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
|
||||
elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid ||
|
||||
elemType == UIElement::ElementType::TabBar) {
|
||||
static_cast<Container*>(elem)->setBackgroundColorExpr(val);
|
||||
} else if (elemType == UIElement::ElementType::Label) {
|
||||
static_cast<Label*>(elem)->setColorExpr(val);
|
||||
} else if (elemType == UIElement::ElementType::Divider) {
|
||||
static_cast<Divider*>(elem)->setColorExpr(val);
|
||||
} else if (elemType == UIElement::ElementType::Icon) {
|
||||
static_cast<Icon*>(elem)->setColorExpr(val);
|
||||
} else if (elemType == UIElement::ElementType::BatteryIcon) {
|
||||
static_cast<BatteryIcon*>(elem)->setColor(val);
|
||||
}
|
||||
}
|
||||
|
||||
// Container properties
|
||||
else if (key == "Border") {
|
||||
if (auto c = elem->asContainer()) {
|
||||
if (val.find('{') != std::string::npos) {
|
||||
c->setBorderExpr(val);
|
||||
} else {
|
||||
c->setBorder(val == "true" || val == "1" || val == "yes");
|
||||
}
|
||||
}
|
||||
} else if (key == "Padding") {
|
||||
if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
|
||||
elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
|
||||
static_cast<Container*>(elem)->setPadding(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::TabBar) {
|
||||
static_cast<TabBar*>(elem)->setPadding(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "BorderRadius") {
|
||||
if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
|
||||
elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
|
||||
static_cast<Container*>(elem)->setBorderRadius(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::Bitmap) {
|
||||
static_cast<BitmapElement*>(elem)->setBorderRadius(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::Rectangle) {
|
||||
static_cast<Rectangle*>(elem)->setBorderRadius(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::Badge) {
|
||||
static_cast<Badge*>(elem)->setBorderRadius(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setBorderRadius(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "Spacing") {
|
||||
if (elemType == UIElement::ElementType::HStack) {
|
||||
static_cast<HStack*>(elem)->setSpacing(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::VStack) {
|
||||
static_cast<VStack*>(elem)->setSpacing(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setSpacing(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "VAlign") {
|
||||
if (elemType == UIElement::ElementType::HStack) {
|
||||
static_cast<HStack*>(elem)->setVAlignFromString(val);
|
||||
}
|
||||
} else if (key == "HAlign") {
|
||||
if (elemType == UIElement::ElementType::VStack) {
|
||||
static_cast<VStack*>(elem)->setHAlignFromString(val);
|
||||
}
|
||||
}
|
||||
|
||||
// Label properties
|
||||
else if (key == "Text") {
|
||||
if (elemType == UIElement::ElementType::Label) {
|
||||
static_cast<Label*>(elem)->setText(val);
|
||||
} else if (elemType == UIElement::ElementType::Badge) {
|
||||
static_cast<Badge*>(elem)->setText(val);
|
||||
}
|
||||
} else if (key == "Font") {
|
||||
if (elemType == UIElement::ElementType::Label) {
|
||||
if (fontMap.count(val)) {
|
||||
static_cast<Label*>(elem)->setFont(fontMap[val]);
|
||||
}
|
||||
} else if (elemType == UIElement::ElementType::Badge) {
|
||||
if (fontMap.count(val)) {
|
||||
static_cast<Badge*>(elem)->setFont(fontMap[val]);
|
||||
}
|
||||
}
|
||||
} else if (key == "Centered") {
|
||||
if (elemType == UIElement::ElementType::Label) {
|
||||
static_cast<Label*>(elem)->setCentered(val == "true" || val == "1");
|
||||
}
|
||||
} else if (key == "Align") {
|
||||
if (elemType == UIElement::ElementType::Label) {
|
||||
Label::Alignment align = Label::Alignment::Left;
|
||||
if (val == "Center" || val == "center") align = Label::Alignment::Center;
|
||||
if (val == "Right" || val == "right") align = Label::Alignment::Right;
|
||||
static_cast<Label*>(elem)->setAlignment(align);
|
||||
}
|
||||
} else if (key == "MaxLines") {
|
||||
if (elemType == UIElement::ElementType::Label) {
|
||||
static_cast<Label*>(elem)->setMaxLines(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "Ellipsis") {
|
||||
if (elemType == UIElement::ElementType::Label) {
|
||||
static_cast<Label*>(elem)->setEllipsis(val == "true" || val == "1");
|
||||
}
|
||||
}
|
||||
|
||||
// Bitmap/Icon properties
|
||||
else if (key == "Src") {
|
||||
if (elemType == UIElement::ElementType::Bitmap) {
|
||||
auto b = static_cast<BitmapElement*>(elem);
|
||||
if (val.find('{') == std::string::npos && val.find('/') == std::string::npos) {
|
||||
b->setSrc(getAssetPath(val));
|
||||
} else {
|
||||
b->setSrc(val);
|
||||
}
|
||||
} else if (elemType == UIElement::ElementType::Icon) {
|
||||
static_cast<Icon*>(elem)->setSrc(val);
|
||||
}
|
||||
} else if (key == "ScaleToFit") {
|
||||
if (elemType == UIElement::ElementType::Bitmap) {
|
||||
static_cast<BitmapElement*>(elem)->setScaleToFit(val == "true" || val == "1");
|
||||
}
|
||||
} else if (key == "PreserveAspect") {
|
||||
if (elemType == UIElement::ElementType::Bitmap) {
|
||||
static_cast<BitmapElement*>(elem)->setPreserveAspect(val == "true" || val == "1");
|
||||
}
|
||||
} else if (key == "IconSize") {
|
||||
if (elemType == UIElement::ElementType::Icon) {
|
||||
static_cast<Icon*>(elem)->setIconSize(parseIntSafe(val));
|
||||
}
|
||||
}
|
||||
|
||||
// List properties
|
||||
else if (key == "Source") {
|
||||
if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setSource(val);
|
||||
}
|
||||
} else if (key == "ItemTemplate") {
|
||||
if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setItemTemplateId(val);
|
||||
}
|
||||
} else if (key == "ItemHeight") {
|
||||
if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setItemHeight(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "ItemWidth") {
|
||||
if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setItemWidth(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "Direction") {
|
||||
if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setDirectionFromString(val);
|
||||
}
|
||||
} else if (key == "Columns") {
|
||||
if (elemType == UIElement::ElementType::List) {
|
||||
static_cast<List*>(elem)->setColumns(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::Grid) {
|
||||
static_cast<Grid*>(elem)->setColumns(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "RowSpacing") {
|
||||
if (elemType == UIElement::ElementType::Grid) {
|
||||
static_cast<Grid*>(elem)->setRowSpacing(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "ColSpacing") {
|
||||
if (elemType == UIElement::ElementType::Grid) {
|
||||
static_cast<Grid*>(elem)->setColSpacing(parseIntSafe(val));
|
||||
}
|
||||
}
|
||||
|
||||
// ProgressBar properties
|
||||
else if (key == "Value") {
|
||||
if (elemType == UIElement::ElementType::ProgressBar) {
|
||||
static_cast<ProgressBar*>(elem)->setValue(val);
|
||||
} else if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setValue(val);
|
||||
} else if (elemType == UIElement::ElementType::BatteryIcon) {
|
||||
static_cast<BatteryIcon*>(elem)->setValue(val);
|
||||
}
|
||||
} else if (key == "Max") {
|
||||
if (elemType == UIElement::ElementType::ProgressBar) {
|
||||
static_cast<ProgressBar*>(elem)->setMax(val);
|
||||
}
|
||||
} else if (key == "FgColor") {
|
||||
if (elemType == UIElement::ElementType::ProgressBar) {
|
||||
static_cast<ProgressBar*>(elem)->setFgColor(val);
|
||||
} else if (elemType == UIElement::ElementType::Badge) {
|
||||
static_cast<Badge*>(elem)->setFgColor(val);
|
||||
}
|
||||
} else if (key == "BgColor") {
|
||||
if (elemType == UIElement::ElementType::ProgressBar) {
|
||||
static_cast<ProgressBar*>(elem)->setBgColor(val);
|
||||
} else if (elemType == UIElement::ElementType::Badge) {
|
||||
static_cast<Badge*>(elem)->setBgColor(val);
|
||||
} else if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
|
||||
elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
|
||||
static_cast<Container*>(elem)->setBackgroundColorExpr(val);
|
||||
}
|
||||
} else if (key == "ShowBorder") {
|
||||
if (elemType == UIElement::ElementType::ProgressBar) {
|
||||
static_cast<ProgressBar*>(elem)->setShowBorder(val == "true" || val == "1");
|
||||
}
|
||||
}
|
||||
|
||||
// Divider properties
|
||||
else if (key == "Horizontal") {
|
||||
if (elemType == UIElement::ElementType::Divider) {
|
||||
static_cast<Divider*>(elem)->setHorizontal(val == "true" || val == "1");
|
||||
}
|
||||
} else if (key == "Thickness") {
|
||||
if (elemType == UIElement::ElementType::Divider) {
|
||||
static_cast<Divider*>(elem)->setThickness(parseIntSafe(val));
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle properties
|
||||
else if (key == "OnColor") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setOnColor(val);
|
||||
}
|
||||
} else if (key == "OffColor") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setOffColor(val);
|
||||
}
|
||||
} else if (key == "KnobColor") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setKnobColor(val);
|
||||
}
|
||||
} else if (key == "TrackWidth") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setTrackWidth(parseIntSafe(val));
|
||||
} else if (elemType == UIElement::ElementType::ScrollIndicator) {
|
||||
static_cast<ScrollIndicator*>(elem)->setTrackWidth(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "TrackHeight") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setTrackHeight(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "KnobSize") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setKnobSize(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "KnobRadius") {
|
||||
if (elemType == UIElement::ElementType::Toggle) {
|
||||
static_cast<Toggle*>(elem)->setKnobRadius(parseIntSafe(val));
|
||||
}
|
||||
}
|
||||
|
||||
// TabBar properties
|
||||
else if (key == "Selected") {
|
||||
if (elemType == UIElement::ElementType::TabBar) {
|
||||
static_cast<TabBar*>(elem)->setSelected(val);
|
||||
}
|
||||
} else if (key == "TabSpacing") {
|
||||
if (elemType == UIElement::ElementType::TabBar) {
|
||||
static_cast<TabBar*>(elem)->setTabSpacing(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "IndicatorHeight") {
|
||||
if (elemType == UIElement::ElementType::TabBar) {
|
||||
static_cast<TabBar*>(elem)->setIndicatorHeight(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "ShowIndicator") {
|
||||
if (elemType == UIElement::ElementType::TabBar) {
|
||||
static_cast<TabBar*>(elem)->setShowIndicator(val == "true" || val == "1");
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollIndicator properties
|
||||
else if (key == "Position") {
|
||||
if (elemType == UIElement::ElementType::ScrollIndicator) {
|
||||
static_cast<ScrollIndicator*>(elem)->setPosition(val);
|
||||
}
|
||||
} else if (key == "Total") {
|
||||
if (elemType == UIElement::ElementType::ScrollIndicator) {
|
||||
static_cast<ScrollIndicator*>(elem)->setTotal(val);
|
||||
}
|
||||
} else if (key == "VisibleCount") {
|
||||
if (elemType == UIElement::ElementType::ScrollIndicator) {
|
||||
static_cast<ScrollIndicator*>(elem)->setVisibleCount(val);
|
||||
}
|
||||
}
|
||||
|
||||
// Badge properties
|
||||
else if (key == "PaddingH") {
|
||||
if (elemType == UIElement::ElementType::Badge) {
|
||||
static_cast<Badge*>(elem)->setPaddingH(parseIntSafe(val));
|
||||
}
|
||||
} else if (key == "PaddingV") {
|
||||
if (elemType == UIElement::ElementType::Badge) {
|
||||
static_cast<Badge*>(elem)->setPaddingV(parseIntSafe(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>* ThemeManager::getCachedAsset(const std::string& path) {
|
||||
if (assetCache.count(path)) {
|
||||
return &assetCache.at(path);
|
||||
}
|
||||
|
||||
if (!SdMan.exists(path.c_str())) return nullptr;
|
||||
|
||||
FsFile file;
|
||||
if (SdMan.openFileForRead("ThemeCache", path, file)) {
|
||||
size_t size = file.size();
|
||||
auto& buf = assetCache[path];
|
||||
buf.resize(size);
|
||||
file.read(buf.data(), size);
|
||||
file.close();
|
||||
return &buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ThemeManager::cacheAsset(const std::string& path, std::vector<uint8_t>&& data) {
|
||||
assetCache[path] = std::move(data);
|
||||
}
|
||||
|
||||
const ProcessedAsset* ThemeManager::getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
|
||||
int targetW, int targetH) {
|
||||
std::string cacheKey = path;
|
||||
if (targetW > 0 && targetH > 0) {
|
||||
cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH);
|
||||
}
|
||||
|
||||
if (processedCache.count(cacheKey)) {
|
||||
const auto& asset = processedCache.at(cacheKey);
|
||||
if (asset.orientation == orientation) {
|
||||
return &asset;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ThemeManager::cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, int targetW, int targetH) {
|
||||
std::string cacheKey = path;
|
||||
if (targetW > 0 && targetH > 0) {
|
||||
cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH);
|
||||
}
|
||||
processedCache[cacheKey] = asset;
|
||||
}
|
||||
|
||||
void ThemeManager::clearAssetCaches() {
|
||||
assetCache.clear();
|
||||
processedCache.clear();
|
||||
}
|
||||
|
||||
void ThemeManager::unloadTheme() {
|
||||
for (auto& kv : elements) {
|
||||
delete kv.second;
|
||||
}
|
||||
elements.clear();
|
||||
clearAssetCaches();
|
||||
invalidateAllCaches();
|
||||
}
|
||||
|
||||
void ThemeManager::invalidateAllCaches() {
|
||||
for (auto& kv : screenCaches) {
|
||||
kv.second.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void ThemeManager::invalidateScreenCache(const std::string& screenName) {
|
||||
if (screenCaches.count(screenName)) {
|
||||
screenCaches[screenName].invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ThemeManager::computeContextHash(const ThemeContext& context, const std::string& screenName) {
|
||||
uint32_t hash = 2166136261u;
|
||||
for (char c : screenName) {
|
||||
hash ^= static_cast<uint32_t>(c);
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void ThemeManager::loadTheme(const std::string& themeName) {
|
||||
unloadTheme();
|
||||
currentThemeName = themeName;
|
||||
|
||||
std::map<std::string, std::map<std::string, std::string>> sections;
|
||||
|
||||
if (themeName == "Default" || themeName.empty()) {
|
||||
std::string path = "/themes/Default/theme.ini";
|
||||
if (SdMan.exists(path.c_str())) {
|
||||
FsFile file;
|
||||
if (SdMan.openFileForRead("Theme", path, file)) {
|
||||
sections = IniParser::parse(file);
|
||||
file.close();
|
||||
}
|
||||
} else {
|
||||
sections = IniParser::parseString(getDefaultThemeIni());
|
||||
}
|
||||
currentThemeName = "Default";
|
||||
} else {
|
||||
std::string path = "/themes/" + themeName + "/theme.ini";
|
||||
|
||||
if (!SdMan.exists(path.c_str())) {
|
||||
sections = IniParser::parseString(getDefaultThemeIni());
|
||||
currentThemeName = "Default";
|
||||
} else {
|
||||
FsFile file;
|
||||
if (SdMan.openFileForRead("Theme", path, file)) {
|
||||
sections = IniParser::parse(file);
|
||||
file.close();
|
||||
} else {
|
||||
sections = IniParser::parseString(getDefaultThemeIni());
|
||||
currentThemeName = "Default";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read theme configuration from [Global] section
|
||||
navBookCount = 1;
|
||||
if (sections.count("Global")) {
|
||||
const auto& global = sections.at("Global");
|
||||
if (global.count("NavBookCount")) {
|
||||
navBookCount = parseIntSafe(global.at("NavBookCount"));
|
||||
if (navBookCount < 1) navBookCount = 1;
|
||||
if (navBookCount > 10) navBookCount = 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1: Create elements
|
||||
for (const auto& sec : sections) {
|
||||
const std::string& id = sec.first;
|
||||
const std::map<std::string, std::string>& props = sec.second;
|
||||
|
||||
if (id == "Global") continue;
|
||||
|
||||
auto it = props.find("Type");
|
||||
if (it == props.end()) continue;
|
||||
|
||||
const std::string& type = it->second;
|
||||
if (type.empty()) continue;
|
||||
|
||||
UIElement* elem = createElement(id, type);
|
||||
if (elem) {
|
||||
elements[id] = elem;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: Apply properties and wire parent relationships
|
||||
std::vector<List*> lists;
|
||||
for (const auto& sec : sections) {
|
||||
const std::string& id = sec.first;
|
||||
if (id == "Global") continue;
|
||||
if (elements.find(id) == elements.end()) continue;
|
||||
|
||||
UIElement* elem = elements[id];
|
||||
applyProperties(elem, sec.second);
|
||||
|
||||
if (elem->getType() == UIElement::ElementType::List) {
|
||||
lists.push_back(static_cast<List*>(elem));
|
||||
}
|
||||
|
||||
// Wire parent relationship (fallback if Children not specified)
|
||||
if (sec.second.count("Parent")) {
|
||||
const std::string& parentId = sec.second.at("Parent");
|
||||
if (elements.count(parentId)) {
|
||||
UIElement* parent = elements[parentId];
|
||||
if (auto c = parent->asContainer()) {
|
||||
const auto& children = c->getChildren();
|
||||
if (std::find(children.begin(), children.end(), elem) == children.end()) {
|
||||
c->addChild(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Children property - explicit ordering
|
||||
if (sec.second.count("Children")) {
|
||||
if (auto c = elem->asContainer()) {
|
||||
c->clearChildren();
|
||||
|
||||
std::string s = sec.second.at("Children");
|
||||
size_t pos = 0;
|
||||
|
||||
auto processChild = [&](const std::string& childName) {
|
||||
std::string childId = childName;
|
||||
size_t start = childId.find_first_not_of(" ");
|
||||
size_t end = childId.find_last_not_of(" ");
|
||||
if (start == std::string::npos) return;
|
||||
childId = childId.substr(start, end - start + 1);
|
||||
|
||||
if (elements.count(childId)) {
|
||||
c->addChild(elements[childId]);
|
||||
}
|
||||
};
|
||||
|
||||
while ((pos = s.find(',')) != std::string::npos) {
|
||||
processChild(s.substr(0, pos));
|
||||
s.erase(0, pos + 1);
|
||||
}
|
||||
processChild(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: Resolve list templates
|
||||
for (auto* l : lists) {
|
||||
l->resolveTemplate(elements);
|
||||
}
|
||||
}
|
||||
|
||||
void ThemeManager::renderScreen(const std::string& screenName, const GfxRenderer& renderer,
|
||||
const ThemeContext& context) {
|
||||
if (elements.count(screenName) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIElement* root = elements[screenName];
|
||||
root->layout(context, 0, 0, renderer.getScreenWidth(), renderer.getScreenHeight());
|
||||
root->draw(renderer, context);
|
||||
}
|
||||
|
||||
void ThemeManager::renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer,
|
||||
const ThemeContext& context, const ThemeContext* prevContext) {
|
||||
if (elements.count(screenName) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIElement* root = elements[screenName];
|
||||
|
||||
// Layout uses internal caching - will skip if params unchanged
|
||||
root->layout(context, 0, 0, renderer.getScreenWidth(), renderer.getScreenHeight());
|
||||
|
||||
// If no previous context provided, do full draw
|
||||
if (!prevContext) {
|
||||
root->draw(renderer, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw elements - dirty tracking is handled internally by each element
|
||||
root->draw(renderer, context);
|
||||
}
|
||||
|
||||
} // namespace ThemeEngine
|
||||
@ -7,7 +7,6 @@
|
||||
|
||||
#include "Xtc.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
@ -87,6 +86,15 @@ std::string Xtc::getTitle() const {
|
||||
return filepath.substr(lastSlash, lastDot - lastSlash);
|
||||
}
|
||||
|
||||
std::string Xtc::getAuthor() const {
|
||||
if (!loaded || !parser) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try to get author from XTC metadata
|
||||
return parser->getAuthor();
|
||||
}
|
||||
|
||||
bool Xtc::hasChapters() const {
|
||||
if (!loaded || !parser) {
|
||||
return false;
|
||||
|
||||
@ -56,6 +56,7 @@ class Xtc {
|
||||
|
||||
// Metadata
|
||||
std::string getTitle() const;
|
||||
std::string getAuthor() const;
|
||||
bool hasChapters() const;
|
||||
const std::vector<xtc::ChapterInfo>& getChapters() const;
|
||||
|
||||
|
||||
@ -47,8 +47,21 @@ XtcError XtcParser::open(const char* filepath) {
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
// Read title if available
|
||||
readTitle();
|
||||
// Read title & author if available
|
||||
if (m_header.hasMetadata) {
|
||||
m_lastError = readTitle();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
m_lastError = readAuthor();
|
||||
if (m_lastError != XtcError::OK) {
|
||||
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
||||
m_file.close();
|
||||
return m_lastError;
|
||||
}
|
||||
}
|
||||
|
||||
// Read page table
|
||||
m_lastError = readPageTable();
|
||||
@ -124,24 +137,34 @@ XtcError XtcParser::readHeader() {
|
||||
}
|
||||
|
||||
XtcError XtcParser::readTitle() {
|
||||
// Title is usually at offset 0x38 (56) for 88-byte headers
|
||||
// Read title as null-terminated UTF-8 string
|
||||
if (m_header.titleOffset == 0) {
|
||||
m_header.titleOffset = 0x38; // Default offset
|
||||
}
|
||||
|
||||
if (!m_file.seek(m_header.titleOffset)) {
|
||||
constexpr auto titleOffset = 0x38;
|
||||
if (!m_file.seek(titleOffset)) {
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
char titleBuf[128] = {0};
|
||||
m_file.read(reinterpret_cast<uint8_t*>(titleBuf), sizeof(titleBuf) - 1);
|
||||
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
||||
m_title = titleBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
XtcError XtcParser::readAuthor() {
|
||||
// Read author as null-terminated UTF-8 string with max length 64, directly following title
|
||||
constexpr auto authorOffset = 0xB8;
|
||||
if (!m_file.seek(authorOffset)) {
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
char authorBuf[64] = {0};
|
||||
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
||||
m_author = authorBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
XtcError XtcParser::readPageTable() {
|
||||
if (m_header.pageTableOffset == 0) {
|
||||
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
||||
|
||||
@ -67,8 +67,9 @@ class XtcParser {
|
||||
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
|
||||
size_t chunkSize = 1024);
|
||||
|
||||
// Get title from metadata
|
||||
// Get title/author from metadata
|
||||
std::string getTitle() const { return m_title; }
|
||||
std::string getAuthor() const { return m_author; }
|
||||
|
||||
bool hasChapters() const { return m_hasChapters; }
|
||||
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
|
||||
@ -86,6 +87,7 @@ class XtcParser {
|
||||
std::vector<PageInfo> m_pageTable;
|
||||
std::vector<ChapterInfo> m_chapters;
|
||||
std::string m_title;
|
||||
std::string m_author;
|
||||
uint16_t m_defaultWidth;
|
||||
uint16_t m_defaultHeight;
|
||||
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
|
||||
@ -96,6 +98,7 @@ class XtcParser {
|
||||
XtcError readHeader();
|
||||
XtcError readPageTable();
|
||||
XtcError readTitle();
|
||||
XtcError readAuthor();
|
||||
XtcError readChapters();
|
||||
};
|
||||
|
||||
|
||||
@ -38,14 +38,16 @@ struct XtcHeader {
|
||||
uint8_t versionMajor; // 0x04: Format version major (typically 1) (together with minor = 1.0)
|
||||
uint8_t versionMinor; // 0x05: Format version minor (typically 0)
|
||||
uint16_t pageCount; // 0x06: Total page count
|
||||
uint32_t flags; // 0x08: Flags/reserved
|
||||
uint32_t headerSize; // 0x0C: Size of header section (typically 88)
|
||||
uint32_t reserved1; // 0x10: Reserved
|
||||
uint32_t tocOffset; // 0x14: TOC offset (0 if unused) - 4 bytes, not 8!
|
||||
uint8_t readDirection; // 0x08: Reading direction (0-2)
|
||||
uint8_t hasMetadata; // 0x09: Has metadata (0-1)
|
||||
uint8_t hasThumbnails; // 0x0A: Has thumbnails (0-1)
|
||||
uint8_t hasChapters; // 0x0B: Has chapters (0-1)
|
||||
uint32_t currentPage; // 0x0C: Current page (1-based) (0-65535)
|
||||
uint64_t metadataOffset; // 0x10: Metadata offset (0 if unused)
|
||||
uint64_t pageTableOffset; // 0x18: Page table offset
|
||||
uint64_t dataOffset; // 0x20: First page data offset
|
||||
uint64_t reserved2; // 0x28: Reserved
|
||||
uint32_t titleOffset; // 0x30: Title string offset
|
||||
uint64_t thumbOffset; // 0x28: Thumbnail offset
|
||||
uint32_t chapterOffset; // 0x30: Chapter data offset
|
||||
uint32_t padding; // 0x34: Padding to 56 bytes
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
#include <SDCardManager.h>
|
||||
#include <miniz.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
|
||||
// Setup inflator
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
@ -74,6 +76,10 @@ bool ZipFile::loadAllFileStatSlims() {
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
// Set cursor to start of central directory for sequential access
|
||||
lastCentralDirPos = zipDetails.centralDirOffset;
|
||||
lastCentralDirPosValid = true;
|
||||
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@ -102,15 +108,35 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
// Phase 1: Try scanning from cursor position first
|
||||
uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset;
|
||||
uint32_t wrapPos = zipDetails.centralDirOffset;
|
||||
bool wrapped = false;
|
||||
bool found = false;
|
||||
|
||||
file.seek(startPos);
|
||||
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
bool found = false;
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) break; // End of list
|
||||
while (true) {
|
||||
uint32_t entryStart = file.position();
|
||||
|
||||
if (file.read(&sig, 4) != 4 || sig != 0x02014b50) {
|
||||
// End of central directory
|
||||
if (!wrapped && lastCentralDirPosValid && startPos != zipDetails.centralDirOffset) {
|
||||
// Wrap around to beginning
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
wrapped = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we've wrapped and reached our start position, stop
|
||||
if (wrapped && entryStart >= startPos) {
|
||||
break;
|
||||
}
|
||||
|
||||
file.seekCur(6);
|
||||
file.read(&fileStat->method, 2);
|
||||
@ -123,15 +149,25 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
file.read(&fileStat->localHeaderOffset, 4);
|
||||
|
||||
if (nameLen < 256) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
if (strcmp(itemName, filename) == 0) {
|
||||
// Found it! Update cursor to next entry
|
||||
file.seekCur(m + k);
|
||||
lastCentralDirPos = file.position();
|
||||
lastCentralDirPosValid = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Name too long, skip it
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
// Skip the rest of this entry (extra field + comment)
|
||||
// Skip extra field + comment
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
@ -253,6 +289,8 @@ bool ZipFile::close() {
|
||||
if (file) {
|
||||
file.close();
|
||||
}
|
||||
lastCentralDirPos = 0;
|
||||
lastCentralDirPosValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -266,6 +304,80 @@ bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes) {
|
||||
if (targets.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const bool wasOpen = isOpen();
|
||||
if (!wasOpen && !open()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!loadZipDetails()) {
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
|
||||
int matched = 0;
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) break;
|
||||
|
||||
file.seekCur(6);
|
||||
uint16_t method;
|
||||
file.read(&method, 2);
|
||||
file.seekCur(8);
|
||||
uint32_t compressedSize, uncompressedSize;
|
||||
file.read(&compressedSize, 4);
|
||||
file.read(&uncompressedSize, 4);
|
||||
uint16_t nameLen, m, k;
|
||||
file.read(&nameLen, 2);
|
||||
file.read(&m, 2);
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
uint32_t localHeaderOffset;
|
||||
file.read(&localHeaderOffset, 4);
|
||||
|
||||
if (nameLen < 256) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
uint64_t hash = fnvHash64(itemName, nameLen);
|
||||
SizeTarget key = {hash, nameLen, 0};
|
||||
|
||||
auto it = std::lower_bound(targets.begin(), targets.end(), key, [](const SizeTarget& a, const SizeTarget& b) {
|
||||
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||
});
|
||||
|
||||
while (it != targets.end() && it->hash == hash && it->len == nameLen) {
|
||||
if (it->index < sizes.size()) {
|
||||
sizes[it->index] = uncompressedSize;
|
||||
matched++;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) {
|
||||
const bool wasOpen = isOpen();
|
||||
if (!wasOpen && !open()) {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ZipFile {
|
||||
public:
|
||||
@ -19,12 +20,33 @@ class ZipFile {
|
||||
bool isSet;
|
||||
};
|
||||
|
||||
// Target for batch uncompressed size lookup (sorted by hash, then len)
|
||||
struct SizeTarget {
|
||||
uint64_t hash; // FNV-1a 64-bit hash of normalized path
|
||||
uint16_t len; // Length of path for collision reduction
|
||||
uint16_t index; // Caller's index (e.g. spine index)
|
||||
};
|
||||
|
||||
// FNV-1a 64-bit hash computed from char buffer (no std::string allocation)
|
||||
static uint64_t fnvHash64(const char* s, size_t len) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
hash ^= static_cast<uint8_t>(s[i]);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string& filePath;
|
||||
FsFile file;
|
||||
ZipDetails zipDetails = {0, 0, false};
|
||||
std::unordered_map<std::string, FileStatSlim> fileStatSlimCache;
|
||||
|
||||
// Cursor for sequential central-dir scanning optimization
|
||||
uint32_t lastCentralDirPos = 0;
|
||||
bool lastCentralDirPosValid = false;
|
||||
|
||||
bool loadFileStatSlim(const char* filename, FileStatSlim* fileStat);
|
||||
long getDataOffset(const FileStatSlim& fileStat);
|
||||
bool loadZipDetails();
|
||||
@ -39,6 +61,10 @@ class ZipFile {
|
||||
bool close();
|
||||
bool loadAllFileStatSlims();
|
||||
bool getInflatedFileSize(const char* filename, size_t* size);
|
||||
// Batch lookup: scan ZIP central dir once and fill sizes for matching targets.
|
||||
// targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize.
|
||||
// Returns number of targets matched.
|
||||
int fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes);
|
||||
// Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple
|
||||
// These functions will open and close the zip as needed
|
||||
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
|
||||
|
||||
51
lib/hal/HalDisplay.cpp
Normal file
51
lib/hal/HalDisplay.cpp
Normal file
@ -0,0 +1,51 @@
|
||||
#include <HalDisplay.h>
|
||||
#include <HalGPIO.h>
|
||||
|
||||
#define SD_SPI_MISO 7
|
||||
|
||||
HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {}
|
||||
|
||||
HalDisplay::~HalDisplay() {}
|
||||
|
||||
void HalDisplay::begin() { einkDisplay.begin(); }
|
||||
|
||||
void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); }
|
||||
|
||||
void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
bool fromProgmem) const {
|
||||
einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem);
|
||||
}
|
||||
|
||||
EInkDisplay::RefreshMode convertRefreshMode(HalDisplay::RefreshMode mode) {
|
||||
switch (mode) {
|
||||
case HalDisplay::FULL_REFRESH:
|
||||
return EInkDisplay::FULL_REFRESH;
|
||||
case HalDisplay::HALF_REFRESH:
|
||||
return EInkDisplay::HALF_REFRESH;
|
||||
case HalDisplay::FAST_REFRESH:
|
||||
default:
|
||||
return EInkDisplay::FAST_REFRESH;
|
||||
}
|
||||
}
|
||||
|
||||
void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode) { einkDisplay.displayBuffer(convertRefreshMode(mode)); }
|
||||
|
||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
|
||||
|
||||
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
||||
|
||||
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
|
||||
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
|
||||
}
|
||||
|
||||
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); }
|
||||
|
||||
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); }
|
||||
|
||||
void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); }
|
||||
|
||||
void HalDisplay::displayGrayBuffer() { einkDisplay.displayGrayBuffer(); }
|
||||
52
lib/hal/HalDisplay.h
Normal file
52
lib/hal/HalDisplay.h
Normal file
@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <EInkDisplay.h>
|
||||
|
||||
class HalDisplay {
|
||||
public:
|
||||
// Constructor with pin configuration
|
||||
HalDisplay();
|
||||
|
||||
// Destructor
|
||||
~HalDisplay();
|
||||
|
||||
// Refresh modes
|
||||
enum RefreshMode {
|
||||
FULL_REFRESH, // Full refresh with complete waveform
|
||||
HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed
|
||||
FAST_REFRESH // Fast refresh using custom LUT
|
||||
};
|
||||
|
||||
// Initialize the display hardware and driver
|
||||
void begin();
|
||||
|
||||
// Display dimensions
|
||||
static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH;
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT;
|
||||
static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
|
||||
static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
|
||||
|
||||
// Frame buffer operations
|
||||
void clearScreen(uint8_t color = 0xFF) const;
|
||||
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
bool fromProgmem = false) const;
|
||||
|
||||
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH);
|
||||
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// Power management
|
||||
void deepSleep();
|
||||
|
||||
// Access to frame buffer
|
||||
uint8_t* getFrameBuffer() const;
|
||||
|
||||
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
||||
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
|
||||
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
|
||||
void cleanupGrayscaleBuffers(const uint8_t* bwBuffer);
|
||||
|
||||
void displayGrayBuffer();
|
||||
|
||||
private:
|
||||
EInkDisplay einkDisplay;
|
||||
};
|
||||
55
lib/hal/HalGPIO.cpp
Normal file
55
lib/hal/HalGPIO.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
#include <HalGPIO.h>
|
||||
#include <SPI.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
void HalGPIO::begin() {
|
||||
inputMgr.begin();
|
||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
}
|
||||
|
||||
void HalGPIO::update() { inputMgr.update(); }
|
||||
|
||||
bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); }
|
||||
|
||||
bool HalGPIO::wasPressed(uint8_t buttonIndex) const { return inputMgr.wasPressed(buttonIndex); }
|
||||
|
||||
bool HalGPIO::wasAnyPressed() const { return inputMgr.wasAnyPressed(); }
|
||||
|
||||
bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleased(buttonIndex); }
|
||||
|
||||
bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
||||
|
||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||
|
||||
void HalGPIO::startDeepSleep() {
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// 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();
|
||||
}
|
||||
// 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 {
|
||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||
return digitalRead(UART0_RXD) == HIGH;
|
||||
}
|
||||
|
||||
bool HalGPIO::isWakeupByPowerButton() const {
|
||||
const auto wakeupCause = esp_sleep_get_wakeup_cause();
|
||||
const auto resetReason = esp_reset_reason();
|
||||
if (isUsbConnected()) {
|
||||
return wakeupCause == ESP_SLEEP_WAKEUP_GPIO;
|
||||
} else {
|
||||
return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON);
|
||||
}
|
||||
}
|
||||
61
lib/hal/HalGPIO.h
Normal file
61
lib/hal/HalGPIO.h
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <BatteryMonitor.h>
|
||||
#include <InputManager.h>
|
||||
|
||||
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
||||
#define EPD_SCLK 8 // SPI Clock
|
||||
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
|
||||
#define EPD_CS 21 // Chip Select
|
||||
#define EPD_DC 4 // Data/Command
|
||||
#define EPD_RST 5 // Reset
|
||||
#define EPD_BUSY 6 // Busy
|
||||
|
||||
#define SPI_MISO 7 // SPI MISO, shared between SD card and display (Master In Slave Out)
|
||||
|
||||
#define BAT_GPIO0 0 // Battery voltage
|
||||
|
||||
#define UART0_RXD 20 // Used for USB connection detection
|
||||
|
||||
class HalGPIO {
|
||||
#if CROSSPOINT_EMULATED == 0
|
||||
InputManager inputMgr;
|
||||
#endif
|
||||
|
||||
public:
|
||||
HalGPIO() = default;
|
||||
|
||||
// Start button GPIO and setup SPI for screen and SD card
|
||||
void begin();
|
||||
|
||||
// Button input methods
|
||||
void update();
|
||||
bool isPressed(uint8_t buttonIndex) const;
|
||||
bool wasPressed(uint8_t buttonIndex) const;
|
||||
bool wasAnyPressed() const;
|
||||
bool wasReleased(uint8_t buttonIndex) const;
|
||||
bool wasAnyReleased() 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
|
||||
bool isUsbConnected() const;
|
||||
|
||||
// Check if wakeup was caused by power button press
|
||||
bool isWakeupByPowerButton() const;
|
||||
|
||||
// Button indices
|
||||
static constexpr uint8_t BTN_BACK = 0;
|
||||
static constexpr uint8_t BTN_CONFIRM = 1;
|
||||
static constexpr uint8_t BTN_LEFT = 2;
|
||||
static constexpr uint8_t BTN_RIGHT = 3;
|
||||
static constexpr uint8_t BTN_UP = 4;
|
||||
static constexpr uint8_t BTN_DOWN = 5;
|
||||
static constexpr uint8_t BTN_POWER = 6;
|
||||
};
|
||||
@ -2,7 +2,7 @@
|
||||
default_envs = default
|
||||
|
||||
[crosspoint]
|
||||
version = 0.15.0
|
||||
version = 0.16.0
|
||||
|
||||
[base]
|
||||
platform = espressif32 @ 6.12.0
|
||||
|
||||
@ -11,10 +11,18 @@
|
||||
// Initialize the static instance
|
||||
CrossPointSettings CrossPointSettings::instance;
|
||||
|
||||
void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
|
||||
uint8_t tempValue;
|
||||
serialization::readPod(file, tempValue);
|
||||
if (tempValue < maxValue) {
|
||||
member = tempValue;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||
// Increment this when adding new persisted settings fields
|
||||
constexpr uint8_t SETTINGS_COUNT = 20;
|
||||
constexpr uint8_t SETTINGS_COUNT = 24; // 23 upstream + themeName
|
||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||
} // namespace
|
||||
|
||||
@ -49,6 +57,11 @@ bool CrossPointSettings::saveToFile() const {
|
||||
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::writeString(outputFile, std::string(themeName));
|
||||
// New fields added at end for backward compatibility
|
||||
outputFile.close();
|
||||
|
||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||
@ -75,35 +88,35 @@ bool CrossPointSettings::loadFromFile() {
|
||||
// load settings that exist (support older files with fewer fields)
|
||||
uint8_t settingsRead = 0;
|
||||
do {
|
||||
serialization::readPod(inputFile, sleepScreen);
|
||||
readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, extraParagraphSpacing);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, shortPwrBtn);
|
||||
readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, statusBar);
|
||||
readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, orientation);
|
||||
readAndValidate(inputFile, orientation, ORIENTATION_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, frontButtonLayout);
|
||||
readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, sideButtonLayout);
|
||||
readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, fontFamily);
|
||||
readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, fontSize);
|
||||
readAndValidate(inputFile, fontSize, FONT_SIZE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, lineSpacing);
|
||||
readAndValidate(inputFile, lineSpacing, LINE_COMPRESSION_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, paragraphAlignment);
|
||||
readAndValidate(inputFile, paragraphAlignment, PARAGRAPH_ALIGNMENT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, sleepTimeout);
|
||||
readAndValidate(inputFile, sleepTimeout, SLEEP_TIMEOUT_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, refreshFrequency);
|
||||
readAndValidate(inputFile, refreshFrequency, REFRESH_FREQUENCY_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, screenMargin);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, sleepScreenCoverMode);
|
||||
readAndValidate(inputFile, sleepScreenCoverMode, SLEEP_SCREEN_COVER_MODE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string urlStr;
|
||||
@ -114,12 +127,36 @@ bool CrossPointSettings::loadFromFile() {
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, textAntiAliasing);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, hideBatteryPercentage);
|
||||
readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, longPressChapterSkip);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
serialization::readPod(inputFile, hyphenationEnabled);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string usernameStr;
|
||||
serialization::readString(inputFile, usernameStr);
|
||||
strncpy(opdsUsername, usernameStr.c_str(), sizeof(opdsUsername) - 1);
|
||||
opdsUsername[sizeof(opdsUsername) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string passwordStr;
|
||||
serialization::readString(inputFile, passwordStr);
|
||||
strncpy(opdsPassword, passwordStr.c_str(), sizeof(opdsPassword) - 1);
|
||||
opdsPassword[sizeof(opdsPassword) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
{
|
||||
std::string themeStr;
|
||||
serialization::readString(inputFile, themeStr);
|
||||
strncpy(themeName, themeStr.c_str(), sizeof(themeName) - 1);
|
||||
themeName[sizeof(themeName) - 1] = '\0';
|
||||
}
|
||||
if (++settingsRead >= fileSettingsCount) break;
|
||||
// New fields added at end for backward compatibility
|
||||
} while (false);
|
||||
|
||||
inputFile.close();
|
||||
|
||||
@ -15,18 +15,31 @@ class CrossPointSettings {
|
||||
CrossPointSettings(const CrossPointSettings&) = delete;
|
||||
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
|
||||
|
||||
// Should match with SettingsActivity text
|
||||
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 };
|
||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 };
|
||||
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4, SLEEP_SCREEN_MODE_COUNT };
|
||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
|
||||
enum SLEEP_SCREEN_COVER_FILTER {
|
||||
NO_FILTER = 0,
|
||||
BLACK_AND_WHITE = 1,
|
||||
INVERTED_BLACK_AND_WHITE = 2,
|
||||
SLEEP_SCREEN_COVER_FILTER_COUNT
|
||||
};
|
||||
|
||||
// Status bar display type enum
|
||||
enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 };
|
||||
enum STATUS_BAR_MODE {
|
||||
NONE = 0,
|
||||
NO_PROGRESS = 1,
|
||||
FULL = 2,
|
||||
FULL_WITH_PROGRESS_BAR = 3,
|
||||
ONLY_PROGRESS_BAR = 4,
|
||||
STATUS_BAR_MODE_COUNT
|
||||
};
|
||||
|
||||
enum ORIENTATION {
|
||||
PORTRAIT = 0, // 480x800 logical coordinates (current default)
|
||||
LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
||||
INVERTED = 2, // 480x800 logical coordinates, inverted
|
||||
LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation
|
||||
LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation
|
||||
ORIENTATION_COUNT
|
||||
};
|
||||
|
||||
// Front button layout options
|
||||
@ -36,37 +49,60 @@ class CrossPointSettings {
|
||||
BACK_CONFIRM_LEFT_RIGHT = 0,
|
||||
LEFT_RIGHT_BACK_CONFIRM = 1,
|
||||
LEFT_BACK_CONFIRM_RIGHT = 2,
|
||||
BACK_CONFIRM_RIGHT_LEFT = 3
|
||||
BACK_CONFIRM_RIGHT_LEFT = 3,
|
||||
FRONT_BUTTON_LAYOUT_COUNT
|
||||
};
|
||||
|
||||
// Side button layout options
|
||||
// Default: Previous, Next
|
||||
// Swapped: Next, Previous
|
||||
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
|
||||
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT };
|
||||
|
||||
// Font family options
|
||||
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 };
|
||||
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };
|
||||
// Font size options
|
||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
||||
enum PARAGRAPH_ALIGNMENT { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3 };
|
||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT };
|
||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT };
|
||||
enum PARAGRAPH_ALIGNMENT {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
RIGHT_ALIGN = 3,
|
||||
PARAGRAPH_ALIGNMENT_COUNT
|
||||
};
|
||||
|
||||
// Auto-sleep timeout options (in minutes)
|
||||
enum SLEEP_TIMEOUT { SLEEP_1_MIN = 0, SLEEP_5_MIN = 1, SLEEP_10_MIN = 2, SLEEP_15_MIN = 3, SLEEP_30_MIN = 4 };
|
||||
enum SLEEP_TIMEOUT {
|
||||
SLEEP_1_MIN = 0,
|
||||
SLEEP_5_MIN = 1,
|
||||
SLEEP_10_MIN = 2,
|
||||
SLEEP_15_MIN = 3,
|
||||
SLEEP_30_MIN = 4,
|
||||
SLEEP_TIMEOUT_COUNT
|
||||
};
|
||||
|
||||
// E-ink refresh frequency (pages between full refreshes)
|
||||
enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 };
|
||||
enum REFRESH_FREQUENCY {
|
||||
REFRESH_1 = 0,
|
||||
REFRESH_5 = 1,
|
||||
REFRESH_10 = 2,
|
||||
REFRESH_15 = 3,
|
||||
REFRESH_30 = 4,
|
||||
REFRESH_FREQUENCY_COUNT
|
||||
};
|
||||
|
||||
// Short power button press actions
|
||||
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 };
|
||||
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT };
|
||||
|
||||
// Hide battery percentage
|
||||
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 };
|
||||
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
|
||||
|
||||
// Sleep screen settings
|
||||
uint8_t sleepScreen = DARK;
|
||||
// Sleep screen cover mode settings
|
||||
uint8_t sleepScreenCoverMode = FIT;
|
||||
// Sleep screen cover filter
|
||||
uint8_t sleepScreenCoverFilter = NO_FILTER;
|
||||
// Status bar settings
|
||||
uint8_t statusBar = FULL;
|
||||
// Text rendering settings
|
||||
@ -95,10 +131,14 @@ class CrossPointSettings {
|
||||
uint8_t screenMargin = 5;
|
||||
// OPDS browser settings
|
||||
char opdsServerUrl[128] = "";
|
||||
char opdsUsername[64] = "";
|
||||
char opdsPassword[64] = "";
|
||||
// Hide battery percentage
|
||||
uint8_t hideBatteryPercentage = HIDE_NEVER;
|
||||
// Long-press chapter skip on side buttons
|
||||
uint8_t longPressChapterSkip = 1;
|
||||
// Theme name (theme-engine addition)
|
||||
char themeName[64] = "Default";
|
||||
|
||||
~CrossPointSettings() = default;
|
||||
|
||||
|
||||
@ -2,103 +2,77 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
|
||||
decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button button) const {
|
||||
namespace {
|
||||
using ButtonIndex = uint8_t;
|
||||
|
||||
struct FrontLayoutMap {
|
||||
ButtonIndex back;
|
||||
ButtonIndex confirm;
|
||||
ButtonIndex left;
|
||||
ButtonIndex right;
|
||||
};
|
||||
|
||||
struct SideLayoutMap {
|
||||
ButtonIndex pageBack;
|
||||
ButtonIndex pageForward;
|
||||
};
|
||||
|
||||
// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT.
|
||||
constexpr FrontLayoutMap kFrontLayouts[] = {
|
||||
{HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT},
|
||||
{HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM},
|
||||
{HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT},
|
||||
{HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT},
|
||||
};
|
||||
|
||||
// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
|
||||
constexpr SideLayoutMap kSideLayouts[] = {
|
||||
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
|
||||
{HalGPIO::BTN_DOWN, HalGPIO::BTN_UP},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
|
||||
const auto frontLayout = static_cast<CrossPointSettings::FRONT_BUTTON_LAYOUT>(SETTINGS.frontButtonLayout);
|
||||
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
|
||||
const auto& front = kFrontLayouts[frontLayout];
|
||||
const auto& side = kSideLayouts[sideLayout];
|
||||
|
||||
switch (button) {
|
||||
case Button::Back:
|
||||
switch (frontLayout) {
|
||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
||||
return InputManager::BTN_LEFT;
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
return InputManager::BTN_CONFIRM;
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
/* fall through */
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
/* fall through */
|
||||
default:
|
||||
return InputManager::BTN_BACK;
|
||||
}
|
||||
return (gpio.*fn)(front.back);
|
||||
case Button::Confirm:
|
||||
switch (frontLayout) {
|
||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
||||
return InputManager::BTN_RIGHT;
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
return InputManager::BTN_LEFT;
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
/* fall through */
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
/* fall through */
|
||||
default:
|
||||
return InputManager::BTN_CONFIRM;
|
||||
}
|
||||
return (gpio.*fn)(front.confirm);
|
||||
case Button::Left:
|
||||
switch (frontLayout) {
|
||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
||||
/* fall through */
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
return InputManager::BTN_BACK;
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
return InputManager::BTN_RIGHT;
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
/* fall through */
|
||||
default:
|
||||
return InputManager::BTN_LEFT;
|
||||
}
|
||||
return (gpio.*fn)(front.left);
|
||||
case Button::Right:
|
||||
switch (frontLayout) {
|
||||
case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM:
|
||||
return InputManager::BTN_CONFIRM;
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
return InputManager::BTN_LEFT;
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
/* fall through */
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
/* fall through */
|
||||
default:
|
||||
return InputManager::BTN_RIGHT;
|
||||
}
|
||||
return (gpio.*fn)(front.right);
|
||||
case Button::Up:
|
||||
return InputManager::BTN_UP;
|
||||
return (gpio.*fn)(HalGPIO::BTN_UP);
|
||||
case Button::Down:
|
||||
return InputManager::BTN_DOWN;
|
||||
return (gpio.*fn)(HalGPIO::BTN_DOWN);
|
||||
case Button::Power:
|
||||
return InputManager::BTN_POWER;
|
||||
return (gpio.*fn)(HalGPIO::BTN_POWER);
|
||||
case Button::PageBack:
|
||||
switch (sideLayout) {
|
||||
case CrossPointSettings::NEXT_PREV:
|
||||
return InputManager::BTN_DOWN;
|
||||
case CrossPointSettings::PREV_NEXT:
|
||||
/* fall through */
|
||||
default:
|
||||
return InputManager::BTN_UP;
|
||||
}
|
||||
return (gpio.*fn)(side.pageBack);
|
||||
case Button::PageForward:
|
||||
switch (sideLayout) {
|
||||
case CrossPointSettings::NEXT_PREV:
|
||||
return InputManager::BTN_UP;
|
||||
case CrossPointSettings::PREV_NEXT:
|
||||
/* fall through */
|
||||
default:
|
||||
return InputManager::BTN_DOWN;
|
||||
}
|
||||
return (gpio.*fn)(side.pageForward);
|
||||
}
|
||||
|
||||
return InputManager::BTN_BACK;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MappedInputManager::wasPressed(const Button button) const { return inputManager.wasPressed(mapButton(button)); }
|
||||
bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
|
||||
|
||||
bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); }
|
||||
bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
|
||||
|
||||
bool MappedInputManager::isPressed(const Button button) const { return inputManager.isPressed(mapButton(button)); }
|
||||
bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
|
||||
|
||||
bool MappedInputManager::wasAnyPressed() const { return inputManager.wasAnyPressed(); }
|
||||
bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); }
|
||||
|
||||
bool MappedInputManager::wasAnyReleased() const { return inputManager.wasAnyReleased(); }
|
||||
bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); }
|
||||
|
||||
unsigned long MappedInputManager::getHeldTime() const { return inputManager.getHeldTime(); }
|
||||
unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); }
|
||||
|
||||
MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
|
||||
const char* next) const {
|
||||
@ -109,6 +83,8 @@ MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const
|
||||
return {previous, next, back, confirm};
|
||||
case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT:
|
||||
return {previous, back, confirm, next};
|
||||
case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT:
|
||||
return {back, confirm, next, previous};
|
||||
case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT:
|
||||
default:
|
||||
return {back, confirm, previous, next};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <InputManager.h>
|
||||
#include <HalGPIO.h>
|
||||
|
||||
class MappedInputManager {
|
||||
public:
|
||||
@ -13,7 +13,7 @@ class MappedInputManager {
|
||||
const char* btn4;
|
||||
};
|
||||
|
||||
explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {}
|
||||
explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {}
|
||||
|
||||
bool wasPressed(Button button) const;
|
||||
bool wasReleased(Button button) const;
|
||||
@ -24,6 +24,7 @@ class MappedInputManager {
|
||||
Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const;
|
||||
|
||||
private:
|
||||
InputManager& inputManager;
|
||||
decltype(InputManager::BTN_BACK) mapButton(Button button) const;
|
||||
HalGPIO& gpio;
|
||||
|
||||
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user