Merge remote-tracking branch 'pablohc/refactor/pr-1258-cover-overlay' into mybuild

# Conflicts:
#	lib/I18n/translations/english.yaml
#	scripts/gen_i18n.py
#	src/CrossPointSettings.h
#	src/SettingsList.h
#	src/activities/boot_sleep/SleepActivity.cpp
#	src/activities/boot_sleep/SleepActivity.h
This commit is contained in:
jpirnay
2026-03-30 16:12:00 +02:00
10 changed files with 41730 additions and 478 deletions
+416
View File
@@ -0,0 +1,416 @@
#pragma once
#include <cstdint>
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
// Forward declaration for string arrays
namespace i18n_strings {
extern const char* const STRINGS_EN[];
extern const char* const STRINGS_ES[];
extern const char* const STRINGS_FR[];
extern const char* const STRINGS_DE[];
extern const char* const STRINGS_CS[];
extern const char* const STRINGS_PO[];
extern const char* const STRINGS_RU[];
extern const char* const STRINGS_SV[];
extern const char* const STRINGS_RO[];
extern const char* const STRINGS_CA[];
extern const char* const STRINGS_UK[];
extern const char* const STRINGS_BE[];
extern const char* const STRINGS_IT[];
extern const char* const STRINGS_PL[];
extern const char* const STRINGS_FI[];
extern const char* const STRINGS_DA[];
extern const char* const STRINGS_NL[];
extern const char* const STRINGS_TR[];
extern const char* const STRINGS_KK[];
} // namespace i18n_strings
// Language enum
enum class Language : uint8_t {
EN = 0,
ES = 1,
FR = 2,
DE = 3,
CS = 4,
PT = 5,
RU = 6,
SV = 7,
RO = 8,
CA = 9,
UK = 10,
BE = 11,
IT = 12,
PL = 13,
FI = 14,
DA = 15,
NL = 16,
TR = 17,
KK = 18,
_COUNT
};
// Language display names (defined in I18nStrings.cpp)
extern const char* const LANGUAGE_NAMES[];
// Character sets for each language (defined in I18nStrings.cpp)
extern const char* const CHARACTER_SETS[];
// String IDs
enum class StrId : uint16_t {
STR_CROSSPOINT,
STR_BOOTING,
STR_SLEEPING,
STR_ENTERING_SLEEP,
STR_BROWSE_FILES,
STR_FILE_TRANSFER,
STR_SETTINGS_TITLE,
STR_CONTINUE_READING,
STR_NO_OPEN_BOOK,
STR_START_READING,
STR_NO_FILES_FOUND,
STR_SELECT_CHAPTER,
STR_NO_CHAPTERS,
STR_END_OF_BOOK,
STR_EMPTY_CHAPTER,
STR_INDEXING,
STR_MEMORY_ERROR,
STR_PAGE_LOAD_ERROR,
STR_EMPTY_FILE,
STR_OUT_OF_BOUNDS,
STR_LOADING,
STR_LOADING_POPUP,
STR_WIFI_NETWORKS,
STR_NO_NETWORKS,
STR_NETWORKS_FOUND,
STR_SCANNING,
STR_CONNECTING,
STR_CONNECTED,
STR_CONNECTION_FAILED,
STR_FORGET_NETWORK,
STR_SAVE_PASSWORD,
STR_PRESS_OK_SCAN,
STR_JOIN_NETWORK,
STR_CREATE_HOTSPOT,
STR_JOIN_DESC,
STR_HOTSPOT_DESC,
STR_STARTING_HOTSPOT,
STR_HOTSPOT_MODE,
STR_CONNECT_WIFI_HINT,
STR_OPEN_URL_HINT,
STR_OR_HTTP_PREFIX,
STR_SCAN_QR_HINT,
STR_CALIBRE_WIRELESS,
STR_CALIBRE_WEB_URL,
STR_NETWORK_LEGEND,
STR_MAC_ADDRESS,
STR_CHECKING_WIFI,
STR_ENTER_WIFI_PASSWORD,
STR_TO_PREFIX,
STR_CALIBRE_RECEIVING,
STR_CALIBRE_RECEIVED,
STR_CALIBRE_INSTRUCTION_1,
STR_CALIBRE_INSTRUCTION_2,
STR_CALIBRE_INSTRUCTION_3,
STR_CALIBRE_INSTRUCTION_4,
STR_CAT_DISPLAY,
STR_CAT_READER,
STR_CAT_CONTROLS,
STR_CAT_SYSTEM,
STR_SLEEP_SCREEN,
STR_SLEEP_COVER_MODE,
STR_HIDE_BATTERY,
STR_EXTRA_SPACING,
STR_TEXT_AA,
STR_IMAGES,
STR_IMAGES_DISPLAY,
STR_IMAGES_PLACEHOLDER,
STR_IMAGES_SUPPRESS,
STR_SHORT_PWR_BTN,
STR_ORIENTATION,
STR_SIDE_BTN_LAYOUT,
STR_LONG_PRESS_SKIP,
STR_FONT_FAMILY,
STR_FONT_SIZE,
STR_LINE_SPACING,
STR_SCREEN_MARGIN,
STR_PARA_ALIGNMENT,
STR_HYPHENATION,
STR_TIME_TO_SLEEP,
STR_SHOW_HIDDEN_FILES,
STR_REFRESH_FREQ,
STR_KOREADER_SYNC,
STR_CHECK_UPDATES,
STR_LANGUAGE,
STR_CLEAR_READING_CACHE,
STR_USERNAME,
STR_PASSWORD,
STR_SYNC_SERVER_URL,
STR_DOCUMENT_MATCHING,
STR_AUTHENTICATE,
STR_KOREADER_USERNAME,
STR_KOREADER_PASSWORD,
STR_FILENAME,
STR_BINARY,
STR_SET_CREDENTIALS_FIRST,
STR_WIFI_CONN_FAILED,
STR_AUTHENTICATING,
STR_AUTH_SUCCESS,
STR_KOREADER_AUTH,
STR_SYNC_READY,
STR_AUTH_FAILED,
STR_DONE,
STR_CLEAR_CACHE_WARNING_1,
STR_CLEAR_CACHE_WARNING_2,
STR_CLEAR_CACHE_WARNING_3,
STR_CLEAR_CACHE_WARNING_4,
STR_CLEARING_CACHE,
STR_CACHE_CLEARED,
STR_ITEMS_REMOVED,
STR_FAILED_LOWER,
STR_CLEAR_CACHE_FAILED,
STR_CHECK_SERIAL_OUTPUT,
STR_DARK,
STR_LIGHT,
STR_CUSTOM,
STR_COVER,
STR_NONE_OPT,
STR_FIT,
STR_CROP,
STR_NEVER,
STR_IN_READER,
STR_ALWAYS,
STR_IGNORE,
STR_SLEEP,
STR_PAGE_TURN,
STR_PORTRAIT,
STR_LANDSCAPE_CW,
STR_INVERTED,
STR_LANDSCAPE_CCW,
STR_PREV_NEXT,
STR_NEXT_PREV,
STR_BOOKERLY,
STR_NOTO_SANS,
STR_OPEN_DYSLEXIC,
STR_SMALL,
STR_MEDIUM,
STR_LARGE,
STR_X_LARGE,
STR_TIGHT,
STR_NORMAL,
STR_WIDE,
STR_JUSTIFY,
STR_ALIGN_LEFT,
STR_CENTER,
STR_ALIGN_RIGHT,
STR_MIN_1,
STR_MIN_5,
STR_MIN_10,
STR_MIN_15,
STR_MIN_30,
STR_PAGES_1,
STR_PAGES_5,
STR_PAGES_10,
STR_PAGES_15,
STR_PAGES_30,
STR_UPDATE,
STR_CHECKING_UPDATE,
STR_NEW_UPDATE,
STR_CURRENT_VERSION,
STR_NEW_VERSION,
STR_UPDATING,
STR_NO_UPDATE,
STR_UPDATE_FAILED,
STR_UPDATE_COMPLETE,
STR_POWER_ON_HINT,
STR_NO_ENTRIES,
STR_DOWNLOADING,
STR_DOWNLOAD_FAILED,
STR_ERROR_MSG,
STR_UNNAMED,
STR_NO_SERVER_URL,
STR_FETCH_FEED_FAILED,
STR_PARSE_FEED_FAILED,
STR_NETWORK_PREFIX,
STR_IP_ADDRESS_PREFIX,
STR_ERROR_GENERAL_FAILURE,
STR_ERROR_NETWORK_NOT_FOUND,
STR_ERROR_CONNECTION_TIMEOUT,
STR_SD_CARD,
STR_BACK,
STR_EXIT,
STR_HOME,
STR_SELECT,
STR_SELECTED,
STR_TOGGLE,
STR_CONFIRM,
STR_CANCEL,
STR_CONNECT,
STR_OPEN,
STR_DOWNLOAD,
STR_RETRY,
STR_YES,
STR_NO,
STR_SHOW,
STR_HIDE,
STR_STATE_ON,
STR_STATE_OFF,
STR_NOT_SET,
STR_DIR_LEFT,
STR_DIR_RIGHT,
STR_DIR_UP,
STR_DIR_DOWN,
STR_OK_BUTTON,
STR_SLEEP_COVER_FILTER,
STR_FILTER_CONTRAST,
STR_CUSTOMISE_STATUS_BAR,
STR_CHAPTER_PAGE_COUNT,
STR_BOOK_PROGRESS_PERCENTAGE,
STR_PROGRESS_BAR,
STR_PROGRESS_BAR_THICKNESS,
STR_PROGRESS_BAR_THIN,
STR_PROGRESS_BAR_MEDIUM,
STR_PROGRESS_BAR_THICK,
STR_BOOK,
STR_CHAPTER,
STR_EXAMPLE_CHAPTER,
STR_EXAMPLE_BOOK,
STR_PREVIEW,
STR_TITLE,
STR_BATTERY,
STR_UI_THEME,
STR_THEME_CLASSIC,
STR_THEME_LYRA,
STR_THEME_LYRA_EXTENDED,
STR_SUNLIGHT_FADING_FIX,
STR_REMAP_FRONT_BUTTONS,
STR_OPDS_BROWSER,
STR_COVER_CUSTOM,
STR_MENU_RECENT_BOOKS,
STR_NO_RECENT_BOOKS,
STR_CALIBRE_DESC,
STR_FORGET_AND_REMOVE,
STR_FORGET_BUTTON,
STR_CALIBRE_STARTING,
STR_CALIBRE_SETUP,
STR_CALIBRE_STATUS,
STR_CLEAR_BUTTON,
STR_DEFAULT_VALUE,
STR_REMAP_PROMPT,
STR_UNASSIGNED,
STR_ALREADY_ASSIGNED,
STR_REMAP_RESET_HINT,
STR_REMAP_CANCEL_HINT,
STR_HW_BACK_LABEL,
STR_HW_CONFIRM_LABEL,
STR_HW_LEFT_LABEL,
STR_HW_RIGHT_LABEL,
STR_GO_TO_PERCENT,
STR_GO_HOME_BUTTON,
STR_SYNC_PROGRESS,
STR_DELETE_CACHE,
STR_DELETE,
STR_DISPLAY_QR,
STR_CHAPTER_PREFIX,
STR_PAGES_SEPARATOR,
STR_BOOK_PREFIX,
STR_CALIBRE_URL_HINT,
STR_PERCENT_STEP_HINT,
STR_SYNCING_TIME,
STR_CALC_HASH,
STR_HASH_FAILED,
STR_FETCH_PROGRESS,
STR_UPLOAD_PROGRESS,
STR_NO_CREDENTIALS_MSG,
STR_KOREADER_SETUP_HINT,
STR_PROGRESS_FOUND,
STR_REMOTE_LABEL,
STR_LOCAL_LABEL,
STR_PAGE_OVERALL_FORMAT,
STR_PAGE_TOTAL_OVERALL_FORMAT,
STR_DEVICE_FROM_FORMAT,
STR_APPLY_REMOTE,
STR_UPLOAD_LOCAL,
STR_NO_REMOTE_MSG,
STR_UPLOAD_PROMPT,
STR_UPLOAD_SUCCESS,
STR_SYNC_FAILED_MSG,
STR_SECTION_PREFIX,
STR_UPLOAD,
STR_BOOK_S_STYLE,
STR_EMBEDDED_STYLE,
STR_OPDS_SERVER_URL,
STR_FOOTNOTES,
STR_NO_FOOTNOTES,
STR_LINK,
STR_SCREENSHOT_BUTTON,
STR_AUTO_TURN_ENABLED,
STR_AUTO_TURN_PAGES_PER_MIN,
STR_SLEEP_COVER_OVERLAY,
STR_OVERLAY_WHITE,
STR_OVERLAY_GRAY,
STR_OVERLAY_BLACK,
STR_OVERLAY_OFF,
STR_OVERLAY_READING_PROGRESS,
STR_OVERLAY_READING_PROGRESS_NO_TOTAL,
STR_OVERLAY_CHAPTER_PAGE_SUFFIX,
// Sentinel - must be last
_COUNT
};
// Helper function to get string array for a language
inline const char* const* getStringArray(Language lang) {
switch (lang) {
case Language::EN:
return i18n_strings::STRINGS_EN;
case Language::ES:
return i18n_strings::STRINGS_ES;
case Language::FR:
return i18n_strings::STRINGS_FR;
case Language::DE:
return i18n_strings::STRINGS_DE;
case Language::CS:
return i18n_strings::STRINGS_CS;
case Language::PT:
return i18n_strings::STRINGS_PO;
case Language::RU:
return i18n_strings::STRINGS_RU;
case Language::SV:
return i18n_strings::STRINGS_SV;
case Language::RO:
return i18n_strings::STRINGS_RO;
case Language::CA:
return i18n_strings::STRINGS_CA;
case Language::UK:
return i18n_strings::STRINGS_UK;
case Language::BE:
return i18n_strings::STRINGS_BE;
case Language::IT:
return i18n_strings::STRINGS_IT;
case Language::PL:
return i18n_strings::STRINGS_PL;
case Language::FI:
return i18n_strings::STRINGS_FI;
case Language::DA:
return i18n_strings::STRINGS_DA;
case Language::NL:
return i18n_strings::STRINGS_NL;
case Language::TR:
return i18n_strings::STRINGS_TR;
case Language::KK:
return i18n_strings::STRINGS_KK;
default:
return i18n_strings::STRINGS_EN;
}
}
// Helper function to get language count
constexpr uint8_t getLanguageCount() { return static_cast<uint8_t>(Language::_COUNT); }
// Sorted language indices by code (auto-generated by gen_i18n.py)
// Order: English, Беларуская, Català, Čeština, Dansk, Deutsch, Español, Suomi, Français, Italiano
// Қазақша, Nederlands, Polski, Português (Brasil), Română, Русский, Svenska, Türkçe, Українська
constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {0, 11, 9, 4, 15, 3, 1, 14, 2, 12, 18, 16, 13, 5, 8, 6, 7, 17, 10};
static_assert(sizeof(SORTED_LANGUAGE_INDICES) / sizeof(SORTED_LANGUAGE_INDICES[0]) == getLanguageCount(),
"SORTED_LANGUAGE_INDICES size mismatch");
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <string>
#include "I18nKeys.h"
// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.
namespace i18n_strings {
extern const char* const STRINGS_EN[];
extern const char* const STRINGS_ES[];
extern const char* const STRINGS_FR[];
extern const char* const STRINGS_DE[];
extern const char* const STRINGS_CS[];
extern const char* const STRINGS_PO[];
extern const char* const STRINGS_RU[];
extern const char* const STRINGS_SV[];
extern const char* const STRINGS_RO[];
extern const char* const STRINGS_CA[];
extern const char* const STRINGS_UK[];
extern const char* const STRINGS_BE[];
extern const char* const STRINGS_IT[];
extern const char* const STRINGS_PL[];
extern const char* const STRINGS_FI[];
extern const char* const STRINGS_DA[];
extern const char* const STRINGS_NL[];
extern const char* const STRINGS_TR[];
extern const char* const STRINGS_KK[];
} // namespace i18n_strings
+7
View File
@@ -343,6 +343,13 @@ STR_AUTHOR: "Author"
STR_SERIES: "Series" STR_SERIES: "Series"
STR_FILE_SIZE: "Size" STR_FILE_SIZE: "Size"
STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay" STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay"
STR_OVERLAY_WHITE: "White"
STR_OVERLAY_GRAY: "Gray"
STR_OVERLAY_BLACK: "Black"
STR_OVERLAY_OFF: "Off"
STR_OVERLAY_READING_PROGRESS: "Reading progress: Page %lu/%u - %.0f%%"
STR_OVERLAY_READING_PROGRESS_NO_TOTAL: "Reading progress: Page %lu"
STR_OVERLAY_CHAPTER_PAGE_SUFFIX: " - Page %d/%d - %.0f%% read"
STR_SYSTEM_INFO: "System Information" STR_SYSTEM_INFO: "System Information"
STR_LOAD_XTC_FAILED: "Failed to load XTC file" STR_LOAD_XTC_FAILED: "Failed to load XTC file"
STR_LOAD_EPUB_FAILED: "Failed to load EPUB file" STR_LOAD_EPUB_FAILED: "Failed to load EPUB file"
+114 -380
View File
@@ -15,25 +15,18 @@ Each YAML file must contain:
The English file is the reference. Missing keys in other languages are The English file is the reference. Missing keys in other languages are
automatically filled from English, with a warning. automatically filled from English, with a warning.
By default the script scans the src/ and lib/ trees for STR_* references and
reports any translation keys that are never used. Pass --strip-unused to
omit those keys from the generated output entirely.
Usage: Usage:
python gen_i18n.py [translations_dir [output_dir]] [options] python gen_i18n.py <translations_dir> <output_dir>
Examples: Example:
python gen_i18n.py
python gen_i18n.py lib/I18n/translations lib/I18n/ python gen_i18n.py lib/I18n/translations lib/I18n/
python gen_i18n.py --strip-unused
python gen_i18n.py --strip-unused --src-dirs src lib/EpdFont
""" """
import sys import sys
import os import os
import re import re
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple from typing import List, Dict, Tuple
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -112,8 +105,7 @@ def parse_yaml_file(filepath: str) -> Dict[str, str]:
def load_translations( def load_translations(
translations_dir: str, translations_dir: str,
verbose: bool = False, ) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]]]:
) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]], List[Set[str]]]:
""" """
Read every YAML file in *translations_dir* and return: Read every YAML file in *translations_dir* and return:
language_codes e.g. ["EN", "ES", ...] language_codes e.g. ["EN", "ES", ...]
@@ -146,29 +138,6 @@ def load_translations(
if english_file is None: if english_file is None:
raise ValueError("No YAML file with _language_code: EN found") raise ValueError("No YAML file with _language_code: EN found")
duplicate_orders: Dict[str, List[str]] = {}
order_to_files: Dict[str, List[str]] = {}
for fname, data in parsed.items():
order = data.get("_order")
if not order:
continue
order_to_files.setdefault(order, []).append(fname)
for order, files in order_to_files.items():
if len(files) > 1:
duplicate_orders[order] = sorted(files)
if duplicate_orders:
duplicate_messages = [
f"_order {order}: {', '.join(files)}"
for order, files in sorted(
duplicate_orders.items(), key=lambda item: int(item[0])
)
]
raise ValueError(
"Duplicate _order values found:\n " + "\n ".join(duplicate_messages) + "\nEach _order value must be unique to ensure a deterministic language order."
)
# Order: English first, then by _order metadata (falls back to filename) # Order: English first, then by _order metadata (falls back to filename)
def sort_key(fname: str) -> Tuple[int, int, str]: def sort_key(fname: str) -> Tuple[int, int, str]:
"""English always first (0), then by _order, then by filename.""" """English always first (0), then by _order, then by filename."""
@@ -205,20 +174,16 @@ def load_translations(
raise ValueError(f"Invalid C++ identifier in English file: '{key}'") raise ValueError(f"Invalid C++ identifier in English file: '{key}'")
# Build translations dict, filling missing keys from English # Build translations dict, filling missing keys from English
inherited_sets: List[Set[str]] = [set() for _ in ordered_files]
translations: Dict[str, List[str]] = {} translations: Dict[str, List[str]] = {}
for key in string_keys: for key in string_keys:
row: List[str] = [] row: List[str] = []
for lang_idx, fname in enumerate(ordered_files): for fname in ordered_files:
data = parsed[fname] data = parsed[fname]
value = data.get(key, "") value = data.get(key, "")
if not value.strip() and fname != english_file: if not value.strip() and fname != english_file:
value = english_data[key] value = english_data[key]
inherited_sets[lang_idx].add(key) lang_code = parsed[fname].get("_language_code", fname)
if verbose: print(f" INFO: '{key}' missing in {lang_code}, using English fallback")
print(
f" INFO: '{key}' missing in {language_codes[lang_idx]}, using English fallback"
)
row.append(value) row.append(value)
translations[key] = row translations[key] = row
@@ -230,72 +195,60 @@ def load_translations(
extra = [k for k in data if not k.startswith("_") and k not in english_data] extra = [k for k in data if not k.startswith("_") and k not in english_data]
if extra: if extra:
lang_code = data.get("_language_code", fname) lang_code = data.get("_language_code", fname)
if verbose: print(f" WARNING: {lang_code} has keys not in English: {', '.join(extra)}")
print(
f" WARNING: {lang_code} has keys not in English: {', '.join(extra)}"
)
if verbose: print(f"Loaded {len(language_codes)} languages, {len(string_keys)} string keys")
print(f"Loaded {len(language_codes)} languages, {len(string_keys)} string keys") return language_codes, language_names, string_keys, translations
return language_codes, language_names, string_keys, translations, inherited_sets
# ---------------------------------------------------------------------------
# Unused-string detection
# ---------------------------------------------------------------------------
_GENERATED_FILENAMES: Set[str] = {"I18nKeys.h", "I18nStrings.h", "I18nStrings.cpp"}
def find_used_string_keys(
src_dirs: List[str],
skip_filenames: Optional[Set[str]] = None,
) -> Set[str]:
"""
Scan C/C++ source files under *src_dirs* for STR_* identifiers.
Files whose basename appears in *skip_filenames* are skipped so that
the generated I18n files don't count as "usage" of themselves.
Returns the set of all STR_KEY names that appear at least once.
"""
if skip_filenames is None:
skip_filenames = _GENERATED_FILENAMES
pattern = re.compile(r"\bSTR_[A-Za-z0-9_]+\b")
used: Set[str] = set()
for src_dir in src_dirs:
p = Path(src_dir)
if not p.is_dir():
continue
for f in p.rglob("*"):
if f.suffix not in {".cpp", ".h", ".c"}:
continue
if f.name in skip_filenames:
continue
try:
text = f.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
for m in pattern.finditer(text):
used.add(m.group(0))
return used
def report_unused_keys(
string_keys: List[str],
used_keys: Set[str],
) -> List[str]:
"""Return a sorted list of keys from *string_keys* absent in *used_keys*."""
return [k for k in sorted(string_keys) if k not in used_keys]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# C++ string escaping # C++ string escaping
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
LANG_ABBREVIATIONS = {
"english": "EN",
"español": "ES",
"espanol": "ES",
"italiano": "IT",
"svenska": "SV",
"français": "FR",
"francais": "FR",
"deutsch": "DE",
"german": "DE",
"polski": "PL",
"português": "PT",
"portugues": "PT",
"português (brasil)": "PO",
"中文": "ZH",
"chinese": "ZH",
"日本語": "JA",
"japanese": "JA",
"한국어": "KO",
"korean": "KO",
"русский": "RU",
"russian": "RU",
"العربية": "AR",
"arabic": "AR",
"עברית": "HE",
"hebrew": "HE",
"فارسی": "FA",
"persian": "FA",
"čeština": "CS",
"türkçe": "TR",
"turkish": "TR",
"Қазақша": "KK",
"kazakh": "KK",
}
def get_lang_abbreviation(lang_code: str, lang_name: str) -> str:
"""Return a 2-letter abbreviation for a language."""
lower = lang_name.lower()
if lower in LANG_ABBREVIATIONS:
return LANG_ABBREVIATIONS[lower]
return lang_code[:2].upper()
def escape_cpp_string(s: str) -> List[str]: def escape_cpp_string(s: str) -> List[str]:
r""" r"""
Convert *s* into one or more C++ string literal segments. Convert *s* into one or more C++ string literal segments.
@@ -437,7 +390,6 @@ def generate_keys_header(
language_names: List[str], language_names: List[str],
string_keys: List[str], string_keys: List[str],
output_path: str, output_path: str,
verbose: bool = False,
) -> None: ) -> None:
"""Generate I18nKeys.h.""" """Generate I18nKeys.h."""
lines: List[str] = [ lines: List[str] = [
@@ -446,13 +398,13 @@ def generate_keys_header(
"", "",
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.", "// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
"", "",
"// Forward declarations for flat string data blobs and offset tables", "// Forward declaration for string arrays",
"namespace i18n_strings {", "namespace i18n_strings {",
] ]
for code in languages: for code, name in zip(languages, language_names):
lines.append(f"extern const char STRINGS_{code}_DATA[];") abbrev = get_lang_abbreviation(code, name)
lines.append(f"extern const uint16_t OFFSETS_{code}[];") lines.append(f"extern const char* const STRINGS_{abbrev}[];")
lines.append("} // namespace i18n_strings") lines.append("} // namespace i18n_strings")
lines.append("") lines.append("")
@@ -484,28 +436,17 @@ def generate_keys_header(
lines.append("};") lines.append("};")
lines.append("") lines.append("")
# LangStrings struct # getStringArray helper
lines.append("// Holds a flat string blob and its offset table for one language") lines.append("// Helper function to get string array for a language")
lines.append("struct LangStrings {") lines.append("inline const char* const* getStringArray(Language lang) {")
lines.append(" const char* data;")
lines.append(" const uint16_t* offsets;")
lines.append("};")
lines.append("")
# getLanguageStrings helper
lines.append("// Helper function to get string data for a language")
lines.append("inline LangStrings getLanguageStrings(Language lang) {")
lines.append(" switch (lang) {") lines.append(" switch (lang) {")
for code in languages: for code, name in zip(languages, language_names):
abbrev = get_lang_abbreviation(code, name)
lines.append(f" case Language::{code}:") lines.append(f" case Language::{code}:")
lines.append( lines.append(f" return i18n_strings::STRINGS_{abbrev};")
f" return {{i18n_strings::STRINGS_{code}_DATA, i18n_strings::OFFSETS_{code}}};" first_abbrev = get_lang_abbreviation(languages[0], language_names[0])
)
first_code = languages[0]
lines.append(" default:") lines.append(" default:")
lines.append( lines.append(f" return i18n_strings::STRINGS_{first_abbrev};")
f" return {{i18n_strings::STRINGS_{first_code}_DATA, i18n_strings::OFFSETS_{first_code}}};"
)
lines.append(" }") lines.append(" }")
lines.append("}") lines.append("}")
lines.append("") lines.append("")
@@ -526,9 +467,21 @@ def generate_keys_header(
key=lambda i: languages[i], key=lambda i: languages[i],
) )
sorted_indices = [english_idx] + rest sorted_indices = [english_idx] + rest
comment_names = ", ".join(language_names[i] for i in sorted_indices) names = [language_names[i] for i in sorted_indices]
comment_names = ", ".join(names)
lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)") lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)")
lines.append(f"// Order: {comment_names}") if len(f"// Order: {comment_names}") <= 120:
lines.append(f"// Order: {comment_names}")
else:
current = "// Order: "
for name in names:
candidate = current + name + ", "
if len(candidate) > 100:
lines.append(current.rstrip(", "))
current = "// " + name + ", "
else:
current = candidate
lines.append(current.rstrip(", "))
lines.append( lines.append(
"constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {" "constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {"
f"{', '.join(str(i) for i in sorted_indices)}" f"{', '.join(str(i) for i in sorted_indices)}"
@@ -540,18 +493,19 @@ def generate_keys_header(
) )
lines.append(' "SORTED_LANGUAGE_INDICES size mismatch");') lines.append(' "SORTED_LANGUAGE_INDICES size mismatch");')
_write_file(output_path, lines, verbose) _write_file(output_path, lines)
def generate_strings_header( def generate_strings_header(
languages: List[str], languages: List[str],
language_names: List[str], language_names: List[str],
output_path: str, output_path: str,
verbose: bool = False,
) -> None: ) -> None:
"""Generate I18nStrings.h.""" """Generate I18nStrings.h."""
lines: List[str] = [ lines: List[str] = [
"#pragma once", "#pragma once",
"#include <string>",
"",
'#include "I18nKeys.h"', '#include "I18nKeys.h"',
"", "",
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.", "// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
@@ -560,13 +514,13 @@ def generate_strings_header(
"", "",
] ]
for code in languages: for code, name in zip(languages, language_names):
lines.append(f"extern const char STRINGS_{code}_DATA[];") abbrev = get_lang_abbreviation(code, name)
lines.append(f"extern const uint16_t OFFSETS_{code}[];") lines.append(f"extern const char* const STRINGS_{abbrev}[];")
lines.append("") lines.append("")
lines.append("} // namespace i18n_strings") lines.append("} // namespace i18n_strings")
_write_file(output_path, lines, verbose) _write_file(output_path, lines)
def generate_strings_cpp( def generate_strings_cpp(
@@ -575,12 +529,10 @@ def generate_strings_cpp(
string_keys: List[str], string_keys: List[str],
translations: Dict[str, List[str]], translations: Dict[str, List[str]],
output_path: str, output_path: str,
verbose: bool = False,
) -> None: ) -> None:
"""Generate I18nStrings.cpp.""" """Generate I18nStrings.cpp."""
lines: List[str] = [ lines: List[str] = [
'#include "I18nStrings.h"', '#include "I18nStrings.h"',
"#include <cstddef>",
"", "",
"// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.", "// THIS FILE IS AUTO-GENERATED BY gen_i18n.py. DO NOT EDIT.",
"", "",
@@ -603,41 +555,18 @@ def generate_strings_cpp(
lines.append("};") lines.append("};")
lines.append("") lines.append("")
# Per-language flat string blobs and offset tables # Per-language string arrays
lines.append("namespace i18n_strings {") lines.append("namespace i18n_strings {")
lines.append("") lines.append("")
for lang_idx, code in enumerate(languages): for lang_idx, (code, name) in enumerate(zip(languages, language_names)):
lang_strings = [translations[key][lang_idx] for key in string_keys] abbrev = get_lang_abbreviation(code, name)
lines.append(f"const char* const STRINGS_{abbrev}[] = {{")
# Precompute byte offsets (UTF-8 encoded, +1 per string for null terminator) for key in string_keys:
offsets: List[int] = [] text = translations[key][lang_idx]
current_offset = 0 _append_string_entry(lines, text)
for s in lang_strings:
offsets.append(current_offset)
current_offset += len(s.encode("utf-8")) + 1
if current_offset > 65535:
raise ValueError(
f"Language {code}: total string data ({current_offset} bytes) "
"exceeds uint16_t offset range (65535)"
)
# Flat string data blob — all strings concatenated with \0 separators.
# clang-format off/on avoids reformatting of the adjacent string literals.
lines.append("// clang-format off")
lines.append(f"const char STRINGS_{code}_DATA[] =")
for text in lang_strings:
_append_string_data_entry(lines, text)
lines.append(";")
lines.append("// clang-format on")
lines.append("")
# Offset table — one uint16_t per StrId
lines.append(f"const uint16_t OFFSETS_{code}[] = {{")
chunk_size = 12
for i in range(0, len(offsets), chunk_size):
chunk = offsets[i : i + chunk_size]
lines.append(" " + ", ".join(str(o) for o in chunk) + ",")
lines.append("};") lines.append("};")
lines.append("") lines.append("")
@@ -646,15 +575,16 @@ def generate_strings_cpp(
# Compile-time size checks # Compile-time size checks
lines.append("// Compile-time validation of array sizes") lines.append("// Compile-time validation of array sizes")
for code in languages: for code, name in zip(languages, language_names):
abbrev = get_lang_abbreviation(code, name)
lines.append( lines.append(
f"static_assert(sizeof(i18n_strings::OFFSETS_{code}) " f"static_assert(sizeof(i18n_strings::STRINGS_{abbrev}) "
f"/ sizeof(i18n_strings::OFFSETS_{code}[0]) ==" f"/ sizeof(i18n_strings::STRINGS_{abbrev}[0]) =="
) )
lines.append(" static_cast<size_t>(StrId::_COUNT),") lines.append(" static_cast<size_t>(StrId::_COUNT),")
lines.append(f' "OFFSETS_{code} size mismatch");') lines.append(f' "STRINGS_{abbrev} size mismatch");')
_write_file(output_path, lines, verbose) _write_file(output_path, lines)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -662,91 +592,6 @@ def generate_strings_cpp(
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _print_language_table(
language_codes: List[str],
language_names: List[str],
inherited_sets: List[Set[str]],
string_keys: List[str],
unused_keys: Set[str],
data_sizes: List[int],
) -> None:
"""Print a per-language summary table."""
total = len(string_keys)
headers = ("Language", "Code", "Own", "Fallback", "Unused", "Data (B)")
rows = []
for code, name, inherited, size in zip(
language_codes, language_names, inherited_sets, data_sizes
):
own = total - len(inherited)
fallback = len(inherited)
# strings this language translated but the code never calls
unused = len(unused_keys - inherited)
rows.append((name, code, str(own), str(fallback), str(unused), str(size)))
# EN first, then alphabetically by ISO code
rows.sort(key=lambda r: (0 if r[1] == "EN" else 1, r[1]))
col_widths = [len(h) for h in headers]
for row in rows:
for i, cell in enumerate(row):
col_widths[i] = max(col_widths[i], len(cell))
fmt = " ".join(f"{{:<{w}}}" for w in col_widths)
sep = " ".join("-" * w for w in col_widths)
def _safe_print(line: str) -> None:
print(
line.encode(sys.stdout.encoding or "utf-8", errors="replace").decode(
sys.stdout.encoding or "utf-8", errors="replace"
)
)
_safe_print(fmt.format(*headers))
_safe_print(sep)
for row in rows:
_safe_print(fmt.format(*row))
used = total - len(unused_keys)
total_size = sum(data_sizes)
n_lang = len(rows)
n_keys = len(string_keys)
# Current layout: uint16_t offset table (2 B per string per language)
offset_table_size = n_lang * n_keys * 2
current_total = total_size + offset_table_size
# Previous layout: const char* pointer array (4 B per string per language)
old_pointer_table_size = n_lang * n_keys * 4
old_total = total_size + old_pointer_table_size
saved = old_total - current_total
print(
f"\n Total: {total} | Used in code: {used} | Never used: {len(unused_keys)}"
)
print(
f" Flash (now): {total_size:>7,} B strings + {offset_table_size:>6,} B offset tables (uint16_t)"
f" = {current_total:>7,} B"
)
print(
f" Flash (before): {total_size:>7,} B strings + {old_pointer_table_size:>6,} B pointer tables (ptr32)"
f" = {old_total:>7,} B"
)
print(f" Saved by offset tables: {saved:,} B")
def _append_string_data_entry(lines: List[str], text: str) -> None:
"""
Escape *text*, append a \\0 null separator, and format as indented C++
string literal lines for inclusion in a flat char data array blob.
"""
segments = escape_cpp_string(text)
# Append the null entry separator to the last segment
if segments and segments[-1] != "":
segments[-1] += "\\0"
elif segments:
segments[-1] = "\\0"
else:
segments = ["\\0"]
lines.extend(format_cpp_string_literal(segments))
def _append_string_entry(lines: List[str], text: str, comment: str = "") -> None: def _append_string_entry(lines: List[str], text: str, comment: str = "") -> None:
"""Escape *text*, format as indented C++ lines, append comma (and optional comment).""" """Escape *text*, format as indented C++ lines, append comma (and optional comment)."""
segments = escape_cpp_string(text) segments = escape_cpp_string(text)
@@ -756,12 +601,11 @@ def _append_string_entry(lines: List[str], text: str, comment: str = "") -> None
lines.extend(formatted) lines.extend(formatted)
def _write_file(path: str, lines: List[str], verbose: bool = False) -> None: def _write_file(path: str, lines: List[str]) -> None:
with open(path, "w", encoding="utf-8", newline="\n") as f: with open(path, "w", encoding="utf-8", newline="\n") as f:
f.write("\n".join(lines)) f.write("\n".join(lines))
f.write("\n") f.write("\n")
if verbose: print(f"Generated: {path}")
print(f"Generated: {path}")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -769,17 +613,10 @@ def _write_file(path: str, lines: List[str], verbose: bool = False) -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def main( def main(translations_dir=None, output_dir=None) -> None:
translations_dir: Optional[str] = None,
output_dir: Optional[str] = None,
src_dirs: Optional[List[str]] = None,
strip_unused: bool = False,
verbose: bool = False,
) -> None:
# Default paths (relative to project root) # Default paths (relative to project root)
default_translations_dir = "lib/I18n/translations" default_translations_dir = "lib/I18n/translations"
default_output_dir = "lib/I18n/" default_output_dir = "lib/I18n/"
default_src_dirs = ["src", "lib"]
if translations_dir is None or output_dir is None: if translations_dir is None or output_dir is None:
if len(sys.argv) == 3: if len(sys.argv) == 3:
@@ -790,9 +627,6 @@ def main(
translations_dir = default_translations_dir translations_dir = default_translations_dir
output_dir = default_output_dir output_dir = default_output_dir
if src_dirs is None:
src_dirs = default_src_dirs
if not os.path.isdir(translations_dir): if not os.path.isdir(translations_dir):
print(f"Error: Translations directory not found: {translations_dir}") print(f"Error: Translations directory not found: {translations_dir}")
sys.exit(1) sys.exit(1)
@@ -801,91 +635,32 @@ def main(
print(f"Error: Output directory not found: {output_dir}") print(f"Error: Output directory not found: {output_dir}")
sys.exit(1) sys.exit(1)
if verbose: print(f"Reading translations from: {translations_dir}")
print(f"Reading translations from: {translations_dir}") print(f"Output directory: {output_dir}")
print(f"Output directory: {output_dir}") print()
print()
try: try:
languages, language_names, string_keys, translations, inherited_sets = ( languages, language_names, string_keys, translations = load_translations(
load_translations(translations_dir, verbose) translations_dir
) )
# --- Unused-string detection ---
scan_dirs = [d for d in src_dirs if os.path.isdir(d)]
if scan_dirs:
used_keys = find_used_string_keys(scan_dirs)
unused_set = set(report_unused_keys(string_keys, used_keys))
else:
used_keys = set(string_keys)
unused_set = set()
# --- Missing-string detection (used in code but absent from English) ---
missing_keys = sorted(used_keys - set(string_keys))
if missing_keys:
print(
f"\n CRITICAL: {len(missing_keys)} string(s) used in source but missing from english.yaml:"
)
for key in missing_keys:
print(f" - {key}")
print()
sys.exit(1)
# Compute per-language data blob sizes:
# sum of UTF-8 byte length + 1 (null terminator) per string
data_sizes = [
sum(len(translations[k][i].encode("utf-8")) + 1 for k in string_keys)
for i in range(len(languages))
]
_print_language_table(
languages,
language_names,
inherited_sets,
string_keys,
unused_set,
data_sizes,
)
print()
if verbose and unused_set:
print(f" Unused keys ({len(unused_set)}):")
for key in sorted(unused_set):
print(f" - {key}")
print()
if unused_set and strip_unused:
string_keys = [k for k in string_keys if k not in unused_set]
translations = {
k: v for k, v in translations.items() if k not in unused_set
}
inherited_sets = [s - unused_set for s in inherited_sets]
print(f" Stripping {len(unused_set)} unused string(s) from output.")
out = Path(output_dir) out = Path(output_dir)
generate_keys_header( generate_keys_header(
languages, language_names, string_keys, str(out / "I18nKeys.h"), verbose languages, language_names, string_keys, str(out / "I18nKeys.h")
)
generate_strings_header(
languages, language_names, str(out / "I18nStrings.h"), verbose
) )
generate_strings_header(languages, language_names, str(out / "I18nStrings.h"))
generate_strings_cpp( generate_strings_cpp(
languages, languages,
language_names, language_names,
string_keys, string_keys,
translations, translations,
str(out / "I18nStrings.cpp"), str(out / "I18nStrings.cpp"),
verbose,
) )
print() print()
print("Code generation complete!") print("Code generation complete!")
print(f" Languages: {len(languages)}") print(f" Languages: {len(languages)}")
print(f" String keys: {len(string_keys)}") print(f" String keys: {len(string_keys)}")
if unused_set and not strip_unused:
print(
f" Unused keys: {len(unused_set)} (pass --strip-unused to remove them)"
)
except Exception as e: except Exception as e:
print(f"\nError: {e}") print(f"\nError: {e}")
@@ -893,52 +668,11 @@ def main(
if __name__ == "__main__": if __name__ == "__main__":
import argparse main()
parser = argparse.ArgumentParser(
description="Generate I18n C++ files from per-language YAML translations."
)
parser.add_argument(
"translations_dir",
nargs="?",
default=None,
help="Path to the translations directory (default: lib/I18n/translations)",
)
parser.add_argument(
"output_dir",
nargs="?",
default=None,
help="Path to the output directory (default: lib/I18n/)",
)
parser.add_argument(
"--src-dirs",
nargs="+",
metavar="DIR",
default=None,
help="Source directories to scan for STR_* usage (default: src lib)",
)
parser.add_argument(
"--strip-unused",
action="store_true",
help="Remove unused STR_* keys from the generated output",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Print per-key INFO/WARNING messages and file generation details",
)
args = parser.parse_args()
main(
args.translations_dir,
args.output_dir,
args.src_dirs,
args.strip_unused,
args.verbose,
)
else: else:
try: try:
Import("env") Import("env")
main(strip_unused=True) print("Running i18n generation script from PlatformIO...")
main()
except NameError: except NameError:
pass pass
+1 -1
View File
@@ -166,7 +166,7 @@ class CrossPointSettings {
// Sleep screen cover filter // Sleep screen cover filter
uint8_t sleepScreenCoverFilter = NO_FILTER; uint8_t sleepScreenCoverFilter = NO_FILTER;
// Apply information overlay with reading progress on sleep cover // Apply information overlay with reading progress on sleep cover
uint8_t sleepCoverOverlay = 1; uint8_t sleepCoverOverlay = 0;
// Status bar settings (statusBar retained for migration only) // Status bar settings (statusBar retained for migration only)
uint8_t statusBar = FULL; uint8_t statusBar = FULL;
uint8_t statusBarChapterPageCount = 1; uint8_t statusBarChapterPageCount = 1;
+13
View File
@@ -143,6 +143,19 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
applyLegacyStatusBarSettings(s); applyLegacyStatusBarSettings(s);
} }
// Legacy migration: sleepCoverOverlay was a Toggle (0=off, 1=on). Now it's a 4-way Enum:
// {WHITE(0), GRAY(1), BLACK(2), OFF(3)}. Translate old values before the generic loop clamps.
if (!doc["sleepCoverOverlay"].isNull()) {
uint8_t legacyOverlay = doc["sleepCoverOverlay"] | (uint8_t)0;
if (legacyOverlay == 0) {
s.sleepCoverOverlay = 3; // OFF
doc["sleepCoverOverlay"] = 3;
if (needsResave) *needsResave = true;
} else if (legacyOverlay == 1) {
s.sleepCoverOverlay = 1; // GRAY (old "on" = gray dither)
}
}
for (const auto& info : getSettingsList()) { for (const auto& info : getSettingsList()) {
if (!info.key) continue; if (!info.key) continue;
// Dynamic entries (KOReader etc.) are stored in their own files — skip. // Dynamic entries (KOReader etc.) are stored in their own files — skip.
+4 -2
View File
@@ -23,8 +23,10 @@ inline const std::vector<SettingInfo>& getSettingsList() {
SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter,
{StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED},
"sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY), "sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY),
SettingInfo::Toggle(StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, "sleepCoverOverlay", SettingInfo::Enum(
StrId::STR_CAT_DISPLAY), StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay,
{StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK, StrId::STR_OVERLAY_OFF},
"sleepCoverOverlay", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage,
{StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage",
StrId::STR_CAT_DISPLAY), StrId::STR_CAT_DISPLAY),
+119 -93
View File
@@ -6,6 +6,7 @@
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h> #include <I18n.h>
#include <PNGdec.h> #include <PNGdec.h>
#include <Serialization.h>
#include <Txt.h> #include <Txt.h>
#include <Xtc.h> #include <Xtc.h>
@@ -242,7 +243,7 @@ void SleepActivity::renderCustomSleepScreen() const {
delay(100); delay(100);
Bitmap bitmap(file, true); Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderBitmapSleepScreen(bitmap); renderBitmapSleepScreen(bitmap, BookOverlayInfo{});
file.close(); file.close();
dir.close(); dir.close();
return; return;
@@ -260,7 +261,7 @@ void SleepActivity::renderCustomSleepScreen() const {
Bitmap bitmap(file, true); Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
LOG_DBG("SLP", "Loading: /sleep.bmp"); LOG_DBG("SLP", "Loading: /sleep.bmp");
renderBitmapSleepScreen(bitmap); renderBitmapSleepScreen(bitmap, BookOverlayInfo{});
file.close(); file.close();
return; return;
} }
@@ -287,20 +288,15 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} }
std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const { BookOverlayInfo SleepActivity::getBookOverlayInfo(const std::string& bookPath) const {
std::string title; BookOverlayInfo info;
std::string author;
std::string progressLine = "Reading..."; // Default progress text
// Extract metadata based on file type
if (FsHelpers::checkFileExtension(bookPath, ".xtc") || FsHelpers::checkFileExtension(bookPath, ".xtch")) { if (FsHelpers::checkFileExtension(bookPath, ".xtc") || FsHelpers::checkFileExtension(bookPath, ".xtch")) {
// Handle XTC file
Xtc xtc(bookPath, "/.crosspoint"); Xtc xtc(bookPath, "/.crosspoint");
if (xtc.load()) { if (xtc.load()) {
title = xtc.getTitle(); info.title = xtc.getTitle();
author = xtc.getAuthor(); info.author = xtc.getAuthor();
// Load progress - XTC stores current page
FsFile f; FsFile f;
if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) { if (Storage.openFileForRead("SLP", xtc.getCachePath() + "/progress.bin", f)) {
uint8_t data[4]; uint8_t data[4];
@@ -309,65 +305,62 @@ std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24); (static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
uint32_t totalPages = xtc.getPageCount(); uint32_t totalPages = xtc.getPageCount();
float progress = xtc.calculateProgress(currentPage) * 100.0f; float progress = xtc.calculateProgress(currentPage) * 100.0f;
char progressStr[32]; char buf[64];
snprintf(progressStr, sizeof(progressStr), "%lu/%u %.0f%%", (unsigned long)currentPage + 1, totalPages, snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1, totalPages,
progress); progress);
progressLine = progressStr; info.progressText = buf;
} }
f.close(); f.close();
} }
} }
} else if (FsHelpers::checkFileExtension(bookPath, ".txt")) { } else if (FsHelpers::checkFileExtension(bookPath, ".txt")) {
// Handle TXT file
Txt txt(bookPath, "/.crosspoint"); Txt txt(bookPath, "/.crosspoint");
if (txt.load()) { if (txt.load()) {
title = txt.getTitle(); info.title = txt.getTitle();
// TXT files don't have author metadata
// Load progress - TXT stores current page
FsFile f; FsFile f;
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", f)) { if (Storage.openFileForRead("SLP", txt.getCachePath() + "/progress.bin", f)) {
uint8_t data[4]; uint8_t data[4];
if (f.read(data, 4) == 4) { if (f.read(data, 4) == 4) {
uint32_t currentPage = data[0] + (data[1] << 8); uint32_t currentPage = data[0] + (data[1] << 8);
// Load total pages from page index cache
uint32_t totalPages = 0; uint32_t totalPages = 0;
FsFile indexFile; FsFile indexFile;
if (Storage.openFileForRead("SLP", txt.getCachePath() + "/index.bin", indexFile)) { if (Storage.openFileForRead("SLP", txt.getCachePath() + "/index.bin", indexFile)) {
// Read numPages directly (it's at a fixed offset after headers) uint32_t magic;
// Skip: magic(4) + version(4) + fileSize(4) + cachedWidth(4) + cachedLines(4) + fontId(4) + margin(4) + serialization::readPod(indexFile, magic);
// alignment(4) = 32 bytes uint8_t version;
indexFile.seek(32); serialization::readPod(indexFile, version);
if (indexFile.read(reinterpret_cast<uint8_t*>(&totalPages), sizeof(totalPages)) == sizeof(totalPages)) { static constexpr uint32_t INDEX_CACHE_MAGIC = 0x54585449; // "TXTI"
// Successfully read totalPages static constexpr uint8_t INDEX_CACHE_VERSION = 2;
if (magic == INDEX_CACHE_MAGIC && version == INDEX_CACHE_VERSION) {
indexFile.seek(32);
serialization::readPod(indexFile, totalPages);
} }
indexFile.close(); indexFile.close();
} }
if (totalPages > 0) { if (totalPages > 0) {
float progress = (currentPage + 1) * 100.0f / totalPages; float progress = (currentPage + 1) * 100.0f / totalPages;
char progressStr[32]; char buf[64];
snprintf(progressStr, sizeof(progressStr), "%lu/%u %.0f%%", (unsigned long)currentPage + 1, totalPages, snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1, totalPages,
progress); progress);
progressLine = progressStr; info.progressText = buf;
} else { } else {
char progressStr[16]; char buf[64];
snprintf(progressStr, sizeof(progressStr), "Page %lu", (unsigned long)currentPage + 1); snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS_NO_TOTAL), (unsigned long)currentPage + 1);
progressLine = progressStr; info.progressText = buf;
} }
} }
f.close(); f.close();
} }
} }
} else if (FsHelpers::checkFileExtension(bookPath, ".epub")) { } else if (FsHelpers::checkFileExtension(bookPath, ".epub")) {
// Handle EPUB file
Epub epub(bookPath, "/.crosspoint"); Epub epub(bookPath, "/.crosspoint");
if (epub.load(true, true)) { // Skip CSS loading for metadata only if (epub.load(true, true)) {
title = epub.getTitle(); info.title = epub.getTitle();
author = epub.getAuthor(); info.author = epub.getAuthor();
// Load progress and get chapter information
FsFile f; FsFile f;
if (Storage.openFileForRead("SLP", epub.getCachePath() + "/progress.bin", f)) { if (Storage.openFileForRead("SLP", epub.getCachePath() + "/progress.bin", f)) {
uint8_t data[6]; uint8_t data[6];
@@ -379,21 +372,21 @@ std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const
float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount); float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
float bookProgress = epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; float bookProgress = epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
// Get chapter name from TOC
const int tocIndex = epub.getTocIndexForSpineIndex(currentSpineIndex); const int tocIndex = epub.getTocIndexForSpineIndex(currentSpineIndex);
std::string chapterName; if (tocIndex != -1) {
if (tocIndex == -1) {
chapterName = "Unnamed Chapter";
} else {
const auto tocItem = epub.getTocItem(tocIndex); const auto tocItem = epub.getTocItem(tocIndex);
chapterName = tocItem.title; info.chapterName = tocItem.title;
char suffix[64];
snprintf(suffix, sizeof(suffix), tr(STR_OVERLAY_CHAPTER_PAGE_SUFFIX), currentPage + 1, pageCount,
bookProgress);
info.progressSuffix = suffix;
info.progressText = std::string(tr(STR_CHAPTER_PREFIX)) + info.chapterName + info.progressSuffix;
} else {
char buf[80];
snprintf(buf, sizeof(buf), tr(STR_OVERLAY_READING_PROGRESS), (unsigned long)currentPage + 1,
(unsigned)pageCount, bookProgress);
info.progressText = buf;
} }
// Create chapter info line with chapter name and progress
char chapterProgressStr[128];
snprintf(chapterProgressStr, sizeof(chapterProgressStr), "%s: %d/%d %.0f%%", chapterName.c_str(),
currentPage + 1, pageCount, bookProgress);
progressLine = chapterProgressStr;
} }
} }
f.close(); f.close();
@@ -401,21 +394,10 @@ std::string SleepActivity::getBookOverlayText(const std::string& bookPath) const
} }
} }
// Format overlay text (two lines) return info;
std::string overlayText;
if (!title.empty()) {
overlayText = title;
if (!author.empty()) {
overlayText += " - " + author;
}
overlayText += "\n"; // New line
overlayText += progressLine;
}
return overlayText;
} }
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const BookOverlayInfo& overlayInfo) const {
int x, y; int x, y;
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
@@ -436,7 +418,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight()); ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
} }
x = 0; x = 0;
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2); y = 0;
LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y); LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y);
} else { } else {
// image taller than viewport ratio, scaled down image needs to be centered horizontally // image taller than viewport ratio, scaled down image needs to be centered horizontally
@@ -452,7 +434,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
} else { } else {
// center the image // center the image
x = (pageWidth - bitmap.getWidth()) / 2; x = (pageWidth - bitmap.getWidth()) / 2;
y = (pageHeight - bitmap.getHeight()) / 2; y = 0;
} }
LOG_DBG("SLP", "drawing to %d x %d", x, y); LOG_DBG("SLP", "drawing to %d x %d", x, y);
@@ -467,43 +449,84 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
renderer.invertScreen(); renderer.invertScreen();
} }
// Draw overlay text if provided const uint8_t overlayMode = SETTINGS.sleepCoverOverlay;
const std::string overlayText = SETTINGS.sleepCoverOverlay ? getBookOverlayText(APP_STATE.openEpubPath) : "";
const auto drawOverlay = [&]() { const auto drawOverlay = [&]() {
if (overlayText.empty()) { const bool hasTitle = !overlayInfo.title.empty();
const bool hasProgress = !overlayInfo.progressText.empty();
const bool hasAuthor = !overlayInfo.author.empty();
if (!hasTitle && !hasAuthor && !hasProgress) {
return; return;
} }
// Split text into lines const int lineHeight12 = renderer.getLineHeight(BOOKERLY_12_FONT_ID);
size_t newlinePos = overlayText.find('\n'); const int lineHeight10 = renderer.getLineHeight(UI_10_FONT_ID);
std::string line1 = (newlinePos != std::string::npos) ? overlayText.substr(0, newlinePos) : overlayText; constexpr int lineSpacing = 3;
std::string line2 = (newlinePos != std::string::npos && newlinePos + 1 < overlayText.length()) constexpr int sectionSpacing = 10;
? overlayText.substr(newlinePos + 1) const int availableWidth = pageWidth - 20;
: "";
// Truncate lines if too long for the screen int textBlockHeight = 0;
line1 = renderer.truncatedText(UI_10_FONT_ID, line1.c_str(), pageWidth - 20); if (hasTitle) {
line2 = renderer.truncatedText(UI_10_FONT_ID, line2.c_str(), pageWidth - 20); textBlockHeight += lineHeight12;
if (hasAuthor) {
textBlockHeight += lineSpacing;
} else if (hasProgress) {
textBlockHeight += sectionSpacing;
}
}
if (hasAuthor) {
textBlockHeight += lineHeight10;
if (hasProgress) {
textBlockHeight += sectionSpacing;
}
}
if (hasProgress) {
textBlockHeight += lineHeight10;
}
// Compute overlay height to exactly fit the text lines with equal top/bottom padding const bool textBlack = (overlayMode != 2);
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const int topPadding = lineHeight12 / 3;
constexpr int lineSpacing = 4; const int bottomPadding = lineHeight10 * 2 / 3;
constexpr int overlayPadding = 10; const int overlayHeight = textBlockHeight + topPadding + bottomPadding;
const int numLines = line2.empty() ? 1 : 2; const int overlayY = pageHeight - overlayHeight;
const int textBlockHeight = numLines * lineHeight + (numLines - 1) * lineSpacing;
const int overlayHeight = textBlockHeight + overlayPadding * 2;
const int overlayY = pageHeight - overlayHeight - 20;
renderer.fillRect(0, overlayY, pageWidth, overlayHeight, false); // White background
// Center text block vertically in the overlay rectangle if (overlayMode == 1) {
const int textStartY = overlayY + (overlayHeight - textBlockHeight) / 2; renderer.fillRectDither(0, overlayY, pageWidth, overlayHeight, Color::LightGray);
const int textX1 = (pageWidth - renderer.getTextWidth(UI_10_FONT_ID, line1.c_str())) / 2; } else {
renderer.drawText(UI_10_FONT_ID, textX1, textStartY, line1.c_str(), true); // Black text renderer.fillRect(0, overlayY, pageWidth, overlayHeight, overlayMode == 2);
}
if (!line2.empty()) { int currentY = overlayY + topPadding;
const int line2Y = textStartY + lineHeight + lineSpacing;
const int textX2 = (pageWidth - renderer.getTextWidth(UI_10_FONT_ID, line2.c_str())) / 2; if (hasTitle) {
renderer.drawText(UI_10_FONT_ID, textX2, line2Y, line2.c_str(), true); // Black text const std::string titleStr =
renderer.truncatedText(BOOKERLY_12_FONT_ID, overlayInfo.title.c_str(), availableWidth, EpdFontFamily::BOLD);
renderer.drawCenteredText(BOOKERLY_12_FONT_ID, currentY, titleStr.c_str(), textBlack, EpdFontFamily::BOLD);
const int spacingAfterTitle = hasAuthor ? lineSpacing : (hasProgress ? sectionSpacing : lineSpacing);
currentY += lineHeight12 + spacingAfterTitle;
}
if (hasAuthor) {
const std::string authorStr = renderer.truncatedText(UI_10_FONT_ID, overlayInfo.author.c_str(), availableWidth);
renderer.drawCenteredText(UI_10_FONT_ID, currentY, authorStr.c_str(), textBlack);
currentY += lineHeight10 + sectionSpacing;
}
if (hasProgress) {
std::string progressStr;
if (!overlayInfo.chapterName.empty()) {
const std::string prefix = tr(STR_CHAPTER_PREFIX);
const int prefixWidth = renderer.getTextWidth(UI_10_FONT_ID, prefix.c_str());
const int suffixWidth = renderer.getTextWidth(UI_10_FONT_ID, overlayInfo.progressSuffix.c_str());
const int maxChapterWidth = availableWidth - prefixWidth - suffixWidth;
const std::string truncatedChapter =
maxChapterWidth > 0
? renderer.truncatedText(UI_10_FONT_ID, overlayInfo.chapterName.c_str(), maxChapterWidth)
: "";
progressStr = prefix + truncatedChapter + overlayInfo.progressSuffix;
} else {
progressStr = renderer.truncatedText(UI_10_FONT_ID, overlayInfo.progressText.c_str(), availableWidth);
}
renderer.drawCenteredText(UI_10_FONT_ID, currentY, progressStr.c_str(), textBlack);
} }
}; };
@@ -602,7 +625,10 @@ void SleepActivity::renderCoverSleepScreen() const {
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str()); LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str());
renderBitmapSleepScreen(bitmap); const uint8_t overlayMode = SETTINGS.sleepCoverOverlay;
const BookOverlayInfo coverOverlayInfo =
overlayMode < 3 ? getBookOverlayInfo(APP_STATE.openEpubPath) : BookOverlayInfo{};
renderBitmapSleepScreen(bitmap, coverOverlayInfo);
file.close(); file.close();
return; return;
} }
+13 -2
View File
@@ -1,8 +1,19 @@
#pragma once #pragma once
#include <string>
#include "../Activity.h" #include "../Activity.h"
class Bitmap; class Bitmap;
struct BookOverlayInfo {
std::string title;
std::string author;
std::string progressText;
std::string chapterName;
std::string progressSuffix;
};
class SleepActivity final : public Activity { class SleepActivity final : public Activity {
public: public:
explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) explicit SleepActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
@@ -13,8 +24,8 @@ class SleepActivity final : public Activity {
void renderDefaultSleepScreen() const; void renderDefaultSleepScreen() const;
void renderCustomSleepScreen() const; void renderCustomSleepScreen() const;
void renderCoverSleepScreen() const; void renderCoverSleepScreen() const;
void renderBitmapSleepScreen(const Bitmap& bitmap) const; void renderBitmapSleepScreen(const Bitmap& bitmap, const BookOverlayInfo& overlayInfo) const;
void renderBlankSleepScreen() const; void renderBlankSleepScreen() const;
void renderOverlaySleepScreen() const; void renderOverlaySleepScreen() const;
std::string getBookOverlayText(const std::string& bookPath) const; BookOverlayInfo getBookOverlayInfo(const std::string& bookPath) const;
}; };