diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 23253403..891beed4 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -474,4 +474,17 @@ STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_SUN_INFO: "Sun" STR_READER_TOOLS: "Tools" STR_READER_NAVIGATION: "Navigation" -STR_READER_APPEARANCE: "Appearance" \ No newline at end of file +STR_READER_APPEARANCE: "Appearance" +STR_MENU_SYS_SYSTEM: "System" +STR_MENU_SYS_NETWORK: "Network" +STR_MENU_SYS_TOOLS: "Tools" +STR_MENU_DISP_SLEEP: "Sleep Screen" +STR_MENU_DISP_BATTERY: "Battery symbol" +STR_MENU_DISP_REFRESH: "Screen refresh" +STR_MENU_READER_FONT: "Reader Font" +STR_MENU_READER_FONT_SETTINGS: "Font Settings" +STR_MENU_READER_LAYOUT: "Layout Settings" +STR_MENU_READER_TWEAKS: "Reader Tweaks" +STR_MENU_READER_SPACING: "Spacing" +STR_MENU_KOSYNC_SERVER: "Server Settings" +STR_MENU_KOSYNC_AUTH: "Login / Register" \ No newline at end of file diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 0bf0d8b2..de9ec54c 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -461,3 +461,31 @@ STR_WEATHER_DESC_THUNDERSTORM: "Orage" STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Orage avec grêle" STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Orage avec forte grêle" STR_WEATHER_DESC_UNKNOWN: "Inconnu" + +STR_IMAGE_DITHERING: "Tramage d'image" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer diffus" +STR_UNSUPPORTED_IMAGE_FORMAT: "Format d'image non pris en charge" +STR_COULD_NOT_RENDER_IMAGE: "Impossible d'afficher l'image" +STR_FAILED_TO_SET_SLEEP_SCREEN: "Impossible de définir l'écran de veille" +STR_SET_SLEEP_SCREEN: "Définir l'écran de veille" +STR_SLEEP_SCREEN_SET: "Écran de veille mis à jour !" +STR_IMAGE_DISPLAY_BW: ">> N/B" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Niveaux de gris" +STR_READER_TOOLS: "Outils" +STR_READER_NAVIGATION: "Navigation" +STR_READER_APPEARANCE: "Apparence" +STR_MENU_SYS_SYSTEM: "Système" +STR_MENU_SYS_NETWORK: "Réseau" +STR_MENU_SYS_TOOLS: "Outils" +STR_MENU_DISP_SLEEP: "Écran de veille" +STR_MENU_DISP_BATTERY: "Symbole batterie" +STR_MENU_DISP_REFRESH: "Actualisation" +STR_MENU_READER_FONT: "Police du lecteur" +STR_MENU_READER_FONT_SETTINGS: "Paramètres police" +STR_MENU_READER_LAYOUT: "Paramètres mise en page" +STR_MENU_READER_TWEAKS: "Ajustements lecteur" +STR_MENU_READER_SPACING: "Espacement" +STR_MENU_KOSYNC_SERVER: "Paramètres serveur" +STR_MENU_KOSYNC_AUTH: "Connexion / Inscription" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 8f69dd64..4525d70f 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -349,6 +349,23 @@ STR_SLEEP_SCREEN_SET: "Standby-Bild aktualisiert!" STR_IMAGE_DISPLAY_BW: ">> S/W" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Grau" +STR_READER_TOOLS: "Werkzeuge" +STR_READER_NAVIGATION: "Navigation" +STR_READER_APPEARANCE: "Darstellung" +STR_MENU_SYS_SYSTEM: "System" +STR_MENU_SYS_NETWORK: "Netzwerk" +STR_MENU_SYS_TOOLS: "Werkzeuge" +STR_MENU_DISP_SLEEP: "Standby-Bildschirm" +STR_MENU_DISP_BATTERY: "Batteriesymbol" +STR_MENU_DISP_REFRESH: "Bildschirmaktualisierung" +STR_MENU_READER_FONT: "Leserschrift" +STR_MENU_READER_FONT_SETTINGS: "Schrifteinstellungen" +STR_MENU_READER_LAYOUT: "Layout-Einstellungen" +STR_MENU_READER_TWEAKS: "Lese-Tweaks" +STR_MENU_READER_SPACING: "Abstand" +STR_MENU_KOSYNC_SERVER: "Server-Einstellungen" +STR_MENU_KOSYNC_AUTH: "Anmelden / Registrieren" + STR_WEATHER: "Wetter" STR_WEATHER_LOCATION: "Ort" STR_WEATHER_NO_LOCATION: "Kein Ort für Wetter festgelegt" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 10c58894..c8c60457 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -306,3 +306,186 @@ STR_AUTO_TURN_PAGES_PER_MIN: "Cambio pagina automatico (pag/min)" STR_SLEEP_IMAGE_PICK_MODE: "Selezione immagine standby" STR_RANDOM: "Casuale" STR_SEQUENTIAL: "Sequenziale" + +STR_IMAGE_DITHERING: "Dithering immagine" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer diffuso" +STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Crea fallback per TOC non valido" +STR_USE_CLOCK: "Mostra orologio" +STR_CLOCK_SETTINGS: "Impostazioni orologio" +STR_CLOCK_SETTINGS_WARNING: "Consuma più batteria; l'orologio può scorrere" +STR_CLOCK: "Orologio" +STR_CLOCK_FORMAT: "Formato orologio" +STR_TIMEZONE: "Fuso orario" +STR_24H: "24 ore" +STR_12H: "12 ore" +STR_TZ_UTC: "UTC (GMT/BST)" +STR_TZ_CET: "Europa centrale (CET/CEST)" +STR_TZ_EET: "Europa orientale (EET/EEST)" +STR_TZ_EST: "USA Est (EST/EDT)" +STR_TZ_CST: "USA Centrale (CST/CDT)" +STR_TZ_MST: "USA Montagna (MST/MDT)" +STR_TZ_PST: "USA Pacifico (PST/PDT)" +STR_TZ_AEST: "Australia Est (AEST/AEDT)" +STR_TZ_NZST: "Nuova Zelanda (NZST/NZDT)" +STR_TZ_MSK: "Russia (MSK)" +STR_TZ_UTC_MINUS3: "Sud America (UTC-3)" +STR_TZ_UTC_PLUS4: "Golfo (UTC+4)" +STR_TZ_IST: "India (UTC+5:30)" +STR_TZ_UTC_PLUS7: "SE Asia (UTC+7)" +STR_TZ_UTC_PLUS8: "Cina/SE Asia (UTC+8)" +STR_TZ_UTC_PLUS9: "Giappone/Corea (UTC+9)" +STR_SYNC_TIME: "Sincronizza orario" +STR_DETECT_TIMEZONE: "Rileva fuso orario" +STR_SYNCING_CLOCK: "Sincronizzazione orologio..." +STR_DETECTING_TIMEZONE: "Rilevamento fuso orario..." +STR_TIME_SYNCED: "Orario sincronizzato" +STR_TIME_SYNC_FAILED: "Sincronizzazione orario fallita" +STR_CLOCK_DRIFT: "Scostamento: %s" +STR_LAST_NTP_SYNC: "Ultima sincronizzazione: %s" +STR_TIMEZONE_DETECTED: "Fuso orario rilevato" +STR_TIMEZONE_DETECT_FAILED: "Impossibile rilevare il fuso orario" +STR_DST_ACTIVE: "Ora legale: attiva" +STR_DST_INACTIVE: "Ora legale: inattiva" +STR_DST_UNKNOWN: "Ora legale: sconosciuta" +STR_REGISTER: "Registrati" +STR_REGISTERING: "Registrazione in corso..." +STR_REGISTER_SUCCESS: "Account creato con successo!" +STR_REGISTER_FAILED: "Registrazione fallita" +STR_USERNAME_TAKEN: "Nome utente già usato" +STR_PAGE_OVERLAY: "Sovrapposizione pagina" +STR_RECENTS: "Recenti" +STR_REMOVE: "Rimuovi" +STR_MAPPING_REMOTE: "Mappatura posizione remota..." +STR_MAPPING_LOCAL: "Calcolo posizione locale..." +STR_WEATHER: "Meteo" +STR_WEATHER_LOCATION: "Località" +STR_WEATHER_NO_LOCATION: "Nessuna località impostata" +STR_WEATHER_FETCH_FAILED: "Impossibile recuperare il meteo" +STR_WEATHER_SETTINGS: "Impostazioni meteo" +STR_WEATHER_SETTINGS_SHORT: "Impostazioni" +STR_WEATHER_REFRESH: "Aggiorna" +STR_WEATHER_FEELS_LIKE: "Percepita" +STR_WEATHER_HUMIDITY: "Umidità" +STR_WEATHER_WIND: "Vento" +STR_WEATHER_PRESSURE: "Pressione" +STR_WEATHER_LAST_UPDATED: "Ultimo aggiornamento" +STR_WEATHER_PRECIP: "Prec." +STR_WEATHER_PRECIP_UNIT: "Unità precipitazioni" +STR_WEATHER_WIND_UNIT: "Unità velocità vento" +STR_WEATHER_TEMP_UNIT: "Unità temperatura" +STR_WEATHER_48H_FORECAST: "Previsioni 48h" +STR_WEATHER_SEARCH_CITY: "Cerca città" +STR_WEATHER_LONGITUDE: "Longitudine" +STR_WEATHER_LATITUDE: "Latitudine" +STR_WEATHER_SEARCH_RESULTS: "Risultati ricerca" +STR_WEATHER_DAY_MON: "Lun" +STR_WEATHER_DAY_TUE: "Mar" +STR_WEATHER_DAY_WED: "Mer" +STR_WEATHER_DAY_THU: "Gio" +STR_WEATHER_DAY_FRI: "Ven" +STR_WEATHER_DAY_SAT: "Sab" +STR_WEATHER_DAY_SUN: "Dom" +STR_WEATHER_MONTH_JAN: "Gen" +STR_WEATHER_MONTH_FEB: "Feb" +STR_WEATHER_MONTH_MAR: "Mar" +STR_WEATHER_MONTH_APR: "Apr" +STR_WEATHER_MONTH_MAY: "Mag" +STR_WEATHER_MONTH_JUN: "Giu" +STR_WEATHER_MONTH_JUL: "Lug" +STR_WEATHER_MONTH_AUG: "Ago" +STR_WEATHER_MONTH_SEP: "Set" +STR_WEATHER_MONTH_OCT: "Ott" +STR_WEATHER_MONTH_NOV: "Nov" +STR_WEATHER_MONTH_DEC: "Dic" +STR_WEATHER_MOON_NEW: "Luna nuova" +STR_WEATHER_MOON_WAXING_CRESCENT: "Falce crescente" +STR_WEATHER_MOON_FIRST_QUARTER: "Primo quarto" +STR_WEATHER_MOON_WAXING_GIBBOUS: "Gibbosa crescente" +STR_WEATHER_MOON_FULL: "Luna piena" +STR_WEATHER_MOON_WANING_GIBBOUS: "Gibbosa calante" +STR_WEATHER_MOON_LAST_QUARTER: "Ultimo quarto" +STR_WEATHER_MOON_WANING_CRESCENT: "Falce calante" +STR_WEATHER_DESC_CLEAR_SKY: "Cielo sereno" +STR_WEATHER_DESC_MAINLY_CLEAR: "Principalmente sereno" +STR_WEATHER_DESC_PARTLY_CLOUDY: "Parzialmente nuvoloso" +STR_WEATHER_DESC_OVERCAST: "Coperto" +STR_WEATHER_DESC_FOG: "Nebbia" +STR_WEATHER_DESC_RIME_FOG: "Nebbia gelata" +STR_WEATHER_DESC_LIGHT_DRIZZLE: "Pioggerella leggera" +STR_WEATHER_DESC_DRIZZLE: "Pioggerella" +STR_WEATHER_DESC_DENSE_DRIZZLE: "Pioggerella fitta" +STR_WEATHER_DESC_FREEZING_DRIZZLE: "Pioggerella gelata" +STR_WEATHER_DESC_DENSE_FREEZING_DRIZZLE: "Pioggerella gelata fitta" +STR_WEATHER_DESC_SLIGHT_RAIN: "Pioggia leggera" +STR_WEATHER_DESC_MODERATE_RAIN: "Pioggia moderata" +STR_WEATHER_DESC_HEAVY_RAIN: "Pioggia forte" +STR_WEATHER_DESC_FREEZING_RAIN: "Pioggia gelata" +STR_WEATHER_DESC_HEAVY_FREEZING_RAIN: "Pioggia gelata intensa" +STR_WEATHER_DESC_SLIGHT_SNOW: "Nevicata leggera" +STR_WEATHER_DESC_MODERATE_SNOW: "Nevicata moderata" +STR_WEATHER_DESC_HEAVY_SNOW: "Nevicata intensa" +STR_WEATHER_DESC_SNOW_GRAINS: "Fiocchi di neve" +STR_WEATHER_DESC_SLIGHT_SHOWERS: "Rovesci leggeri" +STR_WEATHER_DESC_MODERATE_SHOWERS: "Rovesci moderati" +STR_WEATHER_DESC_VIOLENT_SHOWERS: "Rovesci violenti" +STR_WEATHER_DESC_SNOW_SHOWERS: "Rovesci di neve" +STR_WEATHER_DESC_HEAVY_SNOW_SHOWERS: "Rovesci di neve intensi" +STR_WEATHER_DESC_THUNDERSTORM: "Temporale" +STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Temporale con grandine" +STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Temporale con grandine forte" +STR_WEATHER_DESC_UNKNOWN: "Sconosciuto" +STR_INFO: "Info" +STR_AUTHOR: "Autore" +STR_SERIES: "Serie" +STR_FILE_SIZE: "Dimensione" +STR_SLEEP_COVER_OVERLAY: "Overlay info standby" +STR_OVERLAY_WHITE: "Bianco" +STR_OVERLAY_GRAY: "Grigio" +STR_OVERLAY_BLACK: "Nero" +STR_OVERLAY_OFF: "Disattivato" +STR_OVERLAY_READING_PROGRESS: "Avanzamento lettura: Pagina %lu/%u - %.0f%%" +STR_OVERLAY_READING_PROGRESS_NO_TOTAL: "Avanzamento lettura: Pagina %lu" +STR_OVERLAY_CHAPTER_PAGE_SUFFIX: " - Pagina %d/%d - %.0f%%" +STR_SYSTEM_INFO: "Informazioni di sistema" +STR_LOAD_XTC_FAILED: "Impossibile caricare file XTC" +STR_LOAD_EPUB_FAILED: "Impossibile caricare file EPUB" +STR_FW_VERSION: "Versione FW" +STR_CHIP: "Chip" +STR_CPU: "CPU" +STR_MHZ: "MHz" +STR_FREE_RAM: "RAM libera" +STR_MIN_FREE: "Min. libera" +STR_MAX_BLOCK: "Blocco max" +STR_FLASH_USED: "Flash usato" +STR_UPTIME: "Tempo di attività" +STR_CHARGING: "In carica" +STR_GATHERING_DATA: "Raccolta dati..." +STR_READING: "Lettura..." +STR_SD_UPDATE_PROMPT: "Premere Aggiorna" +STR_UNSUPPORTED_IMAGE_FORMAT: "Formato immagine non supportato" +STR_COULD_NOT_RENDER_IMAGE: "Impossibile visualizzare l'immagine" +STR_FAILED_TO_SET_SLEEP_SCREEN: "Impossibile impostare schermo standby" +STR_SET_SLEEP_SCREEN: "Imposta standby" +STR_SLEEP_SCREEN_SET: "Schermo standby aggiornato!" +STR_IMAGE_DISPLAY_BW: ">> B/N" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gradazioni di grigio" +STR_WEATHER_MOON_INFO: "Luna" +STR_WEATHER_SUN_INFO: "Sole" +STR_READER_TOOLS: "Strumenti" +STR_READER_NAVIGATION: "Navigazione" +STR_READER_APPEARANCE: "Aspetto" +STR_MENU_SYS_SYSTEM: "Sistema" +STR_MENU_SYS_NETWORK: "Rete" +STR_MENU_SYS_TOOLS: "Strumenti" +STR_MENU_DISP_SLEEP: "Schermo standby" +STR_MENU_DISP_BATTERY: "Icona batteria" +STR_MENU_DISP_REFRESH: "Aggiorna schermo" +STR_MENU_READER_FONT: "Carattere lettore" +STR_MENU_READER_FONT_SETTINGS: "Impostazioni carattere" +STR_MENU_READER_LAYOUT: "Impostazioni layout" +STR_MENU_READER_TWEAKS: "Personalizzazioni lettore" +STR_MENU_READER_SPACING: "Spaziatura" +STR_MENU_KOSYNC_SERVER: "Impostazioni server" +STR_MENU_KOSYNC_AUTH: "Accesso / Registrazione" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index e4054460..6907d14b 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -458,3 +458,34 @@ STR_WEATHER_DESC_THUNDERSTORM: "Гроза" STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Гроза с градом" STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Гроза с сильным градом" STR_WEATHER_DESC_UNKNOWN: "Неизвестно" + +STR_TEXT_DARKNESS: "Насыщенность текста" +STR_EXTRA_DARK: "Очень тёмный" +STR_MAX_DARK: "Максимум" +STR_IMAGE_DITHERING: "Дизеринг изображения" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Диффузный Bayer" +STR_UNSUPPORTED_IMAGE_FORMAT: "Формат изображения не поддерживается" +STR_COULD_NOT_RENDER_IMAGE: "Не удалось отобразить изображение" +STR_FAILED_TO_SET_SLEEP_SCREEN: "Не удалось установить экран сна" +STR_SET_SLEEP_SCREEN: "Установить экран сна" +STR_SLEEP_SCREEN_SET: "Экран сна обновлён!" +STR_IMAGE_DISPLAY_BW: ">> Ч/Б" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Оттенки серого" +STR_READER_TOOLS: "Инструменты" +STR_READER_NAVIGATION: "Навигация" +STR_READER_APPEARANCE: "Внешний вид" +STR_MENU_SYS_SYSTEM: "Система" +STR_MENU_SYS_NETWORK: "Сеть" +STR_MENU_SYS_TOOLS: "Инструменты" +STR_MENU_DISP_SLEEP: "Экран сна" +STR_MENU_DISP_BATTERY: "Значок батареи" +STR_MENU_DISP_REFRESH: "Обновление экрана" +STR_MENU_READER_FONT: "Шрифт чтения" +STR_MENU_READER_FONT_SETTINGS: "Настройки шрифта" +STR_MENU_READER_LAYOUT: "Макет" +STR_MENU_READER_TWEAKS: "Параметры чтения" +STR_MENU_READER_SPACING: "Интервал" +STR_MENU_KOSYNC_SERVER: "Настройки сервера" +STR_MENU_KOSYNC_AUTH: "Вход / Регистрация" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index cb03c08b..af89d7eb 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -455,3 +455,37 @@ STR_WEATHER_DESC_THUNDERSTORM: "Tormenta eléctrica" STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Tormenta con granizo" STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Tormenta con granizo fuerte" STR_WEATHER_DESC_UNKNOWN: "Desconocido" + +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "Sin señal" +STR_SIGNAL_QUALITY_POOR: "Mala" +STR_SIGNAL_QUALITY_WEAK: "Débil" +STR_SIGNAL_QUALITY_GOOD: "Buena" +STR_SIGNAL_QUALITY_EXCELLENT: "Excelente" +STR_IMAGE_DITHERING: "Trama de imagen" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer difuso" +STR_UNSUPPORTED_IMAGE_FORMAT: "Formato de imagen no compatible" +STR_COULD_NOT_RENDER_IMAGE: "No se pudo mostrar la imagen" +STR_FAILED_TO_SET_SLEEP_SCREEN: "No se pudo configurar la pantalla de suspensión" +STR_SET_SLEEP_SCREEN: "Configurar suspensión" +STR_SLEEP_SCREEN_SET: "Pantalla de suspensión actualizada!" +STR_IMAGE_DISPLAY_BW: ">> N/B" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Escala de grises" +STR_READER_TOOLS: "Herramientas" +STR_READER_NAVIGATION: "Navegación" +STR_READER_APPEARANCE: "Apariencia" +STR_MENU_SYS_SYSTEM: "Sistema" +STR_MENU_SYS_NETWORK: "Red" +STR_MENU_SYS_TOOLS: "Herramientas" +STR_MENU_DISP_SLEEP: "Pantalla de suspensión" +STR_MENU_DISP_BATTERY: "Icono de batería" +STR_MENU_DISP_REFRESH: "Refrescar pantalla" +STR_MENU_READER_FONT: "Fuente lector" +STR_MENU_READER_FONT_SETTINGS: "Ajustes de fuente" +STR_MENU_READER_LAYOUT: "Ajustes de diseño" +STR_MENU_READER_TWEAKS: "Ajustes de lectura" +STR_MENU_READER_SPACING: "Espaciado" +STR_MENU_KOSYNC_SERVER: "Ajustes del servidor" +STR_MENU_KOSYNC_AUTH: "Iniciar sesión / Registrarse" diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index 520be52f..24a0575a 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -280,8 +280,62 @@ def find_used_string_keys( text = f.read_text(encoding="utf-8", errors="replace") except OSError: continue - for m in pattern.finditer(text): - used.add(m.group(0)) + in_block_comment = False + for line in text.splitlines(): + quote_char = None + escaped = False + processed_line = [] + idx = 0 + while idx < len(line): + ch = line[idx] + if escaped: + escaped = False + if not in_block_comment: + processed_line.append(ch) + idx += 1 + continue + + if quote_char is None: + if in_block_comment: + if ( + ch == "*" + and idx + 1 < len(line) + and line[idx + 1] == "/" + ): + in_block_comment = False + idx += 2 + continue + idx += 1 + continue + if ch in ['"', "'"]: + quote_char = ch + processed_line.append(ch) + idx += 1 + continue + if ch == "/" and idx + 1 < len(line): + if line[idx + 1] == "/": + break + if line[idx + 1] == "*": + in_block_comment = True + idx += 2 + continue + processed_line.append(ch) + idx += 1 + continue + + if ch == "\\": + escaped = True + processed_line.append(ch) + idx += 1 + continue + if ch == quote_char: + quote_char = None + processed_line.append(ch) + idx += 1 + + line = "".join(processed_line) + for m in pattern.finditer(line): + used.add(m.group(0)) return used @@ -389,11 +443,7 @@ def format_cpp_string_literal(segments: List[str], indent: str = " ") -> List last_space = idx # Handle escapes to step correctly - if current[idx] == "\\": - idx += 2 - else: - idx += 1 - + idx += 2 if current[idx] == "\\" else 1 # If we found a space, split after it if last_space != -1: # Include the space in the first line @@ -455,17 +505,19 @@ def generate_keys_header( ] for code in languages: - lines.append(f"extern const char STRINGS_{code}_DATA[];") - lines.append(f"extern const uint16_t OFFSETS_{code}[];") - + lines.extend( + ( + f"extern const char STRINGS_{code}_DATA[];", + f"extern const uint16_t OFFSETS_{code}[];", + ) + ) lines.append("} // namespace i18n_strings") lines.append("") # Language enum lines.append("// Language enum") lines.append("enum class Language : uint8_t {") - for i, lang in enumerate(languages): - lines.append(f" {lang} = {i},") + lines.extend(f" {lang} = {i}," for i, lang in enumerate(languages)) lines.append(" _COUNT") lines.append("};") lines.append("") @@ -505,9 +557,11 @@ def generate_keys_header( lines.append("inline LangStrings getLanguageStrings(Language lang) {") lines.append(" switch (lang) {") for code in languages: - lines.append(f" case Language::{code}:") - lines.append( - f" return {{i18n_strings::STRINGS_{code}_DATA, i18n_strings::OFFSETS_{code}}};" + lines.extend( + ( + f" case Language::{code}:", + f" return {{i18n_strings::STRINGS_{code}_DATA, i18n_strings::OFFSETS_{code}}};", + ) ) first_code = languages[0] lines.append(" default:") @@ -535,8 +589,10 @@ def generate_keys_header( ) sorted_indices = [english_idx] + rest lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)") - for rank, idx in enumerate(sorted_indices): - lines.append(f"// {rank:>2}: {languages[idx]:<4} {language_names[idx]}") + lines.extend( + f"// {rank:>2}: {languages[idx]:<4} {language_names[idx]}" + for rank, idx in enumerate(sorted_indices) + ) lines.append( "constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {" f"{', '.join(str(i) for i in sorted_indices)}" @@ -570,11 +626,13 @@ def generate_strings_header( ] for code in languages: - lines.append(f"extern const char STRINGS_{code}_DATA[];") - lines.append(f"extern const uint16_t OFFSETS_{code}[];") - - lines.append("") - lines.append("} // namespace i18n_strings") + lines.extend( + ( + f"extern const char STRINGS_{code}_DATA[];", + f"extern const uint16_t OFFSETS_{code}[];", + ) + ) + lines.extend(("", "} // namespace i18n_strings")) _write_file(output_path, lines, verbose) @@ -594,11 +652,10 @@ def generate_strings_cpp( "", "#include ", "", + "// Language codes", + "const char* const LANGUAGE_CODES[] = {", ] - # LANGUAGE_NAMES array - lines.append("// Language codes") - lines.append("const char* const LANGUAGE_CODES[] = {") for code in languages: _append_string_entry(lines, code) lines.append("};") @@ -662,13 +719,13 @@ def generate_strings_cpp( # Compile-time size checks lines.append("// Compile-time validation of array sizes") for code in languages: - lines.append( - f"static_assert(sizeof(i18n_strings::OFFSETS_{code}) " - f"/ sizeof(i18n_strings::OFFSETS_{code}[0]) ==" + lines.extend( + ( + f"static_assert(sizeof(i18n_strings::OFFSETS_{code}) / sizeof(i18n_strings::OFFSETS_{code}[0]) ==", + " static_cast(StrId::_COUNT),", + f' "OFFSETS_{code} size mismatch");', + ) ) - lines.append(" static_cast(StrId::_COUNT),") - lines.append(f' "OFFSETS_{code} size mismatch");') - _write_file(output_path, lines, verbose) diff --git a/scripts/generate_test_epub.py b/scripts/generate_test_epub.py index d8a7f4db..99868cf6 100644 --- a/scripts/generate_test_epub.py +++ b/scripts/generate_test_epub.py @@ -27,17 +27,17 @@ def get_font(size=20): import sys candidates = [] - if sys.platform == "win32": + if sys.platform == "darwin": + candidates = [ + "/System/Library/Fonts/Helvetica.ttc", + "/Library/Fonts/Arial.ttf", + ] + elif sys.platform == "win32": windir = os.environ.get("WINDIR", "C:\\Windows") candidates = [ os.path.join(windir, "Fonts", "arial.ttf"), os.path.join(windir, "Fonts", "calibri.ttf"), ] - elif sys.platform == "darwin": - candidates = [ - "/System/Library/Fonts/Helvetica.ttc", - "/Library/Fonts/Arial.ttf", - ] else: candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", diff --git a/src/SettingsList.h b/src/SettingsList.h index 45b85dba..f10ac8af 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -6,166 +6,202 @@ #include "CrossPointSettings.h" #include "KOReaderCredentialStore.h" -#include "activities/settings/SettingsActivity.h" +#include "activities/settings/SettingInfo.h" // Shared settings list used by both the device settings UI and the web settings API. -// Each entry has a key (for JSON API) and category (for grouping). -// ACTION-type entries and entries without a key are device-only. -inline const std::vector& getSettingsList() { - static const std::vector list = { - // --- Display --- - SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, - {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, - StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, - "sleepScreen", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, - {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, - {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, - "sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum( - StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, - {StrId::STR_OVERLAY_OFF, StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK}, - "sleepCoverOverlay", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_IMAGE_PICK_MODE, &CrossPointSettings::sleepImagePickMode, - {StrId::STR_RANDOM, StrId::STR_SEQUENTIAL}, "sleepImagePickMode", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, - {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", +// +// Fields that drive UI behaviour: +// category — which tab the setting appears under (STR_CAT_DISPLAY, STR_CAT_READER, …). +// Entries with STR_NONE_OPT or web-only categories are skipped by the device UI. +// subcategory — optional section heading within a tab. Items remain in their defined order; +// no reordering or grouping occurs. When an item's subcategory differs from the +// previous item's, SettingsActivity::onEnter() automatically inserts a separator +// row before it. Add with .withSubcategory(StrId::STR_MY_SECTION). +// Items without a subcategory (STR_NONE_OPT) never trigger a separator. +// submenu — optional submenu grouping. Items with the same submenu StrId are hidden from +// the main list and collected behind a single placeholder entry. Selecting that +// entry launches SettingsSubmenuActivity with those items. withSubcategory() +// works inside a submenu exactly as it does in the parent tab. +// Add with .withSubmenu(StrId::STR_MY_SUBMENU). +// key — JSON property name used by the web settings API (nullptr = device-only). +// +// ACTION-type entries and entries without a key are device-only and are added directly +// in SettingsActivity::onEnter(), not here. +// +// Implementation note: the list is a namespace-level static (not a function-local static) so +// it is initialised during the global-static phase before setup() runs. A function-local +// static would trigger __cxa_guard_acquire on the first call, which creates a FreeRTOS mutex +// deep inside the heap allocator chain — enough stack to overflow the 8 KB loop task stack +// when called from inside SETTINGS.loadFromFile() at boot time. +namespace SettingsListDetail { +inline const std::vector list = { + // --- Display --- + SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, + {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, + "sleepTimeout", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, + {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, + StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, + "sleepScreen", StrId::STR_CAT_DISPLAY) + .withSubcategory(StrId::STR_MENU_DISP_SLEEP), + SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, + {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, + {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, "sleepScreenCoverFilter", + StrId::STR_CAT_DISPLAY), + SettingInfo::Enum( + StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, + {StrId::STR_OVERLAY_OFF, StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK}, + "sleepCoverOverlay", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_IMAGE_PICK_MODE, &CrossPointSettings::sleepImagePickMode, + {StrId::STR_RANDOM, StrId::STR_SEQUENTIAL}, "sleepImagePickMode", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, + {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", + StrId::STR_CAT_DISPLAY) + .withSubcategory(StrId::STR_MENU_DISP_BATTERY), + SettingInfo::Enum( + StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency, + {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, + "refreshFrequency", StrId::STR_CAT_DISPLAY) + .withSubcategory(StrId::STR_MENU_DISP_REFRESH), + SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum( - StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency, - {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, - "refreshFrequency", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, - {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme", - StrId::STR_CAT_DISPLAY), - SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", - StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, + {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme", + StrId::STR_CAT_DISPLAY), - // --- Reader --- - SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily, - {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily", + // --- Reader --- + // General reader settings + SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, + {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, + "orientation", StrId::STR_CAT_READER), + // Font + SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily, + {StrId::STR_BOOKERLY, StrId::STR_NOTO_SANS, StrId::STR_OPEN_DYSLEXIC}, "fontFamily", + StrId::STR_CAT_READER) + .withSubcategory(StrId::STR_MENU_READER_FONT), + SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize, + {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize", + StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_FONT_SETTINGS), + SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", + StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_FONT_SETTINGS), + SettingInfo::Enum(StrId::STR_TEXT_DARKNESS, &CrossPointSettings::textDarkness, + {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, "textDarkness", + StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_FONT_SETTINGS), + + // Formatting settings + SettingInfo::Enum( + StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment, + {StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT, StrId::STR_BOOK_S_STYLE}, + "paragraphAlignment", StrId::STR_CAT_READER) + .withSubcategory(StrId::STR_MENU_READER_LAYOUT), + SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle", StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_FONT_SIZE, &CrossPointSettings::fontSize, - {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE}, "fontSize", + SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing, - {StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER), - SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment, - {StrId::STR_JUSTIFY, StrId::STR_ALIGN_LEFT, StrId::STR_CENTER, StrId::STR_ALIGN_RIGHT, - StrId::STR_BOOK_S_STYLE}, - "paragraphAlignment", StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle", - StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation, - {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, - "orientation", StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing", - StrId::STR_CAT_READER), - SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", - StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_TEXT_DARKNESS, &CrossPointSettings::textDarkness, - {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, - "textDarkness", StrId::STR_CAT_READER), - SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, - {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, - "imageRendering", StrId::STR_CAT_READER), -#ifdef ENABLE_IMAGE_DITHERING_EXTENSION - SettingInfo::Enum( - StrId::STR_IMAGE_DITHERING, &CrossPointSettings::imageDithering, - {StrId::STR_IMAGE_DITHER_BAYER, StrId::STR_IMAGE_DITHER_ATKINSON, StrId::STR_IMAGE_DITHER_DIFFUSED_BAYER}, - "imageDithering", StrId::STR_CAT_READER), -#endif - SettingInfo::Toggle(StrId::STR_CREATE_FALLBACK_FOR_INVALID_TOC, &CrossPointSettings::syntheticTocFallback, - "syntheticTocFallback", StrId::STR_CAT_READER), - // --- Controls --- - SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, - {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), - SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", - StrId::STR_CAT_CONTROLS), - SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, - {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN}, "shortPwrBtn", + SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, + {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, + "imageRendering", StrId::STR_CAT_READER), + SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin", + StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_SPACING), + SettingInfo::Enum(StrId::STR_LINE_SPACING, &CrossPointSettings::lineSpacing, + {StrId::STR_TIGHT, StrId::STR_NORMAL, StrId::STR_WIDE}, "lineSpacing", StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_SPACING), + SettingInfo::Toggle(StrId::STR_EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing, "extraParagraphSpacing", + StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_SPACING), + // Generic reader settings + SettingInfo::Toggle(StrId::STR_CREATE_FALLBACK_FOR_INVALID_TOC, &CrossPointSettings::syntheticTocFallback, + "syntheticTocFallback", StrId::STR_CAT_READER) + .withSubcategory(StrId::STR_MENU_READER_TWEAKS), + + // --- Controls --- + SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout, + {StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS), + SettingInfo::Toggle(StrId::STR_LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip, "longPressChapterSkip", StrId::STR_CAT_CONTROLS), + SettingInfo::Enum(StrId::STR_SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, + {StrId::STR_IGNORE, StrId::STR_SLEEP, StrId::STR_PAGE_TURN}, "shortPwrBtn", + StrId::STR_CAT_CONTROLS), - // --- System --- - SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, - {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, - "sleepTimeout", StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", - StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_SHOW_FILE_EXTENSIONS, &CrossPointSettings::showFileExtensions, - "showFileExtensions", StrId::STR_CAT_SYSTEM), - SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H}, - "clockFormat12h", StrId::STR_CAT_SYSTEM), - SettingInfo::Enum(StrId::STR_TIMEZONE, &CrossPointSettings::timeZone, - {StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, - StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, - StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, - StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, - "timeZone", StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM), + // --- System --- + SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", + StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_SHOW_FILE_EXTENSIONS, &CrossPointSettings::showFileExtensions, "showFileExtensions", + StrId::STR_CAT_SYSTEM), + SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H}, + "clockFormat12h", StrId::STR_CAT_SYSTEM), + SettingInfo::Enum(StrId::STR_TIMEZONE, &CrossPointSettings::timeZone, + {StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, + StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, + StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, + StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, + "timeZone", StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM), - // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- - SettingInfo::DynamicString( - StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, - [](const std::string& v) { - KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); - KOREADER_STORE.saveToFile(); - }, - "koUsername", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicString( - StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, - [](const std::string& v) { - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); - KOREADER_STORE.saveToFile(); - }, - "koPassword", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicString( - StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, - [](const std::string& v) { - KOREADER_STORE.setServerUrl(v); - KOREADER_STORE.saveToFile(); - }, - "koServerUrl", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicEnum( - StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, - [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, - [](uint8_t v) { - KOREADER_STORE.setMatchMethod(static_cast(v)); - KOREADER_STORE.saveToFile(); - }, - "koMatchMethod", StrId::STR_KOREADER_SYNC), + // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- + SettingInfo::DynamicString( + StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, + [](const std::string& v) { + KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); + KOREADER_STORE.saveToFile(); + }, + "koUsername", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicString( + StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, + [](const std::string& v) { + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); + KOREADER_STORE.saveToFile(); + }, + "koPassword", StrId::STR_KOREADER_SYNC) + .withObfuscated(), + SettingInfo::DynamicString( + StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, + [](const std::string& v) { + KOREADER_STORE.setServerUrl(v); + KOREADER_STORE.saveToFile(); + }, + "koServerUrl", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicEnum( + StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, + [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, + [](uint8_t v) { + KOREADER_STORE.setMatchMethod(static_cast(v)); + KOREADER_STORE.saveToFile(); + }, + "koMatchMethod", StrId::STR_KOREADER_SYNC), - // --- OPDS Browser (web-only, uses CrossPointSettings char arrays) --- - SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), - "opdsServerUrl", StrId::STR_OPDS_BROWSER), - SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername", - StrId::STR_OPDS_BROWSER), - SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword", - StrId::STR_OPDS_BROWSER) - .withObfuscated(), - // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- - SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, - "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, - "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, - {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", + // --- OPDS Browser (web-only, uses CrossPointSettings char arrays) --- + SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), + "opdsServerUrl", StrId::STR_OPDS_BROWSER), + SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername", + StrId::STR_OPDS_BROWSER), + SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword", + StrId::STR_OPDS_BROWSER) + .withObfuscated(), + // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- + SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, + "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, + "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, + {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", + StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, + {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, + "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, + {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", + StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, - {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, - "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, - {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", + SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", - StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", - StrId::STR_CUSTOMISE_STATUS_BAR), - }; - return list; -} +}; +} // namespace SettingsListDetail + +inline const std::vector& getSettingsList() { return SettingsListDetail::list; } diff --git a/src/activities/MenuListActivity.cpp b/src/activities/MenuListActivity.cpp new file mode 100644 index 00000000..4fd19076 --- /dev/null +++ b/src/activities/MenuListActivity.cpp @@ -0,0 +1,68 @@ +#include "MenuListActivity.h" + +#include + +#include "MappedInputManager.h" +#include "components/UITheme.h" + +void MenuListActivity::initMenuList() { + const int count = static_cast(menuItems.size()); + const auto pred = UITheme::makeSelectablePredicate(count, [this](int i) { return menuItems[i].getTitle(); }); + buttonNavigator.setSelectablePredicate(pred, count); + if (count > 0 && !pred(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } +} + +void MenuListActivity::onEnter() { + Activity::onEnter(); + initMenuList(); + requestUpdate(); +} + +void MenuListActivity::handleNavigation() { + buttonNavigator.onNext([this] { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onPrevious([this] { + selectedIndex = buttonNavigator.previousIndex(selectedIndex); + requestUpdate(); + }); +} + +void MenuListActivity::toggleCurrentItem() { + if (selectedIndex < 0 || selectedIndex >= static_cast(menuItems.size())) return; + const auto& item = menuItems[selectedIndex]; + if (item.isSeparator) return; + + if (item.type == SettingType::ACTION) { + onActionSelected(selectedIndex); + return; + } + + menuItems[selectedIndex].toggleValue(); + onSettingToggled(selectedIndex); + requestUpdate(); +} + +std::string MenuListActivity::getItemValueString(int index) const { return menuItems[index].getDisplayValue(); } + +void MenuListActivity::drawMenuList(const Rect& rect) { + const int count = static_cast(menuItems.size()); + GUI.drawList( + renderer, rect, count, selectedIndex, [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, + [this](int index) { return getItemValueString(index); }, true); +} + +void MenuListActivity::loop() { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + onBackPressed(); + return; + } + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + toggleCurrentItem(); + return; + } + handleNavigation(); +} diff --git a/src/activities/MenuListActivity.h b/src/activities/MenuListActivity.h new file mode 100644 index 00000000..f8a8fdda --- /dev/null +++ b/src/activities/MenuListActivity.h @@ -0,0 +1,107 @@ +#pragma once +#include + +#include "Activity.h" +#include "settings/SettingInfo.h" +#include "util/ButtonNavigator.h" + +struct Rect; + +// Base class for activities that display a scrollable list of SettingInfo items. +// Provides common navigation, toggle/cycle logic, and drawList rendering. +// +// Subclasses populate `menuItems` (typically in the constructor or onEnter()), +// then rely on the default loop()/onEnter() or override selectively. +// +// === Minimal example === +// +// class MyMenuActivity final : public MenuListActivity { +// public: +// explicit MyMenuActivity(GfxRenderer& r, MappedInputManager& m) +// : MenuListActivity("MyMenu", r, m) { +// // Separator header +// menuItems.push_back(SettingInfo::Separator(StrId::STR_MY_SECTION)); +// +// // Toggle bound to a CrossPointSettings field +// menuItems.push_back(SettingInfo::Toggle(StrId::STR_MY_TOGGLE, &CrossPointSettings::myFlag)); +// +// // Enum bound to a CrossPointSettings field +// menuItems.push_back(SettingInfo::Enum(StrId::STR_MY_ENUM, &CrossPointSettings::myEnum, +// {StrId::STR_OPTION_A, StrId::STR_OPTION_B, StrId::STR_OPTION_C})); +// +// // DynamicEnum with local state (getter/setter lambdas) +// menuItems.push_back(SettingInfo::DynamicEnum(StrId::STR_MY_DYNAMIC, +// {StrId::STR_LOW, StrId::STR_HIGH}, +// [this]() -> uint8_t { return localValue; }, +// [this](uint8_t v) { localValue = v; })); +// +// // Action item (handled in onActionSelected) +// menuItems.push_back(SettingInfo::Action(StrId::STR_DO_SOMETHING, SettingAction::None)); +// } +// +// // render(): draw header/footer around the list +// void render(RenderLock&&) override { +// renderer.clearScreen(); +// const Rect r = UITheme::getContentRect(renderer, true, false); +// drawMenuList(r); // <-- draws the item list +// GUI.drawButtonHints(renderer, ...); +// renderer.displayBuffer(); +// } +// +// private: +// uint8_t localValue = 0; +// +// // Called when an ACTION item is confirmed +// void onActionSelected(int index) override { +// if (menuItems[index].nameId == StrId::STR_DO_SOMETHING) { /* ... */ } +// } +// +// // Called after a TOGGLE/ENUM/VALUE is cycled +// void onSettingToggled(int) override { SETTINGS.saveToFile(); } +// }; +// +// The base class provides: +// onEnter() — wires up the selectable-predicate (skips separators) and requestUpdate(). +// loop() — handles Back (→ onBackPressed), Confirm (→ toggleCurrentItem), and nav. +// drawMenuList(rect) — calls GUI.drawList with SettingInfo-based title/value lambdas. +// getItemValueString(i) — override for custom per-item value display. +// onBackPressed() — override to customise Back behaviour (default: finish()). +// +class MenuListActivity : public Activity { + protected: + std::vector menuItems; + int selectedIndex = 0; + ButtonNavigator buttonNavigator; + + // Call after building/rebuilding menuItems to wire up the selectable predicate. + void initMenuList(); + + // Handle up/down navigation via buttonNavigator. Call from loop() if overriding. + void handleNavigation(); + + // Toggle/cycle the currently selected item. For ACTION items, delegates to onActionSelected(). + void toggleCurrentItem(); + + // Draw the list into the given rect using GUI.drawList(). + void drawMenuList(const Rect& rect); + + // Override to provide custom value display for specific items. + // Default returns SettingInfo::getDisplayValue(). + [[nodiscard]] virtual std::string getItemValueString(int index) const; + + // Called when an ACTION-type item is confirmed. Override to handle actions. + virtual void onActionSelected(int index) {} + + // Called when Back is pressed. Default calls finish(). + virtual void onBackPressed() { finish(); } + + // Called after a TOGGLE/ENUM/VALUE item has been toggled. + // Override to persist changes or trigger side-effects. + virtual void onSettingToggled(int /*index*/) {} + + public: + using Activity::Activity; + + void onEnter() override; + void loop() override; +}; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 313f4890..8e1c54cf 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -14,151 +14,174 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, const uint8_t initialTextDarkness) - : Activity("EpubReaderMenu", renderer, mappedInput), - menuItems(buildMenuItems(hasFootnotes)), - title(title), + : MenuListActivity("EpubReaderMenu", renderer, mappedInput), pendingOrientation(currentOrientation), pendingEmbeddedStyleOverride(initialEmbeddedStyleOverride), pendingImageRenderingOverride(initialImageRenderingOverride), pendingTextDarkness(initialTextDarkness), + title(title), currentPage(currentPage), totalPages(totalPages), - bookProgressPercent(bookProgressPercent) {} + bookProgressPercent(bookProgressPercent) { + buildMenuItems(hasFootnotes); +} -std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { - std::vector items; - items.reserve(18); - // Navigation - items.push_back({MenuAction::NONE, StrId::STR_READER_NAVIGATION, true}); - items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); - items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); +void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { + menuItems.reserve(18); + + // --- Navigation --- + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_NAVIGATION)); + menuItems.push_back(SettingInfo::Action(StrId::STR_SELECT_CHAPTER, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_GO_TO_PERCENT, SettingAction::None)); if (hasFootnotes) { - items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); + menuItems.push_back(SettingInfo::Action(StrId::STR_FOOTNOTES, SettingAction::None)); } - items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); + // Auto page turn: ACTION type with custom cycling in onActionSelected + menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None)); - // Appearance - items.push_back({MenuAction::NONE, StrId::STR_READER_APPEARANCE, true}); - items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE}); - items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES}); - items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS}); - items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); + // --- Appearance --- + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE)); - // Synchronisation (only if credentials are set, to avoid confusion) + // Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2 + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_EMBEDDED_STYLE, {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF}, + [this]() -> uint8_t { + if (pendingEmbeddedStyleOverride < 0) return 0; + if (pendingEmbeddedStyleOverride > 0) return 1; + return 2; + }, + [this](uint8_t v) { + if (v == 0) + pendingEmbeddedStyleOverride = -1; + else if (v == 1) + pendingEmbeddedStyleOverride = 1; + else + pendingEmbeddedStyleOverride = 0; + })); + + // Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2) + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_IMAGES, + {StrId::STR_DEFAULT_VALUE, StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, + [this]() -> uint8_t { return (pendingImageRenderingOverride < 0) ? 0 : (pendingImageRenderingOverride + 1); }, + [this](uint8_t v) { pendingImageRenderingOverride = (v == 0) ? -1 : static_cast(v - 1); })); + + // Text darkness: straightforward 0-3 cycle + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_TEXT_DARKNESS, {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, + [this]() -> uint8_t { return pendingTextDarkness; }, [this](uint8_t v) { pendingTextDarkness = v; })); + + // Orientation: straightforward 0-3 cycle + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_ORIENTATION, + {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, + [this]() -> uint8_t { return pendingOrientation; }, [this](uint8_t v) { pendingOrientation = v; })); + + // --- Synchronisation (only if credentials are set) --- if (KOREADER_STORE.hasCredentials()) { - items.push_back({MenuAction::NONE, StrId::STR_KOREADER_SYNC, true}); - items.push_back({MenuAction::PULL_REMOTE, StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES}); - items.push_back({MenuAction::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE}); + menuItems.push_back(SettingInfo::Separator(StrId::STR_KOREADER_SYNC)); + menuItems.push_back(SettingInfo::Action(StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE, SettingAction::None)); } - // Tools - items.push_back({MenuAction::NONE, StrId::STR_READER_TOOLS, true}); - items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); - items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); - items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); - items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); - return items; + // --- Tools --- + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_TOOLS)); + menuItems.push_back(SettingInfo::Action(StrId::STR_SCREENSHOT_BUTTON, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_DISPLAY_QR, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_DELETE_CACHE, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_GO_HOME_BUTTON, SettingAction::None)); } -std::function EpubReaderMenuActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; -} - -void EpubReaderMenuActivity::onEnter() { - Activity::onEnter(); - const auto selectablePredicate = buildSelectablePredicate(); - buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast(menuItems.size())); - if (!selectablePredicate(selectedIndex)) { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); +EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForNameId(StrId nameId) { + switch (nameId) { + case StrId::STR_SELECT_CHAPTER: + return MenuAction::SELECT_CHAPTER; + case StrId::STR_GO_TO_PERCENT: + return MenuAction::GO_TO_PERCENT; + case StrId::STR_FOOTNOTES: + return MenuAction::FOOTNOTES; + case StrId::STR_AUTO_TURN_PAGES_PER_MIN: + return MenuAction::AUTO_PAGE_TURN; + case StrId::STR_EMBEDDED_STYLE: + return MenuAction::EMBEDDED_STYLE; + case StrId::STR_IMAGES: + return MenuAction::IMAGE_RENDERING; + case StrId::STR_TEXT_DARKNESS: + return MenuAction::TEXT_DARKNESS; + case StrId::STR_ORIENTATION: + return MenuAction::ROTATE_SCREEN; + case StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES: + return MenuAction::PULL_REMOTE; + case StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE: + return MenuAction::PUSH_LOCAL; + case StrId::STR_SCREENSHOT_BUTTON: + return MenuAction::SCREENSHOT; + case StrId::STR_DISPLAY_QR: + return MenuAction::DISPLAY_QR; + case StrId::STR_DELETE_CACHE: + return MenuAction::DELETE_CACHE; + case StrId::STR_GO_HOME_BUTTON: + return MenuAction::GO_HOME; + default: + return MenuAction::NONE; } - requestUpdate(); } -void EpubReaderMenuActivity::onExit() { Activity::onExit(); } +void EpubReaderMenuActivity::finishWithAction(MenuAction action) { + setResult(MenuResult{static_cast(action), pendingOrientation, selectedPageTurnOption, + pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness}); + finish(); +} -void EpubReaderMenuActivity::loop() { - // Handle navigation - buttonNavigator.onNext([this] { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); +void EpubReaderMenuActivity::onActionSelected(int index) { + const auto& item = menuItems[index]; + + // Auto page turn cycles locally (not a DynamicEnum because labels are raw strings) + if (item.nameId == StrId::STR_AUTO_TURN_PAGES_PER_MIN) { + selectedPageTurnOption = (selectedPageTurnOption + 1) % 5; requestUpdate(); - }); - - buttonNavigator.onPrevious([this] { - selectedIndex = buttonNavigator.previousIndex(selectedIndex); - requestUpdate(); - }); - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto selectedAction = menuItems[selectedIndex].action; - if (selectedAction == MenuAction::NONE) { - return; - } - if (selectedAction == MenuAction::ROTATE_SCREEN) { - // Cycle orientation preview locally; actual rotation happens on menu exit. - pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::AUTO_PAGE_TURN) { - selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size(); - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::EMBEDDED_STYLE) { - // Cycle per-book override: default -> ON -> OFF -> default. - if (pendingEmbeddedStyleOverride < 0) { - pendingEmbeddedStyleOverride = 1; - } else if (pendingEmbeddedStyleOverride > 0) { - pendingEmbeddedStyleOverride = 0; - } else { - pendingEmbeddedStyleOverride = -1; - } - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::IMAGE_RENDERING) { - // Cycle per-book override: default -> display -> placeholder -> suppress -> default. - if (pendingImageRenderingOverride < 0) { - pendingImageRenderingOverride = 0; - } else if (pendingImageRenderingOverride >= 2) { - pendingImageRenderingOverride = -1; - } else { - pendingImageRenderingOverride++; - } - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::TEXT_DARKNESS) { - pendingTextDarkness = (pendingTextDarkness + 1) % textDarknessLabels.size(); - requestUpdate(); - return; - } - - setResult(MenuResult{static_cast(selectedAction), pendingOrientation, selectedPageTurnOption, - pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness}); - finish(); - return; - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - result.data = MenuResult{-1, - pendingOrientation, - selectedPageTurnOption, - pendingEmbeddedStyleOverride, - pendingImageRenderingOverride, - pendingTextDarkness}; - setResult(std::move(result)); - finish(); return; } + + // All other ACTION items finish with a result + finishWithAction(actionForNameId(item.nameId)); } +void EpubReaderMenuActivity::onSettingToggled(int /*index*/) { + // DynamicEnum items update pending state via their setters — no persistence needed. +} + +void EpubReaderMenuActivity::onBackPressed() { + ActivityResult result; + result.isCancelled = true; + result.data = MenuResult{-1, + pendingOrientation, + selectedPageTurnOption, + pendingEmbeddedStyleOverride, + pendingImageRenderingOverride, + pendingTextDarkness}; + setResult(std::move(result)); + finish(); +} + +std::string EpubReaderMenuActivity::getItemValueString(int index) const { + const auto& item = menuItems[index]; + + // Auto page turn: custom labels + if (item.nameId == StrId::STR_AUTO_TURN_PAGES_PER_MIN) { + if (selectedPageTurnOption == 0) return std::string(tr(STR_STATE_OFF)); + return std::string(pageTurnLabels[selectedPageTurnOption]); + } + + // Plain ACTION items (select chapter, screenshot, etc.) show no value + if (item.type == SettingType::ACTION) return {}; + + // DynamicEnum items use the standard display + return MenuListActivity::getItemValueString(index); +} + +void EpubReaderMenuActivity::onEnter() { MenuListActivity::onEnter(); } + void EpubReaderMenuActivity::render(RenderLock&&) { renderer.clearScreen(); const Rect contentRect = UITheme::getContentRect(renderer, true, false); @@ -166,7 +189,6 @@ void EpubReaderMenuActivity::render(RenderLock&&) { // Title const std::string truncTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentRect.width - 40, EpdFontFamily::BOLD); - // Manual centering so we can respect the content gutter. const int titleX = contentRect.x + (contentRect.width - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2; @@ -184,44 +206,7 @@ void EpubReaderMenuActivity::render(RenderLock&&) { // Menu Items const int startY = 75 + contentRect.y; const int listHeight = contentRect.height - (startY - contentRect.y); - - GUI.drawList( - renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), - selectedIndex, - [this](int index) { - const auto& item = menuItems[index]; - const auto title = I18N.get(item.labelId); - return item.isSeparator ? UITheme::makeSeparatorTitle(title) : title; - }, - nullptr, nullptr, - [this](int index) { - const auto& item = menuItems[index]; - switch (item.action) { - case MenuAction::ROTATE_SCREEN: - return std::string(I18N.get(orientationLabels[pendingOrientation])); - case MenuAction::AUTO_PAGE_TURN: - return std::string(pageTurnLabels[selectedPageTurnOption]); - case MenuAction::EMBEDDED_STYLE: - if (pendingEmbeddedStyleOverride == 1) { - return std::string(tr(STR_STATE_ON)); - } else if (pendingEmbeddedStyleOverride == 0) { - return std::string(tr(STR_STATE_OFF)); - } - return std::string(tr(STR_DEFAULT_VALUE)); - case MenuAction::IMAGE_RENDERING: - if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) { - return std::string(I18N.get(imageRenderingLabels[pendingImageRenderingOverride])); - } - return std::string(tr(STR_DEFAULT_VALUE)); - case MenuAction::TEXT_DARKNESS: { - const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0; - return std::string(I18N.get(textDarknessLabels[idx])); - } - default: - return std::string(); - } - }, - true); + drawMenuList(Rect{contentRect.x, startY, contentRect.width, listHeight}); // Footer / Hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 93bd66d8..ea1d5dc7 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -5,12 +5,12 @@ #include #include -#include "../Activity.h" -#include "util/ButtonNavigator.h" +#include "../MenuListActivity.h" -class EpubReaderMenuActivity final : public Activity { +class EpubReaderMenuActivity final : public MenuListActivity { public: - // Menu actions available from the reader menu. + // Menu actions identified by StrId of the menu item. + // Used by the parent activity to interpret the result. enum class MenuAction { NONE, SELECT_CHAPTER, @@ -36,40 +36,31 @@ class EpubReaderMenuActivity final : public Activity { const uint8_t initialTextDarkness); void onEnter() override; - void onExit() override; - void loop() override; void render(RenderLock&&) override; private: - struct MenuItem { - MenuAction action; - StrId labelId; - bool isSeparator = false; - }; + void buildMenuItems(bool hasFootnotes); + void finishWithAction(MenuAction action); - static std::vector buildMenuItems(bool hasFootnotes); + // MenuListActivity overrides + std::string getItemValueString(int index) const override; + void onActionSelected(int index) override; + void onBackPressed() override; + void onSettingToggled(int index) override; - std::function buildSelectablePredicate() const; + // Map from StrId to MenuAction for result passing + static MenuAction actionForNameId(StrId nameId); - // Fixed menu layout - const std::vector menuItems; - - int selectedIndex = 0; - - ButtonNavigator buttonNavigator; - std::string title = "Reader Menu"; + // Pending state (mutated locally, returned to parent on finish) uint8_t pendingOrientation = 0; uint8_t selectedPageTurnOption = 0; int8_t pendingEmbeddedStyleOverride = -1; int8_t pendingImageRenderingOverride = -1; uint8_t pendingTextDarkness = 1; - const std::vector orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, - StrId::STR_LANDSCAPE_CCW}; - const std::vector textDarknessLabels = {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, - StrId::STR_MAX_DARK}; - const std::vector imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, - StrId::STR_IMAGES_SUPPRESS}; - const std::vector pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"}; + + static constexpr const char* pageTurnLabels[] = {"", "1", "3", "6", "12"}; + + std::string title = "Reader Menu"; int currentPage = 0; int totalPages = 0; int bookProgressPercent = 0; diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 9b99f3a8..681f2ade 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -19,32 +19,33 @@ const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; } // namespace +std::string ClockSettingsActivity::MenuItem::getTitle() const { + const auto t = I18N.get(labelId); + return isSeparator ? UITheme::makeSeparatorTitle(t) : t; +} + std::vector ClockSettingsActivity::buildMenuItems() { std::vector items; items.reserve(7); // Settings - items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); + items.push_back(MenuItem::separator(StrId::STR_SETTINGS_TITLE)); items.push_back({Action::USE_CLOCK, StrId::STR_USE_CLOCK}); items.push_back({Action::CLOCK_FORMAT, StrId::STR_CLOCK_FORMAT}); items.push_back({Action::TIMEZONE, StrId::STR_TIMEZONE}); // Tools - items.push_back({Action::NONE, StrId::STR_READER_TOOLS, true}); + items.push_back(MenuItem::separator(StrId::STR_READER_TOOLS)); items.push_back({Action::DETECT_TIMEZONE, StrId::STR_DETECT_TIMEZONE}); items.push_back({Action::SYNC_TIME, StrId::STR_SYNC_TIME}); return items; } -std::function ClockSettingsActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; -} - void ClockSettingsActivity::onEnter() { Activity::onEnter(); - buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); - if (!buildSelectablePredicate()(selectedIndex)) { + const auto pred = UITheme::makeSelectablePredicate(static_cast(menuItems.size()), + [this](int i) { return menuItems[i].getTitle(); }); + buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); + if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); } requestUpdate(); @@ -125,11 +126,7 @@ void ClockSettingsActivity::render(RenderLock&&) { GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, - [this](int index) { - const auto title = I18N.get(menuItems[index].labelId); - return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; - }, - nullptr, nullptr, + [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, [this](int index) { const auto action = menuItems[index].action; switch (action) { diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index ffeea5d9..54a4196b 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -14,6 +14,8 @@ class ClockSettingsActivity final : public Activity { Action action; StrId labelId; bool isSeparator = false; + static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } + [[nodiscard]] std::string getTitle() const; }; ButtonNavigator buttonNavigator; @@ -22,7 +24,6 @@ class ClockSettingsActivity final : public Activity { static std::vector buildMenuItems(); void handleSelection(); - std::function buildSelectablePredicate() const; public: explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index acd57d01..de554e32 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -3,8 +3,6 @@ #include #include -#include - #include "KOReaderAuthActivity.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" @@ -12,51 +10,60 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -constexpr int MENU_ITEMS = 6; -const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, - StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE, StrId::STR_REGISTER}; -} // namespace - -void KOReaderSettingsActivity::onEnter() { - Activity::onEnter(); - - selectedIndex = 0; - requestUpdate(); +KOReaderSettingsActivity::KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : MenuListActivity("KOReaderSettings", renderer, mappedInput) { + buildMenuItems(); } -void KOReaderSettingsActivity::onExit() { Activity::onExit(); } +void KOReaderSettingsActivity::buildMenuItems() { + // Username, Password, Server URL: ACTION items with custom value display + menuItems.push_back(SettingInfo::Action(StrId::STR_SYNC_SERVER_URL, SettingAction::None) + .withSubcategory(StrId::STR_MENU_KOSYNC_SERVER)); + menuItems.push_back(SettingInfo::Action(StrId::STR_USERNAME, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_PASSWORD, SettingAction::None)); -void KOReaderSettingsActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - finish(); - return; - } + // Document matching: DynamicEnum toggling between Filename and Binary + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, + [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, + [](uint8_t v) { + KOREADER_STORE.setMatchMethod(static_cast(v)); + KOREADER_STORE.saveToFile(); + })); - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - handleSelection(); - return; - } - - // Handle navigation - buttonNavigator.onNext([this] { - selectedIndex = (selectedIndex + 1) % MENU_ITEMS; - requestUpdate(); - }); - - buttonNavigator.onPrevious([this] { - selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; - requestUpdate(); - }); + // Authenticate and Register: ACTION items + menuItems.push_back( + SettingInfo::Action(StrId::STR_AUTHENTICATE, SettingAction::None).withSubcategory(StrId::STR_MENU_KOSYNC_AUTH)); + menuItems.push_back(SettingInfo::Action(StrId::STR_REGISTER, SettingAction::None)); } -void KOReaderSettingsActivity::handleSelection() { - if (selectedIndex == 0) { - // Username +std::string KOReaderSettingsActivity::getItemValueString(int index) const { + const auto& item = menuItems[index]; + + if (item.nameId == StrId::STR_USERNAME) { + auto username = KOREADER_STORE.getUsername(); + return username.empty() ? std::string(tr(STR_NOT_SET)) : username; + } + if (item.nameId == StrId::STR_PASSWORD) { + return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); + } + if (item.nameId == StrId::STR_SYNC_SERVER_URL) { + auto serverUrl = KOREADER_STORE.getServerUrl(); + return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; + } + if (item.nameId == StrId::STR_AUTHENTICATE || item.nameId == StrId::STR_REGISTER) { + return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; + } + + return MenuListActivity::getItemValueString(index); +} + +void KOReaderSettingsActivity::onActionSelected(int index) { + const auto& item = menuItems[index]; + + if (item.nameId == StrId::STR_USERNAME) { startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_USERNAME), - KOREADER_STORE.getUsername(), - 64, // maxLength - false), // not password + KOREADER_STORE.getUsername(), 64, false), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -64,12 +71,9 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); } }); - } else if (selectedIndex == 1) { - // Password + } else if (item.nameId == StrId::STR_PASSWORD) { startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), - KOREADER_STORE.getPassword(), - 64, // maxLength - false), // show characters + KOREADER_STORE.getPassword(), 64, true), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -77,14 +81,11 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); } }); - } else if (selectedIndex == 2) { - // Sync Server URL - prefill with https:// if empty to save typing + } else if (item.nameId == StrId::STR_SYNC_SERVER_URL) { const std::string currentUrl = KOREADER_STORE.getServerUrl(); const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, - 128, // maxLength - URLs can be long - false), // not password + std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 128, false), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -93,28 +94,13 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); } }); - } else if (selectedIndex == 3) { - // Document Matching - toggle between Filename and Binary - const auto current = KOREADER_STORE.getMatchMethod(); - const auto newMethod = - (current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME; - KOREADER_STORE.setMatchMethod(newMethod); - KOREADER_STORE.saveToFile(); - requestUpdate(); - } else if (selectedIndex == 4) { - // Authenticate - if (!KOREADER_STORE.hasCredentials()) { - // Can't authenticate without credentials - just show message briefly - return; - } + } else if (item.nameId == StrId::STR_AUTHENTICATE) { + if (!KOREADER_STORE.hasCredentials()) return; startActivityForResult( std::make_unique(renderer, mappedInput, KOReaderAuthActivity::Mode::LOGIN), [](const ActivityResult&) {}); - } else if (selectedIndex == 5) { - // Register - if (!KOREADER_STORE.hasCredentials()) { - return; - } + } else if (item.nameId == StrId::STR_REGISTER) { + if (!KOREADER_STORE.hasCredentials()) return; startActivityForResult( std::make_unique(renderer, mappedInput, KOReaderAuthActivity::Mode::REGISTER), [](const ActivityResult&) {}); @@ -132,33 +118,8 @@ void KOReaderSettingsActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing * 2; - GUI.drawList( - renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, static_cast(MENU_ITEMS), - static_cast(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, - nullptr, - [this](int index) { - // Draw status for each setting - if (index == 0) { - auto username = KOREADER_STORE.getUsername(); - return username.empty() ? std::string(tr(STR_NOT_SET)) : username; - } else if (index == 1) { - return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); - } else if (index == 2) { - auto serverUrl = KOREADER_STORE.getServerUrl(); - return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; - } else if (index == 3) { - return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME)) - : std::string(tr(STR_BINARY)); - } else if (index == 4) { - return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; - } else if (index == 5) { - return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; - } - return std::string(tr(STR_NOT_SET)); - }, - true); + drawMenuList(Rect{contentRect.x, contentTop, contentRect.width, contentHeight}); - // Draw help text at bottom const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index f32db8a5..2448932e 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -1,26 +1,21 @@ #pragma once -#include "activities/Activity.h" -#include "util/ButtonNavigator.h" +#include "activities/MenuListActivity.h" /** * Submenu for KOReader Sync settings. - * Shows username, password, and authenticate options. + * Shows username, password, server URL, document matching, authenticate, and register. */ -class KOReaderSettingsActivity final : public Activity { +class KOReaderSettingsActivity final : public MenuListActivity { public: - explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("KOReaderSettings", renderer, mappedInput) {} + explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); - void onEnter() override; - void onExit() override; - void loop() override; void render(RenderLock&&) override; private: - ButtonNavigator buttonNavigator; + void buildMenuItems(); - size_t selectedIndex = 0; - - void handleSelection(); + // MenuListActivity overrides + std::string getItemValueString(int index) const override; + void onActionSelected(int index) override; }; diff --git a/src/activities/settings/SettingActionDispatch.cpp b/src/activities/settings/SettingActionDispatch.cpp new file mode 100644 index 00000000..45780107 --- /dev/null +++ b/src/activities/settings/SettingActionDispatch.cpp @@ -0,0 +1,51 @@ +#include "SettingActionDispatch.h" + +#include "ButtonRemapActivity.h" +#include "CalibreSettingsActivity.h" +#include "ClearCacheActivity.h" +#include "ClockSettingsActivity.h" +#include "DetectTimezoneActivity.h" +#include "KOReaderSettingsActivity.h" +#include "LanguageSelectActivity.h" +#include "OtaUpdateActivity.h" +#include "StatusBarSettingsActivity.h" +#include "SyncTimeActivity.h" +#include "SystemInformationActivity.h" +#include "activities/network/WifiSelectionActivity.h" +#include "activities/weather/WeatherSettingsActivity.h" + +std::unique_ptr createActivityForAction(SettingAction action, GfxRenderer& renderer, + MappedInputManager& mappedInput) { + switch (action) { + case SettingAction::RemapFrontButtons: + return std::make_unique(renderer, mappedInput); + case SettingAction::CustomiseStatusBar: + return std::make_unique(renderer, mappedInput); + case SettingAction::ClockSettings: + return std::make_unique(renderer, mappedInput); + case SettingAction::KOReaderSync: + return std::make_unique(renderer, mappedInput); + case SettingAction::OPDSBrowser: + return std::make_unique(renderer, mappedInput); + case SettingAction::Network: + return std::make_unique(renderer, mappedInput, false); + case SettingAction::ClearCache: + return std::make_unique(renderer, mappedInput); + case SettingAction::CheckForUpdates: + return std::make_unique(renderer, mappedInput); + case SettingAction::Language: + return std::make_unique(renderer, mappedInput); + case SettingAction::Weather: + return std::make_unique(renderer, mappedInput); + case SettingAction::SystemInfo: + return std::make_unique(renderer, mappedInput); + case SettingAction::SyncTime: + return std::make_unique(renderer, mappedInput); + case SettingAction::DetectTimezone: + return std::make_unique(renderer, mappedInput); + case SettingAction::Submenu: + case SettingAction::None: + return nullptr; + } + return nullptr; +} diff --git a/src/activities/settings/SettingActionDispatch.h b/src/activities/settings/SettingActionDispatch.h new file mode 100644 index 00000000..926fb875 --- /dev/null +++ b/src/activities/settings/SettingActionDispatch.h @@ -0,0 +1,13 @@ +#pragma once +#include + +#include "SettingInfo.h" + +class Activity; +class GfxRenderer; +class MappedInputManager; + +// Creates the sub-activity corresponding to the given SettingAction. +// Returns nullptr for None, Submenu, or unknown actions (caller handles those). +std::unique_ptr createActivityForAction(SettingAction action, GfxRenderer& renderer, + MappedInputManager& mappedInput); diff --git a/src/activities/settings/SettingInfo.cpp b/src/activities/settings/SettingInfo.cpp new file mode 100644 index 00000000..7525c1ff --- /dev/null +++ b/src/activities/settings/SettingInfo.cpp @@ -0,0 +1,85 @@ +#include "SettingInfo.h" + +#include + +#include "CrossPointSettings.h" +#include "components/UITheme.h" + +std::string SettingInfo::getTitle() const { + const auto t = I18N.get(nameId); + return isSeparator ? UITheme::makeSeparatorTitle(t) : t; +} + +std::string SettingInfo::getDisplayValue() const { + if (isSeparator) return {}; + + switch (type) { + case SettingType::TOGGLE: { + bool value; + if (valuePtr) + value = SETTINGS.*(valuePtr); + else if (valueGetter) + value = valueGetter(); + else + return {}; + return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + } + case SettingType::ENUM: { + uint8_t value; + if (valuePtr) + value = SETTINGS.*(valuePtr); + else if (valueGetter) + value = valueGetter(); + else + return {}; + if (value < enumValues.size()) return std::string(I18N.get(enumValues[value])); + return {}; + } + case SettingType::VALUE: { + if (valuePtr) return std::to_string(SETTINGS.*(valuePtr)); + if (valueGetter) return std::to_string(valueGetter()); + return {}; + } + case SettingType::ACTION: + return std::string(">>"); + case SettingType::STRING: + return {}; + } + return {}; +} + +void SettingInfo::toggleValue() const { + if (isSeparator) return; + + switch (type) { + case SettingType::TOGGLE: + if (valuePtr) { + SETTINGS.*(valuePtr) = !(SETTINGS.*(valuePtr)); + } else if (valueGetter && valueSetter) { + valueSetter(!valueGetter()); + } + break; + + case SettingType::ENUM: { + const auto count = static_cast(enumValues.size()); + if (count == 0) break; + if (valuePtr) { + SETTINGS.*(valuePtr) = (SETTINGS.*(valuePtr) + 1) % count; + } else if (valueGetter && valueSetter) { + valueSetter((valueGetter() + 1) % count); + } + break; + } + + case SettingType::VALUE: + if (valuePtr) { + const unsigned current = SETTINGS.*(valuePtr); + SETTINGS.*(valuePtr) = static_cast( + (current + valueRange.step > valueRange.max) ? valueRange.min : current + valueRange.step); + } + break; + + default: + break; + } +} diff --git a/src/activities/settings/SettingInfo.h b/src/activities/settings/SettingInfo.h new file mode 100644 index 00000000..108a6e34 --- /dev/null +++ b/src/activities/settings/SettingInfo.h @@ -0,0 +1,190 @@ +#pragma once +#include + +#include +#include +#include + +#include "CrossPointSettings.h" + +enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; + +enum class SettingAction { + None, + RemapFrontButtons, + CustomiseStatusBar, + ClockSettings, + KOReaderSync, + OPDSBrowser, + Network, + ClearCache, + CheckForUpdates, + Language, + SystemInfo, + DetectTimezone, + SyncTime, + Weather, + Submenu, +}; + +struct SettingInfo { + StrId nameId; + SettingType type; + uint8_t CrossPointSettings::* valuePtr = nullptr; + std::vector enumValues; + SettingAction action = SettingAction::None; + + struct ValueRange { + uint8_t min; + uint8_t max; + uint8_t step; + }; + ValueRange valueRange = {}; + + const char* key = nullptr; // JSON API key (nullptr for ACTION types) + StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping + bool obfuscated = false; // Save/load via base64 obfuscation (passwords) + + // Direct char[] string fields (for settings stored in CrossPointSettings) + size_t stringOffset = 0; + size_t stringMaxLen = 0; + + // Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore) + std::function valueGetter; + std::function valueSetter; + std::function stringGetter; + std::function stringSetter; + + SettingInfo& withObfuscated() { + obfuscated = true; + return *this; + } + + static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::TOGGLE; + s.valuePtr = ptr; + s.key = key; + s.category = category; + return s; + } + + static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector values, + const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ENUM; + s.valuePtr = ptr; + s.enumValues = std::move(values); + s.key = key; + s.category = category; + return s; + } + + static SettingInfo Action(StrId nameId, SettingAction action) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ACTION; + s.action = action; + return s; + } + + static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange, + const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::VALUE; + s.valuePtr = ptr; + s.valueRange = valueRange; + s.key = key; + s.category = category; + return s; + } + + static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::STRING; + s.stringOffset = (size_t)ptr - (size_t)&SETTINGS; + s.stringMaxLen = maxLen; + s.key = key; + s.category = category; + return s; + } + + static SettingInfo DynamicEnum(StrId nameId, std::vector values, std::function getter, + std::function setter, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ENUM; + s.enumValues = std::move(values); + s.valueGetter = std::move(getter); + s.valueSetter = std::move(setter); + s.key = key; + s.category = category; + return s; + } + + static SettingInfo DynamicString(StrId nameId, std::function getter, + std::function setter, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::STRING; + s.stringGetter = std::move(getter); + s.stringSetter = std::move(setter); + s.key = key; + s.category = category; + return s; + } + + static SettingInfo Separator(StrId nameId) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ACTION; + s.isSeparator = true; + return s; + } + + bool isSeparator = false; + StrId subcategory = StrId::STR_NONE_OPT; // Triggers a separator row on first use and on change + StrId submenu = StrId::STR_NONE_OPT; // Routes item into a submenu; hidden from main list + + // Inserts a separator row in the parent tab when this item's subcategory first appears or changes. + SettingInfo& withSubcategory(StrId sub) { + subcategory = sub; + return *this; + } + + // Hides this item from the parent tab and places it inside a SettingsSubmenuActivity instead. + // All items sharing the same submenu StrId are grouped under one placeholder entry. + SettingInfo& withSubmenu(StrId sub) { + submenu = sub; + return *this; + } + + // For internal use by SettingsActivity: placeholder entry that launches the submenu. + static SettingInfo SubmenuEntry(StrId titleId) { + SettingInfo s; + s.nameId = titleId; + s.type = SettingType::ACTION; + s.action = SettingAction::Submenu; + return s; + } + + // Returns the localised title; separators are decorated automatically. + [[nodiscard]] std::string getTitle() const; + + // Returns the current display value string (ON/OFF for toggles, enum label, numeric value, >>). + [[nodiscard]] std::string getDisplayValue() const; + + // Toggles/cycles the underlying value for TOGGLE, ENUM, and VALUE types. + // Does nothing for ACTION and STRING types (callers handle those separately). + // Marked const because it mutates the external SETTINGS global (via valuePtr), + // not the SettingInfo itself. + void toggleValue() const; +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d64ed44f..37f74be7 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -4,36 +4,58 @@ #include #include -#include "ButtonRemapActivity.h" -#include "CalibreSettingsActivity.h" -#include "ClearCacheActivity.h" -#include "ClockSettingsActivity.h" #include "CrossPointSettings.h" -#include "DetectTimezoneActivity.h" -#include "KOReaderSettingsActivity.h" -#include "LanguageSelectActivity.h" #include "MappedInputManager.h" -#include "OtaUpdateActivity.h" +#include "SettingActionDispatch.h" #include "SettingsList.h" -#include "StatusBarSettingsActivity.h" -#include "SyncTimeActivity.h" -#include "SystemInformationActivity.h" -#include "activities/network/WifiSelectionActivity.h" -#include "activities/weather/WeatherSettingsActivity.h" +#include "SettingsSubmenuActivity.h" #include "components/UITheme.h" #include "fontIds.h" const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER, StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM}; +bool SettingsActivity::isListItemSelectable(int settingIdx) const { + return settingIdx >= 0 && settingIdx < settingsCount && !(*currentSettings)[settingIdx].isSeparator; +} + void SettingsActivity::onEnter() { Activity::onEnter(); - // Build per-category vectors from the shared settings list + // Build per-category vectors from the shared settings list. + // addTo tracks the last subcategory per vector and automatically inserts a separator + // row whenever a setting carries a new subcategory label. displaySettings.clear(); readerSettings.clear(); controlsSettings.clear(); systemSettings.clear(); + submenuData.clear(); + + StrId lastDisplaySub = StrId::STR_NONE_OPT; + StrId lastReaderSub = StrId::STR_NONE_OPT; + StrId lastControlsSub = StrId::STR_NONE_OPT; + StrId lastSystemSub = StrId::STR_NONE_OPT; + + auto addTo = [this](std::vector& vec, StrId& lastSub, SettingInfo s) { + if (s.submenu != StrId::STR_NONE_OPT) { + // Item belongs to a submenu — collect it and insert a placeholder in the main + // list the first time this submenu ID is encountered. + auto it = std::find_if(submenuData.begin(), submenuData.end(), + [&s](const SubmenuData& d) { return d.id == s.submenu; }); + if (it == submenuData.end()) { + vec.push_back(SettingInfo::SubmenuEntry(s.submenu)); + submenuData.push_back({s.submenu, {}}); + it = submenuData.end() - 1; + } + it->items.push_back(std::move(s)); + return; + } + if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) { + vec.push_back(SettingInfo::Separator(s.subcategory)); + lastSub = s.subcategory; + } + vec.push_back(std::move(s)); + }; for (const auto& setting : getSettingsList()) { if (setting.category == StrId::STR_NONE_OPT) continue; @@ -43,30 +65,49 @@ void SettingsActivity::onEnter() { continue; } if (setting.category == StrId::STR_CAT_DISPLAY) { - displaySettings.push_back(setting); + addTo(displaySettings, lastDisplaySub, setting); } else if (setting.category == StrId::STR_CAT_READER) { - readerSettings.push_back(setting); + addTo(readerSettings, lastReaderSub, setting); } else if (setting.category == StrId::STR_CAT_CONTROLS) { - controlsSettings.push_back(setting); + addTo(controlsSettings, lastControlsSub, setting); } else if (setting.category == StrId::STR_CAT_SYSTEM) { - systemSettings.push_back(setting); + addTo(systemSettings, lastSystemSub, setting); } // Web-only categories (KOReader Sync, OPDS Browser) are skipped for device UI } - // Append device-only ACTION items + // Device-only ACTION items — subcategory drives separator insertion automatically. controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); - readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); + + addTo(readerSettings, lastReaderSub, + SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); + + addTo(systemSettings, lastSystemSub, SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network) + .withSubcategory(StrId::STR_MENU_SYS_NETWORK)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync) + .withSubcategory(StrId::STR_MENU_SYS_NETWORK)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser) + .withSubcategory(StrId::STR_MENU_SYS_NETWORK)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings) + .withSubcategory(StrId::STR_MENU_SYS_TOOLS)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather) + .withSubcategory(StrId::STR_MENU_SYS_TOOLS)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache) + .withSubcategory(StrId::STR_MENU_SYS_SYSTEM)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates) + .withSubcategory(StrId::STR_MENU_SYS_SYSTEM)); + addTo(systemSettings, lastSystemSub, + SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo) + .withSubcategory(StrId::STR_MENU_SYS_SYSTEM)); // Reset selection to first category selectedCategoryIndex = 0; @@ -115,12 +156,14 @@ void SettingsActivity::loop() { // Handle navigation buttonNavigator.onNextRelease([this] { - selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); + selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1, + [this](int i) { return i == 0 || isListItemSelectable(i - 1); }); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { - selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); + selectedSettingIndex = ButtonNavigator::previousIndex( + selectedSettingIndex, settingsCount + 1, [this](int i) { return i == 0 || isListItemSelectable(i - 1); }); requestUpdate(); }); @@ -163,73 +206,26 @@ void SettingsActivity::toggleCurrentSetting() { } const auto& setting = (*currentSettings)[selectedSetting]; + if (setting.isSeparator) return; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - // Toggle the boolean value using the member pointer - const bool currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = !currentValue; - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - const int8_t currentValue = SETTINGS.*(setting.valuePtr); - if (currentValue + setting.valueRange.step > setting.valueRange.max) { - SETTINGS.*(setting.valuePtr) = setting.valueRange.min; - } else { - SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; - } - } else if (setting.type == SettingType::ACTION) { + if (setting.type == SettingType::ACTION) { auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; - switch (setting.action) { - case SettingAction::RemapFrontButtons: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::CustomiseStatusBar: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::ClockSettings: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::KOReaderSync: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::OPDSBrowser: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Network: - startActivityForResult(std::make_unique(renderer, mappedInput, false), resultHandler); - break; - case SettingAction::ClearCache: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::CheckForUpdates: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Language: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Weather: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::SystemInfo: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::SyncTime: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::DetectTimezone: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::None: - // Do nothing - break; + if (setting.action == SettingAction::Submenu) { + const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(), + [&setting](const SubmenuData& d) { return d.id == setting.nameId; }); + if (it != submenuData.cend()) { + startActivityForResult( + std::make_unique(renderer, mappedInput, setting.nameId, it->items), resultHandler); + } + } else { + auto activity = createActivityForAction(setting.action, renderer, mappedInput); + if (activity) startActivityForResult(std::move(activity), resultHandler); } - return; // Results will be handled in the result handler, so we can return early here - } else { return; } + setting.toggleValue(); SETTINGS.saveToFile(); } @@ -258,23 +254,8 @@ void SettingsActivity::render(RenderLock&&) { Rect{contentRect.x, contentTop, contentRect.width, contentRect.height - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)}, - settingsCount, selectedSettingIndex - 1, - [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr, - [&settings](int i) { - const auto& setting = settings[i]; - std::string valueText = ""; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - const bool value = SETTINGS.*(setting.valuePtr); - valueText = value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(setting.valuePtr); - valueText = I18N.get(setting.enumValues[value]); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - valueText = std::to_string(SETTINGS.*(setting.valuePtr)); - } - return valueText; - }, - true); + settingsCount, selectedSettingIndex - 1, [&settings](int index) { return settings[index].getTitle(); }, nullptr, + nullptr, [&settings](int i) { return settings[i].getDisplayValue(); }, true); // Draw help text const auto confirmLabel = (selectedSettingIndex == 0) diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 1a1bf5f6..60cf75b6 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -1,149 +1,12 @@ #pragma once #include -#include -#include #include -#include "CrossPointSettings.h" +#include "SettingInfo.h" #include "activities/Activity.h" #include "util/ButtonNavigator.h" -enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; - -enum class SettingAction { - None, - RemapFrontButtons, - CustomiseStatusBar, - ClockSettings, - KOReaderSync, - OPDSBrowser, - Network, - ClearCache, - CheckForUpdates, - Language, - SystemInfo, - DetectTimezone, - SyncTime, - Weather, -}; - -struct SettingInfo { - StrId nameId; - SettingType type; - uint8_t CrossPointSettings::* valuePtr = nullptr; - std::vector enumValues; - SettingAction action = SettingAction::None; - - struct ValueRange { - uint8_t min; - uint8_t max; - uint8_t step; - }; - ValueRange valueRange = {}; - - const char* key = nullptr; // JSON API key (nullptr for ACTION types) - StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping - bool obfuscated = false; // Save/load via base64 obfuscation (passwords) - - // Direct char[] string fields (for settings stored in CrossPointSettings) - size_t stringOffset = 0; - size_t stringMaxLen = 0; - - // Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore) - std::function valueGetter; - std::function valueSetter; - std::function stringGetter; - std::function stringSetter; - - SettingInfo& withObfuscated() { - obfuscated = true; - return *this; - } - - static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::TOGGLE; - s.valuePtr = ptr; - s.key = key; - s.category = category; - return s; - } - - static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector values, - const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ENUM; - s.valuePtr = ptr; - s.enumValues = std::move(values); - s.key = key; - s.category = category; - return s; - } - - static SettingInfo Action(StrId nameId, SettingAction action) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ACTION; - s.action = action; - return s; - } - - static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange, - const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::VALUE; - s.valuePtr = ptr; - s.valueRange = valueRange; - s.key = key; - s.category = category; - return s; - } - - static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::STRING; - s.stringOffset = (size_t)ptr - (size_t)&SETTINGS; - s.stringMaxLen = maxLen; - s.key = key; - s.category = category; - return s; - } - - static SettingInfo DynamicEnum(StrId nameId, std::vector values, std::function getter, - std::function setter, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ENUM; - s.enumValues = std::move(values); - s.valueGetter = std::move(getter); - s.valueSetter = std::move(setter); - s.key = key; - s.category = category; - return s; - } - - static SettingInfo DynamicString(StrId nameId, std::function getter, - std::function setter, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::STRING; - s.stringGetter = std::move(getter); - s.stringSetter = std::move(setter); - s.key = key; - s.category = category; - return s; - } -}; - class SettingsActivity final : public Activity { ButtonNavigator buttonNavigator; @@ -161,8 +24,15 @@ class SettingsActivity final : public Activity { static constexpr int categoryCount = 4; static const StrId categoryNames[categoryCount]; + struct SubmenuData { + StrId id; + std::vector items; + }; + std::vector submenuData; + void enterCategory(int categoryIndex); void toggleCurrentSetting(); + [[nodiscard]] bool isListItemSelectable(int settingIdx) const; public: explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp new file mode 100644 index 00000000..5ac7ffff --- /dev/null +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -0,0 +1,39 @@ +#include "SettingsSubmenuActivity.h" + +#include +#include + +#include "CrossPointSettings.h" +#include "MappedInputManager.h" +#include "SettingActionDispatch.h" +#include "components/UITheme.h" +#include "fontIds.h" + +void SettingsSubmenuActivity::onActionSelected(int index) { + const auto& setting = menuItems[index]; + auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; + + auto activity = createActivityForAction(setting.action, renderer, mappedInput); + if (activity) startActivityForResult(std::move(activity), resultHandler); +} + +void SettingsSubmenuActivity::onSettingToggled(int /*index*/) { SETTINGS.saveToFile(); } + +void SettingsSubmenuActivity::render(RenderLock&&) { + renderer.clearScreen(); + + const auto& metrics = UITheme::getInstance().getMetrics(); + const Rect contentRect = UITheme::getContentRect(renderer, true, false); + + GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.headerHeight}, + I18N.get(titleId)); + + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; + drawMenuList(Rect{contentRect.x, contentTop, contentRect.width, contentHeight}); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/settings/SettingsSubmenuActivity.h b/src/activities/settings/SettingsSubmenuActivity.h new file mode 100644 index 00000000..6fff3691 --- /dev/null +++ b/src/activities/settings/SettingsSubmenuActivity.h @@ -0,0 +1,26 @@ +#pragma once +#include + +#include + +#include "SettingInfo.h" +#include "activities/MenuListActivity.h" + +// Displays a flat list of SettingInfo items launched from a SettingsActivity submenu entry. +// Supports subcategory separators (withSubcategory) exactly as the parent settings tabs do. +class SettingsSubmenuActivity final : public MenuListActivity { + StrId titleId; + + // MenuListActivity overrides + void onActionSelected(int index) override; + void onSettingToggled(int index) override; + + public: + explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId, + std::vector items) + : MenuListActivity("SettingsSubmenu", renderer, mappedInput), titleId(titleId) { + menuItems = std::move(items); + } + + void render(RenderLock&&) override; +}; diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 67a09e16..d4ec15ad 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -108,6 +108,11 @@ std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; } +std::function UITheme::makeSelectablePredicate(int total, std::function titleGetter) { + return + [total, titleGetter](int index) { return index >= 0 && index < total && !isSeparatorTitle(titleGetter(index)); }; +} + std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { size_t pos = coverBmpPath.find("[HEIGHT]", 0); if (pos != std::string::npos) { diff --git a/src/components/UITheme.h b/src/components/UITheme.h index abdc1906..19f576de 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -26,6 +26,33 @@ class UITheme { static std::string makeSeparatorTitle(StrId labelId); static bool isSeparatorTitle(const std::string& title); static std::string stripSeparatorTitle(const std::string& title); + // Returns a selectable predicate for use with ButtonNavigator::setSelectablePredicate(). + // Items whose title is marked as a separator (via makeSeparatorTitle) are skipped during + // navigation. Pass the same title getter you pass to drawList so rendering and navigation + // always agree on which items are separators. + // + // Preferred approach: derive from MenuListActivity (see MenuListActivity.h) which handles + // separator skipping, navigation, and drawList automatically via SettingInfo items. + // + // Manual usage (for activities that don't derive from MenuListActivity). + // Assumes `items` is a member variable (std::vector items): + // + // // populate in constructor or onEnter: + // items.push_back(SettingInfo::Separator(StrId::STR_MY_SECTION)); + // items.push_back(SettingInfo::Toggle(StrId::STR_MY_TOGGLE, &CrossPointSettings::myFlag)); + // + // // onEnter: wire navigation so separators are skipped: + // const auto pred = UITheme::makeSelectablePredicate(items.size(), + // [this](int i) { return items[i].getTitle(); }); + // buttonNavigator.setSelectablePredicate(pred, items.size()); + // + // // render: pass the same getter to drawList; separator rows are drawn automatically: + // const auto& s = items; // local ref for lambda capture + // GUI.drawList(renderer, rect, s.size(), selectedIndex, + // [&s](int i) { return s[i].getTitle(); }, + // nullptr, nullptr, + // [&s](int i) { return s[i].getDisplayValue(); }, true); + static std::function makeSelectablePredicate(int total, std::function titleGetter); // Returns the drawable content Rect accounting for screen orientation and visible button hints. // Bottom hints occupy the physical bottom edge; side hints occupy the physical right edge. diff --git a/src/main.cpp b/src/main.cpp index faa059ba..2f8894ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -204,7 +204,6 @@ void setup() { HalSystem::checkPanic(); HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info - SETTINGS.loadFromFile(); HalClock::applyTimezone(SETTINGS.timeZone); I18N.loadSettings();