Merge pull request #64 from jpirnay/refactor-separator
refactor: Reorganize menu structure
This commit is contained in:
@@ -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"
|
||||
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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: "Вход / Регистрация"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+88
-31
@@ -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 <cstddef>",
|
||||
"",
|
||||
"// 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<size_t>(StrId::_COUNT),",
|
||||
f' "OFFSETS_{code} size mismatch");',
|
||||
)
|
||||
)
|
||||
lines.append(" static_cast<size_t>(StrId::_COUNT),")
|
||||
lines.append(f' "OFFSETS_{code} size mismatch");')
|
||||
|
||||
_write_file(output_path, lines, verbose)
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+187
-151
@@ -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<SettingInfo>& getSettingsList() {
|
||||
static const std::vector<SettingInfo> 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<SettingInfo> 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<uint8_t>(KOREADER_STORE.getMatchMethod()); },
|
||||
[](uint8_t v) {
|
||||
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(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<uint8_t>(KOREADER_STORE.getMatchMethod()); },
|
||||
[](uint8_t v) {
|
||||
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(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<SettingInfo>& getSettingsList() { return SettingsListDetail::list; }
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "MenuListActivity.h"
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
|
||||
void MenuListActivity::initMenuList() {
|
||||
const int count = static_cast<int>(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<int>(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<int>(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();
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
|
||||
#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<SettingInfo> 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;
|
||||
};
|
||||
@@ -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::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
||||
std::vector<MenuItem> 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<int8_t>(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<bool(int)> EpubReaderMenuActivity::buildSelectablePredicate() const {
|
||||
return [this](int index) {
|
||||
return index >= 0 && index < static_cast<int>(menuItems.size()) && !menuItems[index].isSeparator;
|
||||
};
|
||||
}
|
||||
|
||||
void EpubReaderMenuActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
const auto selectablePredicate = buildSelectablePredicate();
|
||||
buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast<int>(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<int>(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<int>(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<int>(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));
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<MenuItem> 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<bool(int)> buildSelectablePredicate() const;
|
||||
// Map from StrId to MenuAction for result passing
|
||||
static MenuAction actionForNameId(StrId nameId);
|
||||
|
||||
// Fixed menu layout
|
||||
const std::vector<MenuItem> 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<StrId> orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED,
|
||||
StrId::STR_LANDSCAPE_CCW};
|
||||
const std::vector<StrId> textDarknessLabels = {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK,
|
||||
StrId::STR_MAX_DARK};
|
||||
const std::vector<StrId> imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER,
|
||||
StrId::STR_IMAGES_SUPPRESS};
|
||||
const std::vector<const char*> 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;
|
||||
|
||||
@@ -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::MenuItem> ClockSettingsActivity::buildMenuItems() {
|
||||
std::vector<MenuItem> 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<bool(int)> ClockSettingsActivity::buildSelectablePredicate() const {
|
||||
return [this](int index) {
|
||||
return index >= 0 && index < static_cast<int>(menuItems.size()) && !menuItems[index].isSeparator;
|
||||
};
|
||||
}
|
||||
|
||||
void ClockSettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast<int>(menuItems.size()));
|
||||
if (!buildSelectablePredicate()(selectedIndex)) {
|
||||
const auto pred = UITheme::makeSelectablePredicate(static_cast<int>(menuItems.size()),
|
||||
[this](int i) { return menuItems[i].getTitle(); });
|
||||
buttonNavigator.setSelectablePredicate(pred, static_cast<int>(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<int>(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) {
|
||||
|
||||
@@ -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<MenuItem> buildMenuItems();
|
||||
void handleSelection();
|
||||
std::function<bool(int)> buildSelectablePredicate() const;
|
||||
|
||||
public:
|
||||
explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#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<uint8_t>(KOREADER_STORE.getMatchMethod()); },
|
||||
[](uint8_t v) {
|
||||
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(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<KeyboardEntryActivity>(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<KeyboardResult>(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<KeyboardEntryActivity>(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<KeyboardResult>(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<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl,
|
||||
128, // maxLength - URLs can be long
|
||||
false), // not password
|
||||
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 128, false),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(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<KOReaderAuthActivity>(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<KOReaderAuthActivity>(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<int>(MENU_ITEMS),
|
||||
static_cast<int>(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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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<Activity> createActivityForAction(SettingAction action, GfxRenderer& renderer,
|
||||
MappedInputManager& mappedInput) {
|
||||
switch (action) {
|
||||
case SettingAction::RemapFrontButtons:
|
||||
return std::make_unique<ButtonRemapActivity>(renderer, mappedInput);
|
||||
case SettingAction::CustomiseStatusBar:
|
||||
return std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput);
|
||||
case SettingAction::ClockSettings:
|
||||
return std::make_unique<ClockSettingsActivity>(renderer, mappedInput);
|
||||
case SettingAction::KOReaderSync:
|
||||
return std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput);
|
||||
case SettingAction::OPDSBrowser:
|
||||
return std::make_unique<CalibreSettingsActivity>(renderer, mappedInput);
|
||||
case SettingAction::Network:
|
||||
return std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false);
|
||||
case SettingAction::ClearCache:
|
||||
return std::make_unique<ClearCacheActivity>(renderer, mappedInput);
|
||||
case SettingAction::CheckForUpdates:
|
||||
return std::make_unique<OtaUpdateActivity>(renderer, mappedInput);
|
||||
case SettingAction::Language:
|
||||
return std::make_unique<LanguageSelectActivity>(renderer, mappedInput);
|
||||
case SettingAction::Weather:
|
||||
return std::make_unique<WeatherSettingsActivity>(renderer, mappedInput);
|
||||
case SettingAction::SystemInfo:
|
||||
return std::make_unique<SystemInformationActivity>(renderer, mappedInput);
|
||||
case SettingAction::SyncTime:
|
||||
return std::make_unique<SyncTimeActivity>(renderer, mappedInput);
|
||||
case SettingAction::DetectTimezone:
|
||||
return std::make_unique<DetectTimezoneActivity>(renderer, mappedInput);
|
||||
case SettingAction::Submenu:
|
||||
case SettingAction::None:
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
#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<Activity> createActivityForAction(SettingAction action, GfxRenderer& renderer,
|
||||
MappedInputManager& mappedInput);
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "SettingInfo.h"
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#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<uint8_t>(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<uint8_t>(
|
||||
(current + valueRange.step > valueRange.max) ? valueRange.min : current + valueRange.step);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
#include <I18n.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<StrId> 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<uint8_t()> valueGetter;
|
||||
std::function<void(uint8_t)> valueSetter;
|
||||
std::function<std::string()> stringGetter;
|
||||
std::function<void(const std::string&)> 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<StrId> 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<StrId> values, std::function<uint8_t()> getter,
|
||||
std::function<void(uint8_t)> 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<std::string()> getter,
|
||||
std::function<void(const std::string&)> 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;
|
||||
};
|
||||
@@ -4,36 +4,58 @@
|
||||
#include <HalClock.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#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<SettingInfo>& 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<uint8_t>(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<ButtonRemapActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::CustomiseStatusBar:
|
||||
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::ClockSettings:
|
||||
startActivityForResult(std::make_unique<ClockSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::KOReaderSync:
|
||||
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::OPDSBrowser:
|
||||
startActivityForResult(std::make_unique<CalibreSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::Network:
|
||||
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false), resultHandler);
|
||||
break;
|
||||
case SettingAction::ClearCache:
|
||||
startActivityForResult(std::make_unique<ClearCacheActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::CheckForUpdates:
|
||||
startActivityForResult(std::make_unique<OtaUpdateActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::Language:
|
||||
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::Weather:
|
||||
startActivityForResult(std::make_unique<WeatherSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::SystemInfo:
|
||||
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::SyncTime:
|
||||
startActivityForResult(std::make_unique<SyncTimeActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::DetectTimezone:
|
||||
startActivityForResult(std::make_unique<DetectTimezoneActivity>(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<SettingsSubmenuActivity>(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)
|
||||
|
||||
@@ -1,149 +1,12 @@
|
||||
#pragma once
|
||||
#include <I18n.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<StrId> 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<uint8_t()> valueGetter;
|
||||
std::function<void(uint8_t)> valueSetter;
|
||||
std::function<std::string()> stringGetter;
|
||||
std::function<void(const std::string&)> 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<StrId> 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<StrId> values, std::function<uint8_t()> getter,
|
||||
std::function<void(uint8_t)> 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<std::string()> getter,
|
||||
std::function<void(const std::string&)> 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<SettingInfo> items;
|
||||
};
|
||||
std::vector<SubmenuData> submenuData;
|
||||
|
||||
void enterCategory(int categoryIndex);
|
||||
void toggleCurrentSetting();
|
||||
[[nodiscard]] bool isListItemSelectable(int settingIdx) const;
|
||||
|
||||
public:
|
||||
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "SettingsSubmenuActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#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();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <I18n.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#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<SettingInfo> items)
|
||||
: MenuListActivity("SettingsSubmenu", renderer, mappedInput), titleId(titleId) {
|
||||
menuItems = std::move(items);
|
||||
}
|
||||
|
||||
void render(RenderLock&&) override;
|
||||
};
|
||||
@@ -108,6 +108,11 @@ std::string UITheme::stripSeparatorTitle(const std::string& title) {
|
||||
return isSeparatorTitle(title) ? title.substr(2) : title;
|
||||
}
|
||||
|
||||
std::function<bool(int)> UITheme::makeSelectablePredicate(int total, std::function<std::string(int)> 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) {
|
||||
|
||||
@@ -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<SettingInfo> 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<bool(int)> makeSelectablePredicate(int total, std::function<std::string(int)> 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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user