Rewrite plugin installer as a declarative manifest/engine framework for reliable mix-and-match plugin installs, with backup/restore support and assorted plugin fixes (Hardcover file API, Bookerly/Hardcover SettingAction registration, GitHubSync include path, Lockscreen render timing)
This commit is contained in:
1 parent
3ad7a7ab39
commit
fec251f4d1
23 files changed
+2079
-3243
No files matched your search
@@ -1,425 +0,0 @@
|
||||
#!/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 = [12, 14, 16, 18]
|
||||
styles = ["Regular"]
|
||||
|
||||
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 = [12, 14, 16, 18]
|
||||
for size in sizes:
|
||||
path = os.path.join(fonts_dir, f"bookerly_{size}_regular.h")
|
||||
if os.path.exists(path):
|
||||
h = hashlib.sha256(open(path, "rb").read()).hexdigest()
|
||||
ids[f"BOOKERLY_{size}_FONT_ID"] = int(h, 16) % (2 ** 32) - (2 ** 31)
|
||||
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 = [12, 14, 16, 18]
|
||||
for size in sizes:
|
||||
key = f"BOOKERLY_{size}_FONT_ID"
|
||||
if key not in new_ids:
|
||||
continue
|
||||
fname = f"bookerly_{size}_regular"
|
||||
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'EpdFontFamily {varbase}FontFamily(&{varbase}RegularFont, &{varbase}RegularFont, &{varbase}RegularFont, &{varbase}RegularFont);',
|
||||
]
|
||||
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) // 2} font families.")
|
||||
|
||||
|
||||
def patch_cross_point_settings_h(repo_dir):
|
||||
path = find_first("CrossPointSettings.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "BOOKERLY = 3" in content:
|
||||
print(" CrossPointSettings.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if '"activities/settings/BookerlyPlugin.h"' not in content:
|
||||
content = content.replace(
|
||||
'#include "activities/settings/SmallerFontsPlugin.h"',
|
||||
'#include "activities/settings/SmallerFontsPlugin.h"\n#include "activities/settings/BookerlyPlugin.h"'
|
||||
)
|
||||
|
||||
if 'BOOKERLY = 3' not in content:
|
||||
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 };'
|
||||
)
|
||||
|
||||
|
||||
|
||||
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(
|
||||
' 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'
|
||||
)
|
||||
|
||||
use_resolve = 'resolve' in content
|
||||
bookerly_return = 'resolve(BOOKERLY_12_FONT_ID)' if use_resolve else 'BOOKERLY_12_FONT_ID'
|
||||
bookerly_case = (
|
||||
' case BOOKERLY:\n'
|
||||
f' return {bookerly_return};\n'
|
||||
)
|
||||
|
||||
anchor = None
|
||||
for candidate in [
|
||||
' case EXTRA_LARGE:\n return resolve(OPENDYSLEXIC_14_FONT_ID);\n }\n }\n}\n',
|
||||
' case EXTRA_LARGE:\n return OPENDYSLEXIC_14_FONT_ID;\n }\n }\n}\n',
|
||||
]:
|
||||
if candidate in content:
|
||||
anchor = candidate
|
||||
break
|
||||
|
||||
if anchor:
|
||||
content = content.replace(
|
||||
anchor,
|
||||
' case EXTRA_LARGE:\n return ' + ('resolve(OPENDYSLEXIC_14_FONT_ID)' if 'resolve' in anchor else 'OPENDYSLEXIC_14_FONT_ID') + ';\n }\n'
|
||||
+ bookerly_case +
|
||||
' }\n}\n'
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
already_patched = 'STR_NONE_OPT}, "fontFamily"' in content and 'bookerlyInstalled' in content
|
||||
if already_patched:
|
||||
print(" SettingsList.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if 'STR_NONE_OPT}, "fontFamily"' not in content:
|
||||
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"'
|
||||
)
|
||||
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsList.h patched.")
|
||||
|
||||
|
||||
def patch_settings_activity_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if 'BookerlyInstalled' in content:
|
||||
print(" SettingsActivity.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if 'categoryCount = 4' in content:
|
||||
content = content.replace(
|
||||
'static constexpr int categoryCount = 4;',
|
||||
'static constexpr int categoryCount = 5;'
|
||||
)
|
||||
|
||||
if 'pluginsSettings' not in content:
|
||||
content = content.replace(
|
||||
' std::vector<SettingInfo> systemSettings;',
|
||||
' std::vector<SettingInfo> systemSettings;\n std::vector<SettingInfo> pluginsSettings;'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' PongGame,\n};',
|
||||
' PongGame,\n BookerlyInstalled,\n};'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.h patched.")
|
||||
|
||||
|
||||
def patch_settings_activity_cpp(repo_dir):
|
||||
path = find_first("SettingsActivity.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if '"Bookerly Font"' in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "fontIds.h"',
|
||||
'#include "fontIds.h"\n#include "BookerlyPlugin.h"'
|
||||
)
|
||||
|
||||
if 'SettingAction::BookerlyInstalled' not in content:
|
||||
content = content.replace(
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n'
|
||||
' pluginsSettings.push_back(SettingInfo::Action(\n'
|
||||
' StrId::STR_NONE_OPT, SettingAction::BookerlyInstalled\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
if '"Bookerly Font"' not in content:
|
||||
content = content.replace(
|
||||
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr,',
|
||||
' [&settings, this](int index) -> std::string {\n'
|
||||
' if (selectedCategoryIndex == 4) {\n'
|
||||
' const auto& s = settings[index];\n'
|
||||
' if (s.type == SettingType::ACTION && s.action == SettingAction::BookerlyInstalled) return "Bookerly Font";\n'
|
||||
' }\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
' }, nullptr, nullptr,'
|
||||
)
|
||||
|
||||
if '"Bookerly"' not in content:
|
||||
content = content.replace(
|
||||
' const uint8_t value = SETTINGS.*(setting.valuePtr);\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n',
|
||||
' const uint8_t value = SETTINGS.*(setting.valuePtr);\n'
|
||||
' if (setting.key && std::string(setting.key) == "fontFamily" && value == CrossPointSettings::BOOKERLY) {\n'
|
||||
' valueText = "Bookerly";\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
if '"Installed"' not in content:
|
||||
content = content.replace(
|
||||
' } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {\n'
|
||||
' valueText = std::to_string(SETTINGS.*(setting.valuePtr));\n'
|
||||
' }',
|
||||
' } else if (setting.type == SettingType::ACTION && setting.action == SettingAction::BookerlyInstalled) {\n'
|
||||
' valueText = "Installed";\n'
|
||||
' } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {\n'
|
||||
' valueText = std::to_string(SETTINGS.*(setting.valuePtr));\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 '"Bookerly"' 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, "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'
|
||||
)
|
||||
|
||||
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.h...")
|
||||
patch_settings_activity_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])
|
||||
@@ -0,0 +1,288 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import subprocess
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, PluginsTabEntry, EnumValueOverride, WebOptionAppend, SettingActionEnumValue,
|
||||
)
|
||||
from framework.engine import find_first, read_file, write_file, PatchError, replace_once, contains_once
|
||||
|
||||
PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# NOTE: this only extends the *static* built-in font-family list
|
||||
# (SettingsList.h's plain SettingInfo::Enum entry). CrossPoint also has a
|
||||
# second, SD-card-font-aware variant (buildFontFamilySetting()) that swaps in
|
||||
# when the user has SD-card fonts installed - Bookerly won't appear there.
|
||||
# That's a narrow pre-existing edge case (SD-card fonts + Bookerly at once)
|
||||
# left as a known follow-up rather than something this pass fully covers.
|
||||
|
||||
|
||||
def _generate_fonts(ctx):
|
||||
fonts_dir = os.path.join(ctx.repo_dir, "lib", "EpdFont", "builtinFonts")
|
||||
scripts_dir = os.path.join(ctx.repo_dir, "lib", "EpdFont", "scripts")
|
||||
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 = [12, 14, 16, 18]
|
||||
for size in sizes:
|
||||
fname = f"bookerly_{size}_regular"
|
||||
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, "Bookerly-Regular.ttf")
|
||||
if not os.path.exists(src_path):
|
||||
raise PatchError(f"Missing font file: {src_path}")
|
||||
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():
|
||||
raise PatchError(f"Failed generating {fname}: {result.stderr[:300]}")
|
||||
with open(out_path, "w") as f:
|
||||
f.write(result.stdout)
|
||||
print(f" ✓ generated {fname}.h")
|
||||
|
||||
ids = {}
|
||||
for size in sizes:
|
||||
path = os.path.join(fonts_dir, f"bookerly_{size}_regular.h")
|
||||
h = hashlib.sha256(open(path, "rb").read()).hexdigest()
|
||||
ids[f"BOOKERLY_{size}_FONT_ID"] = int(h, 16) % (2 ** 32) - (2 ** 31)
|
||||
ctx.bookerly_font_ids = ids
|
||||
|
||||
|
||||
def _patch_font_registration(ctx):
|
||||
ids = ctx.bookerly_font_ids
|
||||
|
||||
# fontIds.h
|
||||
path = find_first("fontIds.h", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
lines = [f"#define {name} ({val})" for name, val in ids.items() if name not in content]
|
||||
if lines:
|
||||
write_file(path, content.rstrip() + "\n" + "\n".join(lines) + "\n")
|
||||
print(f" fontIds.h: added {len(lines)} font ID(s).")
|
||||
|
||||
# all.h
|
||||
path = find_first("all.h", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
added = 0
|
||||
for size in [12, 14, 16, 18]:
|
||||
inc = f"#include <builtinFonts/bookerly_{size}_regular.h>"
|
||||
if inc not in content:
|
||||
content += inc + "\n"
|
||||
added += 1
|
||||
if added:
|
||||
write_file(path, content)
|
||||
print(f" all.h: added {added} include(s).")
|
||||
|
||||
# main.cpp: font family declarations + registration
|
||||
path = find_first("main.cpp", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
decl_lines, insert_lines = [], []
|
||||
for size in [12, 14, 16, 18]:
|
||||
var = f"bookerly{size}"
|
||||
if f"EpdFontFamily {var}FontFamily" in content:
|
||||
continue
|
||||
decl_lines += [
|
||||
f"EpdFont {var}RegularFont(&bookerly_{size}_regular);",
|
||||
f"EpdFontFamily {var}FontFamily(&{var}RegularFont, &{var}RegularFont, &{var}RegularFont, &{var}RegularFont);",
|
||||
]
|
||||
insert_lines.append(f" renderer.insertFont(BOOKERLY_{size}_FONT_ID, {var}FontFamily);")
|
||||
if decl_lines:
|
||||
# "#ifndef OMIT_FONTS" legitimately appears twice (global font
|
||||
# declarations, then again inside setup() around insertFont calls) -
|
||||
# we specifically want the *first* (global declaration) occurrence,
|
||||
# which is what replace(..., count=1) targets.
|
||||
anchor_decl = "#ifndef OMIT_FONTS"
|
||||
if anchor_decl not in content:
|
||||
raise PatchError("main.cpp: could not find `#ifndef OMIT_FONTS` (font declarations section)")
|
||||
content = content.replace(anchor_decl, "\n".join(decl_lines) + "\n" + anchor_decl, 1)
|
||||
|
||||
anchor_insert = " renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);"
|
||||
contains_once(content, anchor_insert, "main.cpp")
|
||||
content = content.replace(anchor_insert, "\n".join(insert_lines) + "\n" + anchor_insert, 1)
|
||||
|
||||
write_file(path, content)
|
||||
print(f" main.cpp: registered {len(decl_lines) // 2} Bookerly size(s).")
|
||||
|
||||
|
||||
def _patch_font_family_enum(ctx):
|
||||
path = find_first("CrossPointSettings.h", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
if "BOOKERLY" in content:
|
||||
return
|
||||
m = re.search(r"enum FONT_FAMILY \{(.*?)FONT_FAMILY_COUNT\s*\};", content)
|
||||
if not m:
|
||||
raise PatchError("CrossPointSettings.h: could not find `enum FONT_FAMILY { ... FONT_FAMILY_COUNT };`")
|
||||
content = content[:m.start()] + m.group(0).replace("FONT_FAMILY_COUNT", "BOOKERLY, FONT_FAMILY_COUNT", 1) + content[m.end():]
|
||||
write_file(path, content)
|
||||
print(" CrossPointSettings.h: added FONT_FAMILY::BOOKERLY.")
|
||||
|
||||
|
||||
def _patch_line_spacing_and_reader_font_id(ctx):
|
||||
path = find_first("CrossPointSettings.cpp", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
label = "CrossPointSettings.cpp"
|
||||
|
||||
if "case BOOKERLY:" not in content:
|
||||
spacing_anchor = (
|
||||
" case NOTOSANS:\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"
|
||||
)
|
||||
spacing_replacement = (
|
||||
" case NOTOSANS:\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"
|
||||
)
|
||||
content = replace_once(content, spacing_anchor, spacing_replacement, label)
|
||||
|
||||
if "BOOKERLY_12_FONT_ID" not in content:
|
||||
font_anchor = (
|
||||
" case NOTOSANS:\n"
|
||||
" switch (fontSize) {\n"
|
||||
" case SMALL:\n"
|
||||
" return NOTOSANS_12_FONT_ID;\n"
|
||||
" case MEDIUM:\n"
|
||||
" default:\n"
|
||||
" return NOTOSANS_14_FONT_ID;\n"
|
||||
" case LARGE:\n"
|
||||
" return NOTOSANS_16_FONT_ID;\n"
|
||||
" case EXTRA_LARGE:\n"
|
||||
" return NOTOSANS_18_FONT_ID;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
)
|
||||
font_replacement = (
|
||||
" case NOTOSANS:\n"
|
||||
" switch (fontSize) {\n"
|
||||
" case SMALL:\n"
|
||||
" return NOTOSANS_12_FONT_ID;\n"
|
||||
" case MEDIUM:\n"
|
||||
" default:\n"
|
||||
" return NOTOSANS_14_FONT_ID;\n"
|
||||
" case LARGE:\n"
|
||||
" return NOTOSANS_16_FONT_ID;\n"
|
||||
" case EXTRA_LARGE:\n"
|
||||
" return NOTOSANS_18_FONT_ID;\n"
|
||||
" }\n"
|
||||
" case BOOKERLY:\n"
|
||||
" switch (fontSize) {\n"
|
||||
" case SMALL:\n"
|
||||
" return BOOKERLY_12_FONT_ID;\n"
|
||||
" case MEDIUM:\n"
|
||||
" default:\n"
|
||||
" return BOOKERLY_14_FONT_ID;\n"
|
||||
" case LARGE:\n"
|
||||
" return BOOKERLY_16_FONT_ID;\n"
|
||||
" case EXTRA_LARGE:\n"
|
||||
" return BOOKERLY_18_FONT_ID;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
)
|
||||
content = replace_once(content, font_anchor, font_replacement, label)
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointSettings.cpp: Bookerly line-spacing + font-id cases added.")
|
||||
|
||||
|
||||
def _patch_settings_list_option(ctx):
|
||||
path = find_first("SettingsList.h", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
label = "SettingsList.h"
|
||||
anchor = (
|
||||
'SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,\n'
|
||||
' {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS}, "fontFamily", StrId::STR_CAT_READER),'
|
||||
)
|
||||
if "StrId::STR_NONE_OPT}, \"fontFamily\"" in content:
|
||||
return
|
||||
replacement = (
|
||||
'SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily,\n'
|
||||
' {StrId::STR_NOTO_SERIF, StrId::STR_NOTO_SANS, StrId::STR_NONE_OPT}, "fontFamily", StrId::STR_CAT_READER),'
|
||||
)
|
||||
content = replace_once(content, anchor, replacement, label)
|
||||
write_file(path, content)
|
||||
print(" SettingsList.h: fontFamily gained a 3rd (Bookerly) option.")
|
||||
|
||||
|
||||
def _pre_patch(ctx):
|
||||
print(" Generating Bookerly font files...")
|
||||
_generate_fonts(ctx)
|
||||
|
||||
|
||||
def _post_patch(ctx):
|
||||
print(" Registering Bookerly font family...")
|
||||
_patch_font_registration(ctx)
|
||||
_patch_font_family_enum(ctx)
|
||||
_patch_line_spacing_and_reader_font_id(ctx)
|
||||
_patch_settings_list_option(ctx)
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
return PluginManifest(
|
||||
name="bookerly",
|
||||
pretty_name="Bookerly Font",
|
||||
phase=0, # must run before smallerfonts (phase 1) wraps getReaderFontId's returns
|
||||
source_files=[
|
||||
SourceFile("BookerlyPlugin.h", "src/activities/settings"),
|
||||
SourceFile("BookerlyPlugin.cpp", "src/activities/settings"),
|
||||
],
|
||||
setting_actions=[
|
||||
SettingActionEnumValue("BookerlyInstalled"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="Bookerly Font",
|
||||
kind="action",
|
||||
action_name="BookerlyInstalled",
|
||||
action_value_text="Installed",
|
||||
activity_launch_expr=None, # informational row only, matches original behaviour
|
||||
),
|
||||
],
|
||||
enum_value_overrides=[
|
||||
EnumValueOverride(key="fontFamily", condition_expr="value == CrossPointSettings::BOOKERLY", text="Bookerly"),
|
||||
],
|
||||
web_option_appends=[
|
||||
WebOptionAppend(key="fontFamily", label="Bookerly"),
|
||||
],
|
||||
pre_patch=_pre_patch,
|
||||
post_patch=_post_patch,
|
||||
)
|
||||
@@ -1,536 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
skip = {"DarkModeSettingsPage.h", "DarkModeSettingsPage.cpp"}
|
||||
|
||||
for fname in os.listdir(plugin_dir):
|
||||
if fname.endswith((".h", ".cpp", ".hpp")) and fname not in skip:
|
||||
src = os.path.join(plugin_dir, fname)
|
||||
dst = os.path.join(dest, fname)
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✓ {fname}")
|
||||
|
||||
|
||||
def patch_cross_point_settings_h(repo_dir):
|
||||
path = find_first("CrossPointSettings.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "darkModeState" in content:
|
||||
print(" CrossPointSettings.h already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#pragma once\n#include <HalStorage.h>',
|
||||
'#pragma once\n#include <HalStorage.h>\n#include "activities/settings/DarkModePlugin.h"'
|
||||
)
|
||||
content = content.replace(
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;\n uint8_t darkModeState = 0;'
|
||||
)
|
||||
|
||||
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 "darkModeState" in content:
|
||||
print(" SettingsList.h already patched, skipping.")
|
||||
return
|
||||
|
||||
entries = (
|
||||
' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"),\n'
|
||||
' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"),\n'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' };\n return list;\n}\n',
|
||||
entries + ' };\n return list;\n}\n'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsList.h patched.")
|
||||
|
||||
|
||||
def patch_settings_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
already_has_plugins_tab = "categoryCount = 5" in content
|
||||
already_has_plugins_vec = "pluginsSettings" in content
|
||||
|
||||
if already_has_plugins_tab and already_has_plugins_vec:
|
||||
print(" SettingsActivity.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if '"DarkModePlugin.h"' not in content:
|
||||
content = content.replace(
|
||||
'#include <I18n.h>',
|
||||
'#include <I18n.h>\n#include "DarkModePlugin.h"'
|
||||
)
|
||||
|
||||
if not already_has_plugins_tab:
|
||||
content = content.replace(
|
||||
'static constexpr int categoryCount = 4;',
|
||||
'static constexpr int categoryCount = 5;'
|
||||
)
|
||||
|
||||
if not already_has_plugins_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 '"DarkModePlugin.h"' not in content:
|
||||
content = content.replace(
|
||||
'#include "SettingsList.h"',
|
||||
'#include "SettingsList.h"\n#include "DarkModePlugin.h"'
|
||||
)
|
||||
|
||||
if "STR_NONE_OPT" not in content:
|
||||
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();'
|
||||
)
|
||||
|
||||
plugins_block = (
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\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'
|
||||
)
|
||||
|
||||
broken_block = (
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT,\n'
|
||||
' &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
|
||||
' "darkModeState"\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
if broken_block in content:
|
||||
content = content.replace(broken_block, plugins_block)
|
||||
elif '"darkModeState"' in content and '"smallerFontsMode"' not in content:
|
||||
content = content.replace(
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\n'
|
||||
' ));\n',
|
||||
plugins_block
|
||||
)
|
||||
elif '"darkModeState"' not in content:
|
||||
content = content.replace(
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n'
|
||||
+ plugins_block
|
||||
)
|
||||
|
||||
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);'
|
||||
)
|
||||
|
||||
if 'selectedCategoryIndex == 4' not in content:
|
||||
content = content.replace(
|
||||
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },',
|
||||
' [&settings, this](int index) -> std::string {\n'
|
||||
' if (selectedCategoryIndex == 4) {\n'
|
||||
' const auto& s = settings[index];\n'
|
||||
' 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'
|
||||
' },'
|
||||
)
|
||||
else:
|
||||
for s_decl in [
|
||||
' const auto& s = settings[index];\n',
|
||||
' const auto& s = settings[index];\n',
|
||||
]:
|
||||
if s_decl in content and '"darkModeState") return "Dark Mode"' not in content:
|
||||
content = content.replace(
|
||||
s_decl,
|
||||
s_decl +
|
||||
s_decl[:len(s_decl) - len(s_decl.lstrip())] +
|
||||
'if (s.key && std::string(s.key) == "darkModeState") return "Dark Mode";\n' +
|
||||
s_decl[:len(s_decl) - len(s_decl.lstrip())] +
|
||||
'if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n',
|
||||
1
|
||||
)
|
||||
break
|
||||
|
||||
both_present = 'DarkModePlugin::stateName' in content and 'SmallerFontsPlugin::modeName' in content
|
||||
only_dark = 'DarkModePlugin::stateName' in content and 'SmallerFontsPlugin::modeName' not in content
|
||||
neither = 'DarkModePlugin::stateName' not in content
|
||||
|
||||
if only_dark:
|
||||
content = content.replace(
|
||||
' if (setting.key && std::string(setting.key) == "darkModeState") {\n'
|
||||
' valueText = DarkModePlugin::stateName(static_cast<DarkModeState>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }',
|
||||
' if (setting.key && std::string(setting.key) == "darkModeState") {\n'
|
||||
' valueText = DarkModePlugin::stateName(static_cast<DarkModeState>(value));\n'
|
||||
' } else if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }'
|
||||
)
|
||||
elif neither:
|
||||
if 'SmallerFontsPlugin::modeName' in content:
|
||||
content = content.replace(
|
||||
' if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }',
|
||||
' if (setting.key && std::string(setting.key) == "darkModeState") {\n'
|
||||
' valueText = DarkModePlugin::stateName(static_cast<DarkModeState>(value));\n'
|
||||
' } else if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }'
|
||||
)
|
||||
else:
|
||||
content = content.replace(
|
||||
' valueText = I18N.get(setting.enumValues[value]);',
|
||||
' if (setting.key && std::string(setting.key) == "darkModeState") {\n'
|
||||
' valueText = DarkModePlugin::stateName(static_cast<DarkModeState>(value));\n'
|
||||
' } else if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.cpp patched.")
|
||||
|
||||
def patch_epub_reader(repo_dir):
|
||||
path = find_first("EpubReaderActivity.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
image_block_patched = (
|
||||
'renderer.fillRect' in content and
|
||||
'isDarkMode' in content.split('renderer.fillRect')[1].split('FAST_REFRESH')[0]
|
||||
)
|
||||
if "isDarkMode" in content and image_block_patched:
|
||||
print(" EpubReaderActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "EpubReaderActivity.h"',
|
||||
'#include "EpubReaderActivity.h"\n#include "activities/settings/DarkModePlugin.h"'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);\n'
|
||||
' renderStatusBar();\n'
|
||||
' fcm->logStats("bw_render");\n',
|
||||
' page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);\n'
|
||||
' renderStatusBar();\n'
|
||||
' if (DarkModePlugin::isDarkMode(static_cast<DarkModeState>(SETTINGS.darkModeState))) renderer.invertScreen();\n'
|
||||
' fcm->logStats("bw_render");\n'
|
||||
)
|
||||
|
||||
old_image_block = (
|
||||
' if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {\n'
|
||||
' renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false);\n'
|
||||
' renderer.displayBuffer(HalDisplay::FAST_REFRESH);\n'
|
||||
'\n'
|
||||
' // Re-render page content to restore images into the blanked area\n'
|
||||
' // Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %)\n'
|
||||
' page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);\n'
|
||||
' renderer.displayBuffer(HalDisplay::FAST_REFRESH);\n'
|
||||
' } else {\n'
|
||||
' renderer.displayBuffer(HalDisplay::HALF_REFRESH);\n'
|
||||
' }\n'
|
||||
)
|
||||
new_image_block = (
|
||||
' if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {\n'
|
||||
' const bool darkMode = DarkModePlugin::isDarkMode(static_cast<DarkModeState>(SETTINGS.darkModeState));\n'
|
||||
' renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, darkMode);\n'
|
||||
' if (darkMode) renderer.invertScreen();\n'
|
||||
' renderer.displayBuffer(HalDisplay::FAST_REFRESH);\n'
|
||||
'\n'
|
||||
' page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);\n'
|
||||
' if (darkMode) renderer.invertScreen();\n'
|
||||
' renderer.displayBuffer(HalDisplay::FAST_REFRESH);\n'
|
||||
' } else {\n'
|
||||
' renderer.displayBuffer(HalDisplay::HALF_REFRESH);\n'
|
||||
' }\n'
|
||||
)
|
||||
if old_image_block in content:
|
||||
content = content.replace(old_image_block, new_image_block)
|
||||
|
||||
write_file(path, content)
|
||||
print(" EpubReaderActivity.cpp patched.")
|
||||
|
||||
|
||||
def patch_txt_reader(repo_dir):
|
||||
path = find_first("TxtReaderActivity.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "isDarkMode" in content:
|
||||
print(" TxtReaderActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "TxtReaderActivity.h"',
|
||||
'#include "TxtReaderActivity.h"\n#include "activities/settings/DarkModePlugin.h"'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' renderLines();\n'
|
||||
' renderStatusBar();\n'
|
||||
'\n'
|
||||
' ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);',
|
||||
' renderLines();\n'
|
||||
' renderStatusBar();\n'
|
||||
' if (DarkModePlugin::isDarkMode(static_cast<DarkModeState>(SETTINGS.darkModeState))) renderer.invertScreen();\n'
|
||||
'\n'
|
||||
' ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" TxtReaderActivity.cpp patched.")
|
||||
|
||||
|
||||
def patch_xtc_reader(repo_dir):
|
||||
path = find_first("XtcReaderActivity.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "isDarkMode" in content:
|
||||
print(" XtcReaderActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "XtcReaderActivity.h"',
|
||||
'#include "XtcReaderActivity.h"\n#include "activities/settings/DarkModePlugin.h"'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' if (pagesUntilFullRefresh <= 1) {\n'
|
||||
' renderer.displayBuffer(HalDisplay::HALF_REFRESH);\n'
|
||||
' pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();\n'
|
||||
' } else {\n'
|
||||
' renderer.displayBuffer();\n'
|
||||
' pagesUntilFullRefresh--;\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' // Pass 2: LSB buffer',
|
||||
' if (DarkModePlugin::isDarkMode(static_cast<DarkModeState>(SETTINGS.darkModeState))) renderer.invertScreen();\n'
|
||||
' if (pagesUntilFullRefresh <= 1) {\n'
|
||||
' renderer.displayBuffer(HalDisplay::HALF_REFRESH);\n'
|
||||
' pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();\n'
|
||||
' } else {\n'
|
||||
' renderer.displayBuffer();\n'
|
||||
' pagesUntilFullRefresh--;\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' // Pass 2: LSB buffer'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' if (pagesUntilFullRefresh <= 1) {\n'
|
||||
' renderer.displayBuffer(HalDisplay::HALF_REFRESH);\n'
|
||||
' pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();\n'
|
||||
' } else {\n'
|
||||
' renderer.displayBuffer();\n'
|
||||
' pagesUntilFullRefresh--;\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' LOG_DBG("XTR"',
|
||||
' if (DarkModePlugin::isDarkMode(static_cast<DarkModeState>(SETTINGS.darkModeState))) renderer.invertScreen();\n'
|
||||
' if (pagesUntilFullRefresh <= 1) {\n'
|
||||
' renderer.displayBuffer(HalDisplay::HALF_REFRESH);\n'
|
||||
' pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();\n'
|
||||
' } else {\n'
|
||||
' renderer.displayBuffer();\n'
|
||||
' pagesUntilFullRefresh--;\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' LOG_DBG("XTR"'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" XtcReaderActivity.cpp patched.")
|
||||
|
||||
|
||||
def patch_web_server(repo_dir):
|
||||
path = find_first("CrossPointWebServer.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if '"Dark Mode"' in content or '"Smaller Fonts"' in content:
|
||||
print(" CrossPointWebServer.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
name_overrides = (
|
||||
'\n'
|
||||
' if (s.key) {\n'
|
||||
' if (strcmp(s.key, "darkModeState") == 0) {\n'
|
||||
' doc["name"] = "Dark Mode";\n'
|
||||
' doc["category"] = "Plugins";\n'
|
||||
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
|
||||
' doc["name"] = "Smaller Fonts";\n'
|
||||
' doc["category"] = "Plugins";\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' doc["category"] = I18N.get(s.category);\n'
|
||||
'\n'
|
||||
' switch (s.type) {',
|
||||
' doc["category"] = I18N.get(s.category);\n'
|
||||
+ name_overrides +
|
||||
'\n'
|
||||
' switch (s.type) {'
|
||||
)
|
||||
|
||||
enum_overrides = (
|
||||
' if (s.key) {\n'
|
||||
' if (strcmp(s.key, "darkModeState") == 0) {\n'
|
||||
' JsonArray opts = doc["options"].to<JsonArray>();\n'
|
||||
' opts.add("Disabled");\n'
|
||||
' opts.add("Enabled");\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'
|
||||
' }\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' for (const auto& opt : s.enumValues) {\n'
|
||||
' options.add(I18N.get(opt));\n'
|
||||
' }\n'
|
||||
' break;\n',
|
||||
' for (const auto& opt : s.enumValues) {\n'
|
||||
' options.add(I18N.get(opt));\n'
|
||||
' }\n'
|
||||
+ enum_overrides +
|
||||
' break;\n'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointWebServer.cpp patched.")
|
||||
|
||||
|
||||
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(" Patching CrossPointSettings.h...")
|
||||
patch_cross_point_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsList.h...")
|
||||
patch_settings_list_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.h...")
|
||||
patch_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.cpp...")
|
||||
patch_settings_cpp(repo_dir)
|
||||
|
||||
print(" Patching EpubReaderActivity.cpp...")
|
||||
patch_epub_reader(repo_dir)
|
||||
|
||||
print(" Patching TxtReaderActivity.cpp...")
|
||||
patch_txt_reader(repo_dir)
|
||||
|
||||
print(" Patching XtcReaderActivity.cpp...")
|
||||
patch_xtc_reader(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])
|
||||
@@ -0,0 +1,37 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, Include, SettingsField, PluginsTabEntry, ReaderInvertHook,
|
||||
)
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
return PluginManifest(
|
||||
name="darkmode",
|
||||
pretty_name="Dark Mode",
|
||||
source_files=[
|
||||
SourceFile("DarkModePlugin.h", "src/activities/settings"),
|
||||
SourceFile("DarkModePlugin.cpp", "src/activities/settings"),
|
||||
],
|
||||
includes=[
|
||||
Include("activities/settings/DarkModePlugin.h", "cross_point_settings_h"),
|
||||
],
|
||||
settings_fields=[
|
||||
SettingsField("uint8_t darkModeState = 0;"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="Dark Mode",
|
||||
kind="enum",
|
||||
key="darkModeState",
|
||||
option_labels=["Disabled", "Enabled"],
|
||||
value_text_expr="DarkModePlugin::stateName(static_cast<DarkModeState>(value))",
|
||||
),
|
||||
],
|
||||
reader_invert_hook=ReaderInvertHook(
|
||||
predicate_expr="DarkModePlugin::isDarkMode(static_cast<DarkModeState>(SETTINGS.darkModeState))",
|
||||
include_header="activities/settings/DarkModePlugin.h",
|
||||
),
|
||||
)
|
||||
@@ -1,564 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import shutil
|
||||
import subprocess
|
||||
import getpass
|
||||
import csv
|
||||
import tempfile
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
NVS_NAMESPACE = "github_sync"
|
||||
XTEINK_CONFIG_FILE = Path.home() / ".xteink"
|
||||
|
||||
|
||||
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 load_xteink_config() -> dict:
|
||||
if not XTEINK_CONFIG_FILE.exists():
|
||||
return {}
|
||||
try:
|
||||
with open(XTEINK_CONFIG_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def save_xteink_config(cfg: dict) -> None:
|
||||
try:
|
||||
saved = load_xteink_config()
|
||||
if "username" in cfg:
|
||||
saved["githubuser"] = cfg["username"]
|
||||
if "repo" in cfg:
|
||||
saved["githubrepo"] = cfg["repo"]
|
||||
if "branch" in cfg:
|
||||
saved["githubbranch"] = cfg["branch"]
|
||||
with open(XTEINK_CONFIG_FILE, "w") as f:
|
||||
json.dump(saved, f, indent=2)
|
||||
print(f" ✓ Config saved to {XTEINK_CONFIG_FILE}")
|
||||
except Exception as e:
|
||||
print(f" ! Could not save config: {e}")
|
||||
|
||||
|
||||
_github_https_deps_ready = False
|
||||
|
||||
def ensure_python_module(module_name: str, pip_package: str | None = None, *, description: str = "") -> None:
|
||||
pip_package = pip_package or module_name
|
||||
try:
|
||||
__import__(module_name)
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
desc = f" — {description}" if description else ""
|
||||
print(f" ! Python module '{module_name}' is not installed{desc}.")
|
||||
input(f" Press Enter to install {pip_package}... ")
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", pip_package],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
sys.exit(f"ERROR: Failed to install {pip_package}:\n{result.stderr.strip()}")
|
||||
print(f" ✓ {pip_package} installed")
|
||||
try:
|
||||
__import__(module_name)
|
||||
except ImportError:
|
||||
sys.exit(f"ERROR: '{module_name}' still not importable after install")
|
||||
|
||||
def ensure_github_https_dependencies() -> None:
|
||||
global _github_https_deps_ready
|
||||
if _github_https_deps_ready:
|
||||
return
|
||||
if os.environ.get("GITHUB_SYNC_SSL_NO_VERIFY", "").strip().lower() in ("1", "true", "yes", "on"):
|
||||
_github_https_deps_ready = True
|
||||
return
|
||||
cf = os.environ.get("SSL_CERT_FILE", "").strip()
|
||||
if cf and os.path.isfile(cf):
|
||||
_github_https_deps_ready = True
|
||||
return
|
||||
try:
|
||||
import certifi # noqa: F401
|
||||
_github_https_deps_ready = True
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
ensure_python_module(
|
||||
"certifi",
|
||||
description="recommended for GitHub HTTPS (fixes macOS SSL certificate errors)",
|
||||
)
|
||||
_github_https_deps_ready = True
|
||||
|
||||
def get_github_ssl_context():
|
||||
flag = os.environ.get("GITHUB_SYNC_SSL_NO_VERIFY", "").strip().lower()
|
||||
if flag in ("1", "true", "yes", "on"):
|
||||
print(" ! SSL verification is OFF (GITHUB_SYNC_SSL_NO_VERIFY). Only use if you trust this network.")
|
||||
return ssl._create_unverified_context()
|
||||
cert_file = os.environ.get("SSL_CERT_FILE", "").strip()
|
||||
if cert_file and os.path.isfile(cert_file):
|
||||
return ssl.create_default_context(cafile=cert_file)
|
||||
try:
|
||||
import certifi
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
except ImportError:
|
||||
pass
|
||||
return ssl.create_default_context()
|
||||
|
||||
def ssl_troubleshoot_hint(err_txt: str | None) -> str:
|
||||
if not err_txt:
|
||||
return ""
|
||||
if "CERTIFICATE_VERIFY_FAILED" in err_txt or "SSL" in err_txt:
|
||||
return (
|
||||
" Try: pip install certifi (then re-run), or run macOS "
|
||||
"'Install Certificates.command' for your Python, or set SSL_CERT_FILE to a CA bundle. "
|
||||
"Last resort: GITHUB_SYNC_SSL_NO_VERIFY=1 (insecure)."
|
||||
)
|
||||
return ""
|
||||
|
||||
def github_api_get(path: str, token: str | None = None, timeout: int = 10):
|
||||
url = f"https://api.github.com{path}"
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "xteink-github-sync-patcher",
|
||||
}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
ctx = get_github_ssl_context()
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
data = json.loads(body) if body else {}
|
||||
return resp.status, data, None
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
data = json.loads(body) if body else {}
|
||||
except Exception:
|
||||
data = {"message": body.strip()} if body else {}
|
||||
return e.code, data, None
|
||||
except Exception as e:
|
||||
return None, None, str(e)
|
||||
|
||||
def validate_github_username(username: str) -> tuple[bool, str]:
|
||||
status, data, err_txt = github_api_get(f"/users/{username}")
|
||||
if err_txt:
|
||||
return False, f"Could not reach GitHub API: {err_txt}{ssl_troubleshoot_hint(err_txt)}"
|
||||
if status == 200:
|
||||
return True, "GitHub username is reachable."
|
||||
if status == 404:
|
||||
return False, "Username not found on GitHub."
|
||||
msg = data.get("message", "Unknown API error") if isinstance(data, dict) else "Unknown API error"
|
||||
return False, f"GitHub API error ({status}): {msg}"
|
||||
|
||||
def validate_pat(token: str) -> tuple[bool, str, str | None]:
|
||||
status, data, err_txt = github_api_get("/user", token=token)
|
||||
if err_txt:
|
||||
return False, f"Could not validate PAT: {err_txt}{ssl_troubleshoot_hint(err_txt)}", None
|
||||
if status == 200 and isinstance(data, dict):
|
||||
login = data.get("login")
|
||||
return True, f"PAT is valid (authenticated as {login}).", login
|
||||
if status in (401, 403):
|
||||
msg = data.get("message", "Unauthorized") if isinstance(data, dict) else "Unauthorized"
|
||||
return False, f"PAT rejected: {msg}", None
|
||||
msg = data.get("message", "Unknown API error") if isinstance(data, dict) else "Unknown API error"
|
||||
return False, f"PAT validation failed ({status}): {msg}", None
|
||||
|
||||
def validate_repo_access(owner: str, repo: str, token: str) -> tuple[bool, str]:
|
||||
status, data, err_txt = github_api_get(f"/repos/{owner}/{repo}", token=token)
|
||||
if err_txt:
|
||||
return False, f"Could not validate repo access: {err_txt}{ssl_troubleshoot_hint(err_txt)}"
|
||||
if status == 200:
|
||||
return True, "Repo is reachable with this PAT."
|
||||
if status == 404:
|
||||
return False, "Repo not found, or PAT cannot access it."
|
||||
if status in (401, 403):
|
||||
msg = data.get("message", "Unauthorized") if isinstance(data, dict) else "Unauthorized"
|
||||
return False, f"Access denied: {msg}"
|
||||
msg = data.get("message", "Unknown API error") if isinstance(data, dict) else "Unknown API error"
|
||||
return False, f"Repo validation failed ({status}): {msg}"
|
||||
|
||||
def prompt_github_config() -> dict:
|
||||
print("\n GitHub Sync Configuration by Justin Oros")
|
||||
print(" Press Enter on any field to use the saved value, or type a new one.")
|
||||
print(" Leave GitHub username blank to skip and configure on-device later.\n")
|
||||
|
||||
saved = load_xteink_config()
|
||||
|
||||
saved_user = saved.get("githubuser", "")
|
||||
saved_repo = saved.get("githubrepo", "xteink")
|
||||
saved_branch = saved.get("githubbranch", "main")
|
||||
|
||||
while True:
|
||||
if saved_user:
|
||||
raw = input(f" GitHub username ({saved_user}): ").strip()
|
||||
username = raw or saved_user
|
||||
else:
|
||||
username = input(" GitHub username: ").strip()
|
||||
if not username:
|
||||
return {}
|
||||
ensure_github_https_dependencies()
|
||||
ok_user, msg = validate_github_username(username)
|
||||
if ok_user:
|
||||
print(f" ✓ {msg}")
|
||||
break
|
||||
print(f" ! {msg}")
|
||||
retry = input(" Try another username? [Y/n]: ").strip().lower()
|
||||
if retry == "n":
|
||||
return {}
|
||||
|
||||
print("\n To generate a Personal Access Token (PAT):")
|
||||
print(" 1. Go to github.com -> Settings -> Developer settings")
|
||||
print(" 2. Personal access tokens -> Fine-grained tokens")
|
||||
print(" 3. Click 'Generate new token'")
|
||||
print(" 4. Token name: xteink")
|
||||
print(" 5. Expiration: No expiration")
|
||||
print(" 6. Repository access: Only selected repositories -> select 'xteink'")
|
||||
print(" 7. Permissions -> Add permissions -> Contents: Read-only")
|
||||
print(" 8. Click 'Generate token' then copy it - GitHub only shows it once\n")
|
||||
|
||||
authenticated_login = None
|
||||
while True:
|
||||
pat = getpass.getpass(" Personal Access Token (PAT): ").strip()
|
||||
if not pat:
|
||||
return {}
|
||||
ok_pat, msg, authenticated_login = validate_pat(pat)
|
||||
if ok_pat:
|
||||
print(f" ✓ {msg}")
|
||||
break
|
||||
print(f" ! {msg}")
|
||||
retry = input(" Try entering PAT again? [Y/n]: ").strip().lower()
|
||||
if retry == "n":
|
||||
return {}
|
||||
|
||||
while True:
|
||||
raw = input(f" Repo name ({saved_repo}): ").strip()
|
||||
repo = raw or saved_repo
|
||||
owner_for_repo = authenticated_login or username
|
||||
ok_repo, msg = validate_repo_access(owner_for_repo, repo, pat)
|
||||
if ok_repo:
|
||||
print(f" ✓ {msg}")
|
||||
if authenticated_login and authenticated_login != username:
|
||||
print(f" ! Username '{username}' differs from PAT owner '{authenticated_login}'. Repo was validated under '{owner_for_repo}'.")
|
||||
break
|
||||
print(f" ! {msg}")
|
||||
retry = input(" Try another repo name? [Y/n]: ").strip().lower()
|
||||
if retry == "n":
|
||||
return {}
|
||||
|
||||
raw = input(f" Branch ({saved_branch}): ").strip()
|
||||
branch = raw or saved_branch
|
||||
|
||||
cfg = {"username": username, "pat": pat, "repo": repo, "branch": branch}
|
||||
save_xteink_config(cfg)
|
||||
return cfg
|
||||
|
||||
def check_nvs_gen() -> bool:
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "esp_idf_nvs_partition_gen", "--help"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
result2 = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "esp-idf-nvs-partition-gen"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
result3 = subprocess.run(
|
||||
["python3", "-m", "esp_idf_nvs_partition_gen", "--help"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return result3.returncode == 0
|
||||
|
||||
def write_nvs_partition(cfg: dict) -> Path | None:
|
||||
if not check_nvs_gen():
|
||||
print(" ! esp-idf-nvs-partition-gen unavailable — enter credentials on-device via Settings -> GitHub Sync.")
|
||||
return None
|
||||
|
||||
nvs_csv = Path(tempfile.mkdtemp()) / "github_sync_nvs.csv"
|
||||
rows = [
|
||||
["key", "type", "encoding", "value"],
|
||||
[NVS_NAMESPACE, "namespace", "", ""],
|
||||
["username", "data", "string", cfg["username"]],
|
||||
["pat", "data", "string", cfg["pat"]],
|
||||
["repo", "data", "string", cfg["repo"]],
|
||||
["branch", "data", "string", cfg["branch"]],
|
||||
]
|
||||
with open(nvs_csv, "w", newline="") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerows(rows)
|
||||
|
||||
nvs_bin = nvs_csv.with_suffix(".bin")
|
||||
result = subprocess.run(
|
||||
["python3", "-m", "esp_idf_nvs_partition_gen", "generate",
|
||||
str(nvs_csv), str(nvs_bin), "0x3000"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ! nvs_partition_gen failed — enter credentials on-device via Settings -> GitHub Sync.\n{result.stderr.strip()}")
|
||||
return None
|
||||
|
||||
print(f" ✓ NVS partition written to {nvs_bin}")
|
||||
return nvs_bin
|
||||
|
||||
|
||||
def copy_plugin_sources(plugin_dir, repo_dir):
|
||||
copies = [
|
||||
(os.path.join(plugin_dir, "GitHubSync.h"),
|
||||
os.path.join(repo_dir, "include", "GitHubSync.h")),
|
||||
(os.path.join(plugin_dir, "GitHubSyncSettingsActivity.h"),
|
||||
os.path.join(repo_dir, "include", "GitHubSyncSettingsActivity.h")),
|
||||
(os.path.join(plugin_dir, "GitHubSync.cpp"),
|
||||
os.path.join(repo_dir, "src", "github_sync", "GitHubSync.cpp")),
|
||||
(os.path.join(plugin_dir, "GitHubSyncSettingsActivity.cpp"),
|
||||
os.path.join(repo_dir, "src", "activities", "settings", "GitHubSyncSettingsActivity.cpp")),
|
||||
]
|
||||
for src, dst in copies:
|
||||
if not os.path.exists(src):
|
||||
sys.exit(f"ERROR: Patch file missing: {src}")
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✓ {os.path.basename(dst)}")
|
||||
|
||||
|
||||
def patch_platformio_ini(repo_dir):
|
||||
ini = os.path.join(repo_dir, "platformio.ini")
|
||||
if not os.path.exists(ini):
|
||||
print(" platformio.ini not found, skipping.")
|
||||
return
|
||||
content = read_file(ini)
|
||||
if "ArduinoJson" in content:
|
||||
print(" platformio.ini already patched, skipping.")
|
||||
return
|
||||
content = content.replace(
|
||||
"lib_deps",
|
||||
"lib_deps\n\tbblanchon/ArduinoJson @ ^7",
|
||||
1
|
||||
)
|
||||
write_file(ini, content)
|
||||
print(" platformio.ini patched.")
|
||||
|
||||
|
||||
def patch_main_cpp(repo_dir):
|
||||
candidates = [
|
||||
os.path.join(repo_dir, "src", "main.cpp"),
|
||||
os.path.join(repo_dir, "src", "Main.cpp"),
|
||||
]
|
||||
main_path = next((p for p in candidates if os.path.exists(p)), None)
|
||||
if not main_path:
|
||||
print(" ! Could not find main.cpp — add GitHub sync call manually.")
|
||||
return
|
||||
|
||||
content = read_file(main_path)
|
||||
|
||||
if '#include "GitHubSync.h"' in content:
|
||||
print(" main.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "CrossPointSettings.h"',
|
||||
'#include "GitHubSync.h"\n#include "CrossPointSettings.h"',
|
||||
1
|
||||
)
|
||||
|
||||
sync_call = (
|
||||
'\n if (GitHubSync::isConfigured()) {\n'
|
||||
' GitHubSyncResult result = GitHubSync::sync();\n'
|
||||
' if (result != GitHubSyncResult::OK) {\n'
|
||||
' LOG_ERR("SYNC", "%s", GitHubSync::resultMessage(result));\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
if "GitHubSync::isConfigured" not in content:
|
||||
content = content.replace(
|
||||
"activityManager.goToBoot();",
|
||||
"activityManager.goToBoot();" + sync_call,
|
||||
1
|
||||
)
|
||||
|
||||
write_file(main_path, content)
|
||||
print(" main.cpp patched.")
|
||||
|
||||
|
||||
def patch_settings_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "GitHubSync," in content:
|
||||
print(" SettingsActivity.h already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
" CheckForUpdates,",
|
||||
" CheckForUpdates,\n GitHubSync,",
|
||||
1
|
||||
)
|
||||
|
||||
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 '#include "GitHubSyncSettingsActivity.h"' in content and "SettingAction::GitHubSync" in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
if '#include "GitHubSyncSettingsActivity.h"' not in content:
|
||||
content = content.replace(
|
||||
'#include "SettingsActivity.h"',
|
||||
'#include "SettingsActivity.h"\n#include "GitHubSyncSettingsActivity.h"',
|
||||
1
|
||||
)
|
||||
|
||||
if "SettingAction::GitHubSync" not in content:
|
||||
content = content.replace(
|
||||
"SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));",
|
||||
"SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));\n"
|
||||
" pluginsSettings.push_back(SettingInfo::Action(StrId::STR_NONE_OPT, SettingAction::GitHubSync));",
|
||||
1
|
||||
)
|
||||
|
||||
if "case SettingAction::GitHubSync:" not in content:
|
||||
content = content.replace(
|
||||
"case SettingAction::CheckForUpdates:",
|
||||
"case SettingAction::CheckForUpdates:\n"
|
||||
" case SettingAction::GitHubSync:\n"
|
||||
" startActivityForResult(std::make_unique<GitHubSyncSettingsActivity>(renderer, mappedInput), resultHandler);\n"
|
||||
" break;",
|
||||
1
|
||||
)
|
||||
|
||||
if '"GitHub Sync"' not in content:
|
||||
content = content.replace(
|
||||
'[&settings, this](int index) -> std::string {\n',
|
||||
'[&settings, this](int index) -> std::string {\n'
|
||||
' if (selectedCategoryIndex == 4) {\n'
|
||||
' const auto& s = settings[index];\n'
|
||||
' if (s.type == SettingType::ACTION && s.action == SettingAction::GitHubSync) return "GitHub Sync";\n'
|
||||
' }\n',
|
||||
1
|
||||
)
|
||||
|
||||
if '"Installed"' not in content:
|
||||
content = content.replace(
|
||||
'} else if (setting.type == SettingType::ACTION && setting.action == SettingAction::BookerlyInstalled) {\n'
|
||||
' valueText = "Installed";\n',
|
||||
'} else if (setting.type == SettingType::ACTION && setting.action == SettingAction::BookerlyInstalled) {\n'
|
||||
' valueText = "Installed";\n'
|
||||
' } else if (setting.type == SettingType::ACTION && setting.action == SettingAction::GitHubSync) {\n'
|
||||
' valueText = "Sync";\n',
|
||||
1
|
||||
)
|
||||
elif "GitHubSync" not in content.split('valueText = "Installed"')[0].split('BookerlyInstalled')[-1]:
|
||||
content = content.replace(
|
||||
'} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {\n'
|
||||
' valueText = std::to_string(SETTINGS.*(setting.valuePtr));\n'
|
||||
' }',
|
||||
'} else if (setting.type == SettingType::ACTION && setting.action == SettingAction::GitHubSync) {\n'
|
||||
' valueText = "Sync";\n'
|
||||
' } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {\n'
|
||||
' valueText = std::to_string(SETTINGS.*(setting.valuePtr));\n'
|
||||
' }',
|
||||
1
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.cpp patched.")
|
||||
|
||||
|
||||
def patch_translation_files(repo_dir):
|
||||
yaml_dir = os.path.join(repo_dir, "lib", "I18n", "translations")
|
||||
if not os.path.isdir(yaml_dir):
|
||||
print(" ! Could not find translation YAML files — add STR_GITHUB_SYNC manually.")
|
||||
return
|
||||
|
||||
yaml_files = glob.glob(os.path.join(yaml_dir, "*.yaml"))
|
||||
if not yaml_files:
|
||||
print(" ! No translation YAML files found.")
|
||||
return
|
||||
|
||||
patched = 0
|
||||
for yf in yaml_files:
|
||||
content = read_file(yf)
|
||||
if "STR_GITHUB_SYNC" in content:
|
||||
patched += 1
|
||||
continue
|
||||
lines = content.splitlines(keepends=True)
|
||||
new_lines = []
|
||||
inserted = False
|
||||
for line in lines:
|
||||
new_lines.append(line)
|
||||
if line.startswith("STR_CHECK_UPDATES:"):
|
||||
new_lines.append('STR_GITHUB_SYNC: "GitHub Sync"\n')
|
||||
inserted = True
|
||||
if inserted:
|
||||
write_file(yf, "".join(new_lines))
|
||||
patched += 1
|
||||
else:
|
||||
print(f" ! STR_CHECK_UPDATES not found in {os.path.basename(yf)} — add STR_GITHUB_SYNC manually.")
|
||||
|
||||
print(f" Translation files patched ({patched} files).")
|
||||
|
||||
|
||||
def patch(repo_dir: str, yes_all: bool = False):
|
||||
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
print(" Copying plugin sources...")
|
||||
copy_plugin_sources(plugin_dir, repo_dir)
|
||||
|
||||
print(" Patching platformio.ini...")
|
||||
patch_platformio_ini(repo_dir)
|
||||
|
||||
print(" Patching main.cpp...")
|
||||
patch_main_cpp(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.h...")
|
||||
patch_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.cpp...")
|
||||
patch_settings_cpp(repo_dir)
|
||||
|
||||
print(" Patching translation files...")
|
||||
patch_translation_files(repo_dir)
|
||||
|
||||
if not yes_all:
|
||||
print("\n GitHub Sync credentials (optional — can also be set on-device).")
|
||||
answer = input(" Configure GitHub credentials now? [Y/n]: ").strip().lower()
|
||||
if answer in ("", "y", "yes"):
|
||||
cfg = prompt_github_config()
|
||||
if cfg:
|
||||
nvs_bin = write_nvs_partition(cfg)
|
||||
if nvs_bin:
|
||||
print(f"\n NVS partition ready: {nvs_bin}")
|
||||
print(" Flash it after firmware upload with:")
|
||||
print(f" esptool.py --chip esp32c3 write_flash 0x9000 {nvs_bin}")
|
||||
else:
|
||||
print(" Skipping credential prompt (--yes mode). Configure on-device via Settings -> GitHub Sync.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||
patch(sys.argv[1])
|
||||
@@ -0,0 +1,66 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, Include, SettingActionEnumValue, PluginsTabEntry, MainHook,
|
||||
)
|
||||
|
||||
# NOTE on scope: the original patch.py also pre-provisioned GitHub credentials
|
||||
# directly into a reserved NVS flash partition at install time (via a
|
||||
# generated nvs.csv + `esptool ... write-flash 0x3000`), so users wouldn't
|
||||
# have to type a PAT on-device. That's flash-time, not source-patch-time
|
||||
# logic, and GitHubSyncSettingsActivity already lets you configure
|
||||
# username/PAT/repo/branch on-device (or you can add web-UI editing later),
|
||||
# so this rewrite drops the NVS pre-seeding step for simplicity/safety and
|
||||
# has you configure GitHub Sync via Settings -> Plugins -> GitHub Sync after
|
||||
# first boot instead. Ask if you want the NVS pre-seeding restored.
|
||||
#
|
||||
# Also fixed in this rewrite: the original main.cpp hook only ran
|
||||
# GitHubSync::sync() inside one narrow boot branch (quick-resume-with-no-
|
||||
# saved-frame), so on most ordinary boots it silently never synced. It's now
|
||||
# a "post_boot" hook that runs exactly once, every boot, after the routing
|
||||
# decision - regardless of which plugins are installed alongside it.
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
return PluginManifest(
|
||||
name="githubsync",
|
||||
pretty_name="GitHub Sync",
|
||||
source_files=[
|
||||
SourceFile("GitHubSync.h", "src/activities/settings"),
|
||||
SourceFile("GitHubSync.cpp", "src/activities/settings"),
|
||||
SourceFile("GitHubSyncSettingsActivity.h", "src/activities/settings"),
|
||||
SourceFile("GitHubSyncSettingsActivity.cpp", "src/activities/settings"),
|
||||
],
|
||||
includes=[
|
||||
Include("GitHubSync.h", "settings_activity_cpp"),
|
||||
Include("GitHubSyncSettingsActivity.h", "settings_activity_cpp"),
|
||||
Include("activities/settings/GitHubSync.h", "main_cpp"),
|
||||
],
|
||||
setting_actions=[
|
||||
SettingActionEnumValue("GitHubSync"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="GitHub Sync",
|
||||
kind="action",
|
||||
action_name="GitHubSync",
|
||||
action_value_text="Sync",
|
||||
activity_launch_expr="std::make_unique<GitHubSyncSettingsActivity>(renderer, mappedInput)",
|
||||
),
|
||||
],
|
||||
main_hooks=[
|
||||
MainHook(
|
||||
point="post_boot",
|
||||
code=(
|
||||
" if (GitHubSync::isConfigured()) {\n"
|
||||
" GitHubSyncResult ghSyncResult = GitHubSync::sync();\n"
|
||||
" if (ghSyncResult != GitHubSyncResult::OK) {\n"
|
||||
' LOG_ERR("SYNC", "%s", GitHubSync::resultMessage(ghSyncResult));\n'
|
||||
" }\n"
|
||||
" }"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -119,7 +119,7 @@ std::string getIsbnFromEpub(Epub& epub, const std::string& path) {
|
||||
// ---- Progress reading -------------------------------------------------------
|
||||
|
||||
int epubProgressPercent(const std::string& cachePath, int& outPage, Epub& epub) {
|
||||
FsFile f;
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("HCV", (cachePath + "/progress.bin").c_str(), f))
|
||||
return -1;
|
||||
uint8_t data[6] = {};
|
||||
@@ -178,7 +178,7 @@ void collectBooks(std::vector<InProgressBook>& inProgress, std::vector<InProgres
|
||||
std::string cachePath = buildCachePath(path);
|
||||
|
||||
// Check progress.bin exists before loading the EPUB
|
||||
FsFile f;
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("HCV", (cachePath + "/progress.bin").c_str(), f)) continue;
|
||||
f.close();
|
||||
|
||||
@@ -351,7 +351,7 @@ std::string readSessionCachePath(int userBookId) {
|
||||
}
|
||||
|
||||
int loadCachedReadId(int userBookId) {
|
||||
FsFile f;
|
||||
HalFile f;
|
||||
std::string path = readSessionCachePath(userBookId);
|
||||
if (!Storage.openFileForRead("HCV", path.c_str(), f)) return 0;
|
||||
uint8_t data[4] = {};
|
||||
@@ -362,7 +362,7 @@ int loadCachedReadId(int userBookId) {
|
||||
}
|
||||
|
||||
void saveCachedReadId(int userBookId, int readId) {
|
||||
FsFile f;
|
||||
HalFile f;
|
||||
std::string path = readSessionCachePath(userBookId);
|
||||
if (!Storage.openFileForWrite("HCV", path.c_str(), f)) {
|
||||
LOG_ERR("HCV", "Could not save read session cache for user_book_id=%d", userBookId);
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
#!/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, token=""):
|
||||
path = find_first("CrossPointSettings.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "hardcoverApiToken" in content:
|
||||
print(" CrossPointSettings.h already patched, skipping.")
|
||||
return
|
||||
|
||||
safe_token = token.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
content = content.replace(
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
f' uint8_t imageRendering = IMAGES_DISPLAY;\n char hardcoverApiToken[640] = "{safe_token}";'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(f" CrossPointSettings.h patched{' (token embedded)' if token else ' (no token)'}.")
|
||||
|
||||
def patch_settings_list_h(repo_dir):
|
||||
path = find_first("SettingsList.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "hardcoverApiToken" in content:
|
||||
print(" SettingsList.h already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
' };\n return list;\n}\n',
|
||||
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.hardcoverApiToken,\n'
|
||||
' sizeof(SETTINGS.hardcoverApiToken), "hardcoverApiToken")\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 "HardcoverSync" in content:
|
||||
print(" SettingAction enum already patched, skipping.")
|
||||
return
|
||||
|
||||
for anchor in [
|
||||
' Language,\n};',
|
||||
' BookerlyInstalled,\n};',
|
||||
' GitHubSync,\n Language,\n};',
|
||||
' Language,\n BookerlyInstalled,\n};',
|
||||
' GitHubSync,\n Language,\n BookerlyInstalled,\n};',
|
||||
]:
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, anchor[:-2] + ' HardcoverSync,\n};')
|
||||
break
|
||||
else:
|
||||
import re
|
||||
content = re.sub(r'(\n};)', r'\n HardcoverSync,\1', content, count=1)
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingAction::HardcoverSync 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 "HardcoverPlugin::syncProgress" in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "SettingsList.h"',
|
||||
'#include "SettingsList.h"\n#include "HardcoverPlugin.h"\n#include "HardcoverSyncActivity.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();'
|
||||
)
|
||||
|
||||
hardcover_push = (
|
||||
' pluginsSettings.push_back(SettingInfo::Action(\n'
|
||||
' StrId::STR_NONE_OPT, SettingAction::HardcoverSync\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
if 'SettingAction::HardcoverSync' 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 + 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);'
|
||||
)
|
||||
|
||||
hardcover_label_line = (
|
||||
' if (s.type == SettingType::ACTION &&\n'
|
||||
' s.action == SettingAction::HardcoverSync) return "Hardcover Progress";\n'
|
||||
)
|
||||
|
||||
if 'Hardcover Progress' not in content:
|
||||
if 'selectedCategoryIndex == 4' in content:
|
||||
|
||||
closing = (
|
||||
' }\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
' }, nullptr, nullptr,'
|
||||
)
|
||||
closing_bookerly = (
|
||||
' }\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
' }, nullptr, nullptr,'
|
||||
)
|
||||
for last_key_line in [
|
||||
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n',
|
||||
' if (s.key && std::string(s.key) == "darkModeState") return "Dark Mode";\n',
|
||||
' s.action == SettingAction::GitHubSync) return "GitHub Sync";\n',
|
||||
' s.action == SettingAction::BookerlyInstalled) return "Bookerly Font";\n',
|
||||
]:
|
||||
for cl in [closing, closing_bookerly]:
|
||||
anchor = last_key_line + cl
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, last_key_line + hardcover_label_line + cl)
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
else:
|
||||
for cl in [closing, closing_bookerly]:
|
||||
if cl in content:
|
||||
content = content.replace(cl, hardcover_label_line + cl, 1)
|
||||
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'
|
||||
+ hardcover_label_line +
|
||||
' }\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
' }, nullptr, nullptr,'
|
||||
)
|
||||
content = content.replace(old_lambda, new_lambda)
|
||||
|
||||
hardcover_value_block = (
|
||||
' if (setting.type == SettingType::ACTION &&\n'
|
||||
' setting.action == SettingAction::HardcoverSync) {\n'
|
||||
' valueText = "Sync";\n'
|
||||
' } else '
|
||||
)
|
||||
if 'SettingAction::HardcoverSync) {\n valueText = "Sync"' not in content:
|
||||
for value_anchor in [
|
||||
' if (setting.type == SettingType::ACTION &&\n'
|
||||
' setting.action == SettingAction::GoodreadsSync) {\n'
|
||||
' valueText = "Sync";\n'
|
||||
' } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
|
||||
' if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
|
||||
]:
|
||||
if value_anchor in content:
|
||||
content = content.replace(value_anchor, hardcover_value_block + value_anchor)
|
||||
break
|
||||
|
||||
hardcover_case = (
|
||||
' case SettingAction::HardcoverSync:\n'
|
||||
' startActivityForResult(std::make_unique<HardcoverSyncActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
)
|
||||
if 'case SettingAction::HardcoverSync:' not in content:
|
||||
for anchor in [
|
||||
' case SettingAction::GoodreadsSync:\n'
|
||||
' startActivityForResult(std::make_unique<GoodreadsSyncActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
' case SettingAction::None:',
|
||||
' case SettingAction::Language:\n'
|
||||
' startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
' case SettingAction::None:',
|
||||
]:
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, hardcover_case + anchor)
|
||||
break
|
||||
|
||||
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 '"hardcoverApiToken"' in content or '"Hardcover API Token"' in content:
|
||||
print(" CrossPointWebServer.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
hardcover_block = (
|
||||
' if (strcmp(s.key, "hardcoverApiToken") == 0) {\n'
|
||||
' doc["name"] = "Hardcover API Token";\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',
|
||||
hardcover_block +
|
||||
' if (strcmp(s.key, "darkModeState") == 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'
|
||||
+ hardcover_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__))
|
||||
|
||||
hardcover_file = os.path.expanduser("~/.hardcover")
|
||||
token = ""
|
||||
|
||||
if os.path.exists(hardcover_file):
|
||||
raw = open(hardcover_file).read().strip()
|
||||
if raw.startswith("Bearer "):
|
||||
raw = raw[len("Bearer "):]
|
||||
token = raw
|
||||
|
||||
if token:
|
||||
print(f" Using Hardcover API token from ~/.hardcover.")
|
||||
else:
|
||||
print(" Hardcover API token required.")
|
||||
print(" Get yours from https://hardcover.app/account/api")
|
||||
print(" Paste your Hardcover API token below.")
|
||||
while not token:
|
||||
raw = input(" Hardcover API token: ").strip()
|
||||
if raw.startswith("Bearer "):
|
||||
raw = raw[len("Bearer "):]
|
||||
token = raw
|
||||
if not token:
|
||||
print(" Token cannot be empty. Please enter your Hardcover API token.")
|
||||
with open(hardcover_file, "w") as f:
|
||||
f.write(f"Bearer {token}\n")
|
||||
print(f" Token saved to ~/.hardcover for future installs.")
|
||||
|
||||
if len(token) >= 640:
|
||||
sys.exit(f"ERROR: Hardcover API token is too long ({len(token)} chars, max 639).")
|
||||
|
||||
print(" Copying plugin sources...")
|
||||
copy_plugin_sources(plugin_dir, repo_dir)
|
||||
|
||||
print(" Patching CrossPointSettings.h...")
|
||||
patch_cross_point_settings_h(repo_dir, token)
|
||||
|
||||
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(" 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: 'Hardcover Progress'", "Hardcover Progress" in cpp_content),
|
||||
("switch case: HardcoverSync", "case SettingAction::HardcoverSync:" in cpp_content),
|
||||
("HardcoverSyncActivity launched", "HardcoverSyncActivity" in cpp_content),
|
||||
("pluginsSettings.clear()", "pluginsSettings.clear();" in cpp_content),
|
||||
("case 4 -> pluginsSettings", "case 4:" 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 Hardcover patch checks failed.")
|
||||
print(" Hardcover 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])
|
||||
@@ -0,0 +1,83 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, Include, SettingsField, PluginsTabEntry, SettingActionEnumValue,
|
||||
)
|
||||
|
||||
HARDCOVER_TOKEN_FILE = os.path.expanduser("~/.hardcover")
|
||||
|
||||
|
||||
def _get_token(ctx):
|
||||
token = ""
|
||||
if os.path.exists(HARDCOVER_TOKEN_FILE):
|
||||
raw = open(HARDCOVER_TOKEN_FILE).read().strip()
|
||||
if raw.startswith("Bearer "):
|
||||
raw = raw[len("Bearer "):]
|
||||
token = raw
|
||||
|
||||
if token:
|
||||
print(f" [hardcover] Using API token from {HARDCOVER_TOKEN_FILE}.")
|
||||
return token
|
||||
|
||||
if not ctx.prompt:
|
||||
return ""
|
||||
|
||||
print(" [hardcover] Hardcover API token required.")
|
||||
print(" [hardcover] Get yours from https://hardcover.app/account/api")
|
||||
while not token:
|
||||
raw = input(" Hardcover API token: ").strip()
|
||||
if raw.startswith("Bearer "):
|
||||
raw = raw[len("Bearer "):]
|
||||
token = raw
|
||||
if not token:
|
||||
print(" Token cannot be empty.")
|
||||
with open(HARDCOVER_TOKEN_FILE, "w") as f:
|
||||
f.write(f"Bearer {token}\n")
|
||||
print(f" [hardcover] Token saved to {HARDCOVER_TOKEN_FILE} for future installs.")
|
||||
return token
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
token = _get_token(ctx)
|
||||
if len(token) >= 640:
|
||||
raise SystemExit(f"ERROR: Hardcover API token is too long ({len(token)} chars, max 639).")
|
||||
safe_token = token.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
return PluginManifest(
|
||||
name="hardcover",
|
||||
pretty_name="Hardcover Sync",
|
||||
source_files=[
|
||||
SourceFile("HardcoverPlugin.h", "src/activities/settings"),
|
||||
SourceFile("HardcoverPlugin.cpp", "src/activities/settings"),
|
||||
SourceFile("HardcoverSyncActivity.h", "src/activities/settings"),
|
||||
SourceFile("HardcoverSyncActivity.cpp", "src/activities/settings"),
|
||||
],
|
||||
includes=[
|
||||
Include("HardcoverPlugin.h", "settings_activity_cpp"),
|
||||
Include("HardcoverSyncActivity.h", "settings_activity_cpp"),
|
||||
],
|
||||
settings_fields=[
|
||||
SettingsField(f'char hardcoverApiToken[640] = "{safe_token}";'),
|
||||
],
|
||||
setting_actions=[
|
||||
SettingActionEnumValue("HardcoverSync"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="Hardcover API Token",
|
||||
kind="string",
|
||||
key="hardcoverApiToken",
|
||||
obfuscated=True,
|
||||
show_on_device=False,
|
||||
),
|
||||
PluginsTabEntry(
|
||||
label="Hardcover Progress",
|
||||
kind="action",
|
||||
action_name="HardcoverSync",
|
||||
action_value_text="Sync",
|
||||
activity_launch_expr="std::make_unique<HardcoverSyncActivity>(renderer, mappedInput)",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -1,547 +0,0 @@
|
||||
#!/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):
|
||||
settings_dest = os.path.join(repo_dir, "src", "activities", "settings")
|
||||
activities_dest = os.path.join(repo_dir, "src", "activities")
|
||||
os.makedirs(settings_dest, exist_ok=True)
|
||||
|
||||
settings_files = {
|
||||
"LockscreenSettingsPage.h", "LockscreenSettingsPage.cpp",
|
||||
"LockscreenPlugin.h", "LockscreenPlugin.cpp",
|
||||
}
|
||||
|
||||
for fname in os.listdir(plugin_dir):
|
||||
if not fname.endswith((".h", ".cpp", ".hpp")):
|
||||
continue
|
||||
src = os.path.join(plugin_dir, fname)
|
||||
dst = os.path.join(settings_dest if fname in settings_files else activities_dest, fname)
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✓ {fname}")
|
||||
|
||||
|
||||
def patch_cross_point_settings_h(repo_dir):
|
||||
path = find_first("CrossPointSettings.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "lockscreenMode" in content:
|
||||
print(" CrossPointSettings.h already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#pragma once\n#include <HalStorage.h>',
|
||||
'#pragma once\n#include <HalStorage.h>\n#include "activities/settings/LockscreenPlugin.h"'
|
||||
)
|
||||
content = content.replace(
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;\n'
|
||||
' uint8_t lockscreenMode = 0;\n'
|
||||
' char lockscreenPinHash[9] = {};'
|
||||
)
|
||||
|
||||
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 "lockscreenMode" in content and "lockscreenPinHash" in content:
|
||||
if '{StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}' in content:
|
||||
content = content.replace(
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n'
|
||||
' StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n',
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
|
||||
)
|
||||
write_file(path, content)
|
||||
print(" SettingsList.h patched (reduced lockscreenMode to 2 options).")
|
||||
else:
|
||||
print(" SettingsList.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if "lockscreenMode" in content and "lockscreenPinHash" not in content:
|
||||
content = content.replace(
|
||||
' "lockscreenMode", StrId::STR_NONE_OPT),\n',
|
||||
' "lockscreenMode", StrId::STR_NONE_OPT),\n'
|
||||
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n'
|
||||
' sizeof(SETTINGS.lockscreenPinHash),\n'
|
||||
' "lockscreenPinHash", StrId::STR_NONE_OPT),\n'
|
||||
)
|
||||
write_file(path, content)
|
||||
print(" SettingsList.h patched (added lockscreenPinHash).")
|
||||
return
|
||||
|
||||
entry = (
|
||||
'\n'
|
||||
' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::lockscreenMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
|
||||
' "lockscreenMode", StrId::STR_NONE_OPT),\n'
|
||||
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n'
|
||||
' sizeof(SETTINGS.lockscreenPinHash),\n'
|
||||
' "lockscreenPinHash", StrId::STR_NONE_OPT),\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_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
already_has_plugins_tab = "categoryCount = 5" in content
|
||||
already_has_plugins_vec = "pluginsSettings" in content
|
||||
|
||||
if already_has_plugins_tab and already_has_plugins_vec:
|
||||
print(" SettingsActivity.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if not already_has_plugins_tab:
|
||||
content = content.replace(
|
||||
'static constexpr int categoryCount = 4;',
|
||||
'static constexpr int categoryCount = 5;'
|
||||
)
|
||||
|
||||
if not already_has_plugins_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 '"LockscreenPlugin.h"' in content and 'lockscreenPinHash' in content and '"lockscreenMode") return "Lockscreen"' in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
changed = False
|
||||
|
||||
if '"LockscreenPlugin.h"' not in content:
|
||||
content = content.replace(
|
||||
'#include "SettingsList.h"',
|
||||
'#include "SettingsList.h"\n'
|
||||
'#include "activities/settings/LockscreenPlugin.h"\n'
|
||||
'#include "activities/LockscreenActivity.h"'
|
||||
)
|
||||
changed = True
|
||||
|
||||
if 'STR_NONE_OPT};' not in content:
|
||||
content = content.replace(
|
||||
'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};',
|
||||
'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n'
|
||||
' StrId::STR_NONE_OPT};'
|
||||
)
|
||||
changed = True
|
||||
|
||||
if 'pluginsSettings.clear()' not in content:
|
||||
result = content.replace(
|
||||
' systemSettings.clear();\n',
|
||||
' systemSettings.clear();\n pluginsSettings.clear();\n'
|
||||
)
|
||||
if 'pluginsSettings.clear()' in result:
|
||||
content = result
|
||||
changed = True
|
||||
|
||||
lockscreen_push = (
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::lockscreenMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
|
||||
' "lockscreenMode", StrId::STR_NONE_OPT\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
if '"lockscreenMode", StrId::STR_NONE_OPT' not in content:
|
||||
for anchor in [
|
||||
' selectedCategoryIndex = 0;\n selectedSettingIndex = 0;',
|
||||
' selectedCategoryIndex = 0;',
|
||||
' currentSettings = &displaySettings;',
|
||||
]:
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, lockscreen_push + anchor, 1)
|
||||
changed = True
|
||||
break
|
||||
|
||||
if 'case 4:' not in content:
|
||||
content = content.replace(
|
||||
' case 3:\n'
|
||||
' currentSettings = &systemSettings;\n'
|
||||
' break;\n',
|
||||
' case 3:\n'
|
||||
' currentSettings = &systemSettings;\n'
|
||||
' break;\n'
|
||||
' case 4:\n'
|
||||
' currentSettings = &pluginsSettings;\n'
|
||||
' break;\n'
|
||||
)
|
||||
changed = True
|
||||
|
||||
if '"Plugins"' not in content:
|
||||
content = content.replace(
|
||||
' tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n'
|
||||
' }',
|
||||
' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n'
|
||||
' tabs.push_back({tabLabel, selectedCategoryIndex == i});\n'
|
||||
' }'
|
||||
)
|
||||
changed = True
|
||||
|
||||
if 'nextCatIdx' not in content:
|
||||
content = content.replace(
|
||||
' const auto confirmLabel = (selectedSettingIndex == 0)\n'
|
||||
' ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n'
|
||||
' : tr(STR_TOGGLE);',
|
||||
' const int nextCatIdx = (selectedCategoryIndex + 1) % categoryCount;\n'
|
||||
' const char* nextCatLabel = (nextCatIdx == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIdx]);\n'
|
||||
' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);'
|
||||
)
|
||||
changed = True
|
||||
|
||||
if 'selectedCategoryIndex == 4' not in content:
|
||||
content = content.replace(
|
||||
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },',
|
||||
' [&settings, this](int index) -> std::string {\n'
|
||||
' if (selectedCategoryIndex == 4) {\n'
|
||||
' const auto& s = settings[index];\n'
|
||||
' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n'
|
||||
' }\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
' },'
|
||||
)
|
||||
changed = True
|
||||
elif '"lockscreenMode") return "Lockscreen"' not in content:
|
||||
import re
|
||||
result = re.sub(
|
||||
r'( if \(selectedCategoryIndex == 4\) \{.*?)( \})',
|
||||
lambda m: m.group(1) + ' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n' + m.group(2),
|
||||
content,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
changed = True
|
||||
|
||||
if 'LockscreenPlugin::modeName' not in content:
|
||||
import re
|
||||
lockscreen_value = (
|
||||
' if (setting.type == SettingType::ENUM &&\n'
|
||||
' setting.key && std::string(setting.key) == "lockscreenMode") {\n'
|
||||
' valueText = LockscreenPlugin::modeName(static_cast<LockscreenMode>(SETTINGS.lockscreenMode));\n'
|
||||
' } else '
|
||||
)
|
||||
result = re.sub(
|
||||
r'( if \(setting\.type == SettingType::(?:ACTION|TOGGLE|ENUM))',
|
||||
lockscreen_value + r'\1',
|
||||
content,
|
||||
count=1,
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
changed = True
|
||||
|
||||
if 'lockscreenPinHash' not in content:
|
||||
content = content.replace(
|
||||
' SETTINGS.saveToFile();\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'void SettingsActivity::render',
|
||||
' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n'
|
||||
' if (SETTINGS.lockscreenMode == 0) {\n'
|
||||
' memset(SETTINGS.lockscreenPinHash, 0, sizeof(SETTINGS.lockscreenPinHash));\n'
|
||||
" } else if (SETTINGS.lockscreenPinHash[0] == '\\0') {\n"
|
||||
' startActivityForResult(\n'
|
||||
' std::make_unique<LockscreenActivity>(renderer, mappedInput, LockscreenActivity::Purpose::CREATE),\n'
|
||||
' [](const ActivityResult&) { SETTINGS.saveToFile(); });\n'
|
||||
' return;\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' SETTINGS.saveToFile();\n'
|
||||
'}\n'
|
||||
'\n'
|
||||
'void SettingsActivity::render'
|
||||
)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.cpp patched.")
|
||||
else:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
|
||||
|
||||
def patch_web_server(repo_dir):
|
||||
path = find_first("CrossPointWebServer.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if 'lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue' in content:
|
||||
print(" CrossPointWebServer.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
target = (
|
||||
' if (s.key && strcmp(s.key, "lockscreenMode") == 0) {\n'
|
||||
' doc["name"] = "Lockscreen";\n'
|
||||
' doc["category"] = "Plugins";\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
if target in content:
|
||||
content = content.replace(
|
||||
target,
|
||||
' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n'
|
||||
)
|
||||
write_file(path, content)
|
||||
print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).")
|
||||
return
|
||||
|
||||
skip = ' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n'
|
||||
|
||||
# Insert continue right after category line regardless of what follows
|
||||
anchor = ' doc["category"] = I18N.get(s.category);\n'
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, anchor + '\n' + skip, 1)
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).")
|
||||
|
||||
|
||||
def patch_main_cpp(repo_dir):
|
||||
path = find_first("main.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
already_has_includes = "LockscreenPlugin" in content
|
||||
already_has_block = "LockscreenActivity lockAct" in content
|
||||
|
||||
if already_has_includes and already_has_block:
|
||||
print(" main.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "CrossPointSettings.h"',
|
||||
'#include "CrossPointSettings.h"\n'
|
||||
'#include "activities/settings/LockscreenPlugin.h"\n'
|
||||
'#include "activities/LockscreenActivity.h"'
|
||||
)
|
||||
|
||||
lockscreen_block = (
|
||||
'\n'
|
||||
' {\n'
|
||||
' const auto lsMode = static_cast<LockscreenMode>(SETTINGS.lockscreenMode);\n'
|
||||
' const bool hasPinSet = SETTINGS.lockscreenPinHash[0] != \'\\0\';\n'
|
||||
' const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n'
|
||||
'\n'
|
||||
' if (shouldLock) {\n'
|
||||
' LockscreenActivity lockAct(renderer, mappedInputManager, LockscreenActivity::Purpose::UNLOCK);\n'
|
||||
' lockAct.onEnter();\n'
|
||||
' lockAct.renderDirect();\n'
|
||||
' while (!lockAct.isDone()) {\n'
|
||||
' gpio.update();\n'
|
||||
' lockAct.loop();\n'
|
||||
' if (lockAct.needsRender()) {\n'
|
||||
' lockAct.renderDirect();\n'
|
||||
' }\n'
|
||||
' delay(10);\n'
|
||||
' }\n'
|
||||
' lockAct.onExit();\n'
|
||||
' renderer.clearScreen();\n'
|
||||
' renderer.displayBuffer();\n'
|
||||
' if (!lockAct.wasSuccessful()) {\n'
|
||||
' enterDeepSleep();\n'
|
||||
' return;\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
anchor = ' RECENT_BOOKS.loadFromFile();\n\n if (HalSystem::isRebootFromPanic())'
|
||||
if anchor in content:
|
||||
content = content.replace(
|
||||
anchor,
|
||||
' RECENT_BOOKS.loadFromFile();\n' + lockscreen_block + '\n if (HalSystem::isRebootFromPanic())'
|
||||
)
|
||||
else:
|
||||
idx = content.find('RECENT_BOOKS')
|
||||
print(' DIAG main.cpp around RECENT_BOOKS:', repr(content[max(0,idx-50):idx+300]))
|
||||
import re
|
||||
m = re.search(r'RECENT_BOOKS\.loadFromFile\(\);.*?\n(\s*\n)*(\s*if)', content, re.DOTALL)
|
||||
if m:
|
||||
actual_anchor = content[m.start():m.end()]
|
||||
content = content.replace(actual_anchor, ' RECENT_BOOKS.loadFromFile();\n' + lockscreen_block + '\n' + content[m.end()-len(m.group(2)):m.end()])
|
||||
print(' main.cpp lockscreen block inserted via regex.')
|
||||
else:
|
||||
print(' WARNING: could not find RECENT_BOOKS anchor in main.cpp')
|
||||
|
||||
write_file(path, content)
|
||||
has_block = "LockscreenActivity lockAct" in content
|
||||
has_sleep = "enterDeepSleep" in content
|
||||
if has_block:
|
||||
idx2 = content.find("LockscreenActivity lockAct")
|
||||
print(" DIAG lockscreen block context:", repr(content[max(0,idx2-200):idx2+50]))
|
||||
print(f" main.cpp patched. lockscreen_block={has_block} enterDeepSleep={has_sleep}")
|
||||
|
||||
|
||||
def patch_settings_html(repo_dir):
|
||||
path = find_first("SettingsPage.html", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "lockscreenPin" in content:
|
||||
print(" SettingsPage.html already patched, skipping.")
|
||||
return
|
||||
|
||||
lines = [
|
||||
"",
|
||||
" <script>",
|
||||
" (function() {",
|
||||
" var _orig = loadSettings;",
|
||||
" loadSettings = async function() {",
|
||||
" await _orig.apply(this, arguments);",
|
||||
" hideInternalRows();",
|
||||
" injectLockscreenPin();",
|
||||
" };",
|
||||
"",
|
||||
" function djb2Hash(str) {",
|
||||
" var hash = 5381;",
|
||||
" for (var i = 0; i < str.length; i++) {",
|
||||
" hash = ((hash << 5) + hash) ^ str.charCodeAt(i);",
|
||||
" hash = hash >>> 0;",
|
||||
" }",
|
||||
" hash = (hash ^ 0xA3F1C2B4) >>> 0;",
|
||||
' return ("00000000" + hash.toString(16).toUpperCase()).slice(-8);',
|
||||
" }",
|
||||
"",
|
||||
" function hideInternalRows() {",
|
||||
' allSettings.forEach(function(s) {',
|
||||
' if (s.hidden) {',
|
||||
' var el = document.getElementById("setting-" + s.key);',
|
||||
' if (el) { var row = el.closest(".setting-row"); if (row) row.style.display = "none"; }',
|
||||
' }',
|
||||
" });",
|
||||
" }",
|
||||
"",
|
||||
" function injectLockscreenPin() {",
|
||||
' var modeEl = document.getElementById("setting-lockscreenMode");',
|
||||
" if (!modeEl) return;",
|
||||
' var row = modeEl.closest(".setting-row");',
|
||||
' if (!row || document.getElementById("lockscreen-pin-row")) return;',
|
||||
' var pinRow = document.createElement("div");',
|
||||
' pinRow.className = "setting-row";',
|
||||
' pinRow.id = "lockscreen-pin-row";',
|
||||
" pinRow.innerHTML =",
|
||||
' \'<span class="setting-name">New PIN (4 digits)</span>\' +',
|
||||
' \'<span class="setting-control">\' +',
|
||||
' \'<input type="password" id="lockscreenPin" maxlength="4" \' +',
|
||||
' \'pattern="[0-9]{4}" inputmode="numeric" \' +',
|
||||
' \'placeholder="Leave blank to keep current" \' +',
|
||||
' \'style="width:200px;" oninput="markChanged()">\' +',
|
||||
" '</span>';",
|
||||
" row.parentNode.insertBefore(pinRow, row.nextSibling);",
|
||||
" updatePinRowVisibility();",
|
||||
' modeEl.addEventListener("change", updatePinRowVisibility);',
|
||||
" }",
|
||||
"",
|
||||
" function updatePinRowVisibility() {",
|
||||
' var modeEl = document.getElementById("setting-lockscreenMode");',
|
||||
' var pinRow = document.getElementById("lockscreen-pin-row");',
|
||||
" if (!modeEl || !pinRow) return;",
|
||||
' pinRow.style.display = parseInt(modeEl.value) === 1 ? "" : "none";',
|
||||
" }",
|
||||
"",
|
||||
" var _origSave = saveSettings;",
|
||||
" saveSettings = async function() {",
|
||||
' var pinEl = document.getElementById("lockscreenPin");',
|
||||
' var modeEl = document.getElementById("setting-lockscreenMode");',
|
||||
" if (pinEl && modeEl) {",
|
||||
" var pin = pinEl.value.trim();",
|
||||
" if (parseInt(modeEl.value) === 0) {",
|
||||
' var s = allSettings.find(function(x) { return x.key === "lockscreenPinHash"; });',
|
||||
' if (s) { s.value = ""; originalValues["lockscreenPinHash"] = "CLEAR"; }',
|
||||
" } else if (pin.length === 4 && /^[0-9]{4}$/.test(pin)) {",
|
||||
" var hash = djb2Hash(pin);",
|
||||
' var s = allSettings.find(function(x) { return x.key === "lockscreenPinHash"; });',
|
||||
' if (s) { s.value = hash; originalValues["lockscreenPinHash"] = "CHANGED"; }',
|
||||
' pinEl.value = "";',
|
||||
" } else if (pin.length > 0) {",
|
||||
' document.getElementById("message").textContent = "PIN must be exactly 4 digits.";',
|
||||
' document.getElementById("message").className = "message error";',
|
||||
' document.getElementById("message").style.display = "block";',
|
||||
' setTimeout(function() { document.getElementById("message").style.display = "none"; }, 4000);',
|
||||
" return;",
|
||||
" }",
|
||||
" }",
|
||||
" await _origSave.apply(this, arguments);",
|
||||
" };",
|
||||
" })();",
|
||||
" </script>",
|
||||
"",
|
||||
]
|
||||
|
||||
js = "\n".join(lines)
|
||||
content = content.replace("</body>", js + "</body>")
|
||||
write_file(path, content)
|
||||
print(" SettingsPage.html patched.")
|
||||
|
||||
|
||||
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(" Patching CrossPointSettings.h...")
|
||||
patch_cross_point_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsList.h...")
|
||||
patch_settings_list_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.h...")
|
||||
patch_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.cpp...")
|
||||
patch_settings_cpp(repo_dir)
|
||||
|
||||
print(" Patching CrossPointWebServer.cpp...")
|
||||
patch_web_server(repo_dir)
|
||||
|
||||
print(" Patching main.cpp...")
|
||||
patch_main_cpp(repo_dir)
|
||||
|
||||
print(" Patching SettingsPage.html (WUI PIN entry)...")
|
||||
patch_settings_html(repo_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||
patch(sys.argv[1])
|
||||
@@ -0,0 +1,101 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, Include, SettingsField, PluginsTabEntry, ToggleHook, MainHook,
|
||||
)
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
return PluginManifest(
|
||||
name="lockscreen",
|
||||
pretty_name="Lockscreen",
|
||||
source_files=[
|
||||
SourceFile("LockscreenPlugin.h", "src/activities/settings"),
|
||||
SourceFile("LockscreenPlugin.cpp", "src/activities/settings"),
|
||||
SourceFile("LockscreenActivity.h", "src/activities"),
|
||||
SourceFile("LockscreenActivity.cpp", "src/activities"),
|
||||
],
|
||||
includes=[
|
||||
Include("activities/settings/LockscreenPlugin.h", "cross_point_settings_h"),
|
||||
Include("activities/settings/LockscreenPlugin.h", "settings_activity_cpp"),
|
||||
Include("activities/LockscreenActivity.h", "settings_activity_cpp"),
|
||||
Include("activities/settings/LockscreenPlugin.h", "main_cpp"),
|
||||
Include("activities/LockscreenActivity.h", "main_cpp"),
|
||||
],
|
||||
settings_fields=[
|
||||
SettingsField("uint8_t lockscreenMode = 0;"),
|
||||
SettingsField("char lockscreenPinHash[9] = {};"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="Lockscreen",
|
||||
kind="enum",
|
||||
key="lockscreenMode",
|
||||
option_labels=["Disabled", "Enabled"],
|
||||
value_text_expr="LockscreenPlugin::modeName(static_cast<LockscreenMode>(value))",
|
||||
hidden_from_web=True,
|
||||
),
|
||||
PluginsTabEntry(
|
||||
label="Lockscreen PIN",
|
||||
kind="string",
|
||||
key="lockscreenPinHash",
|
||||
obfuscated=True,
|
||||
show_on_device=False,
|
||||
hidden_from_web=True,
|
||||
),
|
||||
],
|
||||
toggle_hooks=[
|
||||
ToggleHook(
|
||||
key="lockscreenMode",
|
||||
code=(
|
||||
" if (SETTINGS.lockscreenMode == 0) {\n"
|
||||
" memset(SETTINGS.lockscreenPinHash, 0, sizeof(SETTINGS.lockscreenPinHash));\n"
|
||||
" } else if (SETTINGS.lockscreenPinHash[0] == '\\0') {\n"
|
||||
" startActivityForResult(\n"
|
||||
" std::make_unique<LockscreenActivity>(renderer, mappedInput, LockscreenActivity::Purpose::CREATE),\n"
|
||||
" [](const ActivityResult&) { SETTINGS.saveToFile(); });\n"
|
||||
" return;\n"
|
||||
" }"
|
||||
),
|
||||
),
|
||||
],
|
||||
main_hooks=[
|
||||
MainHook(
|
||||
# Must run after setupDisplayAndFonts(): the lock screen calls
|
||||
# renderDirect(), and before that point display.begin()/
|
||||
# renderer.begin() haven't run and no fonts are registered, so
|
||||
# the PIN prompt would silently draw nothing while still
|
||||
# unlocking underneath - i.e. an invisible, bypassable lock.
|
||||
point="post_display_setup",
|
||||
code=(
|
||||
" {\n"
|
||||
" const auto lsMode = static_cast<LockscreenMode>(SETTINGS.lockscreenMode);\n"
|
||||
" const bool hasPinSet = SETTINGS.lockscreenPinHash[0] != '\\0';\n"
|
||||
" const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n"
|
||||
" if (shouldLock) {\n"
|
||||
" LockscreenActivity lockAct(renderer, mappedInputManager, LockscreenActivity::Purpose::UNLOCK);\n"
|
||||
" lockAct.onEnter();\n"
|
||||
" lockAct.renderDirect();\n"
|
||||
" while (!lockAct.isDone()) {\n"
|
||||
" gpio.update();\n"
|
||||
" lockAct.loop();\n"
|
||||
" if (lockAct.needsRender()) {\n"
|
||||
" lockAct.renderDirect();\n"
|
||||
" }\n"
|
||||
" delay(10);\n"
|
||||
" }\n"
|
||||
" lockAct.onExit();\n"
|
||||
" renderer.clearScreen();\n"
|
||||
" renderer.displayBuffer();\n"
|
||||
" if (!lockAct.wasSuccessful()) {\n"
|
||||
" enterDeepSleep();\n"
|
||||
" return;\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" }"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -1,269 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import re
|
||||
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):
|
||||
activities_dest = os.path.join(repo_dir, "src", "activities")
|
||||
os.makedirs(activities_dest, exist_ok=True)
|
||||
|
||||
for fname in os.listdir(plugin_dir):
|
||||
if not fname.endswith((".h", ".cpp", ".hpp")):
|
||||
continue
|
||||
src = os.path.join(plugin_dir, fname)
|
||||
dst = os.path.join(activities_dest, fname)
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✓ {fname}")
|
||||
|
||||
|
||||
def patch_setting_action_enum(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "PongGame" in content:
|
||||
print(" SettingAction enum already patched, skipping.")
|
||||
return
|
||||
|
||||
content = re.sub(r'(\n};)', r'\n PongGame,\1', content, count=1)
|
||||
write_file(path, content)
|
||||
print(" SettingAction::PongGame 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 "PongActivity" in content and "SettingAction::PongGame" in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include "SettingsList.h"',
|
||||
'#include "SettingsList.h"\n#include "activities/PongActivity.h"'
|
||||
)
|
||||
|
||||
if "StrId::STR_NONE_OPT};" not in content:
|
||||
content = content.replace(
|
||||
"StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};",
|
||||
"StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n"
|
||||
" StrId::STR_NONE_OPT};"
|
||||
)
|
||||
|
||||
if "pluginsSettings.clear();" not in content:
|
||||
result = content.replace(
|
||||
" systemSettings.clear();\n",
|
||||
" systemSettings.clear();\n pluginsSettings.clear();\n"
|
||||
)
|
||||
if "pluginsSettings.clear();" in result:
|
||||
content = result
|
||||
print(" pluginsSettings.clear() inserted.")
|
||||
else:
|
||||
print(" WARNING: could not insert pluginsSettings.clear().")
|
||||
|
||||
pong_push = (
|
||||
" pluginsSettings.push_back(SettingInfo::Action(\n"
|
||||
" StrId::STR_NONE_OPT, SettingAction::PongGame\n"
|
||||
" ));\n"
|
||||
)
|
||||
|
||||
if "SettingAction::PongGame" not in content:
|
||||
inserted = False
|
||||
for anchor in [
|
||||
" selectedCategoryIndex = 0;\n selectedSettingIndex = 0;",
|
||||
" selectedCategoryIndex = 0;",
|
||||
" currentSettings = &displaySettings;",
|
||||
]:
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, pong_push + anchor, 1)
|
||||
inserted = True
|
||||
print(" PongGame push_back inserted.")
|
||||
break
|
||||
if not inserted:
|
||||
print(" WARNING: could not insert PongGame push_back.")
|
||||
|
||||
if "case 4:\n currentSettings = &pluginsSettings;" not in content:
|
||||
result = 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 result != content:
|
||||
content = result
|
||||
print(" case 4 inserted.")
|
||||
else:
|
||||
print(" WARNING: case 3 anchor not found.")
|
||||
|
||||
if '"Plugins"' not in content:
|
||||
result = content.replace(
|
||||
" tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n"
|
||||
" }",
|
||||
' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n'
|
||||
" tabs.push_back({tabLabel, selectedCategoryIndex == i});\n"
|
||||
" }"
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
print(" Plugins tab label inserted.")
|
||||
else:
|
||||
print(" WARNING: tab label anchor not found.")
|
||||
|
||||
if "nextCatLabel" not in content and "nextCatIdx" not in content:
|
||||
result = 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);"
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
print(" nextCatLabel inserted.")
|
||||
else:
|
||||
print(" WARNING: confirmLabel anchor not found.")
|
||||
|
||||
pong_label = (
|
||||
" if (s.type == SettingType::ACTION &&\n"
|
||||
' s.action == SettingAction::PongGame) return "Pong Game";\n'
|
||||
)
|
||||
|
||||
if "Pong Game" not in content:
|
||||
if "selectedCategoryIndex == 4" in content:
|
||||
result = re.sub(
|
||||
r'( if \(selectedCategoryIndex == 4\) \{.*?)( \})',
|
||||
lambda m: m.group(1) + pong_label + m.group(2),
|
||||
content,
|
||||
count=1,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
print(" Pong Game label inserted.")
|
||||
else:
|
||||
print(" WARNING: category==4 block found but label insertion failed.")
|
||||
else:
|
||||
result = content.replace(
|
||||
" [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },",
|
||||
" [&settings, this](int index) -> std::string {\n"
|
||||
" if (selectedCategoryIndex == 4) {\n"
|
||||
" const auto& s = settings[index];\n"
|
||||
+ pong_label +
|
||||
" }\n"
|
||||
" return std::string(I18N.get(settings[index].nameId));\n"
|
||||
" },"
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
print(" Pong Game label inserted (new block).")
|
||||
else:
|
||||
print(" WARNING: label lambda anchor not found.")
|
||||
|
||||
pong_value = (
|
||||
" if (setting.type == SettingType::ACTION &&\n"
|
||||
" setting.action == SettingAction::PongGame) {\n"
|
||||
' valueText = "Launch";\n'
|
||||
" } else "
|
||||
)
|
||||
|
||||
if '"Launch"' not in content:
|
||||
result = re.sub(
|
||||
r'( if \(setting\.type == SettingType::(?:ACTION|TOGGLE|ENUM)\b)',
|
||||
pong_value + r'\1',
|
||||
content,
|
||||
count=1,
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
print(" Launch value text inserted.")
|
||||
else:
|
||||
print(" WARNING: value text anchor not found.")
|
||||
|
||||
pong_case = (
|
||||
" case SettingAction::PongGame:\n"
|
||||
" startActivityForResult(std::make_unique<PongActivity>(renderer, mappedInput), resultHandler);\n"
|
||||
" break;\n"
|
||||
)
|
||||
|
||||
if "case SettingAction::PongGame:" not in content:
|
||||
result = re.sub(
|
||||
r'( case SettingAction::)',
|
||||
pong_case + r'\1',
|
||||
content,
|
||||
count=1,
|
||||
)
|
||||
if result != content:
|
||||
content = result
|
||||
print(" PongGame case inserted.")
|
||||
else:
|
||||
print(" WARNING: SettingAction case anchor not found.")
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.cpp written.")
|
||||
|
||||
|
||||
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(" 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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||
patch(sys.argv[1])
|
||||
@@ -0,0 +1,33 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, Include, SettingActionEnumValue, PluginsTabEntry,
|
||||
)
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
return PluginManifest(
|
||||
name="pong",
|
||||
pretty_name="Pong",
|
||||
source_files=[
|
||||
SourceFile("PongActivity.h", "src/activities"),
|
||||
SourceFile("PongActivity.cpp", "src/activities"),
|
||||
],
|
||||
includes=[
|
||||
Include("activities/PongActivity.h", "settings_activity_cpp"),
|
||||
],
|
||||
setting_actions=[
|
||||
SettingActionEnumValue("PongGame"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="Pong Game",
|
||||
kind="action",
|
||||
action_name="PongGame",
|
||||
action_value_text="Launch",
|
||||
activity_launch_expr="std::make_unique<PongActivity>(renderer, mappedInput)",
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -1,388 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
skip = {"SmallerFontsSettingsPage.h", "SmallerFontsSettingsPage.cpp"}
|
||||
|
||||
for fname in os.listdir(plugin_dir):
|
||||
if fname.endswith((".h", ".cpp", ".hpp")) and fname not in skip:
|
||||
src = os.path.join(plugin_dir, fname)
|
||||
dst = os.path.join(dest, fname)
|
||||
shutil.copy2(src, dst)
|
||||
print(f" ✓ {fname}")
|
||||
|
||||
|
||||
def patch_cross_point_settings_h(repo_dir):
|
||||
path = find_first("CrossPointSettings.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "smallerFontsMode" in content:
|
||||
print(" CrossPointSettings.h already patched, skipping.")
|
||||
return
|
||||
|
||||
for include_anchor in [
|
||||
'#pragma once\n#include <HalStorage.h>',
|
||||
'#pragma once\n\n#include <HalStorage.h>',
|
||||
'#pragma once',
|
||||
]:
|
||||
if include_anchor in content and '"SmallerFontsPlugin.h"' not in content:
|
||||
content = content.replace(
|
||||
include_anchor,
|
||||
include_anchor + '\n#include "activities/settings/SmallerFontsPlugin.h"',
|
||||
1
|
||||
)
|
||||
break
|
||||
|
||||
for member_anchor in [
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
'\tuint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
'uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
]:
|
||||
if member_anchor in content:
|
||||
content = content.replace(
|
||||
member_anchor,
|
||||
member_anchor + '\n uint8_t smallerFontsMode = 0;',
|
||||
1
|
||||
)
|
||||
break
|
||||
|
||||
if "smallerFontsMode" not in content:
|
||||
sys.exit("ERROR: Could not add smallerFontsMode to CrossPointSettings.h — anchor not found")
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointSettings.h patched.")
|
||||
|
||||
|
||||
def patch_cross_point_settings_cpp(repo_dir):
|
||||
path = find_first("CrossPointSettings.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "resolveReaderFontId" in content:
|
||||
print(" CrossPointSettings.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'int CrossPointSettings::getReaderFontId() const {',
|
||||
'int CrossPointSettings::getReaderFontId() const {\n auto resolve = [this](int baseId) {\n return SmallerFontsPlugin::resolveReaderFontId(baseId, static_cast<SmallerFontsMode>(smallerFontsMode));\n };\n'
|
||||
)
|
||||
for name in ["NOTOSERIF_12", "NOTOSERIF_14", "NOTOSERIF_16", "NOTOSERIF_18",
|
||||
"NOTOSANS_12", "NOTOSANS_14", "NOTOSANS_16", "NOTOSANS_18",
|
||||
"OPENDYSLEXIC_8", "OPENDYSLEXIC_10", "OPENDYSLEXIC_12", "OPENDYSLEXIC_14",
|
||||
"BOOKERLY_12"]:
|
||||
content = content.replace(f'return {name}_FONT_ID;', f'return resolve({name}_FONT_ID);')
|
||||
|
||||
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 "smallerFontsMode" in content:
|
||||
print(" SettingsList.h already patched, skipping.")
|
||||
return
|
||||
|
||||
entry = (
|
||||
' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"),\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_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
already_has_plugins_tab = "categoryCount = 5" in content
|
||||
already_has_plugins_vec = "pluginsSettings" in content
|
||||
|
||||
if already_has_plugins_tab and already_has_plugins_vec:
|
||||
print(" SettingsActivity.h already patched, skipping.")
|
||||
return
|
||||
|
||||
if '"SmallerFontsPlugin.h"' not in content:
|
||||
content = content.replace(
|
||||
'#include <I18n.h>',
|
||||
'#include <I18n.h>\n#include "SmallerFontsPlugin.h"'
|
||||
)
|
||||
|
||||
if not already_has_plugins_tab:
|
||||
content = content.replace(
|
||||
'static constexpr int categoryCount = 4;',
|
||||
'static constexpr int categoryCount = 5;'
|
||||
)
|
||||
|
||||
if not already_has_plugins_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 "SmallerFontsPlugin::modeName" in content and "smallerFontsMode" in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
if "STR_NONE_OPT" not in content:
|
||||
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();'
|
||||
)
|
||||
|
||||
plugins_block = (
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\n'
|
||||
' ));\n'
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
sf_only_block = (
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
broken_old_block = (
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT,\n'
|
||||
' &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
|
||||
' "smallerFontsMode"\n'
|
||||
' ));\n'
|
||||
)
|
||||
|
||||
if broken_old_block in content:
|
||||
content = content.replace(broken_old_block, sf_only_block)
|
||||
elif '"darkModeState"' in content and '"smallerFontsMode"' not in content:
|
||||
content = content.replace(
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\n'
|
||||
' ));\n',
|
||||
plugins_block
|
||||
)
|
||||
elif '"smallerFontsMode"' not in content:
|
||||
content = content.replace(
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n'
|
||||
+ sf_only_block
|
||||
)
|
||||
|
||||
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);'
|
||||
)
|
||||
|
||||
if 'selectedCategoryIndex == 4' not in content:
|
||||
content = content.replace(
|
||||
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },',
|
||||
' [&settings, this](int index) -> std::string {\n'
|
||||
' if (selectedCategoryIndex == 4) {\n'
|
||||
' const auto& s = settings[index];\n'
|
||||
' 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'
|
||||
' },'
|
||||
)
|
||||
|
||||
if "SmallerFontsPlugin::modeName" not in content:
|
||||
if "DarkModePlugin::stateName" in content:
|
||||
content = content.replace(
|
||||
' if (setting.key && std::string(setting.key) == "darkModeState") {\n'
|
||||
' valueText = DarkModePlugin::stateName(static_cast<DarkModeState>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }',
|
||||
' if (setting.key && std::string(setting.key) == "darkModeState") {\n'
|
||||
' valueText = DarkModePlugin::stateName(static_cast<DarkModeState>(value));\n'
|
||||
' } else if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\n'
|
||||
' }'
|
||||
)
|
||||
else:
|
||||
content = content.replace(
|
||||
' valueText = I18N.get(setting.enumValues[value]);',
|
||||
' if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n'
|
||||
' valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n'
|
||||
' } else {\n'
|
||||
' valueText = I18N.get(setting.enumValues[value]);\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 '"Dark Mode"' in content or '"Smaller Fonts"' in content:
|
||||
print(" CrossPointWebServer.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
name_overrides = (
|
||||
'\n'
|
||||
' if (s.key) {\n'
|
||||
' if (strcmp(s.key, "darkModeState") == 0) {\n'
|
||||
' doc["name"] = "Dark Mode";\n'
|
||||
' doc["category"] = "Plugins";\n'
|
||||
' } else if (strcmp(s.key, "smallerFontsMode") == 0) {\n'
|
||||
' doc["name"] = "Smaller Fonts";\n'
|
||||
' doc["category"] = "Plugins";\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' doc["category"] = I18N.get(s.category);\n'
|
||||
'\n'
|
||||
' switch (s.type) {',
|
||||
' doc["category"] = I18N.get(s.category);\n'
|
||||
+ name_overrides +
|
||||
'\n'
|
||||
' switch (s.type) {'
|
||||
)
|
||||
|
||||
enum_overrides = (
|
||||
' if (s.key) {\n'
|
||||
' if (strcmp(s.key, "darkModeState") == 0) {\n'
|
||||
' JsonArray opts = doc["options"].to<JsonArray>();\n'
|
||||
' opts.add("Disabled");\n'
|
||||
' opts.add("Enabled");\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'
|
||||
' }\n'
|
||||
' }\n'
|
||||
)
|
||||
|
||||
content = content.replace(
|
||||
' for (const auto& opt : s.enumValues) {\n'
|
||||
' options.add(I18N.get(opt));\n'
|
||||
' }\n'
|
||||
' break;\n',
|
||||
' for (const auto& opt : s.enumValues) {\n'
|
||||
' options.add(I18N.get(opt));\n'
|
||||
' }\n'
|
||||
+ enum_overrides +
|
||||
' break;\n'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointWebServer.cpp patched.")
|
||||
|
||||
|
||||
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(" Patching CrossPointSettings.h...")
|
||||
patch_cross_point_settings_h(repo_dir)
|
||||
|
||||
print(" Patching CrossPointSettings.cpp...")
|
||||
patch_cross_point_settings_cpp(repo_dir)
|
||||
|
||||
print(" Patching SettingsList.h...")
|
||||
patch_settings_list_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.h...")
|
||||
patch_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.cpp...")
|
||||
patch_settings_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])
|
||||
@@ -0,0 +1,86 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
from framework.manifest import (
|
||||
PluginManifest, SourceFile, Include, SettingsField, PluginsTabEntry,
|
||||
)
|
||||
from framework.engine import find_first, read_file, write_file, PatchError
|
||||
|
||||
# NOTE: the plugin's own SmallerFontsPlugin.h/.cpp currently implement only
|
||||
# two real states (Off / Enabled - a single font-size step down), even
|
||||
# though the settings list historically reserved a 3rd ("Smallest") slot and
|
||||
# the README describes three tiers. We match what the code actually does
|
||||
# (2 states) rather than exposing a 3rd option that behaves identically to
|
||||
# the 2nd - see SmallerFontsPlugin.cpp: resolveReaderFontId() only special-
|
||||
# cases MODE_OFF vs "anything else". Worth a real 3-tier implementation as a
|
||||
# follow-up, but that's a feature gap, not part of the install/composability
|
||||
# fix this framework addresses.
|
||||
|
||||
|
||||
def _wrap_font_resolution(ctx):
|
||||
"""Wrap every currently-present `return X_FONT_ID;` line in
|
||||
CrossPointSettings::getReaderFontId() with SmallerFontsPlugin's resolver.
|
||||
Regex-driven (not a hardcoded font name list) so it picks up whatever
|
||||
font plugins (e.g. bookerly) already added earlier in the same run -
|
||||
this plugin has phase=1 specifically so it runs after any font-provider
|
||||
plugin's phase=0 step."""
|
||||
path = find_first("CrossPointSettings.cpp", ctx.repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "SmallerFontsPlugin::resolveReaderFontId" in content:
|
||||
return
|
||||
|
||||
func_pattern = re.compile(r"(int CrossPointSettings::getReaderFontId\(\) const \{)")
|
||||
m = func_pattern.search(content)
|
||||
if not m:
|
||||
raise PatchError("CrossPointSettings.cpp: could not find getReaderFontId()")
|
||||
|
||||
resolver = (
|
||||
"\n auto resolve = [this](int baseId) {\n"
|
||||
" return SmallerFontsPlugin::resolveReaderFontId(baseId, static_cast<SmallerFontsMode>(smallerFontsMode));\n"
|
||||
" };"
|
||||
)
|
||||
content = content[:m.end()] + resolver + content[m.end():]
|
||||
|
||||
# Only wrap `return X_FONT_ID;` lines that occur after the point we just
|
||||
# inserted the resolver (i.e. inside this function), up to its closing brace.
|
||||
start = content.index(resolver) + len(resolver)
|
||||
# crude but reliable: this function is the only remaining thing in the
|
||||
# file after this point that returns *_FONT_ID literals directly.
|
||||
def _wrap(match):
|
||||
return f"return resolve({match.group(1)});"
|
||||
|
||||
content = content[:start] + re.sub(r"return (\w+_FONT_ID);", _wrap, content[start:])
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointSettings.cpp: wrapped font-id returns with SmallerFontsPlugin::resolveReaderFontId.")
|
||||
|
||||
|
||||
def get_manifest(ctx):
|
||||
return PluginManifest(
|
||||
name="smallerfonts",
|
||||
pretty_name="Smaller Fonts",
|
||||
phase=1, # after bookerly (phase 0), so its font-id return line exists to be wrapped
|
||||
source_files=[
|
||||
SourceFile("SmallerFontsPlugin.h", "src/activities/settings"),
|
||||
SourceFile("SmallerFontsPlugin.cpp", "src/activities/settings"),
|
||||
],
|
||||
includes=[
|
||||
Include("activities/settings/SmallerFontsPlugin.h", "cross_point_settings_h"),
|
||||
],
|
||||
settings_fields=[
|
||||
SettingsField("uint8_t smallerFontsMode = 0;"),
|
||||
],
|
||||
plugins_tab_entries=[
|
||||
PluginsTabEntry(
|
||||
label="Smaller Fonts",
|
||||
kind="enum",
|
||||
key="smallerFontsMode",
|
||||
option_labels=["Disabled", "Enabled"],
|
||||
value_text_expr="SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value))",
|
||||
),
|
||||
],
|
||||
post_patch=_wrap_font_resolution,
|
||||
)
|
||||
Reference in new issue
Block a user