commit 61f6e61a548d82f36757649881a5817ae51804c7 Author: Justin Oros Date: Wed Apr 1 16:21:24 2026 -0700 xteink-plugins diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..556cafb Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..afe3a0d --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +![Smaller Fonts plugin settings tab on the xteink device](screenshot.jpg) + +# xteink-plugins + +A plugin system for customizing and extending [CrossPoint Reader](https://github.com/crosspoint-reader/crosspoint-reader) firmware on your xteink device. Plugins are applied as source-level patches before the firmware is compiled and flashed. + +## Plugins + +### Smaller Fonts + +Adds a **Plugins** settings tab to CrossPoint with a "Smaller Fonts" option. When enabled, your chosen reader font is transparently substituted with a smaller variant — no need to change your font preference. + +| Mode | Effect | +|------|--------| +| **Disabled** | No change (default) | +| **Smaller** | Drops the current font size down by one step (e.g. Bookerly 16 → 14) | +| **Smallest** | Drops the current font size down by two steps (e.g. Bookerly 16 → 12) | + +Supports Bookerly, Noto Sans, and OpenDyslexic. The plugin also generates and embeds Bookerly at 8pt and 10pt — sizes not included in the stock firmware. + +## Requirements + +- Python 3.10+ +- [PlatformIO](https://platformio.org/) (`pio` on your PATH) +- `git` +- Your xteink device connected via USB + +## Usage + +From the root of this repository, run: + +```bash +python install.py +``` + +The installer will: + +1. Clone the CrossPoint Reader source repository +2. Prompt you to select which plugins to install and apply them as patches +3. Build the firmware with PlatformIO +4. Auto-detect your device's serial port and flash the firmware + +> **Note:** This script modifies and flashes custom firmware to your device. The author accepts no responsibility for any damage that may occur to your device as a result of using this installer. + +## Repository Structure + +``` +xteink-plugins/ +├── install.py # Interactive installer: clone → patch → build → flash +└── plugins/ + └── smallerfonts/ + ├── patch.py # Patch script applied to the CrossPoint source + ├── SmallerFontsPlugin.h/.cpp # Font resolution logic + └── SmallerFontsSettingsPage.h/.cpp # Settings UI activity +``` + +## Adding a Plugin + +1. Create a new directory under `plugins/` with your plugin's name. +2. Add a `patch.py` file with a `patch(repo_dir: str)` function. This function receives the absolute path to the cloned CrossPoint repository and should make all necessary modifications. + +The installer will automatically discover and offer to install any directory under `plugins/` that contains a `patch.py`. diff --git a/install.py b/install.py new file mode 100644 index 0000000..5c6f258 --- /dev/null +++ b/install.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 + +import os +import sys +import shutil +import subprocess +import glob +import importlib.util + +REPO_URL = "https://github.com/crosspoint-reader/crosspoint-reader.git" +REPO_DIR = "crosspoint-reader" +PLUGINS_DIR = "plugins" + + +def run(cmd, cwd=None, check=True): + print(f" $ {cmd}") + return subprocess.run(cmd, shell=True, cwd=cwd, check=check) + + +def clone_repo(): + print("[1/3] Cloning CrossPoint repository...") + if os.path.exists(REPO_DIR): + print(f" Removing existing '{REPO_DIR}'...") + shutil.rmtree(REPO_DIR) + run(f"git clone --recursive {REPO_URL} {REPO_DIR}") + + +def run_plugin_patch(plugin_dir: str) -> bool: + patch_path = os.path.join(plugin_dir, "patch.py") + if not os.path.exists(patch_path): + print(f" WARNING: No patch.py found in {plugin_dir}, skipping.") + return False + + spec = importlib.util.spec_from_file_location("patch", patch_path) + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + if hasattr(module, "patch"): + module.patch(os.path.abspath(REPO_DIR)) + return True + except Exception as exc: + print(f" ERROR while running patch.py for {plugin_dir}: {exc}") + return False + + +def apply_plugins(): + print("[2/3] Applying plugins...") + + if not os.path.exists(PLUGINS_DIR): + print(f" No '{PLUGINS_DIR}' directory found — skipping plugin step.") + return + + plugin_dirs = sorted( + d for d in os.listdir(PLUGINS_DIR) + if os.path.isdir(os.path.join(PLUGINS_DIR, d)) + ) + + if not plugin_dirs: + print(" No plugin directories found.") + return + + for plugin_name in plugin_dirs: + plugin_path = os.path.join(PLUGINS_DIR, plugin_name) + + print(f"\n Plugin: {plugin_name}") + answer = input(f" Install '{plugin_name}'? [Y/n]: ").strip().lower() + + if answer in ("", "y", "yes"): + print(f" Installing {plugin_name}...") + success = run_plugin_patch(plugin_path) + if success: + print(f" ✓ {plugin_name} installed.") + else: + print(f" ✗ {plugin_name} failed or was skipped.") + else: + print(f" Skipping {plugin_name}.") + + +def list_likely_serial_ports(): + patterns = [ + "/dev/cu.usb*", + "/dev/tty.usb*", + "/dev/cu.wchusb*", + "/dev/tty.wchusb*", + "/dev/cu.SLAB*", + "/dev/tty.SLAB*", + "/dev/ttyACM*", + "/dev/ttyUSB*", + ] + ports = [] + for pattern in patterns: + ports.extend(glob.glob(pattern)) + return sorted(set(ports)) + + +def detect_serial_ports(): + port_map: dict[str, str] = {} + + try: + import serial.tools.list_ports + + usb_keywords = { + "usb", "cp210", "ftdi", "ch340", "ch9102", "esp", + "serial", "uart", "prolific", "silabs", + } + skip_keywords = {"bluetooth", "bt-", "btle", "airpods", "handsfree"} + + for port in serial.tools.list_ports.comports(): + combined = (port.device + " " + (port.description or "") + + " " + (port.hwid or "")).lower() + + if any(s in combined for s in skip_keywords): + continue + if any(k in combined for k in usb_keywords): + port_map[port.device] = port.description or "USB Serial Device" + + except ImportError: + pass + + for device in list_likely_serial_ports(): + if device not in port_map: + port_map[device] = "Detected via glob pattern" + + return sorted(port_map.items()) + + +def prompt_for_upload_port(): + print("\n[3/3] Select serial port for flashing...") + + max_retries = 3 + + for attempt in range(1, max_retries + 1): + print(f"\n Attempt {attempt}/{max_retries}") + print(" Ensure your xteink device is connected via USB and awake.") + input(" Press Enter when ready... ") + + ports = detect_serial_ports() + + if ports: + cu_ports = [(d, desc) for d, desc in ports if d.startswith("/dev/cu.")] + recommended = cu_ports[0][0] if cu_ports else ports[0][0] + + print(f"\n Found {len(ports)} serial port(s):") + for i, (device, desc) in enumerate(ports, 1): + tag = " ← recommended" if device == recommended else "" + print(f" {i}. {device} — {desc}{tag}") + + choice = input(f"\n Press ENTER to use {recommended}, or enter 1-{len(ports)}: ").strip() + + if not choice: + print(f" Selected: {recommended}") + return recommended + + if choice.isdigit(): + idx = int(choice) - 1 + if 0 <= idx < len(ports): + print(f" Selected: {ports[idx][0]}") + return ports[idx][0] + + print(" Invalid choice, please try again.") + + else: + print(" No USB serial ports detected.") + if attempt < max_retries: + print(" Make sure the device is connected, powered on, and awake.") + + print("\n Could not auto-detect a port.") + print(" Common names: macOS: /dev/cu.usb* Linux: /dev/ttyUSB* Windows: COMx") + + while True: + port = input("\n Enter port manually (or 'quit' to exit): ").strip() + if port.lower() == "quit": + sys.exit(1) + if port: + return port + print(" Please enter a valid port name.") + + +def build_and_flash(): + print("\nBuilding firmware with PlatformIO...") + run("pio run --environment default", cwd=REPO_DIR) + + port = prompt_for_upload_port() + + print(f"\n Flashing to {port}...") + run( + f"pio run --target upload --environment default --upload-port {port}", + cwd=REPO_DIR, + ) + + +def main(): + print("=" * 60) + print(" xteink Plugin Installer") + print("=" * 60) + print() + print(" This script modifies and flashes custom firmware to your") + print(" device. The author of this script (Justin Oros) accepts") + print(" no responsibility for any damage that may occur to your") + print(" device as a result of using this installer.") + print() + answer = input(" Do you wish to proceed? [Y/n]: ").strip().lower() + if answer not in ("", "y", "yes"): + print(" Aborted.") + sys.exit(0) + print() + clone_repo() + apply_plugins() + build_and_flash() + print("\nDone.") + +if __name__ == "__main__": + main() diff --git a/plugins/.DS_Store b/plugins/.DS_Store new file mode 100644 index 0000000..9ba66bc Binary files /dev/null and b/plugins/.DS_Store differ diff --git a/plugins/smallerfonts/.DS_Store b/plugins/smallerfonts/.DS_Store new file mode 100644 index 0000000..32455a8 Binary files /dev/null and b/plugins/smallerfonts/.DS_Store differ diff --git a/plugins/smallerfonts/SmallerFontsPlugin.cpp b/plugins/smallerfonts/SmallerFontsPlugin.cpp new file mode 100644 index 0000000..4041db7 --- /dev/null +++ b/plugins/smallerfonts/SmallerFontsPlugin.cpp @@ -0,0 +1,65 @@ +#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; + + 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; + } + + 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/smallerfonts/SmallerFontsPlugin.h b/plugins/smallerfonts/SmallerFontsPlugin.h new file mode 100644 index 0000000..26b6882 --- /dev/null +++ b/plugins/smallerfonts/SmallerFontsPlugin.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +enum class SmallerFontsMode : uint8_t { + MODE_OFF = 0, + SMALLER = 1, + SMALLEST = 2, +}; + +namespace SmallerFontsPlugin { + const char* modeName(SmallerFontsMode mode); + int resolveReaderFontId(int baseFontId, SmallerFontsMode mode); +} diff --git a/plugins/smallerfonts/SmallerFontsSettingsPage.cpp b/plugins/smallerfonts/SmallerFontsSettingsPage.cpp new file mode 100644 index 0000000..5bb6cd3 --- /dev/null +++ b/plugins/smallerfonts/SmallerFontsSettingsPage.cpp @@ -0,0 +1,22 @@ +#include "SmallerFontsSettingsActivity.h" +#include "fontIds.h" + +void SmallerFontsSettingsActivity::render(GfxRenderer& renderer, SmallerFontsMode currentMode) { + const int LABEL_X = 10; + const int VALUE_X = 160; + const int ROW_Y = 60; + renderer.drawText(UI_12_FONT_ID, LABEL_X, ROW_Y, "Smaller Fonts"); + renderer.drawText(UI_12_FONT_ID, VALUE_X, ROW_Y, SmallerFontsPlugin::modeName(currentMode)); +} + +SmallerFontsMode SmallerFontsSettingsActivity::handleNext(SmallerFontsMode currentMode) { + uint8_t v = static_cast(currentMode); + if (v < 3) v++; + return static_cast(v); +} + +SmallerFontsMode SmallerFontsSettingsActivity::handlePrev(SmallerFontsMode currentMode) { + uint8_t v = static_cast(currentMode); + if (v > 0) v--; + return static_cast(v); +} diff --git a/plugins/smallerfonts/SmallerFontsSettingsPage.h b/plugins/smallerfonts/SmallerFontsSettingsPage.h new file mode 100644 index 0000000..8355f45 --- /dev/null +++ b/plugins/smallerfonts/SmallerFontsSettingsPage.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include "SmallerFontsPlugin.h" + +class SmallerFontsSettingsActivity { +public: + static void render(GfxRenderer& renderer, SmallerFontsMode currentMode); + static SmallerFontsMode handleNext(SmallerFontsMode currentMode); + static SmallerFontsMode handlePrev(SmallerFontsMode currentMode); +}; diff --git a/plugins/smallerfonts/patch.py b/plugins/smallerfonts/patch.py new file mode 100644 index 0000000..67e857e --- /dev/null +++ b/plugins/smallerfonts/patch.py @@ -0,0 +1,357 @@ +#!/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(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(fonts_dir, "source") + 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 + ) + + new_fonts = [ + ("bookerly", 8, "Bookerly", "ttf", ["Regular", "Bold", "Italic", "BoldItalic"]), + ("bookerly", 10, "Bookerly", "ttf", ["Regular", "Bold", "Italic", "BoldItalic"]), + ] + + generated = [] + for family, size, folder, ext, styles in new_fonts: + for style in styles: + fname = f"{family}_{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") + generated.append((family, size, style, out_path)) + continue + src_path = os.path.join(source_dir, folder, f"{folder}-{style}.{ext}") + if not os.path.exists(src_path): + src_path = os.path.join(source_dir, folder, f"{folder.replace('NotoSans','NotoSans')}-{style}.{ext}") + extra = ["--2bit", "--compress"] if family != "ubuntu" else [] + with open(out_path, "w") as f: + result = subprocess.run( + [sys.executable, fontconvert, fname, str(size), src_path] + extra, + capture_output=True, text=True + ) + if result.returncode != 0: + print(f" ERROR generating {fname}: {result.stderr[:200]}") + if os.path.exists(out_path): + os.remove(out_path) + continue + if not result.stdout.strip(): + print(f" ERROR: {fname} generated empty output") + if os.path.exists(out_path): + os.remove(out_path) + continue + f.write(result.stdout) + print(f" ✓ generated {fname}.h") + generated.append((family, size, style, out_path)) + + return fonts_dir + + +def compute_new_font_ids(fonts_dir): + ids = {} + + def fid(name, size, suffixes): + paths = [os.path.join(fonts_dir, f"{name}_{size}_{s}.h") for s in suffixes] + if all(os.path.exists(p) for p in paths): + ids[f"{name.upper()}_{size}_FONT_ID"] = font_id_from_files(paths) + + fid("bookerly", 8, ["regular", "bold", "italic", "bolditalic"]) + fid("bookerly", 10, ["regular", "bold", "italic", "bolditalic"]) + + 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) + + includes = { + "BOOKERLY_8_FONT_ID": ["bookerly_8_bold", "bookerly_8_bolditalic", "bookerly_8_italic", "bookerly_8_regular"], + "BOOKERLY_10_FONT_ID": ["bookerly_10_bold", "bookerly_10_bolditalic", "bookerly_10_italic", "bookerly_10_regular"], + } + + added = 0 + for key, fnames in includes.items(): + if key not in new_ids: + continue + for fname in fnames: + 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) + + font_defs = { + "BOOKERLY_8_FONT_ID": ("bookerly", 8, True), + "BOOKERLY_10_FONT_ID": ("bookerly", 10, True), + } + + decl_lines = [] + insert_lines = [] + + for key, (family, size, has_styles) in font_defs.items(): + if key not in new_ids: + continue + varbase = f"{family}{size}" + if f"EpdFontFamily {varbase}FontFamily" in content: + continue + + decl_lines += [ + f'EpdFont {varbase}RegularFont(&{family}_{size}_regular);', + f'EpdFont {varbase}BoldFont(&{family}_{size}_bold);', + f'EpdFont {varbase}ItalicFont(&{family}_{size}_italic);', + f'EpdFont {varbase}BoldItalicFont(&{family}_{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 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 + + content = content.replace( + '#pragma once\n#include ', + '#pragma once\n#include \n#include "activities/settings/SmallerFontsPlugin.h"' + ) + content = content.replace( + ' uint8_t imageRendering = IMAGES_DISPLAY;', + ' uint8_t imageRendering = IMAGES_DISPLAY;\n uint8_t smallerFontsMode = 0;' + ) + + 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));\n };\n' + ) + for name in ["BOOKERLY_12", "BOOKERLY_14", "BOOKERLY_16", "BOOKERLY_18", + "NOTOSANS_12", "NOTOSANS_14", "NOTOSANS_16", "NOTOSANS_18", + "OPENDYSLEXIC_8", "OPENDYSLEXIC_10", "OPENDYSLEXIC_12", "OPENDYSLEXIC_14"]: + 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_h(repo_dir): + path = find_first("SettingsActivity.h", repo_dir) + content = read_file(path) + + if "pluginsSettings" in content: + print(" SettingsActivity.h already patched, skipping.") + return + + content = content.replace( + '#include ', + '#include \n#include "SmallerFontsPlugin.h"' + ) + content = content.replace( + 'static constexpr int categoryCount = 4;', + 'static constexpr int categoryCount = 5;' + ) + 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 "pluginsSettings" in content: + print(" SettingsActivity.cpp already patched, skipping.") + return + + 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};' + ) + content = content.replace( + ' systemSettings.clear();', + ' systemSettings.clear();\n pluginsSettings.clear();' + ) + content = content.replace( + ' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));', + ' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n\n' + ' 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' + ' ));' + ) + 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;' + ) + 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 }' + ) + 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);' + ) + content = content.replace( + ' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },', + ' [&settings, this](int index) -> std::string {\n' + ' if (selectedCategoryIndex == 4) return "Smaller Fonts";\n' + ' return std::string(I18N.get(settings[index].nameId));\n' + ' },' + ) + content = content.replace( + ' } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {\n const uint8_t value = SETTINGS.*(setting.valuePtr);\n valueText = I18N.get(setting.enumValues[value]);\n }', + ' } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {\n const uint8_t value = SETTINGS.*(setting.valuePtr);\n if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n valueText = SmallerFontsPlugin::modeName(static_cast(value));\n } else {\n valueText = I18N.get(setting.enumValues[value]);\n }\n }' + ) + + write_file(path, content) + print(" SettingsActivity.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(" Generating smaller font files...") + fonts_dir = generate_fonts(repo_dir) + + print(" Computing new font IDs...") + new_ids = compute_new_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) + + 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 ") + patch(sys.argv[1]) diff --git a/screenshot.jpg b/screenshot.jpg new file mode 100644 index 0000000..114c482 Binary files /dev/null and b/screenshot.jpg differ