initial version of custom fonts
This commit is contained in:
parent
16caa66b4a
commit
ac1251282b
@ -33,23 +33,10 @@
|
|||||||
#include <builtinFonts/notosans_18_bolditalic.h>
|
#include <builtinFonts/notosans_18_bolditalic.h>
|
||||||
#include <builtinFonts/notosans_18_italic.h>
|
#include <builtinFonts/notosans_18_italic.h>
|
||||||
#include <builtinFonts/notosans_18_regular.h>
|
#include <builtinFonts/notosans_18_regular.h>
|
||||||
#include <builtinFonts/opendyslexic_10_bold.h>
|
|
||||||
#include <builtinFonts/opendyslexic_10_bolditalic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_10_italic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_10_regular.h>
|
|
||||||
#include <builtinFonts/opendyslexic_12_bold.h>
|
|
||||||
#include <builtinFonts/opendyslexic_12_bolditalic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_12_italic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_12_regular.h>
|
|
||||||
#include <builtinFonts/opendyslexic_14_bold.h>
|
|
||||||
#include <builtinFonts/opendyslexic_14_bolditalic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_14_italic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_14_regular.h>
|
|
||||||
#include <builtinFonts/opendyslexic_8_bold.h>
|
|
||||||
#include <builtinFonts/opendyslexic_8_bolditalic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_8_italic.h>
|
|
||||||
#include <builtinFonts/opendyslexic_8_regular.h>
|
|
||||||
#include <builtinFonts/ubuntu_10_bold.h>
|
#include <builtinFonts/ubuntu_10_bold.h>
|
||||||
#include <builtinFonts/ubuntu_10_regular.h>
|
#include <builtinFonts/ubuntu_10_regular.h>
|
||||||
#include <builtinFonts/ubuntu_12_bold.h>
|
#include <builtinFonts/ubuntu_12_bold.h>
|
||||||
#include <builtinFonts/ubuntu_12_regular.h>
|
#include <builtinFonts/ubuntu_12_regular.h>
|
||||||
|
|
||||||
|
// Custom fonts registry (generated by convert-builtin-fonts.sh)
|
||||||
|
#include <builtinFonts/custom/customFonts.h>
|
||||||
|
|||||||
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Bold.ttf
Normal file
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Italic.ttf
Normal file
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Italic.ttf
Normal file
Binary file not shown.
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Regular.ttf
Normal file
BIN
lib/EpdFont/builtinFonts/custom/FernMicro/FernMicro-Regular.ttf
Normal file
Binary file not shown.
62
lib/EpdFont/builtinFonts/custom/customFonts.h
Normal file
62
lib/EpdFont/builtinFonts/custom/customFonts.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Generated by convert-builtin-fonts.sh
|
||||||
|
* Registry of available custom fonts
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EpdFont.h>
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
#define CUSTOM_FONT_COUNT 1
|
||||||
|
|
||||||
|
static const char* CUSTOM_FONT_NAMES[] = {
|
||||||
|
"FernMicro"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include all custom font headers
|
||||||
|
#include <builtinFonts/custom/fernmicro_12_regular.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_12_italic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_12_bold.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_12_bolditalic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_14_regular.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_14_italic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_14_bold.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_14_bolditalic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_16_regular.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_16_italic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_16_bold.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_16_bolditalic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_18_regular.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_18_italic.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_18_bold.h>
|
||||||
|
#include <builtinFonts/custom/fernmicro_18_bolditalic.h>
|
||||||
|
|
||||||
|
// Extern EpdFont declarations for custom fonts
|
||||||
|
extern EpdFont fernmicro12RegularFont;
|
||||||
|
extern EpdFont fernmicro12ItalicFont;
|
||||||
|
extern EpdFont fernmicro12BoldFont;
|
||||||
|
extern EpdFont fernmicro12BoldItalicFont;
|
||||||
|
extern EpdFont fernmicro14RegularFont;
|
||||||
|
extern EpdFont fernmicro14ItalicFont;
|
||||||
|
extern EpdFont fernmicro14BoldFont;
|
||||||
|
extern EpdFont fernmicro14BoldItalicFont;
|
||||||
|
extern EpdFont fernmicro16RegularFont;
|
||||||
|
extern EpdFont fernmicro16ItalicFont;
|
||||||
|
extern EpdFont fernmicro16BoldFont;
|
||||||
|
extern EpdFont fernmicro16BoldItalicFont;
|
||||||
|
extern EpdFont fernmicro18RegularFont;
|
||||||
|
extern EpdFont fernmicro18ItalicFont;
|
||||||
|
extern EpdFont fernmicro18BoldFont;
|
||||||
|
extern EpdFont fernmicro18BoldItalicFont;
|
||||||
|
|
||||||
|
// Extern EpdFontFamily declarations for custom fonts
|
||||||
|
extern EpdFontFamily fernmicro12FontFamily;
|
||||||
|
extern EpdFontFamily fernmicro14FontFamily;
|
||||||
|
extern EpdFontFamily fernmicro16FontFamily;
|
||||||
|
extern EpdFontFamily fernmicro18FontFamily;
|
||||||
|
|
||||||
|
// Function to register all custom fonts with the renderer
|
||||||
|
void registerCustomFonts(GfxRenderer& renderer);
|
||||||
|
|
||||||
2174
lib/EpdFont/builtinFonts/custom/fernmicro_12_bold.h
Normal file
2174
lib/EpdFont/builtinFonts/custom/fernmicro_12_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
2144
lib/EpdFont/builtinFonts/custom/fernmicro_12_bolditalic.h
Normal file
2144
lib/EpdFont/builtinFonts/custom/fernmicro_12_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
1918
lib/EpdFont/builtinFonts/custom/fernmicro_12_italic.h
Normal file
1918
lib/EpdFont/builtinFonts/custom/fernmicro_12_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
1932
lib/EpdFont/builtinFonts/custom/fernmicro_12_regular.h
Normal file
1932
lib/EpdFont/builtinFonts/custom/fernmicro_12_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
2746
lib/EpdFont/builtinFonts/custom/fernmicro_14_bold.h
Normal file
2746
lib/EpdFont/builtinFonts/custom/fernmicro_14_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
2677
lib/EpdFont/builtinFonts/custom/fernmicro_14_bolditalic.h
Normal file
2677
lib/EpdFont/builtinFonts/custom/fernmicro_14_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
2405
lib/EpdFont/builtinFonts/custom/fernmicro_14_italic.h
Normal file
2405
lib/EpdFont/builtinFonts/custom/fernmicro_14_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
2474
lib/EpdFont/builtinFonts/custom/fernmicro_14_regular.h
Normal file
2474
lib/EpdFont/builtinFonts/custom/fernmicro_14_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
3393
lib/EpdFont/builtinFonts/custom/fernmicro_16_bold.h
Normal file
3393
lib/EpdFont/builtinFonts/custom/fernmicro_16_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
3332
lib/EpdFont/builtinFonts/custom/fernmicro_16_bolditalic.h
Normal file
3332
lib/EpdFont/builtinFonts/custom/fernmicro_16_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
3001
lib/EpdFont/builtinFonts/custom/fernmicro_16_italic.h
Normal file
3001
lib/EpdFont/builtinFonts/custom/fernmicro_16_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
3074
lib/EpdFont/builtinFonts/custom/fernmicro_16_regular.h
Normal file
3074
lib/EpdFont/builtinFonts/custom/fernmicro_16_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
4121
lib/EpdFont/builtinFonts/custom/fernmicro_18_bold.h
Normal file
4121
lib/EpdFont/builtinFonts/custom/fernmicro_18_bold.h
Normal file
File diff suppressed because it is too large
Load Diff
4051
lib/EpdFont/builtinFonts/custom/fernmicro_18_bolditalic.h
Normal file
4051
lib/EpdFont/builtinFonts/custom/fernmicro_18_bolditalic.h
Normal file
File diff suppressed because it is too large
Load Diff
3648
lib/EpdFont/builtinFonts/custom/fernmicro_18_italic.h
Normal file
3648
lib/EpdFont/builtinFonts/custom/fernmicro_18_italic.h
Normal file
File diff suppressed because it is too large
Load Diff
3706
lib/EpdFont/builtinFonts/custom/fernmicro_18_regular.h
Normal file
3706
lib/EpdFont/builtinFonts/custom/fernmicro_18_regular.h
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -135,3 +135,56 @@ ruby -rdigest -e 'puts [
|
|||||||
"./notosans_8_regular.h",
|
"./notosans_8_regular.h",
|
||||||
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||||
))"
|
))"
|
||||||
|
|
||||||
|
# Custom font sizes (must match convert-builtin-fonts.sh)
|
||||||
|
CUSTOM_FONT_SIZES=(12 14 16 18)
|
||||||
|
|
||||||
|
# Generate font IDs for all custom fonts
|
||||||
|
echo ""
|
||||||
|
echo "// Custom font IDs"
|
||||||
|
|
||||||
|
CUSTOM_DIR="./custom"
|
||||||
|
declare -a CUSTOM_FONT_NAMES=()
|
||||||
|
declare -a CUSTOM_FONT_UPPERCASE=()
|
||||||
|
|
||||||
|
if [ -d "$CUSTOM_DIR" ]; then
|
||||||
|
for font_dir in "$CUSTOM_DIR"/*/; do
|
||||||
|
if [ -d "$font_dir" ]; then
|
||||||
|
font_folder_name=$(basename "$font_dir")
|
||||||
|
font_name_lower=$(echo "$font_folder_name" | tr '[:upper:]' '[:lower:]')
|
||||||
|
font_name_upper=$(echo "$font_folder_name" | tr '[:lower:]' '[:upper:]')
|
||||||
|
|
||||||
|
# Check if font headers exist (Regular is required)
|
||||||
|
if [ -f "./custom/${font_name_lower}_12_regular.h" ]; then
|
||||||
|
CUSTOM_FONT_NAMES+=("$font_name_lower")
|
||||||
|
CUSTOM_FONT_UPPERCASE+=("$font_name_upper")
|
||||||
|
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
font_id=$(ruby -rdigest -e "puts [
|
||||||
|
\"./custom/${font_name_lower}_${size}_regular.h\",
|
||||||
|
\"./custom/${font_name_lower}_${size}_bold.h\",
|
||||||
|
\"./custom/${font_name_lower}_${size}_bolditalic.h\",
|
||||||
|
\"./custom/${font_name_lower}_${size}_italic.h\",
|
||||||
|
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)")
|
||||||
|
echo "#define ${font_name_upper}_${size}_FONT_ID ($font_id)"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate CUSTOM_FONT_IDS lookup array
|
||||||
|
echo ""
|
||||||
|
echo "// Custom font ID lookup array: CUSTOM_FONT_IDS[fontIndex][sizeIndex]"
|
||||||
|
echo "// Size indices: 0=12pt, 1=14pt, 2=16pt, 3=18pt"
|
||||||
|
|
||||||
|
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||||
|
echo "static const int CUSTOM_FONT_IDS[][4] = {"
|
||||||
|
for i in "${!CUSTOM_FONT_UPPERCASE[@]}"; do
|
||||||
|
font_upper="${CUSTOM_FONT_UPPERCASE[$i]}"
|
||||||
|
echo " {${font_upper}_12_FONT_ID, ${font_upper}_14_FONT_ID, ${font_upper}_16_FONT_ID, ${font_upper}_18_FONT_ID},"
|
||||||
|
done
|
||||||
|
echo "};"
|
||||||
|
else
|
||||||
|
echo "static const int CUSTOM_FONT_IDS[][4] = {};"
|
||||||
|
fi
|
||||||
|
|||||||
@ -9,6 +9,9 @@ BOOKERLY_FONT_SIZES=(12 14 16 18)
|
|||||||
NOTOSANS_FONT_SIZES=(12 14 16 18)
|
NOTOSANS_FONT_SIZES=(12 14 16 18)
|
||||||
OPENDYSLEXIC_FONT_SIZES=(8 10 12 14)
|
OPENDYSLEXIC_FONT_SIZES=(8 10 12 14)
|
||||||
|
|
||||||
|
# Custom font sizes - modify this array to change sizes for user-provided fonts
|
||||||
|
CUSTOM_FONT_SIZES=(12 14 16 18)
|
||||||
|
|
||||||
for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
for size in ${BOOKERLY_FONT_SIZES[@]}; do
|
||||||
for style in ${READER_FONT_STYLES[@]}; do
|
for style in ${READER_FONT_STYLES[@]}; do
|
||||||
font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
font_name="bookerly_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||||
@ -53,3 +56,209 @@ for size in ${UI_FONT_SIZES[@]}; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
python fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf > ../builtinFonts/notosans_8_regular.h
|
python fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf > ../builtinFonts/notosans_8_regular.h
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Custom Fonts Processing
|
||||||
|
# ============================================================================
|
||||||
|
# Process all custom fonts in the custom/ folder
|
||||||
|
# Each subfolder should contain TTF files named: FontName-Regular.ttf, FontName-Bold.ttf, etc.
|
||||||
|
|
||||||
|
CUSTOM_DIR="../builtinFonts/custom"
|
||||||
|
CUSTOM_HEADER="../builtinFonts/custom/customFonts.h"
|
||||||
|
|
||||||
|
# Collect custom font names
|
||||||
|
declare -a CUSTOM_FONT_NAMES=()
|
||||||
|
declare -a CUSTOM_FONT_LOWERCASE=()
|
||||||
|
|
||||||
|
if [ -d "$CUSTOM_DIR" ]; then
|
||||||
|
for font_dir in "$CUSTOM_DIR"/*/; do
|
||||||
|
if [ -d "$font_dir" ]; then
|
||||||
|
# Get the font folder name (e.g., "FernMicro")
|
||||||
|
font_folder_name=$(basename "$font_dir")
|
||||||
|
font_name_lower=$(echo "$font_folder_name" | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
# Check if Regular font exists (required)
|
||||||
|
regular_font=$(find "$font_dir" -maxdepth 1 -iname "*-Regular.ttf" -o -iname "*-Regular.otf" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$regular_font" ]; then
|
||||||
|
echo "Warning: Skipping $font_folder_name - no Regular font found"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
CUSTOM_FONT_NAMES+=("$font_folder_name")
|
||||||
|
CUSTOM_FONT_LOWERCASE+=("$font_name_lower")
|
||||||
|
|
||||||
|
echo "Processing custom font: $font_folder_name"
|
||||||
|
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
for style in ${READER_FONT_STYLES[@]}; do
|
||||||
|
style_lower=$(echo $style | tr '[:upper:]' '[:lower:]')
|
||||||
|
output_name="${font_name_lower}_${size}_${style_lower}"
|
||||||
|
output_path="../builtinFonts/custom/${output_name}.h"
|
||||||
|
|
||||||
|
# Find the font file for this style (try TTF then OTF)
|
||||||
|
font_file=$(find "$font_dir" -maxdepth 1 -iname "*-${style}.ttf" -o -iname "*-${style}.otf" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$font_file" ]; then
|
||||||
|
python fontconvert.py "$output_name" "$size" "$font_file" --2bit > "$output_path"
|
||||||
|
echo "Generated $output_path"
|
||||||
|
else
|
||||||
|
# If style not found, use Regular as fallback
|
||||||
|
echo "Note: $font_folder_name-${style} not found, using Regular"
|
||||||
|
python fontconvert.py "$output_name" "$size" "$regular_font" --2bit > "$output_path"
|
||||||
|
echo "Generated $output_path (fallback from Regular)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate customFonts.h registry (header with extern declarations)
|
||||||
|
echo "Generating customFonts.h registry..."
|
||||||
|
|
||||||
|
CUSTOM_CPP="../../../src/customFonts.cpp"
|
||||||
|
|
||||||
|
cat > "$CUSTOM_HEADER" << 'HEADER_START'
|
||||||
|
/**
|
||||||
|
* Generated by convert-builtin-fonts.sh
|
||||||
|
* Registry of available custom fonts
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <EpdFont.h>
|
||||||
|
#include <EpdFontFamily.h>
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
HEADER_START
|
||||||
|
|
||||||
|
# Write the count
|
||||||
|
echo "#define CUSTOM_FONT_COUNT ${#CUSTOM_FONT_NAMES[@]}" >> "$CUSTOM_HEADER"
|
||||||
|
echo "" >> "$CUSTOM_HEADER"
|
||||||
|
|
||||||
|
# Write font names array
|
||||||
|
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||||
|
echo "static const char* CUSTOM_FONT_NAMES[] = {" >> "$CUSTOM_HEADER"
|
||||||
|
for i in "${!CUSTOM_FONT_NAMES[@]}"; do
|
||||||
|
if [ $i -lt $((${#CUSTOM_FONT_NAMES[@]} - 1)) ]; then
|
||||||
|
echo " \"${CUSTOM_FONT_NAMES[$i]}\"," >> "$CUSTOM_HEADER"
|
||||||
|
else
|
||||||
|
echo " \"${CUSTOM_FONT_NAMES[$i]}\"" >> "$CUSTOM_HEADER"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "};" >> "$CUSTOM_HEADER"
|
||||||
|
else
|
||||||
|
echo "static const char* CUSTOM_FONT_NAMES[] = {};" >> "$CUSTOM_HEADER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> "$CUSTOM_HEADER"
|
||||||
|
|
||||||
|
# Include all generated headers in the header file
|
||||||
|
echo "// Include all custom font headers" >> "$CUSTOM_HEADER"
|
||||||
|
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
for style in ${READER_FONT_STYLES[@]}; do
|
||||||
|
style_lower=$(echo $style | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "#include <builtinFonts/custom/${font_name_lower}_${size}_${style_lower}.h>" >> "$CUSTOM_HEADER"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> "$CUSTOM_HEADER"
|
||||||
|
|
||||||
|
# Generate extern declarations for EpdFont and EpdFontFamily in header
|
||||||
|
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||||
|
echo "// Extern EpdFont declarations for custom fonts" >> "$CUSTOM_HEADER"
|
||||||
|
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
for style in ${READER_FONT_STYLES[@]}; do
|
||||||
|
var_name="${font_name_lower}${size}${style}Font"
|
||||||
|
echo "extern EpdFont ${var_name};" >> "$CUSTOM_HEADER"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> "$CUSTOM_HEADER"
|
||||||
|
echo "// Extern EpdFontFamily declarations for custom fonts" >> "$CUSTOM_HEADER"
|
||||||
|
|
||||||
|
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
family_name="${font_name_lower}${size}FontFamily"
|
||||||
|
echo "extern EpdFontFamily ${family_name};" >> "$CUSTOM_HEADER"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> "$CUSTOM_HEADER"
|
||||||
|
|
||||||
|
# Function declaration in header
|
||||||
|
echo "// Function to register all custom fonts with the renderer" >> "$CUSTOM_HEADER"
|
||||||
|
echo "void registerCustomFonts(GfxRenderer& renderer);" >> "$CUSTOM_HEADER"
|
||||||
|
echo "" >> "$CUSTOM_HEADER"
|
||||||
|
|
||||||
|
# Generate the .cpp file with actual definitions
|
||||||
|
cat > "$CUSTOM_CPP" << 'CPP_START'
|
||||||
|
/**
|
||||||
|
* Generated by convert-builtin-fonts.sh
|
||||||
|
* Custom font definitions
|
||||||
|
*/
|
||||||
|
#include <builtinFonts/custom/customFonts.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
CPP_START
|
||||||
|
|
||||||
|
# Generate EpdFont definitions in .cpp
|
||||||
|
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||||
|
echo "// EpdFont definitions for custom fonts" >> "$CUSTOM_CPP"
|
||||||
|
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
for style in ${READER_FONT_STYLES[@]}; do
|
||||||
|
style_lower=$(echo $style | tr '[:upper:]' '[:lower:]')
|
||||||
|
var_name="${font_name_lower}${size}${style}Font"
|
||||||
|
data_name="${font_name_lower}_${size}_${style_lower}"
|
||||||
|
echo "EpdFont ${var_name}(&${data_name});" >> "$CUSTOM_CPP"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> "$CUSTOM_CPP"
|
||||||
|
echo "// EpdFontFamily definitions for custom fonts" >> "$CUSTOM_CPP"
|
||||||
|
|
||||||
|
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
family_name="${font_name_lower}${size}FontFamily"
|
||||||
|
regular="${font_name_lower}${size}RegularFont"
|
||||||
|
bold="${font_name_lower}${size}BoldFont"
|
||||||
|
italic="${font_name_lower}${size}ItalicFont"
|
||||||
|
bolditalic="${font_name_lower}${size}BoldItalicFont"
|
||||||
|
echo "EpdFontFamily ${family_name}(&${regular}, &${bold}, &${italic}, &${bolditalic});" >> "$CUSTOM_CPP"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> "$CUSTOM_CPP"
|
||||||
|
|
||||||
|
# Generate registerCustomFonts function in .cpp
|
||||||
|
echo "void registerCustomFonts(GfxRenderer& renderer) {" >> "$CUSTOM_CPP"
|
||||||
|
|
||||||
|
if [ ${#CUSTOM_FONT_NAMES[@]} -gt 0 ]; then
|
||||||
|
echo "#if CUSTOM_FONT_COUNT > 0" >> "$CUSTOM_CPP"
|
||||||
|
for font_name_lower in "${CUSTOM_FONT_LOWERCASE[@]}"; do
|
||||||
|
font_name_upper=$(echo "$font_name_lower" | tr '[:lower:]' '[:upper:]')
|
||||||
|
for size in ${CUSTOM_FONT_SIZES[@]}; do
|
||||||
|
family_name="${font_name_lower}${size}FontFamily"
|
||||||
|
echo " renderer.insertFont(${font_name_upper}_${size}_FONT_ID, ${family_name});" >> "$CUSTOM_CPP"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
echo "#else" >> "$CUSTOM_CPP"
|
||||||
|
echo " (void)renderer; // Suppress unused parameter warning" >> "$CUSTOM_CPP"
|
||||||
|
echo "#endif" >> "$CUSTOM_CPP"
|
||||||
|
else
|
||||||
|
echo " (void)renderer; // Suppress unused parameter warning" >> "$CUSTOM_CPP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "}" >> "$CUSTOM_CPP"
|
||||||
|
echo "" >> "$CUSTOM_CPP"
|
||||||
|
|
||||||
|
echo "Generated customFonts.h and customFonts.cpp with ${#CUSTOM_FONT_NAMES[@]} custom font(s)"
|
||||||
|
|||||||
@ -4,26 +4,17 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
#include "HyphenationCommon.h"
|
#include "HyphenationCommon.h"
|
||||||
#include "generated/hyph-de.trie.h"
|
|
||||||
#include "generated/hyph-en.trie.h"
|
#include "generated/hyph-en.trie.h"
|
||||||
#include "generated/hyph-fr.trie.h"
|
|
||||||
#include "generated/hyph-ru.trie.h"
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||||
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
|
||||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
|
||||||
LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
|
||||||
|
|
||||||
using EntryArray = std::array<LanguageEntry, 4>;
|
using EntryArray = std::array<LanguageEntry, 1>;
|
||||||
|
|
||||||
const EntryArray& entries() {
|
const EntryArray& entries() {
|
||||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator}}};
|
||||||
{"french", "fr", &frenchHyphenator},
|
|
||||||
{"german", "de", &germanHyphenator},
|
|
||||||
{"russian", "ru", &russianHyphenator}}};
|
|
||||||
return kEntries;
|
return kEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,383 +0,0 @@
|
|||||||
#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 fr_trie_data[] = {
|
|
||||||
0x00, 0x00, 0x1A, 0xF4, 0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C,
|
|
||||||
0x2B, 0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17, 0x04, 0x1F,
|
|
||||||
0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36, 0x34, 0x21, 0x2A, 0x0D, 0x0C,
|
|
||||||
0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C, 0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D,
|
|
||||||
0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B, 0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E,
|
|
||||||
0x48, 0x2C, 0x0B, 0x29, 0x16, 0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16,
|
|
||||||
0x3E, 0x0D, 0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B, 0x16,
|
|
||||||
0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
|
||||||
0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C,
|
|
||||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD,
|
|
||||||
0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
|
||||||
0x73, 0xFD, 0xA0, 0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
|
||||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD,
|
|
||||||
0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61, 0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD,
|
|
||||||
0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA, 0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74,
|
|
||||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF,
|
|
||||||
0x5E, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21,
|
|
||||||
0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x72,
|
|
||||||
0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xFD,
|
|
||||||
0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0,
|
|
||||||
0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0, 0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61,
|
|
||||||
0x6E, 0x75, 0xF2, 0xFD, 0x21, 0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74,
|
|
||||||
0xFF, 0x06, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21, 0x74,
|
|
||||||
0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2, 0x02, 0x52, 0x6E, 0x75,
|
|
||||||
0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD, 0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF,
|
|
||||||
0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70, 0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD,
|
|
||||||
0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21,
|
|
||||||
0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
|
||||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96, 0xFF, 0x96, 0x41,
|
|
||||||
0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52, 0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62,
|
|
||||||
0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05, 0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06,
|
|
||||||
0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21,
|
|
||||||
0x65, 0xFD, 0xC2, 0x02, 0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
|
||||||
0x74, 0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF, 0xF4, 0xFF,
|
|
||||||
0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7, 0x21, 0x67, 0xFC, 0x41, 0x65,
|
|
||||||
0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02,
|
|
||||||
0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5,
|
|
||||||
0xF1, 0xFD, 0xA0, 0x00, 0x61, 0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8,
|
|
||||||
0xFE, 0xC8, 0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43, 0x64,
|
|
||||||
0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E, 0x70, 0x73, 0x72, 0x67,
|
|
||||||
0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D, 0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01,
|
|
||||||
0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1,
|
|
||||||
0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE,
|
|
||||||
0x47, 0xFE, 0x47, 0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
|
||||||
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43, 0x63, 0x74, 0x75,
|
|
||||||
0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F, 0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC,
|
|
||||||
0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0, 0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21,
|
|
||||||
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72,
|
|
||||||
0xFD, 0x21, 0x76, 0xFD, 0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21,
|
|
||||||
0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD, 0xA0, 0x04,
|
|
||||||
0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x61, 0xFD,
|
|
||||||
0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD, 0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41,
|
|
||||||
0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02, 0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04,
|
|
||||||
0x31, 0x21, 0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61,
|
|
||||||
0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x6F, 0xFE, 0x7B, 0xA0,
|
|
||||||
0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21, 0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04,
|
|
||||||
0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63, 0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3,
|
|
||||||
0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3, 0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73,
|
|
||||||
0xFD, 0x21, 0x74, 0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
|
||||||
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E, 0x72, 0x73, 0xFF,
|
|
||||||
0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9, 0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41,
|
|
||||||
0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C, 0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72,
|
|
||||||
0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05,
|
|
||||||
0x23, 0x21, 0x6E, 0xFD, 0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E,
|
|
||||||
0xFF, 0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF, 0xFD, 0x44,
|
|
||||||
0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9, 0xF3, 0x41, 0xA9, 0xFC, 0x86,
|
|
||||||
0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64, 0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D,
|
|
||||||
0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C, 0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41,
|
|
||||||
0x68, 0xFC, 0x92, 0x23, 0x61, 0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A,
|
|
||||||
0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79, 0x6F,
|
|
||||||
0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74, 0x75,
|
|
||||||
0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0, 0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61,
|
|
||||||
0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE, 0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63,
|
|
||||||
0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63, 0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21,
|
|
||||||
0x6E, 0xFB, 0x41, 0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
|
||||||
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21, 0xC3, 0xFC, 0x21,
|
|
||||||
0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3, 0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6,
|
|
||||||
0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64, 0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F,
|
|
||||||
0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD, 0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68,
|
|
||||||
0xFE, 0xCA, 0x21, 0x6F, 0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF,
|
|
||||||
0x44, 0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5, 0x21, 0x74,
|
|
||||||
0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD, 0x21, 0xA9, 0xF9, 0xA0, 0x01,
|
|
||||||
0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA,
|
|
||||||
0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA, 0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73,
|
|
||||||
0xFA, 0xA4, 0xFA, 0xA4, 0xFF, 0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E,
|
|
||||||
0xFD, 0x44, 0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5, 0x21,
|
|
||||||
0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41, 0xA9, 0xFC, 0x27, 0x21,
|
|
||||||
0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21, 0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75,
|
|
||||||
0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2, 0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61,
|
|
||||||
0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73, 0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA,
|
|
||||||
0x3F, 0x21, 0xA9, 0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
|
||||||
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4, 0xFC, 0xBD, 0x21,
|
|
||||||
0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93, 0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF,
|
|
||||||
0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9, 0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43,
|
|
||||||
0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD, 0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21,
|
|
||||||
0xA0, 0x01, 0xC1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB,
|
|
||||||
0x81, 0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41, 0x73, 0xFE,
|
|
||||||
0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD, 0x43, 0x6F, 0x73, 0x75, 0xFF,
|
|
||||||
0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74,
|
|
||||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41,
|
|
||||||
0x61, 0xFE, 0xA9, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC,
|
|
||||||
0x21, 0x74, 0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD, 0x25,
|
|
||||||
0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB, 0xFD, 0x21, 0x61, 0xFC,
|
|
||||||
0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC,
|
|
||||||
0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68, 0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70,
|
|
||||||
0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA, 0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC,
|
|
||||||
0x41, 0x6C, 0xFB, 0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
|
||||||
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7, 0xFF, 0xFD, 0x41,
|
|
||||||
0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C, 0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08,
|
|
||||||
0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80, 0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9,
|
|
||||||
0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9, 0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF,
|
|
||||||
0xD7, 0xFF, 0xE4, 0xFD, 0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75,
|
|
||||||
0xB9, 0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21, 0xA9, 0xFD,
|
|
||||||
0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9, 0x42, 0x66, 0x78, 0xFB, 0x18,
|
|
||||||
0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1, 0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2,
|
|
||||||
0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41, 0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65,
|
|
||||||
0x63, 0x68, 0x75, 0x6F, 0xFF, 0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF,
|
|
||||||
0xFD, 0xC3, 0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43, 0x63,
|
|
||||||
0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F, 0xF2, 0xFC, 0x21, 0x69,
|
|
||||||
0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41, 0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21,
|
|
||||||
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21,
|
|
||||||
0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7,
|
|
||||||
0xF8, 0xFF, 0xF9, 0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
|
||||||
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41, 0x69, 0xF7, 0xD2,
|
|
||||||
0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73, 0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47,
|
|
||||||
0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7,
|
|
||||||
0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70, 0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9,
|
|
||||||
0x41, 0x75, 0xF8, 0x30, 0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63,
|
|
||||||
0xF8, 0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x73, 0xF8,
|
|
||||||
0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41, 0x69, 0xF8, 0x73, 0x21, 0x75,
|
|
||||||
0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA, 0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6,
|
|
||||||
0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72, 0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6,
|
|
||||||
0xBF, 0xF6, 0xBF, 0x42, 0x63, 0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD,
|
|
||||||
0x21, 0x74, 0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2, 0x61,
|
|
||||||
0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF, 0xF9, 0xF6, 0x99, 0xF6,
|
|
||||||
0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF, 0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8,
|
|
||||||
0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF, 0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61,
|
|
||||||
0xC3, 0x62, 0x63, 0x64, 0x65, 0x69, 0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8,
|
|
||||||
0xB1, 0xF8, 0xE6, 0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
|
||||||
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85, 0xF8, 0x85, 0xA0,
|
|
||||||
0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
|
|
||||||
0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73, 0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2,
|
|
||||||
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21,
|
|
||||||
0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63,
|
|
||||||
0x61, 0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
|
||||||
0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
|
||||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
|
||||||
0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1,
|
|
||||||
0xE1, 0xFD, 0x41, 0x74, 0xFE, 0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70,
|
|
||||||
0xF6, 0xFD, 0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21,
|
|
||||||
0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x2E, 0xFF, 0x33, 0x21,
|
|
||||||
0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63, 0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21,
|
|
||||||
0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22, 0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03,
|
|
||||||
0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
|
|
||||||
0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD,
|
|
||||||
0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64, 0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76,
|
|
||||||
0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6, 0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2,
|
|
||||||
0xFF, 0xC9, 0xFF, 0xD4, 0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
|
||||||
0xBB, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02, 0x41, 0x21,
|
|
||||||
0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1, 0x2E, 0x73, 0x6E, 0xF1, 0xF4,
|
|
||||||
0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2, 0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD,
|
|
||||||
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74, 0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2,
|
|
||||||
0x00, 0xE2, 0x65, 0x75, 0xFF, 0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE,
|
|
||||||
0x62, 0xFF, 0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43, 0x65,
|
|
||||||
0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92, 0x21, 0x65, 0xFD, 0x21,
|
|
||||||
0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65, 0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF,
|
|
||||||
0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
|
||||||
0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF,
|
|
||||||
0xE7, 0xFF, 0xF6, 0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
|
||||||
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
|
||||||
0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92,
|
|
||||||
0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC, 0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF,
|
|
||||||
0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F,
|
|
||||||
0xFF, 0xA8, 0xFF, 0xBF, 0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD,
|
|
||||||
0x8D, 0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41,
|
|
||||||
0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD,
|
|
||||||
0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70, 0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21,
|
|
||||||
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA,
|
|
||||||
0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21,
|
|
||||||
0x74, 0xFD, 0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F, 0xE2,
|
|
||||||
0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21, 0x6E, 0xFC, 0x21, 0x65,
|
|
||||||
0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59,
|
|
||||||
0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE,
|
|
||||||
0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41, 0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00,
|
|
||||||
0xE2, 0x2E, 0x62, 0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
|
||||||
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D, 0xFC, 0xEE, 0xA0,
|
|
||||||
0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5, 0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75,
|
|
||||||
0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72, 0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7,
|
|
||||||
0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD, 0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21,
|
|
||||||
0x72, 0xFC, 0x41, 0x69, 0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
|
||||||
0x42, 0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4, 0x21, 0x69,
|
|
||||||
0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF, 0x21, 0x74, 0xFC, 0x21, 0x69,
|
|
||||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9,
|
|
||||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7, 0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C,
|
|
||||||
0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2,
|
|
||||||
0x88, 0xA1, 0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69, 0xFC,
|
|
||||||
0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE, 0x8A, 0xFD, 0x27, 0xFD,
|
|
||||||
0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF, 0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3,
|
|
||||||
0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1, 0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41,
|
|
||||||
0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74,
|
|
||||||
0xF2, 0x5A, 0xA1, 0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
|
||||||
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2, 0xF5, 0x21, 0xA9,
|
|
||||||
0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0, 0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21,
|
|
||||||
0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41,
|
|
||||||
0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1, 0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
|
||||||
0xFB, 0x41, 0xFF, 0xFB, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73,
|
|
||||||
0xFC, 0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73, 0xFB, 0x34,
|
|
||||||
0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69, 0x6F, 0x61, 0xC3, 0x65, 0x6E,
|
|
||||||
0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08, 0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A,
|
|
||||||
0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21,
|
|
||||||
0x75, 0xFD, 0x41, 0x61, 0xF3, 0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9,
|
|
||||||
0xFF, 0xFC, 0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA,
|
|
||||||
0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF, 0xFC, 0xFB, 0x62, 0x42,
|
|
||||||
0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E, 0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB,
|
|
||||||
0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C, 0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1,
|
|
||||||
0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41,
|
|
||||||
0x68, 0xF8, 0xC0, 0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
|
||||||
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A, 0xC3, 0x69, 0x63,
|
|
||||||
0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39, 0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD,
|
|
||||||
0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03, 0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E,
|
|
||||||
0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A, 0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
|
||||||
0x61, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3,
|
|
||||||
0x73, 0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0x21,
|
|
||||||
0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2, 0x69, 0x75, 0xC3, 0x6F, 0x65,
|
|
||||||
0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41, 0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62,
|
|
||||||
0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9, 0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD,
|
|
||||||
0xFC, 0x3E, 0xFC, 0x3E, 0x41, 0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01,
|
|
||||||
0xFF, 0xFC, 0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
|
||||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C, 0xEB, 0xFD, 0x42, 0xA9,
|
|
||||||
0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1,
|
|
||||||
0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21,
|
|
||||||
0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75,
|
|
||||||
0x65, 0xF9, 0x7E, 0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
|
||||||
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21, 0x75, 0xFC, 0xA0,
|
|
||||||
0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47,
|
|
||||||
0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF, 0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00,
|
|
||||||
0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB, 0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0,
|
|
||||||
0x07, 0x62, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69,
|
|
||||||
0xFA, 0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35, 0xFF, 0xDA,
|
|
||||||
0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6, 0x41, 0x75, 0xF8, 0xC2, 0x22,
|
|
||||||
0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22,
|
|
||||||
0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D, 0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73,
|
|
||||||
0x74, 0x79, 0xFE, 0xAE, 0xFE, 0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF,
|
|
||||||
0xC2, 0xFF, 0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64, 0xF1,
|
|
||||||
0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC, 0x41, 0x6C, 0xF1, 0x8F,
|
|
||||||
0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21, 0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C,
|
|
||||||
0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42, 0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F,
|
|
||||||
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66,
|
|
||||||
0xB5, 0xBC, 0xCE, 0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
|
||||||
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61, 0xFC, 0x22, 0x63,
|
|
||||||
0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD,
|
|
||||||
0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1, 0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F,
|
|
||||||
0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70, 0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21,
|
|
||||||
0x72, 0xF7, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72,
|
|
||||||
0xFA, 0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD, 0x41, 0x61,
|
|
||||||
0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72, 0xFF, 0x9D, 0xFF, 0xC9, 0xFF,
|
|
||||||
0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC, 0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68,
|
|
||||||
0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69, 0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21,
|
|
||||||
0x72, 0xFD, 0x21, 0x76, 0xFD, 0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61,
|
|
||||||
0xFF, 0xA5, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21, 0x61,
|
|
||||||
0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA, 0xFF, 0xDF, 0xFF, 0xEB,
|
|
||||||
0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61, 0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21,
|
|
||||||
0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63, 0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4,
|
|
||||||
0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1,
|
|
||||||
0x41, 0x70, 0xED, 0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
|
||||||
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
|
||||||
0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70, 0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C,
|
|
||||||
0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD, 0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F,
|
|
||||||
0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21, 0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F,
|
|
||||||
0x72, 0x73, 0x75, 0x79, 0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF,
|
|
||||||
0xFD, 0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72, 0xF6, 0xA6,
|
|
||||||
0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74, 0xF0, 0x03, 0xFF, 0xFC, 0x45,
|
|
||||||
0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD, 0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21,
|
|
||||||
0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61, 0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79,
|
|
||||||
0xFB, 0x9D, 0x21, 0x68, 0xFC, 0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D,
|
|
||||||
0xFB, 0xEE, 0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x6D,
|
|
||||||
0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
|
||||||
0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2, 0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8,
|
|
||||||
0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21, 0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7,
|
|
||||||
0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7, 0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F,
|
|
||||||
0xFF, 0xF3, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
|
||||||
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6D, 0x72,
|
|
||||||
0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65, 0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB,
|
|
||||||
0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0,
|
|
||||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5,
|
|
||||||
0x5F, 0xF5, 0x5F, 0x41, 0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA,
|
|
||||||
0x2E, 0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20, 0xFF, 0x4D,
|
|
||||||
0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC, 0xF5, 0x35, 0xC1, 0x00, 0xE1,
|
|
||||||
0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D,
|
|
||||||
0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75, 0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5,
|
|
||||||
0x32, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2,
|
|
||||||
0x65, 0x69, 0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD, 0xC4,
|
|
||||||
0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD, 0xC4, 0xF4, 0xD1, 0x45,
|
|
||||||
0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87,
|
|
||||||
0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2,
|
|
||||||
0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C,
|
|
||||||
0x61, 0xC3, 0x65, 0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
|
||||||
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4, 0x79, 0x41, 0x69,
|
|
||||||
0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5, 0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E,
|
|
||||||
0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5, 0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF,
|
|
||||||
0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21, 0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02,
|
|
||||||
0x6F, 0x73, 0xF5, 0x12, 0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72,
|
|
||||||
0x70, 0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00, 0xE2, 0x75,
|
|
||||||
0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x02, 0x21,
|
|
||||||
0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22, 0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65,
|
|
||||||
0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0, 0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76,
|
|
||||||
0xFF, 0xFC, 0xF3, 0xDA, 0x41, 0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9,
|
|
||||||
0xFB, 0x17, 0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6, 0x66,
|
|
||||||
0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29, 0xC6, 0x00, 0xE2, 0x2E,
|
|
||||||
0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF, 0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64,
|
|
||||||
0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21,
|
|
||||||
0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72,
|
|
||||||
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
|
||||||
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65, 0xF3, 0x44, 0x21,
|
|
||||||
0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9, 0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65,
|
|
||||||
0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5, 0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF,
|
|
||||||
0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F, 0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11,
|
|
||||||
0x41, 0x69, 0xF3, 0x97, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21,
|
|
||||||
0x6E, 0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x6F, 0xF6,
|
|
||||||
0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62, 0x6E, 0xF3, 0x6F, 0xFF, 0xEF,
|
|
||||||
0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82,
|
|
||||||
0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21, 0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F,
|
|
||||||
0xF7, 0x81, 0x21, 0x72, 0xFC, 0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74,
|
|
||||||
0xF7, 0x6F, 0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
|
||||||
0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC, 0xDC, 0xFB, 0x21, 0x74,
|
|
||||||
0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D, 0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73,
|
|
||||||
0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D, 0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3,
|
|
||||||
0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E, 0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
|
||||||
0x72, 0xFF, 0xFB, 0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
|
||||||
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1, 0x00, 0xE1, 0x6D,
|
|
||||||
0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1,
|
|
||||||
0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06, 0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2,
|
|
||||||
0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65,
|
|
||||||
0xC3, 0x69, 0x6F, 0x73, 0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD,
|
|
||||||
0x22, 0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xF1,
|
|
||||||
0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41, 0x76, 0xF3, 0xC0, 0x41, 0x76,
|
|
||||||
0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC, 0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21,
|
|
||||||
0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73, 0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42,
|
|
||||||
0x6E, 0x73, 0xE9, 0xBA, 0xE9, 0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2,
|
|
||||||
0x00, 0xE1, 0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41, 0x74,
|
|
||||||
0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF, 0xFD, 0xF2, 0x88, 0x42,
|
|
||||||
0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73, 0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41,
|
|
||||||
0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2,
|
|
||||||
0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0,
|
|
||||||
0xFD, 0x41, 0x6D, 0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
|
||||||
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB, 0xF2, 0x50, 0xF0,
|
|
||||||
0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42, 0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8,
|
|
||||||
0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0,
|
|
||||||
0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A,
|
|
||||||
0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79,
|
|
||||||
0x6D, 0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80, 0xF0, 0x80,
|
|
||||||
0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1, 0x00, 0xE2, 0x2E, 0xF1, 0xAF,
|
|
||||||
0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0, 0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0,
|
|
||||||
0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0, 0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1,
|
|
||||||
0x75, 0xF9, 0x41, 0x69, 0xFA, 0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E,
|
|
||||||
0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69, 0x6F,
|
|
||||||
0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0, 0x0B, 0xF4, 0x0D, 0xFF,
|
|
||||||
0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC, 0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38,
|
|
||||||
0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61, 0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2,
|
|
||||||
0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF,
|
|
||||||
0xCB, 0xEF, 0xCB, 0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
|
||||||
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0, 0xEF, 0x48, 0xF0,
|
|
||||||
0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F, 0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98,
|
|
||||||
0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4, 0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE,
|
|
||||||
0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65, 0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr SerializedHyphenationPatterns fr_patterns = {
|
|
||||||
fr_trie_data,
|
|
||||||
sizeof(fr_trie_data),
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,168 +0,0 @@
|
|||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <MD5Builder.h>
|
|
||||||
#include <SDCardManager.h>
|
|
||||||
#include <Serialization.h>
|
|
||||||
|
|
||||||
// Initialize the static instance
|
|
||||||
KOReaderCredentialStore KOReaderCredentialStore::instance;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// File format version
|
|
||||||
constexpr uint8_t KOREADER_FILE_VERSION = 1;
|
|
||||||
|
|
||||||
// KOReader credentials file path
|
|
||||||
constexpr char KOREADER_FILE[] = "/.crosspoint/koreader.bin";
|
|
||||||
|
|
||||||
// Default sync server URL
|
|
||||||
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
|
|
||||||
|
|
||||||
// Obfuscation key - "KOReader" in ASCII
|
|
||||||
// This is NOT cryptographic security, just prevents casual file reading
|
|
||||||
constexpr uint8_t OBFUSCATION_KEY[] = {0x4B, 0x4F, 0x52, 0x65, 0x61, 0x64, 0x65, 0x72};
|
|
||||||
constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void KOReaderCredentialStore::obfuscate(std::string& data) const {
|
|
||||||
for (size_t i = 0; i < data.size(); i++) {
|
|
||||||
data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool KOReaderCredentialStore::saveToFile() const {
|
|
||||||
// Make sure the directory exists
|
|
||||||
SdMan.mkdir("/.crosspoint");
|
|
||||||
|
|
||||||
FsFile file;
|
|
||||||
if (!SdMan.openFileForWrite("KRS", KOREADER_FILE, file)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write header
|
|
||||||
serialization::writePod(file, KOREADER_FILE_VERSION);
|
|
||||||
|
|
||||||
// Write username (plaintext - not particularly sensitive)
|
|
||||||
serialization::writeString(file, username);
|
|
||||||
Serial.printf("[%lu] [KRS] Saving username: %s\n", millis(), username.c_str());
|
|
||||||
|
|
||||||
// Write password (obfuscated)
|
|
||||||
std::string obfuscatedPwd = password;
|
|
||||||
obfuscate(obfuscatedPwd);
|
|
||||||
serialization::writeString(file, obfuscatedPwd);
|
|
||||||
|
|
||||||
// Write server URL
|
|
||||||
serialization::writeString(file, serverUrl);
|
|
||||||
|
|
||||||
// Write match method
|
|
||||||
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool KOReaderCredentialStore::loadFromFile() {
|
|
||||||
FsFile file;
|
|
||||||
if (!SdMan.openFileForRead("KRS", KOREADER_FILE, file)) {
|
|
||||||
Serial.printf("[%lu] [KRS] No credentials file found\n", millis());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and verify version
|
|
||||||
uint8_t version;
|
|
||||||
serialization::readPod(file, version);
|
|
||||||
if (version != KOREADER_FILE_VERSION) {
|
|
||||||
Serial.printf("[%lu] [KRS] Unknown file version: %u\n", millis(), version);
|
|
||||||
file.close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read username
|
|
||||||
if (file.available()) {
|
|
||||||
serialization::readString(file, username);
|
|
||||||
} else {
|
|
||||||
username.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and deobfuscate password
|
|
||||||
if (file.available()) {
|
|
||||||
serialization::readString(file, password);
|
|
||||||
obfuscate(password); // XOR is symmetric, so same function deobfuscates
|
|
||||||
} else {
|
|
||||||
password.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read server URL
|
|
||||||
if (file.available()) {
|
|
||||||
serialization::readString(file, serverUrl);
|
|
||||||
} else {
|
|
||||||
serverUrl.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read match method
|
|
||||||
if (file.available()) {
|
|
||||||
uint8_t method;
|
|
||||||
serialization::readPod(file, method);
|
|
||||||
matchMethod = static_cast<DocumentMatchMethod>(method);
|
|
||||||
} else {
|
|
||||||
matchMethod = DocumentMatchMethod::FILENAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderCredentialStore::setCredentials(const std::string& user, const std::string& pass) {
|
|
||||||
username = user;
|
|
||||||
password = pass;
|
|
||||||
Serial.printf("[%lu] [KRS] Set credentials for user: %s\n", millis(), user.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string KOReaderCredentialStore::getMd5Password() const {
|
|
||||||
if (password.empty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate MD5 hash of password using ESP32's MD5Builder
|
|
||||||
MD5Builder md5;
|
|
||||||
md5.begin();
|
|
||||||
md5.add(password.c_str());
|
|
||||||
md5.calculate();
|
|
||||||
|
|
||||||
return md5.toString().c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool KOReaderCredentialStore::hasCredentials() const { return !username.empty() && !password.empty(); }
|
|
||||||
|
|
||||||
void KOReaderCredentialStore::clearCredentials() {
|
|
||||||
username.clear();
|
|
||||||
password.clear();
|
|
||||||
saveToFile();
|
|
||||||
Serial.printf("[%lu] [KRS] Cleared KOReader credentials\n", millis());
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderCredentialStore::setServerUrl(const std::string& url) {
|
|
||||||
serverUrl = url;
|
|
||||||
Serial.printf("[%lu] [KRS] Set server URL: %s\n", millis(), url.empty() ? "(default)" : url.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string KOReaderCredentialStore::getBaseUrl() const {
|
|
||||||
if (serverUrl.empty()) {
|
|
||||||
return DEFAULT_SERVER_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize URL: add http:// if no protocol specified (local servers typically don't have SSL)
|
|
||||||
if (serverUrl.find("://") == std::string::npos) {
|
|
||||||
return "http://" + serverUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
|
||||||
matchMethod = method;
|
|
||||||
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(),
|
|
||||||
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// Document matching method for KOReader sync
|
|
||||||
enum class DocumentMatchMethod : uint8_t {
|
|
||||||
FILENAME = 0, // Match by filename (simpler, works across different file sources)
|
|
||||||
BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Singleton class for storing KOReader sync credentials on the SD card.
|
|
||||||
* Credentials are stored in /sd/.crosspoint/koreader.bin with basic
|
|
||||||
* XOR obfuscation to prevent casual reading (not cryptographically secure).
|
|
||||||
*/
|
|
||||||
class KOReaderCredentialStore {
|
|
||||||
private:
|
|
||||||
static KOReaderCredentialStore instance;
|
|
||||||
std::string username;
|
|
||||||
std::string password;
|
|
||||||
std::string serverUrl; // Custom sync server URL (empty = default)
|
|
||||||
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
|
|
||||||
|
|
||||||
// Private constructor for singleton
|
|
||||||
KOReaderCredentialStore() = default;
|
|
||||||
|
|
||||||
// XOR obfuscation (symmetric - same for encode/decode)
|
|
||||||
void obfuscate(std::string& data) const;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Delete copy constructor and assignment
|
|
||||||
KOReaderCredentialStore(const KOReaderCredentialStore&) = delete;
|
|
||||||
KOReaderCredentialStore& operator=(const KOReaderCredentialStore&) = delete;
|
|
||||||
|
|
||||||
// Get singleton instance
|
|
||||||
static KOReaderCredentialStore& getInstance() { return instance; }
|
|
||||||
|
|
||||||
// Save/load from SD card
|
|
||||||
bool saveToFile() const;
|
|
||||||
bool loadFromFile();
|
|
||||||
|
|
||||||
// Credential management
|
|
||||||
void setCredentials(const std::string& user, const std::string& pass);
|
|
||||||
const std::string& getUsername() const { return username; }
|
|
||||||
const std::string& getPassword() const { return password; }
|
|
||||||
|
|
||||||
// Get MD5 hash of password for API authentication
|
|
||||||
std::string getMd5Password() const;
|
|
||||||
|
|
||||||
// Check if credentials are set
|
|
||||||
bool hasCredentials() const;
|
|
||||||
|
|
||||||
// Clear credentials
|
|
||||||
void clearCredentials();
|
|
||||||
|
|
||||||
// Server URL management
|
|
||||||
void setServerUrl(const std::string& url);
|
|
||||||
const std::string& getServerUrl() const { return serverUrl; }
|
|
||||||
|
|
||||||
// Get base URL for API calls (with http:// normalization if no protocol, falls back to default)
|
|
||||||
std::string getBaseUrl() const;
|
|
||||||
|
|
||||||
// Document matching method
|
|
||||||
void setMatchMethod(DocumentMatchMethod method);
|
|
||||||
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper macro to access credential store
|
|
||||||
#define KOREADER_STORE KOReaderCredentialStore::getInstance()
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
#include "KOReaderDocumentId.h"
|
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <MD5Builder.h>
|
|
||||||
#include <SDCardManager.h>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Extract filename from path (everything after last '/')
|
|
||||||
std::string getFilename(const std::string& path) {
|
|
||||||
const size_t pos = path.rfind('/');
|
|
||||||
if (pos == std::string::npos) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return path.substr(pos + 1);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePath) {
|
|
||||||
const std::string filename = getFilename(filePath);
|
|
||||||
if (filename.empty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
MD5Builder md5;
|
|
||||||
md5.begin();
|
|
||||||
md5.add(filename.c_str());
|
|
||||||
md5.calculate();
|
|
||||||
|
|
||||||
std::string result = md5.toString().c_str();
|
|
||||||
Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t KOReaderDocumentId::getOffset(int i) {
|
|
||||||
// Offset = 1024 << (2*i)
|
|
||||||
// For i = -1: 1024 >> 2 = 256
|
|
||||||
// For i >= 0: 1024 << (2*i)
|
|
||||||
if (i < 0) {
|
|
||||||
return CHUNK_SIZE >> (-2 * i);
|
|
||||||
}
|
|
||||||
return CHUNK_SIZE << (2 * i);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|
||||||
FsFile file;
|
|
||||||
if (!SdMan.openFileForRead("KODoc", filePath, file)) {
|
|
||||||
Serial.printf("[%lu] [KODoc] Failed to open file: %s\n", millis(), filePath.c_str());
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t fileSize = file.fileSize();
|
|
||||||
Serial.printf("[%lu] [KODoc] Calculating hash for file: %s (size: %zu)\n", millis(), filePath.c_str(), fileSize);
|
|
||||||
|
|
||||||
// Initialize MD5 builder
|
|
||||||
MD5Builder md5;
|
|
||||||
md5.begin();
|
|
||||||
|
|
||||||
// Buffer for reading chunks
|
|
||||||
uint8_t buffer[CHUNK_SIZE];
|
|
||||||
size_t totalBytesRead = 0;
|
|
||||||
|
|
||||||
// Read from each offset (i = -1 to 10)
|
|
||||||
for (int i = -1; i < OFFSET_COUNT - 1; i++) {
|
|
||||||
const size_t offset = getOffset(i);
|
|
||||||
|
|
||||||
// Skip if offset is beyond file size
|
|
||||||
if (offset >= fileSize) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seek to offset
|
|
||||||
if (!file.seekSet(offset)) {
|
|
||||||
Serial.printf("[%lu] [KODoc] Failed to seek to offset %zu\n", millis(), offset);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read up to CHUNK_SIZE bytes
|
|
||||||
const size_t bytesToRead = std::min(CHUNK_SIZE, fileSize - offset);
|
|
||||||
const size_t bytesRead = file.read(buffer, bytesToRead);
|
|
||||||
|
|
||||||
if (bytesRead > 0) {
|
|
||||||
md5.add(buffer, bytesRead);
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
// Calculate final hash
|
|
||||||
md5.calculate();
|
|
||||||
std::string result = md5.toString().c_str();
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KODoc] Hash calculated: %s (from %zu bytes)\n", millis(), result.c_str(), totalBytesRead);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate KOReader document ID (partial MD5 hash).
|
|
||||||
*
|
|
||||||
* KOReader identifies documents using a partial MD5 hash of the file content.
|
|
||||||
* The algorithm reads 1024 bytes at specific offsets and computes the MD5 hash
|
|
||||||
* of the concatenated data.
|
|
||||||
*
|
|
||||||
* Offsets are calculated as: 1024 << (2*i) for i = -1 to 10
|
|
||||||
* Producing: 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304,
|
|
||||||
* 16777216, 67108864, 268435456, 1073741824 bytes
|
|
||||||
*
|
|
||||||
* If an offset is beyond the file size, it is skipped.
|
|
||||||
*/
|
|
||||||
class KOReaderDocumentId {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* Calculate the KOReader document hash for a file (binary/content-based).
|
|
||||||
*
|
|
||||||
* @param filePath Path to the file (typically an EPUB)
|
|
||||||
* @return 32-character lowercase hex string, or empty string on failure
|
|
||||||
*/
|
|
||||||
static std::string calculate(const std::string& filePath);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate document hash from filename only (filename-based sync mode).
|
|
||||||
* This is simpler and works when files have the same name across devices.
|
|
||||||
*
|
|
||||||
* @param filePath Path to the file (only the filename portion is used)
|
|
||||||
* @return 32-character lowercase hex MD5 of the filename
|
|
||||||
*/
|
|
||||||
static std::string calculateFromFilename(const std::string& filePath);
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Size of each chunk to read at each offset
|
|
||||||
static constexpr size_t CHUNK_SIZE = 1024;
|
|
||||||
|
|
||||||
// Number of offsets to try (i = -1 to 10, so 12 offsets)
|
|
||||||
static constexpr int OFFSET_COUNT = 12;
|
|
||||||
|
|
||||||
// Calculate offset for index i: 1024 << (2*i)
|
|
||||||
static size_t getOffset(int i);
|
|
||||||
};
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
#include "KOReaderSyncClient.h"
|
|
||||||
|
|
||||||
#include <ArduinoJson.h>
|
|
||||||
#include <HTTPClient.h>
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
#include <WiFi.h>
|
|
||||||
#include <WiFiClientSecure.h>
|
|
||||||
|
|
||||||
#include <ctime>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Device identifier for CrossPoint reader
|
|
||||||
constexpr char DEVICE_NAME[] = "CrossPoint";
|
|
||||||
constexpr char DEVICE_ID[] = "crosspoint-reader";
|
|
||||||
|
|
||||||
void addAuthHeaders(HTTPClient& http) {
|
|
||||||
http.addHeader("Accept", "application/vnd.koreader.v1+json");
|
|
||||||
http.addHeader("x-auth-user", KOREADER_STORE.getUsername().c_str());
|
|
||||||
http.addHeader("x-auth-key", KOREADER_STORE.getMd5Password().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; }
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
|
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
|
||||||
return NO_CREDENTIALS;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth";
|
|
||||||
Serial.printf("[%lu] [KOSync] Authenticating: %s\n", millis(), url.c_str());
|
|
||||||
|
|
||||||
HTTPClient http;
|
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
|
||||||
WiFiClient plainClient;
|
|
||||||
|
|
||||||
if (isHttpsUrl(url)) {
|
|
||||||
secureClient.reset(new WiFiClientSecure);
|
|
||||||
secureClient->setInsecure();
|
|
||||||
http.begin(*secureClient, url.c_str());
|
|
||||||
} else {
|
|
||||||
http.begin(plainClient, url.c_str());
|
|
||||||
}
|
|
||||||
addAuthHeaders(http);
|
|
||||||
|
|
||||||
const int httpCode = http.GET();
|
|
||||||
http.end();
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Auth response: %d\n", millis(), httpCode);
|
|
||||||
|
|
||||||
if (httpCode == 200) {
|
|
||||||
return OK;
|
|
||||||
} else if (httpCode == 401) {
|
|
||||||
return AUTH_FAILED;
|
|
||||||
} else if (httpCode < 0) {
|
|
||||||
return NETWORK_ERROR;
|
|
||||||
}
|
|
||||||
return SERVER_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
|
|
||||||
KOReaderProgress& outProgress) {
|
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
|
||||||
return NO_CREDENTIALS;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash;
|
|
||||||
Serial.printf("[%lu] [KOSync] Getting progress: %s\n", millis(), url.c_str());
|
|
||||||
|
|
||||||
HTTPClient http;
|
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
|
||||||
WiFiClient plainClient;
|
|
||||||
|
|
||||||
if (isHttpsUrl(url)) {
|
|
||||||
secureClient.reset(new WiFiClientSecure);
|
|
||||||
secureClient->setInsecure();
|
|
||||||
http.begin(*secureClient, url.c_str());
|
|
||||||
} else {
|
|
||||||
http.begin(plainClient, url.c_str());
|
|
||||||
}
|
|
||||||
addAuthHeaders(http);
|
|
||||||
|
|
||||||
const int httpCode = http.GET();
|
|
||||||
|
|
||||||
if (httpCode == 200) {
|
|
||||||
// Parse JSON response from response string
|
|
||||||
String responseBody = http.getString();
|
|
||||||
http.end();
|
|
||||||
|
|
||||||
JsonDocument doc;
|
|
||||||
const DeserializationError error = deserializeJson(doc, responseBody);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
Serial.printf("[%lu] [KOSync] JSON parse failed: %s\n", millis(), error.c_str());
|
|
||||||
return JSON_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
outProgress.document = documentHash;
|
|
||||||
outProgress.progress = doc["progress"].as<std::string>();
|
|
||||||
outProgress.percentage = doc["percentage"].as<float>();
|
|
||||||
outProgress.device = doc["device"].as<std::string>();
|
|
||||||
outProgress.deviceId = doc["device_id"].as<std::string>();
|
|
||||||
outProgress.timestamp = doc["timestamp"].as<int64_t>();
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Got progress: %.2f%% at %s\n", millis(), outProgress.percentage * 100,
|
|
||||||
outProgress.progress.c_str());
|
|
||||||
return OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
http.end();
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Get progress response: %d\n", millis(), httpCode);
|
|
||||||
|
|
||||||
if (httpCode == 401) {
|
|
||||||
return AUTH_FAILED;
|
|
||||||
} else if (httpCode == 404) {
|
|
||||||
return NOT_FOUND;
|
|
||||||
} else if (httpCode < 0) {
|
|
||||||
return NETWORK_ERROR;
|
|
||||||
}
|
|
||||||
return SERVER_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgress& progress) {
|
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
|
||||||
Serial.printf("[%lu] [KOSync] No credentials configured\n", millis());
|
|
||||||
return NO_CREDENTIALS;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress";
|
|
||||||
Serial.printf("[%lu] [KOSync] Updating progress: %s\n", millis(), url.c_str());
|
|
||||||
|
|
||||||
HTTPClient http;
|
|
||||||
std::unique_ptr<WiFiClientSecure> secureClient;
|
|
||||||
WiFiClient plainClient;
|
|
||||||
|
|
||||||
if (isHttpsUrl(url)) {
|
|
||||||
secureClient.reset(new WiFiClientSecure);
|
|
||||||
secureClient->setInsecure();
|
|
||||||
http.begin(*secureClient, url.c_str());
|
|
||||||
} else {
|
|
||||||
http.begin(plainClient, url.c_str());
|
|
||||||
}
|
|
||||||
addAuthHeaders(http);
|
|
||||||
http.addHeader("Content-Type", "application/json");
|
|
||||||
|
|
||||||
// Build JSON body (timestamp not required per API spec)
|
|
||||||
JsonDocument doc;
|
|
||||||
doc["document"] = progress.document;
|
|
||||||
doc["progress"] = progress.progress;
|
|
||||||
doc["percentage"] = progress.percentage;
|
|
||||||
doc["device"] = DEVICE_NAME;
|
|
||||||
doc["device_id"] = DEVICE_ID;
|
|
||||||
|
|
||||||
std::string body;
|
|
||||||
serializeJson(doc, body);
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Request body: %s\n", millis(), body.c_str());
|
|
||||||
|
|
||||||
const int httpCode = http.PUT(body.c_str());
|
|
||||||
http.end();
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Update progress response: %d\n", millis(), httpCode);
|
|
||||||
|
|
||||||
if (httpCode == 200 || httpCode == 202) {
|
|
||||||
return OK;
|
|
||||||
} else if (httpCode == 401) {
|
|
||||||
return AUTH_FAILED;
|
|
||||||
} else if (httpCode < 0) {
|
|
||||||
return NETWORK_ERROR;
|
|
||||||
}
|
|
||||||
return SERVER_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* KOReaderSyncClient::errorString(Error error) {
|
|
||||||
switch (error) {
|
|
||||||
case OK:
|
|
||||||
return "Success";
|
|
||||||
case NO_CREDENTIALS:
|
|
||||||
return "No credentials configured";
|
|
||||||
case NETWORK_ERROR:
|
|
||||||
return "Network error";
|
|
||||||
case AUTH_FAILED:
|
|
||||||
return "Authentication failed";
|
|
||||||
case SERVER_ERROR:
|
|
||||||
return "Server error (try again later)";
|
|
||||||
case JSON_ERROR:
|
|
||||||
return "JSON parse error";
|
|
||||||
case NOT_FOUND:
|
|
||||||
return "No progress found";
|
|
||||||
default:
|
|
||||||
return "Unknown error";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Progress data from KOReader sync server.
|
|
||||||
*/
|
|
||||||
struct KOReaderProgress {
|
|
||||||
std::string document; // Document hash
|
|
||||||
std::string progress; // XPath-like progress string
|
|
||||||
float percentage; // Progress percentage (0.0 to 1.0)
|
|
||||||
std::string device; // Device name
|
|
||||||
std::string deviceId; // Device ID
|
|
||||||
int64_t timestamp; // Unix timestamp of last update
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP client for KOReader sync API.
|
|
||||||
*
|
|
||||||
* Base URL: https://sync.koreader.rocks:443/
|
|
||||||
*
|
|
||||||
* API Endpoints:
|
|
||||||
* GET /users/auth - Authenticate (validate credentials)
|
|
||||||
* GET /syncs/progress/:document - Get progress for a document
|
|
||||||
* PUT /syncs/progress - Update progress for a document
|
|
||||||
*
|
|
||||||
* Authentication:
|
|
||||||
* x-auth-user: username
|
|
||||||
* x-auth-key: MD5 hash of password
|
|
||||||
*/
|
|
||||||
class KOReaderSyncClient {
|
|
||||||
public:
|
|
||||||
enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate with the sync server (validate credentials).
|
|
||||||
* @return OK on success, error code on failure
|
|
||||||
*/
|
|
||||||
static Error authenticate();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get reading progress for a document.
|
|
||||||
* @param documentHash The document hash (from KOReaderDocumentId)
|
|
||||||
* @param outProgress Output: the progress data
|
|
||||||
* @return OK on success, NOT_FOUND if no progress exists, error code on failure
|
|
||||||
*/
|
|
||||||
static Error getProgress(const std::string& documentHash, KOReaderProgress& outProgress);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update reading progress for a document.
|
|
||||||
* @param progress The progress data to upload
|
|
||||||
* @return OK on success, error code on failure
|
|
||||||
*/
|
|
||||||
static Error updateProgress(const KOReaderProgress& progress);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get human-readable error message.
|
|
||||||
*/
|
|
||||||
static const char* errorString(Error error);
|
|
||||||
};
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
#include "ProgressMapper.h"
|
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
|
||||||
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos) {
|
|
||||||
KOReaderPosition result;
|
|
||||||
|
|
||||||
// Calculate page progress within current spine item
|
|
||||||
float intraSpineProgress = 0.0f;
|
|
||||||
if (pos.totalPages > 0) {
|
|
||||||
intraSpineProgress = static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate overall book progress (0.0-1.0)
|
|
||||||
result.percentage = epub->calculateProgress(pos.spineIndex, intraSpineProgress);
|
|
||||||
|
|
||||||
// Generate XPath with estimated paragraph position based on page
|
|
||||||
result.xpath = generateXPath(pos.spineIndex, pos.pageNumber, pos.totalPages);
|
|
||||||
|
|
||||||
// Get chapter info for logging
|
|
||||||
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
|
|
||||||
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [ProgressMapper] CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s\n", millis(),
|
|
||||||
chapterName.c_str(), pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
|
|
||||||
int totalPagesInSpine) {
|
|
||||||
CrossPointPosition result;
|
|
||||||
result.spineIndex = 0;
|
|
||||||
result.pageNumber = 0;
|
|
||||||
result.totalPages = totalPagesInSpine;
|
|
||||||
|
|
||||||
const size_t bookSize = epub->getBookSize();
|
|
||||||
if (bookSize == 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, try to get spine index from XPath (DocFragment)
|
|
||||||
int xpathSpineIndex = parseDocFragmentIndex(koPos.xpath);
|
|
||||||
if (xpathSpineIndex >= 0 && xpathSpineIndex < epub->getSpineItemsCount()) {
|
|
||||||
result.spineIndex = xpathSpineIndex;
|
|
||||||
// When we have XPath, go to page 0 of the spine - byte-based page calculation is unreliable
|
|
||||||
result.pageNumber = 0;
|
|
||||||
} else {
|
|
||||||
// Fall back to percentage-based lookup for both spine and page
|
|
||||||
const size_t targetBytes = static_cast<size_t>(bookSize * koPos.percentage);
|
|
||||||
|
|
||||||
// Find the spine item that contains this byte position
|
|
||||||
for (int i = 0; i < epub->getSpineItemsCount(); i++) {
|
|
||||||
const size_t cumulativeSize = epub->getCumulativeSpineItemSize(i);
|
|
||||||
if (cumulativeSize >= targetBytes) {
|
|
||||||
result.spineIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Estimate page number within the spine item using percentage (only when no XPath)
|
|
||||||
if (totalPagesInSpine > 0 && result.spineIndex < epub->getSpineItemsCount()) {
|
|
||||||
const size_t prevCumSize = (result.spineIndex > 0) ? epub->getCumulativeSpineItemSize(result.spineIndex - 1) : 0;
|
|
||||||
const size_t currentCumSize = epub->getCumulativeSpineItemSize(result.spineIndex);
|
|
||||||
const size_t spineSize = currentCumSize - prevCumSize;
|
|
||||||
|
|
||||||
if (spineSize > 0) {
|
|
||||||
const size_t bytesIntoSpine = (targetBytes > prevCumSize) ? (targetBytes - prevCumSize) : 0;
|
|
||||||
const float intraSpineProgress = static_cast<float>(bytesIntoSpine) / static_cast<float>(spineSize);
|
|
||||||
const float clampedProgress = std::max(0.0f, std::min(1.0f, intraSpineProgress));
|
|
||||||
result.pageNumber = static_cast<int>(clampedProgress * totalPagesInSpine);
|
|
||||||
result.pageNumber = std::max(0, std::min(result.pageNumber, totalPagesInSpine - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [ProgressMapper] KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d\n", millis(),
|
|
||||||
koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ProgressMapper::generateXPath(int spineIndex, int pageNumber, int totalPages) {
|
|
||||||
// KOReader uses 1-based DocFragment indices
|
|
||||||
// Use a simple xpath pointing to the DocFragment - KOReader will use the percentage for fine positioning
|
|
||||||
// Avoid specifying paragraph numbers as they may not exist in the target document
|
|
||||||
return "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body";
|
|
||||||
}
|
|
||||||
|
|
||||||
int ProgressMapper::parseDocFragmentIndex(const std::string& xpath) {
|
|
||||||
// Look for DocFragment[N] pattern
|
|
||||||
const size_t start = xpath.find("DocFragment[");
|
|
||||||
if (start == std::string::npos) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t numStart = start + 12; // Length of "DocFragment["
|
|
||||||
const size_t numEnd = xpath.find(']', numStart);
|
|
||||||
if (numEnd == std::string::npos) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const int docFragmentIndex = std::stoi(xpath.substr(numStart, numEnd - numStart));
|
|
||||||
// KOReader uses 1-based indices, we use 0-based
|
|
||||||
return docFragmentIndex - 1;
|
|
||||||
} catch (...) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <Epub.h>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CrossPoint position representation.
|
|
||||||
*/
|
|
||||||
struct CrossPointPosition {
|
|
||||||
int spineIndex; // Current spine item (chapter) index
|
|
||||||
int pageNumber; // Current page within the spine item
|
|
||||||
int totalPages; // Total pages in the current spine item
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* KOReader position representation.
|
|
||||||
*/
|
|
||||||
struct KOReaderPosition {
|
|
||||||
std::string xpath; // XPath-like progress string
|
|
||||||
float percentage; // Progress percentage (0.0 to 1.0)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps between CrossPoint and KOReader position formats.
|
|
||||||
*
|
|
||||||
* CrossPoint tracks position as (spineIndex, pageNumber).
|
|
||||||
* KOReader uses XPath-like strings + percentage.
|
|
||||||
*
|
|
||||||
* Since CrossPoint discards HTML structure during parsing, we generate
|
|
||||||
* synthetic XPath strings based on spine index, using percentage as the
|
|
||||||
* primary sync mechanism.
|
|
||||||
*/
|
|
||||||
class ProgressMapper {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* Convert CrossPoint position to KOReader format.
|
|
||||||
*
|
|
||||||
* @param epub The EPUB book
|
|
||||||
* @param pos CrossPoint position
|
|
||||||
* @return KOReader position
|
|
||||||
*/
|
|
||||||
static KOReaderPosition toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert KOReader position to CrossPoint format.
|
|
||||||
*
|
|
||||||
* Note: The returned pageNumber may be approximate since different
|
|
||||||
* rendering settings produce different page counts.
|
|
||||||
*
|
|
||||||
* @param epub The EPUB book
|
|
||||||
* @param koPos KOReader position
|
|
||||||
* @param totalPagesInSpine Total pages in the target spine item (for page estimation)
|
|
||||||
* @return CrossPoint position
|
|
||||||
*/
|
|
||||||
static CrossPointPosition toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
|
|
||||||
int totalPagesInSpine = 0);
|
|
||||||
|
|
||||||
private:
|
|
||||||
/**
|
|
||||||
* Generate XPath for KOReader compatibility.
|
|
||||||
* Format: /body/DocFragment[spineIndex+1]/body/p[estimatedParagraph]
|
|
||||||
* Paragraph is estimated based on page position within the chapter.
|
|
||||||
*/
|
|
||||||
static std::string generateXPath(int spineIndex, int pageNumber, int totalPages);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse DocFragment index from XPath string.
|
|
||||||
* Returns -1 if not found.
|
|
||||||
*/
|
|
||||||
static int parseDocFragmentIndex(const std::string& xpath);
|
|
||||||
};
|
|
||||||
@ -3,6 +3,7 @@
|
|||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
#include <builtinFonts/custom/customFonts.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ CrossPointSettings CrossPointSettings::instance;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// Increment this when adding new persisted settings fields
|
||||||
constexpr uint8_t SETTINGS_COUNT = 20;
|
constexpr uint8_t SETTINGS_COUNT = 22;
|
||||||
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -49,6 +50,8 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, hideBatteryPercentage);
|
serialization::writePod(outputFile, hideBatteryPercentage);
|
||||||
serialization::writePod(outputFile, longPressChapterSkip);
|
serialization::writePod(outputFile, longPressChapterSkip);
|
||||||
serialization::writePod(outputFile, hyphenationEnabled);
|
serialization::writePod(outputFile, hyphenationEnabled);
|
||||||
|
serialization::writePod(outputFile, customFontIndex);
|
||||||
|
serialization::writePod(outputFile, fallbackFontFamily);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||||
@ -120,6 +123,10 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
serialization::readPod(inputFile, hyphenationEnabled);
|
serialization::readPod(inputFile, hyphenationEnabled);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, customFontIndex);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
serialization::readPod(inputFile, fallbackFontFamily);
|
||||||
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
} while (false);
|
} while (false);
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
@ -128,7 +135,10 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float CrossPointSettings::getReaderLineCompression() const {
|
float CrossPointSettings::getReaderLineCompression() const {
|
||||||
switch (fontFamily) {
|
// For custom fonts, use the fallback font's line compression
|
||||||
|
const uint8_t effectiveFamily = (fontFamily == CUSTOM_FONT) ? fallbackFontFamily : fontFamily;
|
||||||
|
|
||||||
|
switch (effectiveFamily) {
|
||||||
case BOOKERLY:
|
case BOOKERLY:
|
||||||
default:
|
default:
|
||||||
switch (lineSpacing) {
|
switch (lineSpacing) {
|
||||||
@ -150,16 +160,6 @@ float CrossPointSettings::getReaderLineCompression() const {
|
|||||||
case WIDE:
|
case WIDE:
|
||||||
return 1.0f;
|
return 1.0f;
|
||||||
}
|
}
|
||||||
case OPENDYSLEXIC:
|
|
||||||
switch (lineSpacing) {
|
|
||||||
case TIGHT:
|
|
||||||
return 0.90f;
|
|
||||||
case NORMAL:
|
|
||||||
default:
|
|
||||||
return 0.95f;
|
|
||||||
case WIDE:
|
|
||||||
return 1.0f;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,17 +222,25 @@ int CrossPointSettings::getReaderFontId() const {
|
|||||||
case EXTRA_LARGE:
|
case EXTRA_LARGE:
|
||||||
return NOTOSANS_18_FONT_ID;
|
return NOTOSANS_18_FONT_ID;
|
||||||
}
|
}
|
||||||
case OPENDYSLEXIC:
|
case CUSTOM_FONT:
|
||||||
|
#if CUSTOM_FONT_COUNT > 0
|
||||||
|
// Use the custom font ID lookup array
|
||||||
|
// Bounds check the customFontIndex
|
||||||
|
if (customFontIndex < CUSTOM_FONT_COUNT) {
|
||||||
|
return CUSTOM_FONT_IDS[customFontIndex][fontSize];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Fall back to Bookerly if no custom fonts or invalid index
|
||||||
switch (fontSize) {
|
switch (fontSize) {
|
||||||
case SMALL:
|
case SMALL:
|
||||||
return OPENDYSLEXIC_8_FONT_ID;
|
return BOOKERLY_12_FONT_ID;
|
||||||
case MEDIUM:
|
case MEDIUM:
|
||||||
default:
|
default:
|
||||||
return OPENDYSLEXIC_10_FONT_ID;
|
return BOOKERLY_14_FONT_ID;
|
||||||
case LARGE:
|
case LARGE:
|
||||||
return OPENDYSLEXIC_12_FONT_ID;
|
return BOOKERLY_16_FONT_ID;
|
||||||
case EXTRA_LARGE:
|
case EXTRA_LARGE:
|
||||||
return OPENDYSLEXIC_14_FONT_ID;
|
return BOOKERLY_18_FONT_ID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class CrossPointSettings {
|
|||||||
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
|
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 };
|
||||||
|
|
||||||
// Font family options
|
// Font family options
|
||||||
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 };
|
enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, CUSTOM_FONT = 2 };
|
||||||
// Font size options
|
// Font size options
|
||||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
||||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
||||||
@ -77,6 +77,8 @@ class CrossPointSettings {
|
|||||||
uint8_t sideButtonLayout = PREV_NEXT;
|
uint8_t sideButtonLayout = PREV_NEXT;
|
||||||
// Reader font settings
|
// Reader font settings
|
||||||
uint8_t fontFamily = BOOKERLY;
|
uint8_t fontFamily = BOOKERLY;
|
||||||
|
uint8_t customFontIndex = 0; // Which custom font to use (0 to CUSTOM_FONT_COUNT-1)
|
||||||
|
uint8_t fallbackFontFamily = BOOKERLY; // Fallback for missing glyphs/weights in custom fonts
|
||||||
uint8_t fontSize = MEDIUM;
|
uint8_t fontSize = MEDIUM;
|
||||||
uint8_t lineSpacing = NORMAL;
|
uint8_t lineSpacing = NORMAL;
|
||||||
uint8_t paragraphAlignment = JUSTIFIED;
|
uint8_t paragraphAlignment = JUSTIFIED;
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "KOReaderSyncActivity.h"
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@ -12,7 +10,7 @@ namespace {
|
|||||||
constexpr int SKIP_PAGE_MS = 700;
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return false; }
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
||||||
// Add 2 for sync options (top and bottom) if credentials are configured
|
// Add 2 for sync options (top and bottom) if credentials are configured
|
||||||
@ -96,21 +94,7 @@ void EpubReaderChapterSelectionActivity::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
|
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
// KOReader sync functionality removed
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSyncActivity(
|
|
||||||
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
|
|
||||||
[this]() {
|
|
||||||
// On cancel
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this](int newSpineIndex, int newPage) {
|
|
||||||
// On sync complete
|
|
||||||
exitActivity();
|
|
||||||
onSyncPosition(newSpineIndex, newPage);
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::loop() {
|
void EpubReaderChapterSelectionActivity::loop() {
|
||||||
|
|||||||
@ -1,439 +0,0 @@
|
|||||||
#include "KOReaderSyncActivity.h"
|
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
|
||||||
#include <WiFi.h>
|
|
||||||
#include <esp_sntp.h>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "KOReaderDocumentId.h"
|
|
||||||
#include "MappedInputManager.h"
|
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
|
||||||
#include "fontIds.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
void syncTimeWithNTP() {
|
|
||||||
// Stop SNTP if already running (can't reconfigure while running)
|
|
||||||
if (esp_sntp_enabled()) {
|
|
||||||
esp_sntp_stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure SNTP
|
|
||||||
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
|
||||||
esp_sntp_setservername(0, "pool.ntp.org");
|
|
||||||
esp_sntp_init();
|
|
||||||
|
|
||||||
// Wait for time to sync (with timeout)
|
|
||||||
int retry = 0;
|
|
||||||
const int maxRetries = 50; // 5 seconds max
|
|
||||||
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
|
||||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
||||||
retry++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry < maxRetries) {
|
|
||||||
Serial.printf("[%lu] [KOSync] NTP time synced\n", millis());
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%lu] [KOSync] NTP sync timeout, using fallback\n", millis());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::taskTrampoline(void* param) {
|
|
||||||
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
|
||||||
self->displayTaskLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
|
||||||
exitActivity();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
Serial.printf("[%lu] [KOSync] WiFi connection failed, exiting\n", millis());
|
|
||||||
onCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] WiFi connected, starting sync\n", millis());
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = SYNCING;
|
|
||||||
statusMessage = "Syncing time...";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
// Sync time with NTP before making API requests
|
|
||||||
syncTimeWithNTP();
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
statusMessage = "Calculating document hash...";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
performSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::performSync() {
|
|
||||||
// Calculate document hash based on user's preferred method
|
|
||||||
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
|
|
||||||
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
|
|
||||||
} else {
|
|
||||||
documentHash = KOReaderDocumentId::calculate(epubPath);
|
|
||||||
}
|
|
||||||
if (documentHash.empty()) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = SYNC_FAILED;
|
|
||||||
statusMessage = "Failed to calculate document hash";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Serial.printf("[%lu] [KOSync] Document hash: %s\n", millis(), documentHash.c_str());
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
statusMessage = "Fetching remote progress...";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
// Fetch remote progress
|
|
||||||
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
|
||||||
|
|
||||||
if (result == KOReaderSyncClient::NOT_FOUND) {
|
|
||||||
// No remote progress - offer to upload
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = NO_REMOTE_PROGRESS;
|
|
||||||
hasRemoteProgress = false;
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result != KOReaderSyncClient::OK) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = SYNC_FAILED;
|
|
||||||
statusMessage = KOReaderSyncClient::errorString(result);
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert remote progress to CrossPoint position
|
|
||||||
hasRemoteProgress = true;
|
|
||||||
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
|
|
||||||
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, totalPagesInSpine);
|
|
||||||
|
|
||||||
// Calculate local progress in KOReader format (for display)
|
|
||||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
|
|
||||||
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = SHOWING_RESULT;
|
|
||||||
selectedOption = 0; // Default to "Apply"
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::performUpload() {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = UPLOADING;
|
|
||||||
statusMessage = "Uploading progress...";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
// Convert current position to KOReader format
|
|
||||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
|
|
||||||
KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos);
|
|
||||||
|
|
||||||
KOReaderProgress progress;
|
|
||||||
progress.document = documentHash;
|
|
||||||
progress.progress = koPos.xpath;
|
|
||||||
progress.percentage = koPos.percentage;
|
|
||||||
|
|
||||||
const auto result = KOReaderSyncClient::updateProgress(progress);
|
|
||||||
|
|
||||||
if (result != KOReaderSyncClient::OK) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = SYNC_FAILED;
|
|
||||||
statusMessage = KOReaderSyncClient::errorString(result);
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = UPLOAD_COMPLETE;
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::onEnter() {
|
|
||||||
ActivityWithSubactivity::onEnter();
|
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
|
||||||
|
|
||||||
xTaskCreate(&KOReaderSyncActivity::taskTrampoline, "KOSyncTask",
|
|
||||||
4096, // Stack size (larger for network operations)
|
|
||||||
this, // Parameters
|
|
||||||
1, // Priority
|
|
||||||
&displayTaskHandle // Task handle
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for credentials first
|
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
|
||||||
state = NO_CREDENTIALS;
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn on WiFi
|
|
||||||
Serial.printf("[%lu] [KOSync] Turning on WiFi...\n", millis());
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
|
|
||||||
// Check if already connected
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
Serial.printf("[%lu] [KOSync] Already connected to WiFi\n", millis());
|
|
||||||
state = SYNCING;
|
|
||||||
statusMessage = "Syncing time...";
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
// Perform sync directly (will be handled in loop)
|
|
||||||
xTaskCreate(
|
|
||||||
[](void* param) {
|
|
||||||
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
|
||||||
// Sync time first
|
|
||||||
syncTimeWithNTP();
|
|
||||||
xSemaphoreTake(self->renderingMutex, portMAX_DELAY);
|
|
||||||
self->statusMessage = "Calculating document hash...";
|
|
||||||
xSemaphoreGive(self->renderingMutex);
|
|
||||||
self->updateRequired = true;
|
|
||||||
self->performSync();
|
|
||||||
vTaskDelete(nullptr);
|
|
||||||
},
|
|
||||||
"SyncTask", 4096, this, 1, nullptr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch WiFi selection subactivity
|
|
||||||
Serial.printf("[%lu] [KOSync] Launching WifiSelectionActivity...\n", millis());
|
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::onExit() {
|
|
||||||
ActivityWithSubactivity::onExit();
|
|
||||||
|
|
||||||
// Turn off wifi
|
|
||||||
WiFi.disconnect(false);
|
|
||||||
delay(100);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
delay(100);
|
|
||||||
|
|
||||||
// Wait until not rendering to delete task
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
|
||||||
vTaskDelete(displayTaskHandle);
|
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::displayTaskLoop() {
|
|
||||||
while (true) {
|
|
||||||
if (updateRequired) {
|
|
||||||
updateRequired = false;
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
render();
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::render() {
|
|
||||||
if (subActivity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
|
||||||
|
|
||||||
renderer.clearScreen();
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
if (state == NO_CREDENTIALS) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No credentials configured", true, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Set up KOReader account in Settings");
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == SYNCING || state == UPLOADING) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == SHOWING_RESULT) {
|
|
||||||
// Show comparison
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, "Progress found!", true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
// Get chapter names from TOC
|
|
||||||
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
|
|
||||||
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
|
||||||
const std::string remoteChapter = (remoteTocIndex >= 0)
|
|
||||||
? epub->getTocItem(remoteTocIndex).title
|
|
||||||
: ("Section " + std::to_string(remotePosition.spineIndex + 1));
|
|
||||||
const std::string localChapter = (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
|
|
||||||
: ("Section " + std::to_string(currentSpineIndex + 1));
|
|
||||||
|
|
||||||
// Remote progress - chapter and page
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 160, "Remote:", true);
|
|
||||||
char remoteChapterStr[128];
|
|
||||||
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr);
|
|
||||||
char remotePageStr[64];
|
|
||||||
snprintf(remotePageStr, sizeof(remotePageStr), " Page %d, %.2f%% overall", remotePosition.pageNumber + 1,
|
|
||||||
remoteProgress.percentage * 100);
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr);
|
|
||||||
|
|
||||||
if (!remoteProgress.device.empty()) {
|
|
||||||
char deviceStr[64];
|
|
||||||
snprintf(deviceStr, sizeof(deviceStr), " From: %s", remoteProgress.device.c_str());
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local progress - chapter and page
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 270, "Local:", true);
|
|
||||||
char localChapterStr[128];
|
|
||||||
snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str());
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr);
|
|
||||||
char localPageStr[64];
|
|
||||||
snprintf(localPageStr, sizeof(localPageStr), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine,
|
|
||||||
localProgress.percentage * 100);
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
|
|
||||||
|
|
||||||
// Options
|
|
||||||
const int optionY = 350;
|
|
||||||
const int optionHeight = 30;
|
|
||||||
|
|
||||||
// Apply option
|
|
||||||
if (selectedOption == 0) {
|
|
||||||
renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight);
|
|
||||||
}
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, optionY, "Apply remote progress", selectedOption != 0);
|
|
||||||
|
|
||||||
// Upload option
|
|
||||||
if (selectedOption == 1) {
|
|
||||||
renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight);
|
|
||||||
}
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, "Upload local progress", selectedOption != 1);
|
|
||||||
|
|
||||||
// Cancel option
|
|
||||||
if (selectedOption == 2) {
|
|
||||||
renderer.fillRect(0, optionY + optionHeight * 2 - 2, pageWidth - 1, optionHeight);
|
|
||||||
}
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight * 2, "Cancel", selectedOption != 2);
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("", "Select", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == NO_REMOTE_PROGRESS) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?");
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Cancel", "Upload", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == UPLOAD_COMPLETE) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Progress uploaded!", true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == SYNC_FAILED) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Sync failed", true, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::loop() {
|
|
||||||
if (subActivity) {
|
|
||||||
subActivity->loop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == SHOWING_RESULT) {
|
|
||||||
// Navigate options
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
|
||||||
selectedOption = (selectedOption + 2) % 3; // Wrap around
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedOption = (selectedOption + 1) % 3;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
||||||
if (selectedOption == 0) {
|
|
||||||
// Apply remote progress
|
|
||||||
onSyncComplete(remotePosition.spineIndex, remotePosition.pageNumber);
|
|
||||||
} else if (selectedOption == 1) {
|
|
||||||
// Upload local progress
|
|
||||||
performUpload();
|
|
||||||
} else {
|
|
||||||
// Cancel
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == NO_REMOTE_PROGRESS) {
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
||||||
// Calculate hash if not done yet
|
|
||||||
if (documentHash.empty()) {
|
|
||||||
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
|
|
||||||
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
|
|
||||||
} else {
|
|
||||||
documentHash = KOReaderDocumentId::calculate(epubPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
performUpload();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <Epub.h>
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/semphr.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "KOReaderSyncClient.h"
|
|
||||||
#include "ProgressMapper.h"
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity for syncing reading progress with KOReader sync server.
|
|
||||||
*
|
|
||||||
* Flow:
|
|
||||||
* 1. Connect to WiFi (if not connected)
|
|
||||||
* 2. Calculate document hash
|
|
||||||
* 3. Fetch remote progress
|
|
||||||
* 4. Show comparison and options (Apply/Upload/Cancel)
|
|
||||||
* 5. Apply or upload progress
|
|
||||||
*/
|
|
||||||
class KOReaderSyncActivity final : public ActivityWithSubactivity {
|
|
||||||
public:
|
|
||||||
using OnCancelCallback = std::function<void()>;
|
|
||||||
using OnSyncCompleteCallback = std::function<void(int newSpineIndex, int newPageNumber)>;
|
|
||||||
|
|
||||||
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
||||||
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
|
|
||||||
int currentPage, int totalPagesInSpine, OnCancelCallback onCancel,
|
|
||||||
OnSyncCompleteCallback onSyncComplete)
|
|
||||||
: ActivityWithSubactivity("KOReaderSync", renderer, mappedInput),
|
|
||||||
epub(epub),
|
|
||||||
epubPath(epubPath),
|
|
||||||
currentSpineIndex(currentSpineIndex),
|
|
||||||
currentPage(currentPage),
|
|
||||||
totalPagesInSpine(totalPagesInSpine),
|
|
||||||
remoteProgress{},
|
|
||||||
remotePosition{},
|
|
||||||
localProgress{},
|
|
||||||
onCancel(std::move(onCancel)),
|
|
||||||
onSyncComplete(std::move(onSyncComplete)) {}
|
|
||||||
|
|
||||||
void onEnter() override;
|
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
|
||||||
bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum State {
|
|
||||||
WIFI_SELECTION,
|
|
||||||
CONNECTING,
|
|
||||||
SYNCING,
|
|
||||||
SHOWING_RESULT,
|
|
||||||
UPLOADING,
|
|
||||||
UPLOAD_COMPLETE,
|
|
||||||
NO_REMOTE_PROGRESS,
|
|
||||||
SYNC_FAILED,
|
|
||||||
NO_CREDENTIALS
|
|
||||||
};
|
|
||||||
|
|
||||||
std::shared_ptr<Epub> epub;
|
|
||||||
std::string epubPath;
|
|
||||||
int currentSpineIndex;
|
|
||||||
int currentPage;
|
|
||||||
int totalPagesInSpine;
|
|
||||||
|
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
|
||||||
bool updateRequired = false;
|
|
||||||
|
|
||||||
State state = WIFI_SELECTION;
|
|
||||||
std::string statusMessage;
|
|
||||||
std::string documentHash;
|
|
||||||
|
|
||||||
// Remote progress data
|
|
||||||
bool hasRemoteProgress = false;
|
|
||||||
KOReaderProgress remoteProgress;
|
|
||||||
CrossPointPosition remotePosition;
|
|
||||||
|
|
||||||
// Local progress as KOReader format (for display)
|
|
||||||
KOReaderPosition localProgress;
|
|
||||||
|
|
||||||
// Selection in result screen (0=Apply, 1=Upload, 2=Cancel)
|
|
||||||
int selectedOption = 0;
|
|
||||||
|
|
||||||
OnCancelCallback onCancel;
|
|
||||||
OnSyncCompleteCallback onSyncComplete;
|
|
||||||
|
|
||||||
void onWifiSelectionComplete(bool success);
|
|
||||||
void performSync();
|
|
||||||
void performUpload();
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
void render();
|
|
||||||
};
|
|
||||||
@ -8,7 +8,6 @@
|
|||||||
#include "CalibreSettingsActivity.h"
|
#include "CalibreSettingsActivity.h"
|
||||||
#include "ClearCacheActivity.h"
|
#include "ClearCacheActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "KOReaderSettingsActivity.h"
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@ -18,6 +17,33 @@ void CategorySettingsActivity::taskTrampoline(void* param) {
|
|||||||
self->displayTaskLoop();
|
self->displayTaskLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int CategorySettingsActivity::getVisibleSettingsCount() const {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < settingsCount; i++) {
|
||||||
|
if (settingsList[i].shouldShow()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int CategorySettingsActivity::getActualIndex(int visibleIndex) const {
|
||||||
|
int visibleCount = 0;
|
||||||
|
for (int i = 0; i < settingsCount; i++) {
|
||||||
|
if (settingsList[i].shouldShow()) {
|
||||||
|
if (visibleCount == visibleIndex) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
visibleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0; // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingInfo& CategorySettingsActivity::getVisibleSetting(int visibleIndex) const {
|
||||||
|
return settingsList[getActualIndex(visibleIndex)];
|
||||||
|
}
|
||||||
|
|
||||||
void CategorySettingsActivity::onEnter() {
|
void CategorySettingsActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
@ -61,24 +87,28 @@ void CategorySettingsActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation
|
// Handle navigation (using visible settings count)
|
||||||
|
const int visibleCount = getVisibleSettingsCount();
|
||||||
|
if (visibleCount > 0) {
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
|
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (visibleCount - 1);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||||
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
selectedSettingIndex = (selectedSettingIndex < visibleCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CategorySettingsActivity::toggleCurrentSetting() {
|
void CategorySettingsActivity::toggleCurrentSetting() {
|
||||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
const int visibleCount = getVisibleSettingsCount();
|
||||||
|
if (selectedSettingIndex < 0 || selectedSettingIndex >= visibleCount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& setting = settingsList[selectedSettingIndex];
|
const auto& setting = getVisibleSetting(selectedSettingIndex);
|
||||||
|
|
||||||
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
||||||
// Toggle the boolean value using the member pointer
|
// Toggle the boolean value using the member pointer
|
||||||
@ -95,15 +125,7 @@ void CategorySettingsActivity::toggleCurrentSetting() {
|
|||||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||||
}
|
}
|
||||||
} else if (setting.type == SettingType::ACTION) {
|
} else if (setting.type == SettingType::ACTION) {
|
||||||
if (strcmp(setting.name, "KOReader Sync") == 0) {
|
if (strcmp(setting.name, "Calibre Settings") == 0) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
} else if (strcmp(setting.name, "Calibre Settings") == 0) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
||||||
@ -158,10 +180,15 @@ void CategorySettingsActivity::render() const {
|
|||||||
// Draw selection highlight
|
// Draw selection highlight
|
||||||
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
||||||
|
|
||||||
// Draw all settings
|
// Draw only visible settings
|
||||||
|
int visibleIndex = 0;
|
||||||
for (int i = 0; i < settingsCount; i++) {
|
for (int i = 0; i < settingsCount; i++) {
|
||||||
const int settingY = 60 + i * 30; // 30 pixels between settings
|
if (!settingsList[i].shouldShow()) {
|
||||||
const bool isSelected = (i == selectedSettingIndex);
|
continue; // Skip hidden settings
|
||||||
|
}
|
||||||
|
|
||||||
|
const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings
|
||||||
|
const bool isSelected = (visibleIndex == selectedSettingIndex);
|
||||||
|
|
||||||
// Draw setting name
|
// Draw setting name
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
|
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
|
||||||
@ -173,7 +200,9 @@ void CategorySettingsActivity::render() const {
|
|||||||
valueText = value ? "ON" : "OFF";
|
valueText = value ? "ON" : "OFF";
|
||||||
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
||||||
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
||||||
|
if (value < settingsList[i].enumValues.size()) {
|
||||||
valueText = settingsList[i].enumValues[value];
|
valueText = settingsList[i].enumValues[value];
|
||||||
|
}
|
||||||
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
|
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
|
||||||
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
||||||
}
|
}
|
||||||
@ -181,6 +210,8 @@ void CategorySettingsActivity::render() const {
|
|||||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||||
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected);
|
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visibleIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||||
|
|||||||
@ -26,18 +26,29 @@ struct SettingInfo {
|
|||||||
};
|
};
|
||||||
ValueRange valueRange;
|
ValueRange valueRange;
|
||||||
|
|
||||||
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) {
|
// Optional visibility callback - if set and returns false, setting is hidden
|
||||||
return {name, SettingType::TOGGLE, ptr};
|
std::function<bool()> isVisible;
|
||||||
|
|
||||||
|
// Check if this setting should be displayed
|
||||||
|
bool shouldShow() const { return !isVisible || isVisible(); }
|
||||||
|
|
||||||
|
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr,
|
||||||
|
std::function<bool()> visible = nullptr) {
|
||||||
|
return {name, SettingType::TOGGLE, ptr, {}, {}, std::move(visible)};
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
|
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values,
|
||||||
return {name, SettingType::ENUM, ptr, std::move(values)};
|
std::function<bool()> visible = nullptr) {
|
||||||
|
return {name, SettingType::ENUM, ptr, std::move(values), {}, std::move(visible)};
|
||||||
}
|
}
|
||||||
|
|
||||||
static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; }
|
static SettingInfo Action(const char* name, std::function<bool()> visible = nullptr) {
|
||||||
|
return {name, SettingType::ACTION, nullptr, {}, {}, std::move(visible)};
|
||||||
|
}
|
||||||
|
|
||||||
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
|
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
|
||||||
return {name, SettingType::VALUE, ptr, {}, valueRange};
|
std::function<bool()> visible = nullptr) {
|
||||||
|
return {name, SettingType::VALUE, ptr, {}, valueRange, std::move(visible)};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,12 +56,17 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
|
|||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
int selectedSettingIndex = 0;
|
int selectedSettingIndex = 0; // Index into visible settings
|
||||||
const char* categoryName;
|
const char* categoryName;
|
||||||
const SettingInfo* settingsList;
|
const SettingInfo* settingsList;
|
||||||
int settingsCount;
|
int settingsCount;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
|
|
||||||
|
// Helper methods for visible settings navigation
|
||||||
|
int getVisibleSettingsCount() const;
|
||||||
|
int getActualIndex(int visibleIndex) const; // Convert visible index to actual index
|
||||||
|
const SettingInfo& getVisibleSetting(int visibleIndex) const;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void render() const;
|
void render() const;
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
#include "KOReaderAuthActivity.h"
|
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
|
||||||
#include <WiFi.h>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "KOReaderSyncClient.h"
|
|
||||||
#include "MappedInputManager.h"
|
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
|
||||||
#include "fontIds.h"
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::taskTrampoline(void* param) {
|
|
||||||
auto* self = static_cast<KOReaderAuthActivity*>(param);
|
|
||||||
self->displayTaskLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
|
|
||||||
exitActivity();
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = FAILED;
|
|
||||||
errorMessage = "WiFi connection failed";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
state = AUTHENTICATING;
|
|
||||||
statusMessage = "Authenticating...";
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
performAuthentication();
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::performAuthentication() {
|
|
||||||
const auto result = KOReaderSyncClient::authenticate();
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (result == KOReaderSyncClient::OK) {
|
|
||||||
state = SUCCESS;
|
|
||||||
statusMessage = "Successfully authenticated!";
|
|
||||||
} else {
|
|
||||||
state = FAILED;
|
|
||||||
errorMessage = KOReaderSyncClient::errorString(result);
|
|
||||||
}
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::onEnter() {
|
|
||||||
ActivityWithSubactivity::onEnter();
|
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
|
||||||
|
|
||||||
xTaskCreate(&KOReaderAuthActivity::taskTrampoline, "KOAuthTask",
|
|
||||||
4096, // Stack size
|
|
||||||
this, // Parameters
|
|
||||||
1, // Priority
|
|
||||||
&displayTaskHandle // Task handle
|
|
||||||
);
|
|
||||||
|
|
||||||
// Turn on WiFi
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
|
|
||||||
// Check if already connected
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
|
||||||
state = AUTHENTICATING;
|
|
||||||
statusMessage = "Authenticating...";
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
// Perform authentication in a separate task
|
|
||||||
xTaskCreate(
|
|
||||||
[](void* param) {
|
|
||||||
auto* self = static_cast<KOReaderAuthActivity*>(param);
|
|
||||||
self->performAuthentication();
|
|
||||||
vTaskDelete(nullptr);
|
|
||||||
},
|
|
||||||
"AuthTask", 4096, this, 1, nullptr);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launch WiFi selection
|
|
||||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
|
||||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::onExit() {
|
|
||||||
ActivityWithSubactivity::onExit();
|
|
||||||
|
|
||||||
// Turn off wifi
|
|
||||||
WiFi.disconnect(false);
|
|
||||||
delay(100);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
delay(100);
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
|
||||||
vTaskDelete(displayTaskHandle);
|
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::displayTaskLoop() {
|
|
||||||
while (true) {
|
|
||||||
if (updateRequired && !subActivity) {
|
|
||||||
updateRequired = false;
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
render();
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::render() {
|
|
||||||
if (subActivity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.clearScreen();
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
if (state == AUTHENTICATING) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == SUCCESS) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use");
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Done", "", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == FAILED) {
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
|
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderAuthActivity::loop() {
|
|
||||||
if (subActivity) {
|
|
||||||
subActivity->loop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == SUCCESS || state == FAILED) {
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
||||||
onComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/semphr.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity for testing KOReader credentials.
|
|
||||||
* Connects to WiFi and authenticates with the KOReader sync server.
|
|
||||||
*/
|
|
||||||
class KOReaderAuthActivity final : public ActivityWithSubactivity {
|
|
||||||
public:
|
|
||||||
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
||||||
const std::function<void()>& onComplete)
|
|
||||||
: ActivityWithSubactivity("KOReaderAuth", renderer, mappedInput), onComplete(onComplete) {}
|
|
||||||
|
|
||||||
void onEnter() override;
|
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
|
||||||
bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED };
|
|
||||||
|
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
|
||||||
bool updateRequired = false;
|
|
||||||
|
|
||||||
State state = WIFI_SELECTION;
|
|
||||||
std::string statusMessage;
|
|
||||||
std::string errorMessage;
|
|
||||||
|
|
||||||
const std::function<void()> onComplete;
|
|
||||||
|
|
||||||
void onWifiSelectionComplete(bool success);
|
|
||||||
void performAuthentication();
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
void render();
|
|
||||||
};
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
#include "KOReaderSettingsActivity.h"
|
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include "KOReaderAuthActivity.h"
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "MappedInputManager.h"
|
|
||||||
#include "activities/util/KeyboardEntryActivity.h"
|
|
||||||
#include "fontIds.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr int MENU_ITEMS = 5;
|
|
||||||
const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"};
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::taskTrampoline(void* param) {
|
|
||||||
auto* self = static_cast<KOReaderSettingsActivity*>(param);
|
|
||||||
self->displayTaskLoop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::onEnter() {
|
|
||||||
ActivityWithSubactivity::onEnter();
|
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
|
||||||
selectedIndex = 0;
|
|
||||||
updateRequired = true;
|
|
||||||
|
|
||||||
xTaskCreate(&KOReaderSettingsActivity::taskTrampoline, "KOReaderSettingsTask",
|
|
||||||
4096, // Stack size
|
|
||||||
this, // Parameters
|
|
||||||
1, // Priority
|
|
||||||
&displayTaskHandle // Task handle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::onExit() {
|
|
||||||
ActivityWithSubactivity::onExit();
|
|
||||||
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
if (displayTaskHandle) {
|
|
||||||
vTaskDelete(displayTaskHandle);
|
|
||||||
displayTaskHandle = nullptr;
|
|
||||||
}
|
|
||||||
vSemaphoreDelete(renderingMutex);
|
|
||||||
renderingMutex = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::loop() {
|
|
||||||
if (subActivity) {
|
|
||||||
subActivity->loop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
||||||
onBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
||||||
handleSelection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
|
||||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
|
||||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
|
||||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
|
||||||
updateRequired = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::handleSelection() {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
|
|
||||||
if (selectedIndex == 0) {
|
|
||||||
// Username
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KeyboardEntryActivity(
|
|
||||||
renderer, mappedInput, "KOReader Username", KOREADER_STORE.getUsername(), 10,
|
|
||||||
64, // maxLength
|
|
||||||
false, // not password
|
|
||||||
[this](const std::string& username) {
|
|
||||||
KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword());
|
|
||||||
KOREADER_STORE.saveToFile();
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this]() {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
} else if (selectedIndex == 1) {
|
|
||||||
// Password
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KeyboardEntryActivity(
|
|
||||||
renderer, mappedInput, "KOReader Password", KOREADER_STORE.getPassword(), 10,
|
|
||||||
64, // maxLength
|
|
||||||
false, // show characters
|
|
||||||
[this](const std::string& password) {
|
|
||||||
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password);
|
|
||||||
KOREADER_STORE.saveToFile();
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this]() {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
} else if (selectedIndex == 2) {
|
|
||||||
// Sync Server URL - prefill with https:// if empty to save typing
|
|
||||||
const std::string currentUrl = KOREADER_STORE.getServerUrl();
|
|
||||||
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KeyboardEntryActivity(
|
|
||||||
renderer, mappedInput, "Sync Server URL", prefillUrl, 10,
|
|
||||||
128, // maxLength - URLs can be long
|
|
||||||
false, // not password
|
|
||||||
[this](const std::string& url) {
|
|
||||||
// Clear if user just left the prefilled https://
|
|
||||||
const std::string urlToSave = (url == "https://" || url == "http://") ? "" : url;
|
|
||||||
KOREADER_STORE.setServerUrl(urlToSave);
|
|
||||||
KOREADER_STORE.saveToFile();
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this]() {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
} else if (selectedIndex == 3) {
|
|
||||||
// Document Matching - toggle between Filename and Binary
|
|
||||||
const auto current = KOREADER_STORE.getMatchMethod();
|
|
||||||
const auto newMethod =
|
|
||||||
(current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME;
|
|
||||||
KOREADER_STORE.setMatchMethod(newMethod);
|
|
||||||
KOREADER_STORE.saveToFile();
|
|
||||||
updateRequired = true;
|
|
||||||
} else if (selectedIndex == 4) {
|
|
||||||
// Authenticate
|
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
|
||||||
// Can't authenticate without credentials - just show message briefly
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] {
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::displayTaskLoop() {
|
|
||||||
while (true) {
|
|
||||||
if (updateRequired && !subActivity) {
|
|
||||||
updateRequired = false;
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
render();
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void KOReaderSettingsActivity::render() {
|
|
||||||
renderer.clearScreen();
|
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
|
||||||
|
|
||||||
// Draw header
|
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD);
|
|
||||||
|
|
||||||
// Draw selection highlight
|
|
||||||
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30);
|
|
||||||
|
|
||||||
// Draw menu items
|
|
||||||
for (int i = 0; i < MENU_ITEMS; i++) {
|
|
||||||
const int settingY = 60 + i * 30;
|
|
||||||
const bool isSelected = (i == selectedIndex);
|
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected);
|
|
||||||
|
|
||||||
// Draw status for each item
|
|
||||||
const char* status = "";
|
|
||||||
if (i == 0) {
|
|
||||||
status = KOREADER_STORE.getUsername().empty() ? "[Not Set]" : "[Set]";
|
|
||||||
} else if (i == 1) {
|
|
||||||
status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]";
|
|
||||||
} else if (i == 2) {
|
|
||||||
status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]";
|
|
||||||
} else if (i == 3) {
|
|
||||||
status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]";
|
|
||||||
} else if (i == 4) {
|
|
||||||
status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]";
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
|
|
||||||
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw button hints
|
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
||||||
|
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <freertos/FreeRTOS.h>
|
|
||||||
#include <freertos/semphr.h>
|
|
||||||
#include <freertos/task.h>
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "activities/ActivityWithSubactivity.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submenu for KOReader Sync settings.
|
|
||||||
* Shows username, password, and authenticate options.
|
|
||||||
*/
|
|
||||||
class KOReaderSettingsActivity final : public ActivityWithSubactivity {
|
|
||||||
public:
|
|
||||||
explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
|
||||||
const std::function<void()>& onBack)
|
|
||||||
: ActivityWithSubactivity("KOReaderSettings", renderer, mappedInput), onBack(onBack) {}
|
|
||||||
|
|
||||||
void onEnter() override;
|
|
||||||
void onExit() override;
|
|
||||||
void loop() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
|
||||||
bool updateRequired = false;
|
|
||||||
|
|
||||||
int selectedIndex = 0;
|
|
||||||
const std::function<void()> onBack;
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
|
||||||
[[noreturn]] void displayTaskLoop();
|
|
||||||
void render();
|
|
||||||
void handleSelection();
|
|
||||||
};
|
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
#include <builtinFonts/custom/customFonts.h>
|
||||||
|
|
||||||
#include "CategorySettingsActivity.h"
|
#include "CategorySettingsActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@ -21,9 +22,38 @@ const SettingInfo displaySettings[displaySettingsCount] = {
|
|||||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
|
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
|
||||||
|
|
||||||
constexpr int readerSettingsCount = 9;
|
// Helper to get custom font names as a vector
|
||||||
|
std::vector<std::string> getCustomFontNamesVector() {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
#if CUSTOM_FONT_COUNT > 0
|
||||||
|
for (int i = 0; i < CUSTOM_FONT_COUNT; i++) {
|
||||||
|
names.push_back(CUSTOM_FONT_NAMES[i]);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
names.push_back("(none)"); // Placeholder when no custom fonts
|
||||||
|
#endif
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visibility condition for custom font settings
|
||||||
|
bool isCustomFontSelected() { return SETTINGS.fontFamily == CrossPointSettings::CUSTOM_FONT; }
|
||||||
|
|
||||||
|
// Font family options - includes "Custom" only if custom fonts are available
|
||||||
|
std::vector<std::string> getFontFamilyOptions() {
|
||||||
|
std::vector<std::string> options = {"Bookerly", "Noto Sans"};
|
||||||
|
#if CUSTOM_FONT_COUNT > 0
|
||||||
|
options.push_back("Custom");
|
||||||
|
#endif
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int readerSettingsCount = 11;
|
||||||
const SettingInfo readerSettings[readerSettingsCount] = {
|
const SettingInfo readerSettings[readerSettingsCount] = {
|
||||||
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
|
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, getFontFamilyOptions()),
|
||||||
|
SettingInfo::Enum("Custom Font", &CrossPointSettings::customFontIndex, getCustomFontNamesVector(),
|
||||||
|
isCustomFontSelected),
|
||||||
|
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily,
|
||||||
|
{"Bookerly", "Noto Sans"}, isCustomFontSelected),
|
||||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
||||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
||||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
||||||
@ -45,11 +75,11 @@ const SettingInfo controlsSettings[controlsSettingsCount] = {
|
|||||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn,
|
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn,
|
||||||
{"Ignore", "Sleep", "Page Turn", "Dictionary"})};
|
{"Ignore", "Sleep", "Page Turn", "Dictionary"})};
|
||||||
|
|
||||||
constexpr int systemSettingsCount = 5;
|
constexpr int systemSettingsCount = 4;
|
||||||
const SettingInfo systemSettings[systemSettingsCount] = {
|
const SettingInfo systemSettings[systemSettingsCount] = {
|
||||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||||
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
|
SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
|
||||||
SettingInfo::Action("Check for updates")};
|
SettingInfo::Action("Check for updates")};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
43
src/customFonts.cpp
Normal file
43
src/customFonts.cpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Generated by convert-builtin-fonts.sh
|
||||||
|
* Custom font definitions
|
||||||
|
*/
|
||||||
|
#include <builtinFonts/custom/customFonts.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
// EpdFont definitions for custom fonts
|
||||||
|
EpdFont fernmicro12RegularFont(&fernmicro_12_regular);
|
||||||
|
EpdFont fernmicro12ItalicFont(&fernmicro_12_italic);
|
||||||
|
EpdFont fernmicro12BoldFont(&fernmicro_12_bold);
|
||||||
|
EpdFont fernmicro12BoldItalicFont(&fernmicro_12_bolditalic);
|
||||||
|
EpdFont fernmicro14RegularFont(&fernmicro_14_regular);
|
||||||
|
EpdFont fernmicro14ItalicFont(&fernmicro_14_italic);
|
||||||
|
EpdFont fernmicro14BoldFont(&fernmicro_14_bold);
|
||||||
|
EpdFont fernmicro14BoldItalicFont(&fernmicro_14_bolditalic);
|
||||||
|
EpdFont fernmicro16RegularFont(&fernmicro_16_regular);
|
||||||
|
EpdFont fernmicro16ItalicFont(&fernmicro_16_italic);
|
||||||
|
EpdFont fernmicro16BoldFont(&fernmicro_16_bold);
|
||||||
|
EpdFont fernmicro16BoldItalicFont(&fernmicro_16_bolditalic);
|
||||||
|
EpdFont fernmicro18RegularFont(&fernmicro_18_regular);
|
||||||
|
EpdFont fernmicro18ItalicFont(&fernmicro_18_italic);
|
||||||
|
EpdFont fernmicro18BoldFont(&fernmicro_18_bold);
|
||||||
|
EpdFont fernmicro18BoldItalicFont(&fernmicro_18_bolditalic);
|
||||||
|
|
||||||
|
// EpdFontFamily definitions for custom fonts
|
||||||
|
EpdFontFamily fernmicro12FontFamily(&fernmicro12RegularFont, &fernmicro12BoldFont, &fernmicro12ItalicFont, &fernmicro12BoldItalicFont);
|
||||||
|
EpdFontFamily fernmicro14FontFamily(&fernmicro14RegularFont, &fernmicro14BoldFont, &fernmicro14ItalicFont, &fernmicro14BoldItalicFont);
|
||||||
|
EpdFontFamily fernmicro16FontFamily(&fernmicro16RegularFont, &fernmicro16BoldFont, &fernmicro16ItalicFont, &fernmicro16BoldItalicFont);
|
||||||
|
EpdFontFamily fernmicro18FontFamily(&fernmicro18RegularFont, &fernmicro18BoldFont, &fernmicro18ItalicFont, &fernmicro18BoldItalicFont);
|
||||||
|
|
||||||
|
void registerCustomFonts(GfxRenderer& renderer) {
|
||||||
|
#if CUSTOM_FONT_COUNT > 0
|
||||||
|
renderer.insertFont(FERNMICRO_12_FONT_ID, fernmicro12FontFamily);
|
||||||
|
renderer.insertFont(FERNMICRO_14_FONT_ID, fernmicro14FontFamily);
|
||||||
|
renderer.insertFont(FERNMICRO_16_FONT_ID, fernmicro16FontFamily);
|
||||||
|
renderer.insertFont(FERNMICRO_18_FONT_ID, fernmicro18FontFamily);
|
||||||
|
#else
|
||||||
|
(void)renderer; // Suppress unused parameter warning
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
@ -9,10 +9,18 @@
|
|||||||
#define NOTOSANS_14_FONT_ID (-1014561631)
|
#define NOTOSANS_14_FONT_ID (-1014561631)
|
||||||
#define NOTOSANS_16_FONT_ID (-1422711852)
|
#define NOTOSANS_16_FONT_ID (-1422711852)
|
||||||
#define NOTOSANS_18_FONT_ID (1237754772)
|
#define NOTOSANS_18_FONT_ID (1237754772)
|
||||||
#define OPENDYSLEXIC_8_FONT_ID (1331369208)
|
|
||||||
#define OPENDYSLEXIC_10_FONT_ID (-1374689004)
|
|
||||||
#define OPENDYSLEXIC_12_FONT_ID (-795539541)
|
|
||||||
#define OPENDYSLEXIC_14_FONT_ID (-1676627620)
|
|
||||||
#define UI_10_FONT_ID (-1246724383)
|
#define UI_10_FONT_ID (-1246724383)
|
||||||
#define UI_12_FONT_ID (-359249323)
|
#define UI_12_FONT_ID (-359249323)
|
||||||
#define SMALL_FONT_ID (1073217904)
|
#define SMALL_FONT_ID (1073217904)
|
||||||
|
|
||||||
|
// Custom font IDs
|
||||||
|
#define FERNMICRO_12_FONT_ID (-887081534)
|
||||||
|
#define FERNMICRO_14_FONT_ID (1772642890)
|
||||||
|
#define FERNMICRO_16_FONT_ID (1785643080)
|
||||||
|
#define FERNMICRO_18_FONT_ID (-45070249)
|
||||||
|
|
||||||
|
// Custom font ID lookup array: CUSTOM_FONT_IDS[fontIndex][sizeIndex]
|
||||||
|
// Size indices: 0=12pt, 1=14pt, 2=16pt, 3=18pt
|
||||||
|
static const int CUSTOM_FONT_IDS[][4] = {
|
||||||
|
{FERNMICRO_12_FONT_ID, FERNMICRO_14_FONT_ID, FERNMICRO_16_FONT_ID, FERNMICRO_18_FONT_ID},
|
||||||
|
};
|
||||||
|
|||||||
34
src/main.cpp
34
src/main.cpp
@ -12,7 +12,6 @@
|
|||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "activities/boot_sleep/BootActivity.h"
|
#include "activities/boot_sleep/BootActivity.h"
|
||||||
@ -96,31 +95,6 @@ EpdFont notosans18ItalicFont(¬osans_18_italic);
|
|||||||
EpdFont notosans18BoldItalicFont(¬osans_18_bolditalic);
|
EpdFont notosans18BoldItalicFont(¬osans_18_bolditalic);
|
||||||
EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, ¬osans18ItalicFont,
|
EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, ¬osans18ItalicFont,
|
||||||
¬osans18BoldItalicFont);
|
¬osans18BoldItalicFont);
|
||||||
|
|
||||||
EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular);
|
|
||||||
EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold);
|
|
||||||
EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic);
|
|
||||||
EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic);
|
|
||||||
EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, &opendyslexic8BoldFont, &opendyslexic8ItalicFont,
|
|
||||||
&opendyslexic8BoldItalicFont);
|
|
||||||
EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular);
|
|
||||||
EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold);
|
|
||||||
EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic);
|
|
||||||
EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic);
|
|
||||||
EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, &opendyslexic10BoldFont, &opendyslexic10ItalicFont,
|
|
||||||
&opendyslexic10BoldItalicFont);
|
|
||||||
EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular);
|
|
||||||
EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold);
|
|
||||||
EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic);
|
|
||||||
EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic);
|
|
||||||
EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, &opendyslexic12BoldFont, &opendyslexic12ItalicFont,
|
|
||||||
&opendyslexic12BoldItalicFont);
|
|
||||||
EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular);
|
|
||||||
EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold);
|
|
||||||
EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic);
|
|
||||||
EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic);
|
|
||||||
EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, &opendyslexic14BoldFont, &opendyslexic14ItalicFont,
|
|
||||||
&opendyslexic14BoldItalicFont);
|
|
||||||
#endif // OMIT_FONTS
|
#endif // OMIT_FONTS
|
||||||
|
|
||||||
EpdFont smallFont(¬osans_8_regular);
|
EpdFont smallFont(¬osans_8_regular);
|
||||||
@ -265,10 +239,9 @@ void setupDisplayAndFonts() {
|
|||||||
renderer.insertFont(NOTOSANS_14_FONT_ID, notosans14FontFamily);
|
renderer.insertFont(NOTOSANS_14_FONT_ID, notosans14FontFamily);
|
||||||
renderer.insertFont(NOTOSANS_16_FONT_ID, notosans16FontFamily);
|
renderer.insertFont(NOTOSANS_16_FONT_ID, notosans16FontFamily);
|
||||||
renderer.insertFont(NOTOSANS_18_FONT_ID, notosans18FontFamily);
|
renderer.insertFont(NOTOSANS_18_FONT_ID, notosans18FontFamily);
|
||||||
renderer.insertFont(OPENDYSLEXIC_8_FONT_ID, opendyslexic8FontFamily);
|
|
||||||
renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily);
|
// Register custom fonts (if any exist)
|
||||||
renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily);
|
registerCustomFonts(renderer);
|
||||||
renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily);
|
|
||||||
#endif // OMIT_FONTS
|
#endif // OMIT_FONTS
|
||||||
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
|
||||||
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
||||||
@ -320,7 +293,6 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SETTINGS.loadFromFile();
|
SETTINGS.loadFromFile();
|
||||||
KOREADER_STORE.loadFromFile();
|
|
||||||
|
|
||||||
if (!isWakeupAfterFlashing()) {
|
if (!isWakeupAfterFlashing()) {
|
||||||
// For normal wakeups (not immediately after flashing), verify long press
|
// For normal wakeups (not immediately after flashing), verify long press
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user