feat: add Bookerly font plugin with style selection and SmallerFonts integration
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
#include "BookerlyPlugin.h"
|
||||||
|
|
||||||
|
namespace BookerlyPlugin {
|
||||||
|
|
||||||
|
const char* styleName(BookerlyStyle style) {
|
||||||
|
switch (style) {
|
||||||
|
case BookerlyStyle::ITALIC: return "Italic";
|
||||||
|
case BookerlyStyle::BOLD: return "Bold";
|
||||||
|
case BookerlyStyle::BOLD_ITALIC: return "Bold Italic";
|
||||||
|
default: return "Regular";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
enum class BookerlyStyle : uint8_t {
|
||||||
|
REGULAR = 0,
|
||||||
|
ITALIC = 1,
|
||||||
|
BOLD = 2,
|
||||||
|
BOLD_ITALIC = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace BookerlyPlugin {
|
||||||
|
const char* styleName(BookerlyStyle style);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#include "SmallerFontsPlugin.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace SmallerFontsPlugin {
|
||||||
|
|
||||||
|
const char* modeName(SmallerFontsMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case SmallerFontsMode::SMALLER: return "Smaller";
|
||||||
|
case SmallerFontsMode::SMALLEST: return "Smallest";
|
||||||
|
default: return "Disabled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int resolveReaderFontId(int baseFontId, SmallerFontsMode mode) {
|
||||||
|
if (mode == SmallerFontsMode::MODE_OFF) return baseFontId;
|
||||||
|
|
||||||
|
#ifdef BOOKERLY_8_FONT_ID
|
||||||
|
if (baseFontId == BOOKERLY_8_FONT_ID) return BOOKERLY_8_FONT_ID;
|
||||||
|
if (baseFontId == BOOKERLY_10_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_8_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_8_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == BOOKERLY_12_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_10_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_8_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == BOOKERLY_14_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_12_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_10_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == BOOKERLY_16_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_14_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_12_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == BOOKERLY_18_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_16_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_14_FONT_ID;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (baseFontId == NOTOSERIF_12_FONT_ID) return NOTOSERIF_12_FONT_ID;
|
||||||
|
if (baseFontId == NOTOSERIF_14_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return NOTOSERIF_12_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return NOTOSERIF_12_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == NOTOSERIF_16_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return NOTOSERIF_14_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return NOTOSERIF_12_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == NOTOSERIF_18_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return NOTOSERIF_16_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return NOTOSERIF_14_FONT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseFontId == NOTOSANS_12_FONT_ID) return NOTOSANS_12_FONT_ID;
|
||||||
|
if (baseFontId == NOTOSANS_14_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return NOTOSANS_12_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return NOTOSANS_12_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == NOTOSANS_16_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return NOTOSANS_14_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return NOTOSANS_12_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == NOTOSANS_18_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return NOTOSANS_16_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return NOTOSANS_14_FONT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseFontId == OPENDYSLEXIC_8_FONT_ID) return OPENDYSLEXIC_8_FONT_ID;
|
||||||
|
if (baseFontId == OPENDYSLEXIC_10_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return OPENDYSLEXIC_8_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return OPENDYSLEXIC_8_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == OPENDYSLEXIC_12_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return OPENDYSLEXIC_10_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return OPENDYSLEXIC_8_FONT_ID;
|
||||||
|
}
|
||||||
|
if (baseFontId == OPENDYSLEXIC_14_FONT_ID) {
|
||||||
|
if (mode == SmallerFontsMode::SMALLER) return OPENDYSLEXIC_12_FONT_ID;
|
||||||
|
if (mode == SmallerFontsMode::SMALLEST) return OPENDYSLEXIC_10_FONT_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFontId;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,438 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
import shutil
|
||||||
|
import hashlib
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def write_file(path, content):
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
def find_first(filename, repo_dir):
|
||||||
|
results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True)
|
||||||
|
if not results:
|
||||||
|
sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}")
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
def font_id_from_files(paths):
|
||||||
|
total = 0
|
||||||
|
for p in paths:
|
||||||
|
h = hashlib.sha256(open(p, "rb").read()).hexdigest()
|
||||||
|
total += int(h, 16)
|
||||||
|
return total % (2 ** 32) - (2 ** 31)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_fonts(plugin_dir, repo_dir):
|
||||||
|
scripts_dir = os.path.join(repo_dir, "lib", "EpdFont", "scripts")
|
||||||
|
fonts_dir = os.path.join(repo_dir, "lib", "EpdFont", "builtinFonts")
|
||||||
|
source_dir = os.path.join(plugin_dir, "fonts")
|
||||||
|
fontconvert = os.path.join(scripts_dir, "fontconvert.py")
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "freetype-py==2.5.1", "fonttools", "--break-system-packages", "-q"],
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
sizes = [8, 10, 12, 14, 16, 18]
|
||||||
|
styles = ["Regular", "Bold", "Italic", "BoldItalic"]
|
||||||
|
|
||||||
|
for size in sizes:
|
||||||
|
for style in styles:
|
||||||
|
fname = f"bookerly_{size}_{style.lower()}"
|
||||||
|
out_path = os.path.join(fonts_dir, f"{fname}.h")
|
||||||
|
if os.path.exists(out_path):
|
||||||
|
print(f" already exists: {fname}.h")
|
||||||
|
continue
|
||||||
|
src_path = os.path.join(source_dir, f"Bookerly-{style}.ttf")
|
||||||
|
if not os.path.exists(src_path):
|
||||||
|
sys.exit(f"ERROR: Missing font file: {src_path}")
|
||||||
|
with open(out_path, "w") as f:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, fontconvert, fname, str(size), src_path, "--2bit", "--compress"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode != 0 or not result.stdout.strip():
|
||||||
|
print(f" ERROR generating {fname}: {result.stderr[:200]}")
|
||||||
|
if os.path.exists(out_path):
|
||||||
|
os.remove(out_path)
|
||||||
|
continue
|
||||||
|
f.write(result.stdout)
|
||||||
|
print(f" ✓ generated {fname}.h")
|
||||||
|
|
||||||
|
return fonts_dir
|
||||||
|
|
||||||
|
|
||||||
|
def compute_font_ids(fonts_dir):
|
||||||
|
ids = {}
|
||||||
|
sizes = [8, 10, 12, 14, 16, 18]
|
||||||
|
suffixes = ["regular", "bold", "italic", "bolditalic"]
|
||||||
|
for size in sizes:
|
||||||
|
paths = [os.path.join(fonts_dir, f"bookerly_{size}_{s}.h") for s in suffixes]
|
||||||
|
if all(os.path.exists(p) for p in paths):
|
||||||
|
ids[f"BOOKERLY_{size}_FONT_ID"] = font_id_from_files(paths)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
def patch_font_ids_h(repo_dir, new_ids):
|
||||||
|
path = find_first("fontIds.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for name, val in new_ids.items():
|
||||||
|
if name not in content:
|
||||||
|
lines.append(f"#define {name} ({val})")
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
print(" fontIds.h already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
write_file(path, content.rstrip() + "\n" + "\n".join(lines) + "\n")
|
||||||
|
print(f" fontIds.h: added {len(lines)} new font IDs.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_all_h(repo_dir, new_ids):
|
||||||
|
path = find_first("all.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
sizes = [8, 10, 12, 14, 16, 18]
|
||||||
|
suffixes = ["bold", "bolditalic", "italic", "regular"]
|
||||||
|
for size in sizes:
|
||||||
|
key = f"BOOKERLY_{size}_FONT_ID"
|
||||||
|
if key not in new_ids:
|
||||||
|
continue
|
||||||
|
for suffix in suffixes:
|
||||||
|
fname = f"bookerly_{size}_{suffix}"
|
||||||
|
inc = f'#include <builtinFonts/{fname}.h>'
|
||||||
|
if inc not in content:
|
||||||
|
content += inc + "\n"
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
if added:
|
||||||
|
write_file(path, content)
|
||||||
|
print(f" all.h: added {added} includes.")
|
||||||
|
else:
|
||||||
|
print(" all.h already patched, skipping.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_main_cpp(repo_dir, new_ids):
|
||||||
|
path = find_first("main.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
sizes = [8, 10, 12, 14, 16, 18]
|
||||||
|
decl_lines = []
|
||||||
|
insert_lines = []
|
||||||
|
|
||||||
|
for size in sizes:
|
||||||
|
key = f"BOOKERLY_{size}_FONT_ID"
|
||||||
|
varbase = f"bookerly{size}"
|
||||||
|
if key not in new_ids:
|
||||||
|
continue
|
||||||
|
if f"EpdFontFamily {varbase}FontFamily" in content:
|
||||||
|
continue
|
||||||
|
decl_lines += [
|
||||||
|
f'EpdFont {varbase}RegularFont(&bookerly_{size}_regular);',
|
||||||
|
f'EpdFont {varbase}BoldFont(&bookerly_{size}_bold);',
|
||||||
|
f'EpdFont {varbase}ItalicFont(&bookerly_{size}_italic);',
|
||||||
|
f'EpdFont {varbase}BoldItalicFont(&bookerly_{size}_bolditalic);',
|
||||||
|
f'EpdFontFamily {varbase}FontFamily(&{varbase}RegularFont, &{varbase}BoldFont, &{varbase}ItalicFont, &{varbase}BoldItalicFont);',
|
||||||
|
]
|
||||||
|
insert_lines.append(f' renderer.insertFont({key}, {varbase}FontFamily);')
|
||||||
|
|
||||||
|
if not decl_lines:
|
||||||
|
print(" main.cpp already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
anchor_decl = '#ifndef OMIT_FONTS'
|
||||||
|
content = content.replace(anchor_decl, "\n".join(decl_lines) + "\n" + anchor_decl, 1)
|
||||||
|
|
||||||
|
anchor_insert = ' renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);'
|
||||||
|
content = content.replace(anchor_insert, "\n".join(insert_lines) + "\n" + anchor_insert, 1)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(f" main.cpp: added {len(decl_lines) // 5} font families.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_cross_point_settings_h(repo_dir):
|
||||||
|
path = find_first("CrossPointSettings.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "BOOKERLY" in content:
|
||||||
|
print(" CrossPointSettings.h already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'#include "activities/settings/SmallerFontsPlugin.h"',
|
||||||
|
'#include "activities/settings/SmallerFontsPlugin.h"\n#include "activities/settings/BookerlyPlugin.h"'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT };',
|
||||||
|
'enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, BOOKERLY = 3, FONT_FAMILY_COUNT };'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' uint8_t smallerFontsMode = 0;',
|
||||||
|
' uint8_t smallerFontsMode = 0;\n uint8_t bookerlyStyle = 0;'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" CrossPointSettings.h patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_cross_point_settings_cpp(repo_dir, new_ids):
|
||||||
|
path = find_first("CrossPointSettings.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "BOOKERLY" in content:
|
||||||
|
print(" CrossPointSettings.cpp already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT);',
|
||||||
|
'readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT);\n readAndValidate(inputFile, bookerlyStyle, 4);'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' case OPENDYSLEXIC:\n switch (lineSpacing) {\n case TIGHT:\n return 0.90f;\n case NORMAL:\n default:\n return 0.95f;\n case WIDE:\n return 1.0f;\n }\n }\n}\n\nunsigned long',
|
||||||
|
' case OPENDYSLEXIC:\n switch (lineSpacing) {\n case TIGHT:\n return 0.90f;\n case NORMAL:\n default:\n return 0.95f;\n case WIDE:\n return 1.0f;\n }\n case BOOKERLY:\n switch (lineSpacing) {\n case TIGHT:\n return 0.95f;\n case NORMAL:\n default:\n return 1.0f;\n case WIDE:\n return 1.1f;\n }\n }\n}\n\nunsigned long'
|
||||||
|
)
|
||||||
|
|
||||||
|
bookerly_font_id_case = ' case BOOKERLY:\n'
|
||||||
|
sizes = [8, 10, 12, 14, 16, 18]
|
||||||
|
font_sizes = ['SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE']
|
||||||
|
size_map = {
|
||||||
|
'SMALL': sizes[0],
|
||||||
|
'MEDIUM': sizes[1],
|
||||||
|
'LARGE': sizes[2],
|
||||||
|
'EXTRA_LARGE': sizes[3],
|
||||||
|
}
|
||||||
|
bookerly_font_id_case += ' switch (fontSize) {\n'
|
||||||
|
bookerly_font_id_case += f' case SMALL:\n return resolve(BOOKERLY_{sizes[0]}_FONT_ID);\n'
|
||||||
|
bookerly_font_id_case += f' case MEDIUM:\n default:\n return resolve(BOOKERLY_{sizes[1]}_FONT_ID);\n'
|
||||||
|
bookerly_font_id_case += f' case LARGE:\n return resolve(BOOKERLY_{sizes[2]}_FONT_ID);\n'
|
||||||
|
bookerly_font_id_case += f' case EXTRA_LARGE:\n return resolve(BOOKERLY_{sizes[3]}_FONT_ID);\n'
|
||||||
|
bookerly_font_id_case += ' }\n'
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' }\n}\n',
|
||||||
|
' ' + bookerly_font_id_case + ' }\n}\n',
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" CrossPointSettings.cpp patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_settings_list_h(repo_dir):
|
||||||
|
path = find_first("SettingsList.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "Bookerly" in content or "bookerlyStyle" in content:
|
||||||
|
print(" SettingsList.h already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'{StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily"',
|
||||||
|
'{StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC, StrId::STR_NONE_OPT}, "fontFamily"'
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = (
|
||||||
|
' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::bookerlyStyle,\n'
|
||||||
|
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "bookerlyStyle"),\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' };\n return list;\n}\n',
|
||||||
|
entry + ' };\n return list;\n}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" SettingsList.h patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_settings_activity_cpp(repo_dir):
|
||||||
|
path = find_first("SettingsActivity.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "bookerlyStyle" in content:
|
||||||
|
print(" SettingsActivity.cpp already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'#include "DarkModePlugin.h"',
|
||||||
|
'#include "DarkModePlugin.h"\n#include "BookerlyPlugin.h"'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||||
|
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||||
|
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
|
||||||
|
' ));\n',
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||||
|
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||||
|
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
|
||||||
|
' ));\n'
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||||
|
' StrId::STR_NONE_OPT, &CrossPointSettings::bookerlyStyle,\n'
|
||||||
|
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "bookerlyStyle"\n'
|
||||||
|
' ));\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n',
|
||||||
|
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n'
|
||||||
|
' if (s.key && std::string(s.key) == "bookerlyStyle") return "Bookerly Font";\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' } else if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||||
|
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||||
|
' } else {\n',
|
||||||
|
' } else if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||||
|
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||||
|
' } else if (setting.key && std::string(setting.key) == "bookerlyStyle") {\n'
|
||||||
|
' valueText = BookerlyPlugin::styleName(static_cast<BookerlyStyle>(value));\n'
|
||||||
|
' } else {\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' if (s.key && std::string(s.key) == "fontFamily") return "Bookerly";\n',
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||||
|
' },',
|
||||||
|
' if (settings[index].key && std::string(settings[index].key) == "fontFamily") {\n'
|
||||||
|
' const uint8_t val = SETTINGS.*(settings[index].valuePtr);\n'
|
||||||
|
' if (val == CrossPointSettings::BOOKERLY) return std::string(I18N.get(settings[index].nameId)) + " (Bookerly)";\n'
|
||||||
|
' }\n'
|
||||||
|
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||||
|
' },'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" SettingsActivity.cpp patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_web_server(repo_dir):
|
||||||
|
path = find_first("CrossPointWebServer.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "bookerlyStyle" in content:
|
||||||
|
print(" CrossPointWebServer.cpp already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
|
||||||
|
' doc["name"] = "Smaller Fonts";\n'
|
||||||
|
' doc["category"] = "Plugins";\n'
|
||||||
|
' }\n'
|
||||||
|
' }\n',
|
||||||
|
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
|
||||||
|
' doc["name"] = "Smaller Fonts";\n'
|
||||||
|
' doc["category"] = "Plugins";\n'
|
||||||
|
' } else if (strcmp(s.key, "bookerlyStyle") == 0) {\n'
|
||||||
|
' doc["name"] = "Bookerly Font";\n'
|
||||||
|
' doc["category"] = "Plugins";\n'
|
||||||
|
' } else if (strcmp(s.key, "fontFamily") == 0) {\n'
|
||||||
|
' JsonArray opts = doc["options"].to<JsonArray>();\n'
|
||||||
|
' opts.add("Noto Serif");\n'
|
||||||
|
' opts.add("Noto Sans");\n'
|
||||||
|
' opts.add("OpenDyslexic");\n'
|
||||||
|
' opts.add("Bookerly");\n'
|
||||||
|
' }\n'
|
||||||
|
' }\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
|
||||||
|
' JsonArray opts = doc["options"].to<JsonArray>();\n'
|
||||||
|
' opts.add("Disabled");\n'
|
||||||
|
' opts.add("Smaller");\n'
|
||||||
|
' opts.add("Smallest");\n'
|
||||||
|
' }\n'
|
||||||
|
' }\n',
|
||||||
|
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
|
||||||
|
' JsonArray opts = doc["options"].to<JsonArray>();\n'
|
||||||
|
' opts.add("Disabled");\n'
|
||||||
|
' opts.add("Smaller");\n'
|
||||||
|
' opts.add("Smallest");\n'
|
||||||
|
' } else if (strcmp(s.key, "bookerlyStyle") == 0) {\n'
|
||||||
|
' JsonArray opts = doc["options"].to<JsonArray>();\n'
|
||||||
|
' opts.add("Regular");\n'
|
||||||
|
' opts.add("Italic");\n'
|
||||||
|
' opts.add("Bold");\n'
|
||||||
|
' opts.add("Bold Italic");\n'
|
||||||
|
' }\n'
|
||||||
|
' }\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" CrossPointWebServer.cpp patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def copy_plugin_sources(plugin_dir, repo_dir):
|
||||||
|
dest = os.path.join(repo_dir, "src", "activities", "settings")
|
||||||
|
os.makedirs(dest, exist_ok=True)
|
||||||
|
|
||||||
|
for fname in os.listdir(plugin_dir):
|
||||||
|
if fname.endswith((".h", ".cpp", ".hpp")):
|
||||||
|
src = os.path.join(plugin_dir, fname)
|
||||||
|
dst = os.path.join(dest, fname)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
print(f" ✓ {fname}")
|
||||||
|
|
||||||
|
|
||||||
|
def patch(repo_dir: str):
|
||||||
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
print(" Copying plugin sources...")
|
||||||
|
copy_plugin_sources(plugin_dir, repo_dir)
|
||||||
|
|
||||||
|
print(" Generating Bookerly font files...")
|
||||||
|
fonts_dir = generate_fonts(plugin_dir, repo_dir)
|
||||||
|
|
||||||
|
print(" Computing font IDs...")
|
||||||
|
new_ids = compute_font_ids(fonts_dir)
|
||||||
|
for k, v in new_ids.items():
|
||||||
|
print(f" {k} = {v}")
|
||||||
|
|
||||||
|
print(" Patching fontIds.h...")
|
||||||
|
patch_font_ids_h(repo_dir, new_ids)
|
||||||
|
|
||||||
|
print(" Patching all.h...")
|
||||||
|
patch_all_h(repo_dir, new_ids)
|
||||||
|
|
||||||
|
print(" Patching main.cpp...")
|
||||||
|
patch_main_cpp(repo_dir, new_ids)
|
||||||
|
|
||||||
|
print(" Patching CrossPointSettings.h...")
|
||||||
|
patch_cross_point_settings_h(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching CrossPointSettings.cpp...")
|
||||||
|
patch_cross_point_settings_cpp(repo_dir, new_ids)
|
||||||
|
|
||||||
|
print(" Patching SettingsList.h...")
|
||||||
|
patch_settings_list_h(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching SettingsActivity.cpp...")
|
||||||
|
patch_settings_activity_cpp(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching CrossPointWebServer.cpp (web UI)...")
|
||||||
|
patch_web_server(repo_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||||
|
patch(sys.argv[1])
|
||||||
Binary file not shown.
@@ -0,0 +1,84 @@
|
|||||||
|
#include "GitHubSyncActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "GitHubSyncPlugin.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
void GitHubSyncActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
state = SYNCING;
|
||||||
|
result = GitHubSyncPlugin::SyncResult::OK;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GitHubSyncActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
|
void GitHubSyncActivity::render(RenderLock&&) {
|
||||||
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
renderer.clearScreen();
|
||||||
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "GitHub Sync");
|
||||||
|
|
||||||
|
if (state == SYNCING) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Syncing...");
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* line1 = nullptr;
|
||||||
|
const char* line2 = nullptr;
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case GitHubSyncPlugin::SyncResult::OK:
|
||||||
|
line1 = "Sync complete.";
|
||||||
|
break;
|
||||||
|
case GitHubSyncPlugin::SyncResult::NO_WIFI:
|
||||||
|
line1 = "Not connected to WiFi.";
|
||||||
|
line2 = "Connect to WiFi and try again.";
|
||||||
|
break;
|
||||||
|
case GitHubSyncPlugin::SyncResult::GIT_ERROR:
|
||||||
|
line1 = "Sync failed.";
|
||||||
|
line2 = "Check GitHub URL and PAT in Settings > Plugins.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - (line2 ? 15 : 0), line1, true,
|
||||||
|
result == GitHubSyncPlugin::SyncResult::OK ? EpdFontFamily::BOLD
|
||||||
|
: EpdFontFamily::REGULAR);
|
||||||
|
if (line2)
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 15, line2);
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GitHubSyncActivity::doSync() {
|
||||||
|
result = GitHubSyncPlugin::sync();
|
||||||
|
state = DONE;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GitHubSyncActivity::loop() {
|
||||||
|
if (state == SYNCING) {
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
doSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == DONE) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GitHubSyncPlugin.h"
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
|
class GitHubSyncActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
explicit GitHubSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
: Activity("GitHubSync", renderer, mappedInput) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
bool skipLoopDelay() override { return true; }
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum State { SYNCING, DONE };
|
||||||
|
State state = SYNCING;
|
||||||
|
GitHubSyncPlugin::SyncResult result = GitHubSyncPlugin::SyncResult::OK;
|
||||||
|
void doSync();
|
||||||
|
};
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
#include "GitHubSyncPlugin.h"
|
||||||
|
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char GITHUB_API_HOST[] = "api.github.com";
|
||||||
|
constexpr char TREE_SHA_PATH[] = "/.crosspoint/githubsync.sha";
|
||||||
|
constexpr size_t CHUNK_SIZE = 4096;
|
||||||
|
|
||||||
|
struct RepoInfo {
|
||||||
|
std::string owner;
|
||||||
|
std::string repo;
|
||||||
|
std::string branch;
|
||||||
|
};
|
||||||
|
|
||||||
|
RepoInfo parseRepoUrl(const std::string& url) {
|
||||||
|
RepoInfo info;
|
||||||
|
std::string u = url;
|
||||||
|
|
||||||
|
for (const char* prefix : {"https://github.com/", "http://github.com/", "github.com/"}) {
|
||||||
|
if (u.rfind(prefix, 0) == 0) {
|
||||||
|
u = u.substr(strlen(prefix));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!u.empty() && u.back() == '/') u.pop_back();
|
||||||
|
if (u.size() > 4 && u.substr(u.size() - 4) == ".git")
|
||||||
|
u = u.substr(0, u.size() - 4);
|
||||||
|
|
||||||
|
auto slash = u.find('/');
|
||||||
|
if (slash == std::string::npos) return info;
|
||||||
|
info.owner = u.substr(0, slash);
|
||||||
|
info.repo = u.substr(slash + 1);
|
||||||
|
info.branch = "";
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string loadCachedTreeSha() {
|
||||||
|
FsFile f;
|
||||||
|
if (!Storage.openFileForRead("GHS", TREE_SHA_PATH, f)) return {};
|
||||||
|
char buf[64] = {};
|
||||||
|
f.read(buf, sizeof(buf) - 1);
|
||||||
|
f.close();
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveCachedTreeSha(const std::string& sha) {
|
||||||
|
Storage.mkdir("/.crosspoint");
|
||||||
|
FsFile f;
|
||||||
|
if (!Storage.openFileForWrite("GHS", TREE_SHA_PATH, f)) return;
|
||||||
|
f.write(reinterpret_cast<const uint8_t*>(sha.c_str()), sha.size());
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool downloadFile(const std::string& rawUrl, const std::string& pat,
|
||||||
|
const std::string& destPath) {
|
||||||
|
WiFiClientSecure client;
|
||||||
|
client.setInsecure();
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(client, rawUrl.c_str());
|
||||||
|
http.addHeader("User-Agent", "CrossPoint-GitHubSync/1.0");
|
||||||
|
if (!pat.empty()) {
|
||||||
|
std::string auth = "token " + pat;
|
||||||
|
http.addHeader("Authorization", auth.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int code = http.GET();
|
||||||
|
if (code != 200) {
|
||||||
|
LOG_ERR("GHS", "HTTP %d for %s", code, rawUrl.c_str());
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile f;
|
||||||
|
if (!Storage.openFileForWrite("GHS", destPath.c_str(), f)) {
|
||||||
|
LOG_ERR("GHS", "Cannot open %s for write", destPath.c_str());
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
|
uint8_t buf[CHUNK_SIZE];
|
||||||
|
int total = http.getSize();
|
||||||
|
int remaining = total;
|
||||||
|
|
||||||
|
while (http.connected() && (remaining > 0 || total == -1)) {
|
||||||
|
size_t avail = stream->available();
|
||||||
|
if (avail) {
|
||||||
|
size_t toRead = std::min(avail, CHUNK_SIZE);
|
||||||
|
size_t read = stream->readBytes(buf, toRead);
|
||||||
|
f.write(buf, read);
|
||||||
|
if (total != -1) remaining -= read;
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
f.close();
|
||||||
|
http.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchTree(RepoInfo& info, const std::string& pat,
|
||||||
|
std::string& outTreeSha,
|
||||||
|
std::vector<std::pair<std::string, std::string>>& outFiles) {
|
||||||
|
const char* branches[] = {"main", "master", nullptr};
|
||||||
|
|
||||||
|
for (int i = 0; branches[i] != nullptr; i++) {
|
||||||
|
if (!info.branch.empty() && info.branch != branches[i]) continue;
|
||||||
|
|
||||||
|
WiFiClientSecure client;
|
||||||
|
client.setInsecure();
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
char url[512];
|
||||||
|
snprintf(url, sizeof(url),
|
||||||
|
"https://%s/repos/%s/%s/git/trees/%s?recursive=1",
|
||||||
|
GITHUB_API_HOST, info.owner.c_str(), info.repo.c_str(), branches[i]);
|
||||||
|
|
||||||
|
http.begin(client, url);
|
||||||
|
http.addHeader("User-Agent", "CrossPoint-GitHubSync/1.0");
|
||||||
|
http.addHeader("Accept", "application/vnd.github+json");
|
||||||
|
if (!pat.empty()) {
|
||||||
|
std::string auth = "token " + pat;
|
||||||
|
http.addHeader("Authorization", auth.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int code = http.GET();
|
||||||
|
if (code != 200) {
|
||||||
|
LOG_ERR("GHS", "Tree fetch HTTP %d for branch %s", code, branches[i]);
|
||||||
|
http.end();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String body = http.getString();
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
if (body.isEmpty()) {
|
||||||
|
LOG_ERR("GHS", "Empty response for branch %s", branches[i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
if (deserializeJson(doc, body) != DeserializationError::Ok) {
|
||||||
|
LOG_ERR("GHS", "JSON parse error");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
outTreeSha = std::string(doc["sha"] | "");
|
||||||
|
info.branch = branches[i];
|
||||||
|
|
||||||
|
JsonArray tree = doc["tree"].as<JsonArray>();
|
||||||
|
for (JsonObject item : tree) {
|
||||||
|
const char* type = item["type"] | "";
|
||||||
|
const char* path = item["path"] | "";
|
||||||
|
if (strcmp(type, "blob") != 0) continue;
|
||||||
|
std::string p(path);
|
||||||
|
|
||||||
|
bool wanted = false;
|
||||||
|
if (p == "sleep.bmp") wanted = true;
|
||||||
|
else if (p.rfind("sleep/", 0) == 0) wanted = true;
|
||||||
|
else if (p.size() > 5 && p.substr(p.size() - 5) == ".epub") wanted = true;
|
||||||
|
|
||||||
|
if (wanted)
|
||||||
|
outFiles.push_back({p, std::string(item["sha"] | "")});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool syncFiles(RepoInfo& info, const std::string& pat) {
|
||||||
|
std::string treeSha;
|
||||||
|
std::vector<std::pair<std::string, std::string>> files;
|
||||||
|
if (!fetchTree(info, pat, treeSha, files)) return false;
|
||||||
|
|
||||||
|
if (!treeSha.empty() && loadCachedTreeSha() == treeSha) {
|
||||||
|
LOG_DBG("GHS", "Repo unchanged (tree SHA match), nothing to do.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [path, sha] : files) {
|
||||||
|
std::string rawUrl =
|
||||||
|
"https://raw.githubusercontent.com/" + info.owner + "/" +
|
||||||
|
info.repo + "/" + info.branch + "/" + path;
|
||||||
|
|
||||||
|
std::string dest = "/" + path;
|
||||||
|
std::string dir = dest.substr(0, dest.rfind('/'));
|
||||||
|
if (!dir.empty() && dir != "/") Storage.mkdir(dir.c_str());
|
||||||
|
|
||||||
|
LOG_DBG("GHS", "Downloading %s", path.c_str());
|
||||||
|
if (!downloadFile(rawUrl, pat, dest)) {
|
||||||
|
LOG_ERR("GHS", "Failed: %s", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOG_DBG("GHS", "OK: %s", path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCachedTreeSha(treeSha);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace GitHubSyncPlugin {
|
||||||
|
|
||||||
|
SyncResult sync() {
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
LOG_ERR("GHS", "No WiFi");
|
||||||
|
return SyncResult::NO_WIFI;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string url(SETTINGS.githubSyncUrl);
|
||||||
|
if (url.empty()) {
|
||||||
|
LOG_ERR("GHS", "No GitHub URL configured");
|
||||||
|
return SyncResult::GIT_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string pat(SETTINGS.githubSyncPat);
|
||||||
|
RepoInfo info = parseRepoUrl(url);
|
||||||
|
|
||||||
|
if (info.owner.empty() || info.repo.empty()) {
|
||||||
|
LOG_ERR("GHS", "Invalid GitHub URL: %s", url.c_str());
|
||||||
|
return SyncResult::GIT_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!syncFiles(info, pat)) return SyncResult::GIT_ERROR;
|
||||||
|
|
||||||
|
return SyncResult::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
namespace GitHubSyncPlugin {
|
||||||
|
|
||||||
|
enum class SyncResult {
|
||||||
|
OK,
|
||||||
|
NO_WIFI,
|
||||||
|
GIT_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
|
SyncResult sync();
|
||||||
|
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,417 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
def write_file(path, content):
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
def find_first(filename, repo_dir):
|
||||||
|
results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True)
|
||||||
|
if not results:
|
||||||
|
sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}")
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
|
||||||
|
def copy_plugin_sources(plugin_dir, repo_dir):
|
||||||
|
dest = os.path.join(repo_dir, "src", "activities", "settings")
|
||||||
|
os.makedirs(dest, exist_ok=True)
|
||||||
|
for fname in os.listdir(plugin_dir):
|
||||||
|
if fname.endswith((".h", ".cpp", ".hpp")):
|
||||||
|
src = os.path.join(plugin_dir, fname)
|
||||||
|
dst = os.path.join(dest, fname)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
print(f" \u2713 {fname}")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_cross_point_settings_h(repo_dir, url="", pat=""):
|
||||||
|
path = find_first("CrossPointSettings.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "githubSyncUrl" in content:
|
||||||
|
print(" CrossPointSettings.h already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
safe_url = url.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
safe_pat = pat.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||||
|
f' uint8_t imageRendering = IMAGES_DISPLAY;\n'
|
||||||
|
f' char githubSyncUrl[512] = "{safe_url}";\n'
|
||||||
|
f' char githubSyncPat[256] = "{safe_pat}";'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" CrossPointSettings.h patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_settings_list_h(repo_dir):
|
||||||
|
path = find_first("SettingsList.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "githubSyncUrl" in content:
|
||||||
|
print(" SettingsList.h already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' };\n return list;\n}\n',
|
||||||
|
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.githubSyncUrl,\n'
|
||||||
|
' sizeof(SETTINGS.githubSyncUrl), "githubSyncUrl"),\n'
|
||||||
|
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.githubSyncPat,\n'
|
||||||
|
' sizeof(SETTINGS.githubSyncPat), "githubSyncPat")\n'
|
||||||
|
' .withObfuscated(),\n'
|
||||||
|
' };\n return list;\n}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" SettingsList.h patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_setting_action_enum(repo_dir):
|
||||||
|
path = find_first("SettingsActivity.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "GitHubSync" in content:
|
||||||
|
print(" SettingAction enum already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
' Language,\n',
|
||||||
|
' GitHubSync,\n Language,\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" SettingAction::GitHubSync added.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_settings_h(repo_dir):
|
||||||
|
path = find_first("SettingsActivity.h", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
already_has_tab = "categoryCount = 5" in content
|
||||||
|
already_has_vec = "pluginsSettings" in content
|
||||||
|
|
||||||
|
if already_has_tab and already_has_vec:
|
||||||
|
print(" SettingsActivity.h already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not already_has_tab:
|
||||||
|
content = content.replace(
|
||||||
|
'static constexpr int categoryCount = 4;',
|
||||||
|
'static constexpr int categoryCount = 5;'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not already_has_vec:
|
||||||
|
content = content.replace(
|
||||||
|
' std::vector<SettingInfo> systemSettings;',
|
||||||
|
' std::vector<SettingInfo> systemSettings;\n std::vector<SettingInfo> pluginsSettings;'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" SettingsActivity.h patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_settings_cpp(repo_dir):
|
||||||
|
path = find_first("SettingsActivity.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if "GitHubSyncPlugin" in content:
|
||||||
|
print(" SettingsActivity.cpp already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'#include "SettingsList.h"',
|
||||||
|
'#include "SettingsList.h"\n#include "GitHubSyncPlugin.h"\n#include "GitHubSyncActivity.h"'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'StrId::STR_NONE_OPT};' not in content and 'STR_NONE_OPT' not in content.split('categoryNames')[1].split(';')[0]:
|
||||||
|
content = content.replace(
|
||||||
|
'const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,\n'
|
||||||
|
' StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};',
|
||||||
|
'const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,\n'
|
||||||
|
' StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n'
|
||||||
|
' StrId::STR_NONE_OPT};'
|
||||||
|
)
|
||||||
|
|
||||||
|
if "pluginsSettings.clear();" not in content:
|
||||||
|
content = content.replace(
|
||||||
|
' systemSettings.clear();',
|
||||||
|
' systemSettings.clear();\n pluginsSettings.clear();'
|
||||||
|
)
|
||||||
|
|
||||||
|
github_push = (
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Action(\n'
|
||||||
|
' StrId::STR_NONE_OPT, SettingAction::GitHubSync\n'
|
||||||
|
' ));\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
hardcover_push = (
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Action(\n'
|
||||||
|
' StrId::STR_NONE_OPT, SettingAction::HardcoverSync\n'
|
||||||
|
' ));\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'SettingAction::GitHubSync' not in content:
|
||||||
|
anchors = [
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||||
|
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||||
|
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
|
||||||
|
' ));\n',
|
||||||
|
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||||
|
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||||
|
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\n'
|
||||||
|
' ));\n',
|
||||||
|
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
|
||||||
|
]
|
||||||
|
for anchor in anchors:
|
||||||
|
if anchor in content:
|
||||||
|
content = content.replace(anchor, anchor + github_push + hardcover_push)
|
||||||
|
break
|
||||||
|
|
||||||
|
if 'case 4:\n currentSettings = &pluginsSettings;' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
' case 3:\n currentSettings = &systemSettings;\n break;',
|
||||||
|
' case 3:\n currentSettings = &systemSettings;\n break;\n'
|
||||||
|
' case 4:\n currentSettings = &pluginsSettings;\n break;'
|
||||||
|
)
|
||||||
|
|
||||||
|
if '"Plugins"' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
' std::vector<TabInfo> tabs;\n'
|
||||||
|
' tabs.reserve(categoryCount);\n'
|
||||||
|
' for (int i = 0; i < categoryCount; i++) {\n'
|
||||||
|
' tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n'
|
||||||
|
' }',
|
||||||
|
' std::vector<TabInfo> tabs;\n'
|
||||||
|
' tabs.reserve(categoryCount);\n'
|
||||||
|
' for (int i = 0; i < categoryCount; i++) {\n'
|
||||||
|
' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n'
|
||||||
|
' tabs.push_back({tabLabel, selectedCategoryIndex == i});\n'
|
||||||
|
' }'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'nextCatLabel' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
' const auto confirmLabel = (selectedSettingIndex == 0)\n'
|
||||||
|
' ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n'
|
||||||
|
' : tr(STR_TOGGLE);',
|
||||||
|
' const int nextCatIndex = (selectedCategoryIndex + 1) % categoryCount;\n'
|
||||||
|
' const char* nextCatLabel = (nextCatIndex == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIndex]);\n'
|
||||||
|
' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);'
|
||||||
|
)
|
||||||
|
|
||||||
|
github_label_line = (
|
||||||
|
' if (s.type == SettingType::ACTION &&\n'
|
||||||
|
' s.action == SettingAction::GitHubSync) return "GitHub Sync";\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'GitHub Sync' not in content:
|
||||||
|
if 'selectedCategoryIndex == 4' in content:
|
||||||
|
for first_key_line in [
|
||||||
|
' if (s.key && std::string(s.key) == "darkModeState") return "Dark Mode";\n',
|
||||||
|
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n',
|
||||||
|
]:
|
||||||
|
if first_key_line in content:
|
||||||
|
content = content.replace(
|
||||||
|
first_key_line,
|
||||||
|
github_label_line + first_key_line
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
old_lambda = (
|
||||||
|
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr,'
|
||||||
|
)
|
||||||
|
new_lambda = (
|
||||||
|
' [&settings, this](int index) -> std::string {\n'
|
||||||
|
' if (selectedCategoryIndex == 4) {\n'
|
||||||
|
' const auto& s = settings[index];\n'
|
||||||
|
+ github_label_line +
|
||||||
|
' if (s.key && std::string(s.key) == "darkModeState") return "Dark Mode";\n'
|
||||||
|
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n'
|
||||||
|
' }\n'
|
||||||
|
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||||
|
' }, nullptr, nullptr,'
|
||||||
|
)
|
||||||
|
content = content.replace(old_lambda, new_lambda)
|
||||||
|
|
||||||
|
if '"No WiFi"' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
' if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
|
||||||
|
' if (setting.type == SettingType::ACTION &&\n'
|
||||||
|
' setting.action == SettingAction::GitHubSync) {\n'
|
||||||
|
' if (WiFi.status() != WL_CONNECTED) {\n'
|
||||||
|
' valueText = "No WiFi";\n'
|
||||||
|
' } else {\n'
|
||||||
|
' valueText = "Sync";\n'
|
||||||
|
' }\n'
|
||||||
|
' } else if (setting.type == SettingType::ACTION &&\n'
|
||||||
|
' setting.action == SettingAction::HardcoverSync) {\n'
|
||||||
|
' valueText = "Sync";\n'
|
||||||
|
' } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'case SettingAction::GitHubSync:' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
' case SettingAction::Language:\n',
|
||||||
|
' case SettingAction::GitHubSync:\n'
|
||||||
|
' startActivityForResult(std::make_unique<GitHubSyncActivity>(renderer, mappedInput), resultHandler);\n'
|
||||||
|
' break;\n'
|
||||||
|
' case SettingAction::Language:\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" SettingsActivity.cpp patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_wifi_include(repo_dir):
|
||||||
|
path = find_first("SettingsActivity.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if '#include <WiFi.h>' in content:
|
||||||
|
print(" WiFi.h already included in SettingsActivity.cpp, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
'#include "SettingsList.h"',
|
||||||
|
'#include "SettingsList.h"\n#include <WiFi.h>'
|
||||||
|
)
|
||||||
|
write_file(path, content)
|
||||||
|
print(" WiFi.h include added to SettingsActivity.cpp.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_web_server(repo_dir):
|
||||||
|
path = find_first("CrossPointWebServer.cpp", repo_dir)
|
||||||
|
content = read_file(path)
|
||||||
|
|
||||||
|
if '"githubSyncUrl"' in content or '"GitHub Repo"' in content:
|
||||||
|
print(" CrossPointWebServer.cpp already patched, skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
github_block = (
|
||||||
|
' if (strcmp(s.key, "githubSyncUrl") == 0) {\n'
|
||||||
|
' doc["name"] = "GitHub Repo";\n'
|
||||||
|
' doc["category"] = "Plugins";\n'
|
||||||
|
' }\n'
|
||||||
|
' if (strcmp(s.key, "githubSyncPat") == 0) {\n'
|
||||||
|
' doc["name"] = "GitHub PAT (Optional)";\n'
|
||||||
|
' doc["category"] = "Plugins";\n'
|
||||||
|
' }\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'if (s.key) {' in content and 'darkModeState' in content:
|
||||||
|
content = content.replace(
|
||||||
|
' if (strcmp(s.key, "darkModeState") == 0) {\n',
|
||||||
|
github_block +
|
||||||
|
' if (strcmp(s.key, "darkModeState") == 0) {\n'
|
||||||
|
)
|
||||||
|
elif 'if (s.key) {' in content and 'hardcoverApiToken' in content:
|
||||||
|
content = content.replace(
|
||||||
|
' if (strcmp(s.key, "hardcoverApiToken") == 0) {\n',
|
||||||
|
github_block +
|
||||||
|
' if (strcmp(s.key, "hardcoverApiToken") == 0) {\n'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
content = content.replace(
|
||||||
|
' doc["category"] = I18N.get(s.category);\n'
|
||||||
|
'\n'
|
||||||
|
' switch (s.type) {',
|
||||||
|
' doc["category"] = I18N.get(s.category);\n'
|
||||||
|
'\n'
|
||||||
|
' if (s.key) {\n'
|
||||||
|
+ github_block +
|
||||||
|
' }\n'
|
||||||
|
'\n'
|
||||||
|
' switch (s.type) {'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file(path, content)
|
||||||
|
print(" CrossPointWebServer.cpp patched.")
|
||||||
|
|
||||||
|
|
||||||
|
def patch(repo_dir: str, yes_all: bool = False):
|
||||||
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
cache_file = os.path.expanduser("~/.githubsync")
|
||||||
|
url = ""
|
||||||
|
pat = ""
|
||||||
|
|
||||||
|
if os.path.exists(cache_file):
|
||||||
|
with open(cache_file) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("url="):
|
||||||
|
url = line[4:].strip()
|
||||||
|
elif line.startswith("pat="):
|
||||||
|
pat = line[4:].strip()
|
||||||
|
print(f" Using GitHub config from ~/.githubsync (url: {url}).")
|
||||||
|
else:
|
||||||
|
print(" GitHub repository URL is required.")
|
||||||
|
print(" Example: https://github.com/yourusername/yourrepo")
|
||||||
|
while not url:
|
||||||
|
url = input(" GitHub URL: ").strip()
|
||||||
|
if not url:
|
||||||
|
print(" URL cannot be empty.")
|
||||||
|
pat = input(" GitHub PAT (Optional for Private repo): ").strip()
|
||||||
|
with open(cache_file, "w") as f:
|
||||||
|
f.write(f"url={url}\n")
|
||||||
|
f.write(f"pat={pat}\n")
|
||||||
|
print(f" Config saved to ~/.githubsync for future installs.")
|
||||||
|
|
||||||
|
print(" Copying plugin sources...")
|
||||||
|
copy_plugin_sources(plugin_dir, repo_dir)
|
||||||
|
|
||||||
|
print(" Patching CrossPointSettings.h...")
|
||||||
|
patch_cross_point_settings_h(repo_dir, url, pat)
|
||||||
|
|
||||||
|
print(" Patching SettingsList.h...")
|
||||||
|
patch_settings_list_h(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching SettingAction enum...")
|
||||||
|
patch_setting_action_enum(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching SettingsActivity.h...")
|
||||||
|
patch_settings_h(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching SettingsActivity.cpp...")
|
||||||
|
patch_settings_cpp(repo_dir)
|
||||||
|
|
||||||
|
print(" Adding WiFi.h include...")
|
||||||
|
patch_wifi_include(repo_dir)
|
||||||
|
|
||||||
|
print(" Patching CrossPointWebServer.cpp (web UI)...")
|
||||||
|
patch_web_server(repo_dir)
|
||||||
|
|
||||||
|
import glob as _glob
|
||||||
|
cpp_results = _glob.glob(os.path.join(repo_dir, "**", "SettingsActivity.cpp"), recursive=True)
|
||||||
|
if cpp_results:
|
||||||
|
cpp_content = open(cpp_results[0]).read()
|
||||||
|
checks = [
|
||||||
|
("label lambda: 'GitHub Sync'", "GitHub Sync" in cpp_content),
|
||||||
|
("switch case: GitHubSync", "case SettingAction::GitHubSync:" in cpp_content),
|
||||||
|
("GitHubSyncActivity launched", "GitHubSyncActivity" in cpp_content),
|
||||||
|
("pluginsSettings.clear()", "pluginsSettings.clear();" in cpp_content),
|
||||||
|
("case 4 -> pluginsSettings", "case 4:" in cpp_content),
|
||||||
|
("No WiFi button state", "No WiFi" in cpp_content),
|
||||||
|
]
|
||||||
|
all_ok = True
|
||||||
|
for label, ok in checks:
|
||||||
|
print(f" [{'PASS' if ok else 'FAIL'}] {label}")
|
||||||
|
if not ok:
|
||||||
|
all_ok = False
|
||||||
|
if not all_ok:
|
||||||
|
sys.exit("ERROR: One or more GitHub Sync patch checks failed.")
|
||||||
|
print(" GitHub Sync plugin patch verified OK.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||||
|
patch(sys.argv[1])
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user