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