From e72ad0d2a9c63e19f5687617352fbdc60d2358af Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 05:46:10 +0200 Subject: [PATCH 01/18] Refacrored the separator component to be more flexible and reusable across different activities. Updated the EpubReaderMenuActivity and ClockSettingsActivity to utilize the new separator component for better UI consistency and maintainability. This change also involved updating the UITheme component to support the new separator styles. --- .../reader/EpubReaderMenuActivity.cpp | 25 +++++++++-------- .../reader/EpubReaderMenuActivity.h | 3 +-- .../settings/ClockSettingsActivity.cpp | 19 +++++++------ .../settings/ClockSettingsActivity.h | 2 +- src/components/UITheme.cpp | 7 +++++ src/components/UITheme.h | 27 +++++++++++++++++++ 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 313f4890..c82f6510 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -29,7 +29,7 @@ std::vector EpubReaderMenuActivity::buildMenuI std::vector items; items.reserve(18); // Navigation - items.push_back({MenuAction::NONE, StrId::STR_READER_NAVIGATION, true}); + items.push_back(MenuItem::separator(StrId::STR_READER_NAVIGATION)); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); if (hasFootnotes) { @@ -38,7 +38,7 @@ std::vector EpubReaderMenuActivity::buildMenuI items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); // Appearance - items.push_back({MenuAction::NONE, StrId::STR_READER_APPEARANCE, true}); + items.push_back(MenuItem::separator(StrId::STR_READER_APPEARANCE)); 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}); @@ -46,13 +46,13 @@ std::vector EpubReaderMenuActivity::buildMenuI // Synchronisation (only if credentials are set, to avoid confusion) if (KOREADER_STORE.hasCredentials()) { - items.push_back({MenuAction::NONE, StrId::STR_KOREADER_SYNC, true}); + items.push_back(MenuItem::separator(StrId::STR_KOREADER_SYNC)); 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}); } // Tools - items.push_back({MenuAction::NONE, StrId::STR_READER_TOOLS, true}); + items.push_back(MenuItem::separator(StrId::STR_READER_TOOLS)); 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}); @@ -60,17 +60,16 @@ std::vector EpubReaderMenuActivity::buildMenuI return items; } -std::function EpubReaderMenuActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; -} - void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); - const auto selectablePredicate = buildSelectablePredicate(); - buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast(menuItems.size())); - if (!selectablePredicate(selectedIndex)) { + const auto pred = UITheme::makeSelectablePredicate( + static_cast(menuItems.size()), [this](int i) { + const auto& item = menuItems[i]; + const auto t = I18N.get(item.labelId); + return item.isSeparator ? UITheme::makeSeparatorTitle(t) : t; + }); + buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); + if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); } requestUpdate(); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 93bd66d8..216caae2 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -45,12 +45,11 @@ class EpubReaderMenuActivity final : public Activity { MenuAction action; StrId labelId; bool isSeparator = false; + static MenuItem separator(StrId label) { return {MenuAction::NONE, label, true}; } }; static std::vector buildMenuItems(bool hasFootnotes); - std::function buildSelectablePredicate() const; - // Fixed menu layout const std::vector menuItems; diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 9b99f3a8..1e6a9b1c 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -23,28 +23,27 @@ std::vector ClockSettingsActivity::buildMenuIte std::vector items; items.reserve(7); // Settings - items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); + items.push_back(MenuItem::separator(StrId::STR_SETTINGS_TITLE)); items.push_back({Action::USE_CLOCK, StrId::STR_USE_CLOCK}); items.push_back({Action::CLOCK_FORMAT, StrId::STR_CLOCK_FORMAT}); items.push_back({Action::TIMEZONE, StrId::STR_TIMEZONE}); // Tools - items.push_back({Action::NONE, StrId::STR_READER_TOOLS, true}); + items.push_back(MenuItem::separator(StrId::STR_READER_TOOLS)); items.push_back({Action::DETECT_TIMEZONE, StrId::STR_DETECT_TIMEZONE}); items.push_back({Action::SYNC_TIME, StrId::STR_SYNC_TIME}); return items; } -std::function ClockSettingsActivity::buildSelectablePredicate() const { - return [this](int index) { - return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; - }; -} - void ClockSettingsActivity::onEnter() { Activity::onEnter(); - buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); - if (!buildSelectablePredicate()(selectedIndex)) { + const auto pred = UITheme::makeSelectablePredicate( + static_cast(menuItems.size()), [this](int i) { + const auto t = I18N.get(menuItems[i].labelId); + return menuItems[i].isSeparator ? UITheme::makeSeparatorTitle(t) : t; + }); + buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); + if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); } requestUpdate(); diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index ffeea5d9..a5c02136 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -14,6 +14,7 @@ class ClockSettingsActivity final : public Activity { Action action; StrId labelId; bool isSeparator = false; + static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } }; ButtonNavigator buttonNavigator; @@ -22,7 +23,6 @@ class ClockSettingsActivity final : public Activity { static std::vector buildMenuItems(); void handleSelection(); - std::function buildSelectablePredicate() const; public: explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 67a09e16..6b92df15 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -108,6 +108,13 @@ std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; } +std::function UITheme::makeSelectablePredicate(int total, + std::function titleGetter) { + return [total, titleGetter](int index) { + return index >= 0 && index < total && !isSeparatorTitle(titleGetter(index)); + }; +} + std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { size_t pos = coverBmpPath.find("[HEIGHT]", 0); if (pos != std::string::npos) { diff --git a/src/components/UITheme.h b/src/components/UITheme.h index abdc1906..4cacca79 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -26,6 +26,33 @@ class UITheme { static std::string makeSeparatorTitle(StrId labelId); static bool isSeparatorTitle(const std::string& title); static std::string stripSeparatorTitle(const std::string& title); + // Returns a selectable predicate for use with ButtonNavigator::setSelectablePredicate(). + // Items whose title is marked as a separator (via makeSeparatorTitle) are skipped during + // navigation. Pass the same title getter you pass to drawList so rendering and navigation + // always agree on which items are separators. + // + // Typical usage pattern for a menu with section headers: + // + // struct MenuItem { + // Action action; StrId labelId; bool isSeparator = false; + // static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } + // }; + // const std::vector items = buildMenuItems(); + // // In buildMenuItems, use MenuItem::separator(StrId) for section headers. + // + // // Title getter — used by both drawList and makeSelectablePredicate: + // auto titleGetter = [&](int i) { + // const auto t = I18N.get(items[i].labelId); + // return items[i].isSeparator ? UITheme::makeSeparatorTitle(t) : t; + // }; + // + // // onEnter: wire navigation so separators are skipped: + // const auto pred = UITheme::makeSelectablePredicate(items.size(), titleGetter); + // buttonNavigator.setSelectablePredicate(pred, items.size()); + // + // // render: pass the same getter to drawList; separator rows are drawn automatically: + // GUI.drawList(renderer, rect, items.size(), selectedIndex, titleGetter, ...); + static std::function makeSelectablePredicate(int total, std::function titleGetter); // Returns the drawable content Rect accounting for screen orientation and visible button hints. // Bottom hints occupy the physical bottom edge; side hints occupy the physical right edge. From 552dc37d28fce0c85f78c61458968a6e4c32992d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 05:49:27 +0200 Subject: [PATCH 02/18] Stage 2 --- .../reader/EpubReaderMenuActivity.cpp | 17 +++++++---------- src/activities/reader/EpubReaderMenuActivity.h | 1 + .../settings/ClockSettingsActivity.cpp | 15 +++++++-------- src/activities/settings/ClockSettingsActivity.h | 1 + src/components/UITheme.h | 13 +++++-------- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index c82f6510..15641b46 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -25,6 +25,11 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu totalPages(totalPages), bookProgressPercent(bookProgressPercent) {} +std::string EpubReaderMenuActivity::MenuItem::getTitle() const { + const auto t = I18N.get(labelId); + return isSeparator ? UITheme::makeSeparatorTitle(t) : t; +} + std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { std::vector items; items.reserve(18); @@ -63,11 +68,7 @@ std::vector EpubReaderMenuActivity::buildMenuI void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); const auto pred = UITheme::makeSelectablePredicate( - static_cast(menuItems.size()), [this](int i) { - const auto& item = menuItems[i]; - const auto t = I18N.get(item.labelId); - return item.isSeparator ? UITheme::makeSeparatorTitle(t) : t; - }); + static_cast(menuItems.size()), [this](int i) { return menuItems[i].getTitle(); }); buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); @@ -187,11 +188,7 @@ void EpubReaderMenuActivity::render(RenderLock&&) { GUI.drawList( renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), selectedIndex, - [this](int index) { - const auto& item = menuItems[index]; - const auto title = I18N.get(item.labelId); - return item.isSeparator ? UITheme::makeSeparatorTitle(title) : title; - }, + [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, [this](int index) { const auto& item = menuItems[index]; diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 216caae2..a0617dab 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -46,6 +46,7 @@ class EpubReaderMenuActivity final : public Activity { StrId labelId; bool isSeparator = false; static MenuItem separator(StrId label) { return {MenuAction::NONE, label, true}; } + [[nodiscard]] std::string getTitle() const; }; static std::vector buildMenuItems(bool hasFootnotes); diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 1e6a9b1c..4b01adfe 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -19,6 +19,11 @@ const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; } // namespace +std::string ClockSettingsActivity::MenuItem::getTitle() const { + const auto t = I18N.get(labelId); + return isSeparator ? UITheme::makeSeparatorTitle(t) : t; +} + std::vector ClockSettingsActivity::buildMenuItems() { std::vector items; items.reserve(7); @@ -38,10 +43,7 @@ std::vector ClockSettingsActivity::buildMenuIte void ClockSettingsActivity::onEnter() { Activity::onEnter(); const auto pred = UITheme::makeSelectablePredicate( - static_cast(menuItems.size()), [this](int i) { - const auto t = I18N.get(menuItems[i].labelId); - return menuItems[i].isSeparator ? UITheme::makeSeparatorTitle(t) : t; - }); + static_cast(menuItems.size()), [this](int i) { return menuItems[i].getTitle(); }); buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); @@ -124,10 +126,7 @@ void ClockSettingsActivity::render(RenderLock&&) { GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, - [this](int index) { - const auto title = I18N.get(menuItems[index].labelId); - return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; - }, + [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, [this](int index) { const auto action = menuItems[index].action; diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index a5c02136..54a4196b 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -15,6 +15,7 @@ class ClockSettingsActivity final : public Activity { StrId labelId; bool isSeparator = false; static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } + [[nodiscard]] std::string getTitle() const; }; ButtonNavigator buttonNavigator; diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 4cacca79..041b1338 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -36,22 +36,19 @@ class UITheme { // struct MenuItem { // Action action; StrId labelId; bool isSeparator = false; // static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } + // std::string getTitle() const; // implemented in .cpp: makeSeparatorTitle when isSeparator // }; // const std::vector items = buildMenuItems(); // // In buildMenuItems, use MenuItem::separator(StrId) for section headers. // - // // Title getter — used by both drawList and makeSelectablePredicate: - // auto titleGetter = [&](int i) { - // const auto t = I18N.get(items[i].labelId); - // return items[i].isSeparator ? UITheme::makeSeparatorTitle(t) : t; - // }; - // // // onEnter: wire navigation so separators are skipped: - // const auto pred = UITheme::makeSelectablePredicate(items.size(), titleGetter); + // 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: - // GUI.drawList(renderer, rect, items.size(), selectedIndex, titleGetter, ...); + // GUI.drawList(renderer, rect, items.size(), selectedIndex, + // [this](int i) { return items[i].getTitle(); }, ...); static std::function makeSelectablePredicate(int total, std::function titleGetter); // Returns the drawable content Rect accounting for screen orientation and visible button hints. From c6094dce22f7270b2996b589516cddcaa0447000 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 06:12:27 +0200 Subject: [PATCH 03/18] Strart with System settings --- lib/I18n/translations/english.yaml | 5 ++- .../reader/EpubReaderMenuActivity.cpp | 8 ++--- .../settings/ClockSettingsActivity.cpp | 7 ++-- src/activities/settings/SettingsActivity.cpp | 34 +++++++++++++++---- src/activities/settings/SettingsActivity.h | 12 +++++++ src/components/UITheme.cpp | 8 ++--- 6 files changed, 52 insertions(+), 22 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 23253403..53514c4f 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -474,4 +474,7 @@ STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_SUN_INFO: "Sun" STR_READER_TOOLS: "Tools" STR_READER_NAVIGATION: "Navigation" -STR_READER_APPEARANCE: "Appearance" \ No newline at end of file +STR_READER_APPEARANCE: "Appearance" +STR_MENU_SYS_SYSTEM: "System" +STR_MENU_SYS_NETWORK: "Network" +STR_MENU_SYS_TOOLS: "Tools" \ No newline at end of file diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 15641b46..a047c6e9 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -67,8 +67,8 @@ std::vector EpubReaderMenuActivity::buildMenuI void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); - const auto pred = UITheme::makeSelectablePredicate( - static_cast(menuItems.size()), [this](int i) { return menuItems[i].getTitle(); }); + const auto pred = UITheme::makeSelectablePredicate(static_cast(menuItems.size()), + [this](int i) { return menuItems[i].getTitle(); }); buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); @@ -187,9 +187,7 @@ void EpubReaderMenuActivity::render(RenderLock&&) { GUI.drawList( renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), - selectedIndex, - [this](int index) { return menuItems[index].getTitle(); }, - nullptr, nullptr, + selectedIndex, [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, [this](int index) { const auto& item = menuItems[index]; switch (item.action) { diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index 4b01adfe..681f2ade 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -42,8 +42,8 @@ std::vector ClockSettingsActivity::buildMenuIte void ClockSettingsActivity::onEnter() { Activity::onEnter(); - const auto pred = UITheme::makeSelectablePredicate( - static_cast(menuItems.size()), [this](int i) { return menuItems[i].getTitle(); }); + const auto pred = UITheme::makeSelectablePredicate(static_cast(menuItems.size()), + [this](int i) { return menuItems[i].getTitle(); }); buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); @@ -126,8 +126,7 @@ void ClockSettingsActivity::render(RenderLock&&) { GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, - [this](int index) { return menuItems[index].getTitle(); }, - nullptr, nullptr, + [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, [this](int index) { const auto action = menuItems[index].action; switch (action) { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d64ed44f..d03f6c6e 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -26,6 +26,15 @@ const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER, StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM}; +std::string SettingInfo::getTitle() const { + const auto t = I18N.get(nameId); + return isSeparator ? UITheme::makeSeparatorTitle(t) : t; +} + +bool SettingsActivity::isListItemSelectable(int settingIdx) const { + return settingIdx >= 0 && settingIdx < settingsCount && !(*currentSettings)[settingIdx].isSeparator; +} + void SettingsActivity::onEnter() { Activity::onEnter(); @@ -57,15 +66,23 @@ void SettingsActivity::onEnter() { // Append device-only ACTION items 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_LANGUAGE, SettingAction::Language)); + // Network section + systemSettings.push_back(SettingInfo::Separator(StrId::STR_MENU_SYS_NETWORK)); 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)); + // Tools section + systemSettings.push_back(SettingInfo::Separator(StrId::STR_MENU_SYS_TOOLS)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)); + systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); + // System section + systemSettings.push_back(SettingInfo::Separator(StrId::STR_MENU_SYS_SYSTEM)); 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)); // Reset selection to first category @@ -115,12 +132,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,6 +182,7 @@ 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 @@ -258,8 +278,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, + settingsCount, selectedSettingIndex - 1, [&settings](int index) { return settings[index].getTitle(); }, nullptr, + nullptr, [&settings](int i) { const auto& setting = settings[i]; std::string valueText = ""; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 1a1bf5f6..589b2c39 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -142,6 +142,17 @@ struct SettingInfo { 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; + [[nodiscard]] std::string getTitle() const; }; class SettingsActivity final : public Activity { @@ -163,6 +174,7 @@ class SettingsActivity final : public Activity { void enterCategory(int categoryIndex); void toggleCurrentSetting(); + [[nodiscard]] bool isListItemSelectable(int settingIdx) const; public: explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 6b92df15..d4ec15ad 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -108,11 +108,9 @@ std::string UITheme::stripSeparatorTitle(const std::string& title) { return isSeparatorTitle(title) ? title.substr(2) : title; } -std::function UITheme::makeSelectablePredicate(int total, - std::function titleGetter) { - return [total, titleGetter](int index) { - return index >= 0 && index < total && !isSeparatorTitle(titleGetter(index)); - }; +std::function UITheme::makeSelectablePredicate(int total, std::function titleGetter) { + return + [total, titleGetter](int index) { return index >= 0 && index < total && !isSeparatorTitle(titleGetter(index)); }; } std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { From c6e94d69667c39adfaf874f4f493fa56dd20bdd1 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 06:48:05 +0200 Subject: [PATCH 04/18] Exclude commented out strings --- scripts/gen_i18n.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index 520be52f..b593a043 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -280,8 +280,10 @@ 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)) + for line in text.splitlines(): + line = line.split("//", 1)[0] + for m in pattern.finditer(line): + used.add(m.group(0)) return used From 7feff4e241a7b9a646006d5b9fced84e7e19efdc Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 06:49:28 +0200 Subject: [PATCH 05/18] Step 3 --- lib/I18n/translations/english.yaml | 5 +- src/SettingsList.h | 40 ++++++----- src/activities/settings/SettingsActivity.cpp | 70 ++++++++++++++------ src/activities/settings/SettingsActivity.h | 6 ++ 4 files changed, 82 insertions(+), 39 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 53514c4f..a9ce9a21 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -477,4 +477,7 @@ STR_READER_NAVIGATION: "Navigation" STR_READER_APPEARANCE: "Appearance" STR_MENU_SYS_SYSTEM: "System" STR_MENU_SYS_NETWORK: "Network" -STR_MENU_SYS_TOOLS: "Tools" \ No newline at end of file +STR_MENU_SYS_TOOLS: "Tools" +STR_MENU_DISP_SLEEP: "Sleepscreen" +STR_MENU_DISP_BATTERY: "Battery symbol" +STR_MENU_DISP_REFRESH: "Screen refresh" \ No newline at end of file diff --git a/src/SettingsList.h b/src/SettingsList.h index 45b85dba..ef6e99cb 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -9,15 +9,30 @@ #include "activities/settings/SettingsActivity.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. +// +// 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. +// 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. inline const std::vector& getSettingsList() { static const std::vector list = { // --- Display --- + SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, + {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, + "sleepTimeout", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, - "sleepScreen", StrId::STR_CAT_DISPLAY), + "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, @@ -31,16 +46,18 @@ inline const std::vector& getSettingsList() { {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), + 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), + "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_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), // --- Reader --- SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily, @@ -74,12 +91,6 @@ inline const std::vector& getSettingsList() { 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 --- @@ -92,9 +103,6 @@ inline const std::vector& getSettingsList() { 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, diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d03f6c6e..34f2cdca 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -38,12 +38,27 @@ bool SettingsActivity::isListItemSelectable(int settingIdx) const { 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(); + 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 = [](std::vector& vec, StrId& lastSub, SettingInfo s) { + 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; if (setting.category == StrId::STR_CAT_SYSTEM && @@ -52,38 +67,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_LANGUAGE, SettingAction::Language)); - // Network section - systemSettings.push_back(SettingInfo::Separator(StrId::STR_MENU_SYS_NETWORK)); - 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)); - // Tools section - systemSettings.push_back(SettingInfo::Separator(StrId::STR_MENU_SYS_TOOLS)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)); - systemSettings.push_back(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)); - // System section - systemSettings.push_back(SettingInfo::Separator(StrId::STR_MENU_SYS_SYSTEM)); - 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_SYSTEM_INFO, SettingAction::SystemInfo)); + addTo(readerSettings, lastReaderSub, + SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); - readerSettings.push_back(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; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 589b2c39..161f7ddf 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -152,7 +152,13 @@ struct SettingInfo { } bool isSeparator = false; + StrId subcategory = StrId::STR_NONE_OPT; [[nodiscard]] std::string getTitle() const; + + SettingInfo& withSubcategory(StrId sub) { + subcategory = sub; + return *this; + } }; class SettingsActivity final : public Activity { From 9f16066bcef9a8d70387943db472546be8eb1f37 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 07:21:42 +0200 Subject: [PATCH 06/18] Submenu basis --- src/SettingsList.h | 5 + src/activities/settings/SettingsActivity.cpp | 27 ++- src/activities/settings/SettingsActivity.h | 27 ++- .../settings/SettingsSubmenuActivity.cpp | 162 ++++++++++++++++++ .../settings/SettingsSubmenuActivity.h | 30 ++++ 5 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 src/activities/settings/SettingsSubmenuActivity.cpp create mode 100644 src/activities/settings/SettingsSubmenuActivity.h diff --git a/src/SettingsList.h b/src/SettingsList.h index ef6e99cb..cd6a07b2 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -18,6 +18,11 @@ // 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 diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 34f2cdca..d6f186d3 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -15,6 +15,7 @@ #include "MappedInputManager.h" #include "OtaUpdateActivity.h" #include "SettingsList.h" +#include "SettingsSubmenuActivity.h" #include "StatusBarSettingsActivity.h" #include "SyncTimeActivity.h" #include "SystemInformationActivity.h" @@ -45,13 +46,27 @@ void SettingsActivity::onEnter() { 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 = [](std::vector& vec, StrId& lastSub, SettingInfo s) { + auto addTo = [this](std::vector& vec, StrId& lastSub, SettingInfo s) { + if (s.submenu != StrId::STR_NONE_OPT) { + // Item belongs to a submenu — collect it and insert a placeholder in the main + // list the first time this submenu ID is encountered. + auto it = std::find_if(submenuData.begin(), submenuData.end(), + [&s](const SubmenuData& d) { return d.id == s.submenu; }); + if (it == submenuData.end()) { + vec.push_back(SettingInfo::SubmenuEntry(s.submenu)); + submenuData.push_back({s.submenu, {}}); + it = submenuData.end() - 1; + } + it->items.push_back(std::move(s)); + return; + } if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) { vec.push_back(SettingInfo::Separator(s.subcategory)); lastSub = s.subcategory; @@ -267,6 +282,16 @@ void SettingsActivity::toggleCurrentSetting() { case SettingAction::DetectTimezone: startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; + case SettingAction::Submenu: { + const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(), + [&setting](const SubmenuData& d) { return d.id == setting.nameId; }); + if (it != submenuData.cend()) { + startActivityForResult( + std::make_unique(renderer, mappedInput, setting.nameId, it->items), + resultHandler); + } + break; + } case SettingAction::None: // Do nothing break; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 161f7ddf..459084c2 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -26,6 +26,7 @@ enum class SettingAction { DetectTimezone, SyncTime, Weather, + Submenu, }; struct SettingInfo { @@ -152,13 +153,31 @@ struct SettingInfo { } bool isSeparator = false; - StrId subcategory = StrId::STR_NONE_OPT; + 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 [[nodiscard]] std::string getTitle() const; + // 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; + } }; class SettingsActivity final : public Activity { @@ -178,6 +197,12 @@ class SettingsActivity final : public Activity { static constexpr int categoryCount = 4; static const StrId categoryNames[categoryCount]; + struct SubmenuData { + StrId id; + std::vector items; + }; + std::vector submenuData; + void enterCategory(int categoryIndex); void toggleCurrentSetting(); [[nodiscard]] bool isListItemSelectable(int settingIdx) const; diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp new file mode 100644 index 00000000..5e87a504 --- /dev/null +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -0,0 +1,162 @@ +#include "SettingsSubmenuActivity.h" + +#include +#include + +#include "ButtonRemapActivity.h" +#include "CalibreSettingsActivity.h" +#include "ClearCacheActivity.h" +#include "ClockSettingsActivity.h" +#include "CrossPointSettings.h" +#include "KOReaderSettingsActivity.h" +#include "LanguageSelectActivity.h" +#include "MappedInputManager.h" +#include "OtaUpdateActivity.h" +#include "StatusBarSettingsActivity.h" +#include "SystemInformationActivity.h" +#include "activities/network/WifiSelectionActivity.h" +#include "activities/weather/WeatherSettingsActivity.h" +#include "components/UITheme.h" +#include "fontIds.h" + +void SettingsSubmenuActivity::onEnter() { + Activity::onEnter(); + itemCount = static_cast(items.size()); + const auto pred = UITheme::makeSelectablePredicate(itemCount, + [this](int i) { return items[i].getTitle(); }); + buttonNavigator.setSelectablePredicate(pred, itemCount); + if (!pred(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } + requestUpdate(); +} + +void SettingsSubmenuActivity::onExit() { Activity::onExit(); } + +void SettingsSubmenuActivity::loop() { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + finish(); + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + toggleItem(); + requestUpdate(); + return; + } + + buttonNavigator.onNextRelease([this] { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onPreviousRelease([this] { + selectedIndex = buttonNavigator.previousIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onNextContinuous([this] { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onPreviousContinuous([this] { + selectedIndex = buttonNavigator.previousIndex(selectedIndex); + requestUpdate(); + }); +} + +void SettingsSubmenuActivity::toggleItem() { + if (selectedIndex < 0 || selectedIndex >= itemCount) return; + const auto& setting = items[selectedIndex]; + if (setting.isSeparator) return; + + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + SETTINGS.*(setting.valuePtr) = !SETTINGS.*(setting.valuePtr); + } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t cur = SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = (cur + 1) % static_cast(setting.enumValues.size()); + } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + const int8_t cur = SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = (cur + setting.valueRange.step > setting.valueRange.max) + ? setting.valueRange.min + : cur + setting.valueRange.step; + } else if (setting.type == SettingType::ACTION) { + auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; + switch (setting.action) { + case SettingAction::RemapFrontButtons: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::CustomiseStatusBar: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::ClockSettings: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::KOReaderSync: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::OPDSBrowser: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::Network: + startActivityForResult(std::make_unique(renderer, mappedInput, false), resultHandler); + return; + case SettingAction::ClearCache: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::CheckForUpdates: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::Language: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::Weather: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + case SettingAction::SystemInfo: + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); + return; + default: + return; + } + } + + 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; + + GUI.drawList( + renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, itemCount, selectedIndex, + [this](int index) { return items[index].getTitle(); }, nullptr, nullptr, + [this](int i) { + const auto& setting = items[i]; + if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { + return std::string(SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + } + if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t value = SETTINGS.*(setting.valuePtr); + if (value < setting.enumValues.size()) { + return std::string(I18N.get(setting.enumValues[value])); + } + } + if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + return std::to_string(SETTINGS.*(setting.valuePtr)); + } + return std::string(); + }, + true); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/settings/SettingsSubmenuActivity.h b/src/activities/settings/SettingsSubmenuActivity.h new file mode 100644 index 00000000..10d53197 --- /dev/null +++ b/src/activities/settings/SettingsSubmenuActivity.h @@ -0,0 +1,30 @@ +#pragma once +#include + +#include + +#include "activities/Activity.h" +#include "activities/settings/SettingsActivity.h" +#include "util/ButtonNavigator.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 Activity { + StrId titleId; + std::vector items; + int selectedIndex = 0; + int itemCount = 0; + ButtonNavigator buttonNavigator; + + void toggleItem(); + + public: + explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId, + std::vector items) + : Activity("SettingsSubmenu", renderer, mappedInput), titleId(titleId), items(std::move(items)) {} + + void onEnter() override; + void onExit() override; + void loop() override; + void render(RenderLock&&) override; +}; From 1f1777385c31c9bbfa867a467726690d0e5d0937 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 07:41:27 +0200 Subject: [PATCH 07/18] Fix unary operator --- src/activities/settings/SettingsSubmenuActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp index 5e87a504..61f6764c 100644 --- a/src/activities/settings/SettingsSubmenuActivity.cpp +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -69,7 +69,7 @@ void SettingsSubmenuActivity::toggleItem() { if (setting.isSeparator) return; if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - SETTINGS.*(setting.valuePtr) = !SETTINGS.*(setting.valuePtr); + SETTINGS.*(setting.valuePtr) = !(SETTINGS.*(setting.valuePtr)); } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { const uint8_t cur = SETTINGS.*(setting.valuePtr); SETTINGS.*(setting.valuePtr) = (cur + 1) % static_cast(setting.enumValues.size()); From df3fec574d263393225402a732fc1bc372521388 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 14:21:48 +0200 Subject: [PATCH 08/18] Add submenus --- lib/I18n/translations/english.yaml | 7 +- src/SettingsList.h | 312 ++++++++++-------- src/activities/settings/SettingsActivity.cpp | 20 +- .../settings/SettingsSubmenuActivity.cpp | 6 +- src/main.cpp | 4 +- 5 files changed, 192 insertions(+), 157 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index a9ce9a21..10f64940 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -480,4 +480,9 @@ STR_MENU_SYS_NETWORK: "Network" STR_MENU_SYS_TOOLS: "Tools" STR_MENU_DISP_SLEEP: "Sleepscreen" STR_MENU_DISP_BATTERY: "Battery symbol" -STR_MENU_DISP_REFRESH: "Screen refresh" \ No newline at end of file +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" \ No newline at end of file diff --git a/src/SettingsList.h b/src/SettingsList.h index cd6a07b2..928eb68d 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -27,158 +27,180 @@ // // ACTION-type entries and entries without a key are device-only and are added directly // in SettingsActivity::onEnter(), not here. -inline const std::vector& getSettingsList() { - static const std::vector list = { - // --- Display --- - SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, - {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, - "sleepTimeout", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, - {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, - StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, - "sleepScreen", StrId::STR_CAT_DISPLAY) - .withSubcategory(StrId::STR_MENU_DISP_SLEEP), - SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, - {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, - {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, - "sleepScreenCoverFilter", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum( - StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, - {StrId::STR_OVERLAY_OFF, StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK}, - "sleepCoverOverlay", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_SLEEP_IMAGE_PICK_MODE, &CrossPointSettings::sleepImagePickMode, - {StrId::STR_RANDOM, StrId::STR_SEQUENTIAL}, "sleepImagePickMode", StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, - {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", - StrId::STR_CAT_DISPLAY) - .withSubcategory(StrId::STR_MENU_DISP_BATTERY), - SettingInfo::Enum( - StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency, - {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, - "refreshFrequency", StrId::STR_CAT_DISPLAY) - .withSubcategory(StrId::STR_MENU_DISP_REFRESH), - SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", - StrId::STR_CAT_DISPLAY), - SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, - {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme", +// +// Implementation note: the list is a namespace-level static (not a function-local static) so +// it is initialised during the global-static phase before setup() runs. A function-local +// static would trigger __cxa_guard_acquire on the first call, which creates a FreeRTOS mutex +// deep inside the heap allocator chain — enough stack to overflow the 8 KB loop task stack +// when called from inside SETTINGS.loadFromFile() at boot time. +namespace SettingsListDetail { +inline const std::vector list = { + // --- Display --- + SettingInfo::Enum(StrId::STR_TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout, + {StrId::STR_MIN_1, StrId::STR_MIN_5, StrId::STR_MIN_10, StrId::STR_MIN_15, StrId::STR_MIN_30}, + "sleepTimeout", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen, + {StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER, StrId::STR_NONE_OPT, + StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY}, + "sleepScreen", StrId::STR_CAT_DISPLAY) + .withSubcategory(StrId::STR_MENU_DISP_SLEEP), + SettingInfo::Enum(StrId::STR_SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, + {StrId::STR_FIT, StrId::STR_CROP}, "sleepScreenCoverMode", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_COVER_FILTER, &CrossPointSettings::sleepScreenCoverFilter, + {StrId::STR_NONE_OPT, StrId::STR_FILTER_CONTRAST, StrId::STR_INVERTED}, "sleepScreenCoverFilter", + StrId::STR_CAT_DISPLAY), + SettingInfo::Enum( + StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, + {StrId::STR_OVERLAY_OFF, StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK}, + "sleepCoverOverlay", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_IMAGE_PICK_MODE, &CrossPointSettings::sleepImagePickMode, + {StrId::STR_RANDOM, StrId::STR_SEQUENTIAL}, "sleepImagePickMode", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, + {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", + StrId::STR_CAT_DISPLAY) + .withSubcategory(StrId::STR_MENU_DISP_BATTERY), + SettingInfo::Enum( + StrId::STR_REFRESH_FREQ, &CrossPointSettings::refreshFrequency, + {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, + "refreshFrequency", StrId::STR_CAT_DISPLAY) + .withSubcategory(StrId::STR_MENU_DISP_REFRESH), + SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_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_FONT_FAMILY), + 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), + SettingInfo::Toggle(StrId::STR_TEXT_AA, &CrossPointSettings::textAntiAliasing, "textAntiAliasing", + StrId::STR_CAT_READER) + .withSubmenu(StrId::STR_MENU_READER_FONT), + 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), + + // 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), - 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::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", - StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_SHOW_FILE_EXTENSIONS, &CrossPointSettings::showFileExtensions, - "showFileExtensions", StrId::STR_CAT_SYSTEM), - SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H}, - "clockFormat12h", StrId::STR_CAT_SYSTEM), - SettingInfo::Enum(StrId::STR_TIMEZONE, &CrossPointSettings::timeZone, - {StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, - StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, - StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, - StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, - "timeZone", StrId::STR_CAT_SYSTEM), - SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM), + // --- System --- + SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles", + StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_SHOW_FILE_EXTENSIONS, &CrossPointSettings::showFileExtensions, "showFileExtensions", + StrId::STR_CAT_SYSTEM), + SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H}, + "clockFormat12h", StrId::STR_CAT_SYSTEM), + SettingInfo::Enum(StrId::STR_TIMEZONE, &CrossPointSettings::timeZone, + {StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, + StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, + StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3, + StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}, + "timeZone", StrId::STR_CAT_SYSTEM), + SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM), - // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- - SettingInfo::DynamicString( - StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, - [](const std::string& v) { - KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); - KOREADER_STORE.saveToFile(); - }, - "koUsername", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicString( - StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, - [](const std::string& v) { - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); - KOREADER_STORE.saveToFile(); - }, - "koPassword", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicString( - StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, - [](const std::string& v) { - KOREADER_STORE.setServerUrl(v); - KOREADER_STORE.saveToFile(); - }, - "koServerUrl", StrId::STR_KOREADER_SYNC), - SettingInfo::DynamicEnum( - StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, - [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, - [](uint8_t v) { - KOREADER_STORE.setMatchMethod(static_cast(v)); - KOREADER_STORE.saveToFile(); - }, - "koMatchMethod", StrId::STR_KOREADER_SYNC), + // --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- + SettingInfo::DynamicString( + StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, + [](const std::string& v) { + KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); + KOREADER_STORE.saveToFile(); + }, + "koUsername", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicString( + StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, + [](const std::string& v) { + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); + KOREADER_STORE.saveToFile(); + }, + "koPassword", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicString( + StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, + [](const std::string& v) { + KOREADER_STORE.setServerUrl(v); + KOREADER_STORE.saveToFile(); + }, + "koServerUrl", StrId::STR_KOREADER_SYNC), + SettingInfo::DynamicEnum( + StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, + [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, + [](uint8_t v) { + KOREADER_STORE.setMatchMethod(static_cast(v)); + KOREADER_STORE.saveToFile(); + }, + "koMatchMethod", StrId::STR_KOREADER_SYNC), - // --- OPDS Browser (web-only, uses CrossPointSettings char arrays) --- - SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), - "opdsServerUrl", StrId::STR_OPDS_BROWSER), - SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername", - StrId::STR_OPDS_BROWSER), - SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword", - StrId::STR_OPDS_BROWSER) - .withObfuscated(), - // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- - SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, - "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, - "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, - {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", + // --- OPDS Browser (web-only, uses CrossPointSettings char arrays) --- + SettingInfo::String(StrId::STR_OPDS_SERVER_URL, SETTINGS.opdsServerUrl, sizeof(SETTINGS.opdsServerUrl), + "opdsServerUrl", StrId::STR_OPDS_BROWSER), + SettingInfo::String(StrId::STR_USERNAME, SETTINGS.opdsUsername, sizeof(SETTINGS.opdsUsername), "opdsUsername", + StrId::STR_OPDS_BROWSER), + SettingInfo::String(StrId::STR_PASSWORD, SETTINGS.opdsPassword, sizeof(SETTINGS.opdsPassword), "opdsPassword", + StrId::STR_OPDS_BROWSER) + .withObfuscated(), + // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- + SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, + "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_BOOK_PROGRESS_PERCENTAGE, &CrossPointSettings::statusBarBookProgressPercentage, + "statusBarBookProgressPercentage", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_PROGRESS_BAR, &CrossPointSettings::statusBarProgressBar, + {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarProgressBar", + StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, + {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, + "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, + {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", + StrId::STR_CUSTOMISE_STATUS_BAR), + SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_PROGRESS_BAR_THICKNESS, &CrossPointSettings::statusBarProgressBarThickness, - {StrId::STR_PROGRESS_BAR_THIN, StrId::STR_PROGRESS_BAR_MEDIUM, StrId::STR_PROGRESS_BAR_THICK}, - "statusBarProgressBarThickness", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Enum(StrId::STR_TITLE, &CrossPointSettings::statusBarTitle, - {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE}, "statusBarTitle", + SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery", - StrId::STR_CUSTOMISE_STATUS_BAR), - SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock", - StrId::STR_CUSTOMISE_STATUS_BAR), - }; - return list; -} +}; +} // namespace SettingsListDetail + +inline const std::vector& getSettingsList() { return SettingsListDetail::list; } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d6f186d3..16c6d43f 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -333,17 +333,21 @@ void SettingsActivity::render(RenderLock&&) { 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 std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); } - return valueText; + if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { + const uint8_t value = SETTINGS.*(setting.valuePtr); + return std::string(I18N.get(setting.enumValues[value])); + } + if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { + return std::to_string(SETTINGS.*(setting.valuePtr)); + } + if (setting.type == SettingType::ACTION && !setting.isSeparator) { + return std::string(">>"); + } + return std::string(); }, true); diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp index 61f6764c..6e25d106 100644 --- a/src/activities/settings/SettingsSubmenuActivity.cpp +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -22,8 +22,7 @@ void SettingsSubmenuActivity::onEnter() { Activity::onEnter(); itemCount = static_cast(items.size()); - const auto pred = UITheme::makeSelectablePredicate(itemCount, - [this](int i) { return items[i].getTitle(); }); + const auto pred = UITheme::makeSelectablePredicate(itemCount, [this](int i) { return items[i].getTitle(); }); buttonNavigator.setSelectablePredicate(pred, itemCount); if (!pred(selectedIndex)) { selectedIndex = buttonNavigator.nextIndex(selectedIndex); @@ -151,6 +150,9 @@ void SettingsSubmenuActivity::render(RenderLock&&) { if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { return std::to_string(SETTINGS.*(setting.valuePtr)); } + if (setting.type == SettingType::ACTION && !setting.isSeparator) { + return std::string(">>"); + } return std::string(); }, true); diff --git a/src/main.cpp b/src/main.cpp index faa059ba..ceb16897 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -204,10 +204,12 @@ void setup() { HalSystem::checkPanic(); HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info - + LOG_DBG("MAIN", "System initialized, now setting up environment, millis=%lu", millis()); SETTINGS.loadFromFile(); + LOG_DBG("MAIN", "Settings loaded, now setting up clock and localization, millis=%lu", millis()); HalClock::applyTimezone(SETTINGS.timeZone); I18N.loadSettings(); + LOG_DBG("MAIN", "Localization loaded, now setting up theme and button navigation, millis=%lu", millis()); KOREADER_STORE.loadFromFile(); WEATHER_SETTINGS.loadFromFile(); UITheme::getInstance().reload(); From 54807911e72aed68701494776c8cb9265c117fa6 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 20:30:14 +0200 Subject: [PATCH 09/18] Introduce common helper functions --- src/SettingsList.h | 2 +- src/activities/MenuListActivity.cpp | 70 ++++ src/activities/MenuListActivity.h | 107 ++++++ .../reader/EpubReaderMenuActivity.cpp | 309 +++++++++--------- .../reader/EpubReaderMenuActivity.h | 45 +-- .../settings/KOReaderSettingsActivity.cpp | 149 +++------ .../settings/KOReaderSettingsActivity.h | 21 +- .../settings/SettingActionDispatch.cpp | 51 +++ .../settings/SettingActionDispatch.h | 13 + src/activities/settings/SettingInfo.cpp | 85 +++++ src/activities/settings/SettingInfo.h | 190 +++++++++++ src/activities/settings/SettingsActivity.cpp | 118 +------ src/activities/settings/SettingsActivity.h | 175 +--------- .../settings/SettingsSubmenuActivity.cpp | 143 +------- .../settings/SettingsSubmenuActivity.h | 22 +- src/components/UITheme.h | 19 +- 16 files changed, 790 insertions(+), 729 deletions(-) create mode 100644 src/activities/MenuListActivity.cpp create mode 100644 src/activities/MenuListActivity.h create mode 100644 src/activities/settings/SettingActionDispatch.cpp create mode 100644 src/activities/settings/SettingActionDispatch.h create mode 100644 src/activities/settings/SettingInfo.cpp create mode 100644 src/activities/settings/SettingInfo.h diff --git a/src/SettingsList.h b/src/SettingsList.h index 928eb68d..d8eae461 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -6,7 +6,7 @@ #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. // diff --git a/src/activities/MenuListActivity.cpp b/src/activities/MenuListActivity.cpp new file mode 100644 index 00000000..f253e157 --- /dev/null +++ b/src/activities/MenuListActivity.cpp @@ -0,0 +1,70 @@ +#include "MenuListActivity.h" + +#include + +#include "MappedInputManager.h" +#include "components/UITheme.h" + +void MenuListActivity::initMenuList() { + const int count = static_cast(menuItems.size()); + const auto pred = UITheme::makeSelectablePredicate(count, [this](int i) { return menuItems[i].getTitle(); }); + buttonNavigator.setSelectablePredicate(pred, count); + if (count > 0 && !pred(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } +} + +void MenuListActivity::onEnter() { + Activity::onEnter(); + initMenuList(); + requestUpdate(); +} + +void MenuListActivity::handleNavigation() { + buttonNavigator.onNext([this] { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + requestUpdate(); + }); + buttonNavigator.onPrevious([this] { + selectedIndex = buttonNavigator.previousIndex(selectedIndex); + requestUpdate(); + }); +} + +void MenuListActivity::toggleCurrentItem() { + if (selectedIndex < 0 || selectedIndex >= static_cast(menuItems.size())) return; + auto& item = menuItems[selectedIndex]; + if (item.isSeparator) return; + + if (item.type == SettingType::ACTION) { + onActionSelected(selectedIndex); + return; + } + + item.toggleValue(); + onSettingToggled(selectedIndex); + requestUpdate(); +} + +std::string MenuListActivity::getItemValueString(int index) const { + return menuItems[index].getDisplayValue(); +} + +void MenuListActivity::drawMenuList(const Rect& rect) { + const int count = static_cast(menuItems.size()); + GUI.drawList( + renderer, rect, count, selectedIndex, [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, + [this](int index) { return getItemValueString(index); }, true); +} + +void MenuListActivity::loop() { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + onBackPressed(); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + toggleCurrentItem(); + return; + } + handleNavigation(); +} diff --git a/src/activities/MenuListActivity.h b/src/activities/MenuListActivity.h new file mode 100644 index 00000000..f8a8fdda --- /dev/null +++ b/src/activities/MenuListActivity.h @@ -0,0 +1,107 @@ +#pragma once +#include + +#include "Activity.h" +#include "settings/SettingInfo.h" +#include "util/ButtonNavigator.h" + +struct Rect; + +// Base class for activities that display a scrollable list of SettingInfo items. +// Provides common navigation, toggle/cycle logic, and drawList rendering. +// +// Subclasses populate `menuItems` (typically in the constructor or onEnter()), +// then rely on the default loop()/onEnter() or override selectively. +// +// === Minimal example === +// +// class MyMenuActivity final : public MenuListActivity { +// public: +// explicit MyMenuActivity(GfxRenderer& r, MappedInputManager& m) +// : MenuListActivity("MyMenu", r, m) { +// // Separator header +// menuItems.push_back(SettingInfo::Separator(StrId::STR_MY_SECTION)); +// +// // Toggle bound to a CrossPointSettings field +// menuItems.push_back(SettingInfo::Toggle(StrId::STR_MY_TOGGLE, &CrossPointSettings::myFlag)); +// +// // Enum bound to a CrossPointSettings field +// menuItems.push_back(SettingInfo::Enum(StrId::STR_MY_ENUM, &CrossPointSettings::myEnum, +// {StrId::STR_OPTION_A, StrId::STR_OPTION_B, StrId::STR_OPTION_C})); +// +// // DynamicEnum with local state (getter/setter lambdas) +// menuItems.push_back(SettingInfo::DynamicEnum(StrId::STR_MY_DYNAMIC, +// {StrId::STR_LOW, StrId::STR_HIGH}, +// [this]() -> uint8_t { return localValue; }, +// [this](uint8_t v) { localValue = v; })); +// +// // Action item (handled in onActionSelected) +// menuItems.push_back(SettingInfo::Action(StrId::STR_DO_SOMETHING, SettingAction::None)); +// } +// +// // render(): draw header/footer around the list +// void render(RenderLock&&) override { +// renderer.clearScreen(); +// const Rect r = UITheme::getContentRect(renderer, true, false); +// drawMenuList(r); // <-- draws the item list +// GUI.drawButtonHints(renderer, ...); +// renderer.displayBuffer(); +// } +// +// private: +// uint8_t localValue = 0; +// +// // Called when an ACTION item is confirmed +// void onActionSelected(int index) override { +// if (menuItems[index].nameId == StrId::STR_DO_SOMETHING) { /* ... */ } +// } +// +// // Called after a TOGGLE/ENUM/VALUE is cycled +// void onSettingToggled(int) override { SETTINGS.saveToFile(); } +// }; +// +// The base class provides: +// onEnter() — wires up the selectable-predicate (skips separators) and requestUpdate(). +// loop() — handles Back (→ onBackPressed), Confirm (→ toggleCurrentItem), and nav. +// drawMenuList(rect) — calls GUI.drawList with SettingInfo-based title/value lambdas. +// getItemValueString(i) — override for custom per-item value display. +// onBackPressed() — override to customise Back behaviour (default: finish()). +// +class MenuListActivity : public Activity { + protected: + std::vector menuItems; + int selectedIndex = 0; + ButtonNavigator buttonNavigator; + + // Call after building/rebuilding menuItems to wire up the selectable predicate. + void initMenuList(); + + // Handle up/down navigation via buttonNavigator. Call from loop() if overriding. + void handleNavigation(); + + // Toggle/cycle the currently selected item. For ACTION items, delegates to onActionSelected(). + void toggleCurrentItem(); + + // Draw the list into the given rect using GUI.drawList(). + void drawMenuList(const Rect& rect); + + // Override to provide custom value display for specific items. + // Default returns SettingInfo::getDisplayValue(). + [[nodiscard]] virtual std::string getItemValueString(int index) const; + + // Called when an ACTION-type item is confirmed. Override to handle actions. + virtual void onActionSelected(int index) {} + + // Called when Back is pressed. Default calls finish(). + virtual void onBackPressed() { finish(); } + + // Called after a TOGGLE/ENUM/VALUE item has been toggled. + // Override to persist changes or trigger side-effects. + virtual void onSettingToggled(int /*index*/) {} + + public: + using Activity::Activity; + + void onEnter() override; + void loop() override; +}; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index a047c6e9..70289dde 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -14,149 +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) {} - -std::string EpubReaderMenuActivity::MenuItem::getTitle() const { - const auto t = I18N.get(labelId); - return isSeparator ? UITheme::makeSeparatorTitle(t) : t; + bookProgressPercent(bookProgressPercent) { + buildMenuItems(hasFootnotes); } -std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { - std::vector items; - items.reserve(18); - // Navigation - items.push_back(MenuItem::separator(StrId::STR_READER_NAVIGATION)); - 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(MenuItem::separator(StrId::STR_READER_APPEARANCE)); - items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE}); - items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES}); - items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS}); - items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); + // --- Appearance --- + menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE)); - // Synchronisation (only if credentials are set, to avoid confusion) + // Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2 + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_EMBEDDED_STYLE, {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF}, + [this]() -> uint8_t { + if (pendingEmbeddedStyleOverride < 0) return 0; + if (pendingEmbeddedStyleOverride > 0) return 1; + return 2; + }, + [this](uint8_t v) { + if (v == 0) + pendingEmbeddedStyleOverride = -1; + else if (v == 1) + pendingEmbeddedStyleOverride = 1; + else + pendingEmbeddedStyleOverride = 0; + })); + + // Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2) + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_IMAGES, + {StrId::STR_DEFAULT_VALUE, StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, + StrId::STR_IMAGES_SUPPRESS}, + [this]() -> uint8_t { return (pendingImageRenderingOverride < 0) ? 0 : (pendingImageRenderingOverride + 1); }, + [this](uint8_t v) { pendingImageRenderingOverride = (v == 0) ? -1 : static_cast(v - 1); })); + + // Text darkness: straightforward 0-3 cycle + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_TEXT_DARKNESS, + {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, + [this]() -> uint8_t { return pendingTextDarkness; }, + [this](uint8_t v) { pendingTextDarkness = v; })); + + // Orientation: straightforward 0-3 cycle + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_ORIENTATION, + {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, + [this]() -> uint8_t { return pendingOrientation; }, + [this](uint8_t v) { pendingOrientation = v; })); + + // --- Synchronisation (only if credentials are set) --- if (KOREADER_STORE.hasCredentials()) { - items.push_back(MenuItem::separator(StrId::STR_KOREADER_SYNC)); - 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(MenuItem::separator(StrId::STR_READER_TOOLS)); - 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)); +} + +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; + } +} + +void EpubReaderMenuActivity::finishWithAction(MenuAction action) { + setResult(MenuResult{static_cast(action), pendingOrientation, selectedPageTurnOption, + pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness}); + finish(); +} + +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(); + 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() { - Activity::onEnter(); - const auto pred = UITheme::makeSelectablePredicate(static_cast(menuItems.size()), - [this](int i) { return menuItems[i].getTitle(); }); - buttonNavigator.setSelectablePredicate(pred, static_cast(menuItems.size())); - if (!pred(selectedIndex)) { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); - } - requestUpdate(); -} - -void EpubReaderMenuActivity::onExit() { Activity::onExit(); } - -void EpubReaderMenuActivity::loop() { - // Handle navigation - buttonNavigator.onNext([this] { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); - requestUpdate(); - }); - - buttonNavigator.onPrevious([this] { - selectedIndex = buttonNavigator.previousIndex(selectedIndex); - requestUpdate(); - }); - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - const auto selectedAction = menuItems[selectedIndex].action; - if (selectedAction == MenuAction::NONE) { - return; - } - if (selectedAction == MenuAction::ROTATE_SCREEN) { - // Cycle orientation preview locally; actual rotation happens on menu exit. - pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::AUTO_PAGE_TURN) { - selectedPageTurnOption = (selectedPageTurnOption + 1) % pageTurnLabels.size(); - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::EMBEDDED_STYLE) { - // Cycle per-book override: default -> ON -> OFF -> default. - if (pendingEmbeddedStyleOverride < 0) { - pendingEmbeddedStyleOverride = 1; - } else if (pendingEmbeddedStyleOverride > 0) { - pendingEmbeddedStyleOverride = 0; - } else { - pendingEmbeddedStyleOverride = -1; - } - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::IMAGE_RENDERING) { - // Cycle per-book override: default -> display -> placeholder -> suppress -> default. - if (pendingImageRenderingOverride < 0) { - pendingImageRenderingOverride = 0; - } else if (pendingImageRenderingOverride >= 2) { - pendingImageRenderingOverride = -1; - } else { - pendingImageRenderingOverride++; - } - requestUpdate(); - return; - } - - if (selectedAction == MenuAction::TEXT_DARKNESS) { - pendingTextDarkness = (pendingTextDarkness + 1) % textDarknessLabels.size(); - requestUpdate(); - return; - } - - setResult(MenuResult{static_cast(selectedAction), pendingOrientation, selectedPageTurnOption, - pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingTextDarkness}); - finish(); - return; - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - result.data = MenuResult{-1, - pendingOrientation, - selectedPageTurnOption, - pendingEmbeddedStyleOverride, - pendingImageRenderingOverride, - pendingTextDarkness}; - setResult(std::move(result)); - finish(); - return; - } + MenuListActivity::onEnter(); } void EpubReaderMenuActivity::render(RenderLock&&) { @@ -166,7 +191,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,38 +208,7 @@ void EpubReaderMenuActivity::render(RenderLock&&) { // Menu Items const int startY = 75 + contentRect.y; const int listHeight = contentRect.height - (startY - contentRect.y); - - GUI.drawList( - renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), - selectedIndex, [this](int index) { return menuItems[index].getTitle(); }, nullptr, nullptr, - [this](int index) { - const auto& item = menuItems[index]; - switch (item.action) { - case MenuAction::ROTATE_SCREEN: - return std::string(I18N.get(orientationLabels[pendingOrientation])); - case MenuAction::AUTO_PAGE_TURN: - return std::string(pageTurnLabels[selectedPageTurnOption]); - case MenuAction::EMBEDDED_STYLE: - if (pendingEmbeddedStyleOverride == 1) { - return std::string(tr(STR_STATE_ON)); - } else if (pendingEmbeddedStyleOverride == 0) { - return std::string(tr(STR_STATE_OFF)); - } - return std::string(tr(STR_DEFAULT_VALUE)); - case MenuAction::IMAGE_RENDERING: - if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) { - return std::string(I18N.get(imageRenderingLabels[pendingImageRenderingOverride])); - } - return std::string(tr(STR_DEFAULT_VALUE)); - case MenuAction::TEXT_DARKNESS: { - const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0; - return std::string(I18N.get(textDarknessLabels[idx])); - } - default: - return std::string(); - } - }, - true); + drawMenuList(Rect{contentRect.x, startY, contentRect.width, listHeight}); // Footer / Hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index a0617dab..ea1d5dc7 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -5,12 +5,12 @@ #include #include -#include "../Activity.h" -#include "util/ButtonNavigator.h" +#include "../MenuListActivity.h" -class EpubReaderMenuActivity final : public Activity { +class EpubReaderMenuActivity final : public MenuListActivity { public: - // Menu actions available from the reader menu. + // Menu actions identified by StrId of the menu item. + // Used by the parent activity to interpret the result. enum class MenuAction { NONE, SELECT_CHAPTER, @@ -36,40 +36,31 @@ class EpubReaderMenuActivity final : public Activity { const uint8_t initialTextDarkness); void onEnter() override; - void onExit() override; - void loop() override; void render(RenderLock&&) override; private: - struct MenuItem { - MenuAction action; - StrId labelId; - bool isSeparator = false; - static MenuItem separator(StrId label) { return {MenuAction::NONE, label, true}; } - [[nodiscard]] std::string getTitle() const; - }; + void buildMenuItems(bool hasFootnotes); + void finishWithAction(MenuAction action); - static std::vector buildMenuItems(bool hasFootnotes); + // MenuListActivity overrides + std::string getItemValueString(int index) const override; + void onActionSelected(int index) override; + void onBackPressed() override; + void onSettingToggled(int index) override; - // Fixed menu layout - const std::vector menuItems; + // Map from StrId to MenuAction for result passing + static MenuAction actionForNameId(StrId nameId); - int selectedIndex = 0; - - ButtonNavigator buttonNavigator; - std::string title = "Reader Menu"; + // Pending state (mutated locally, returned to parent on finish) uint8_t pendingOrientation = 0; uint8_t selectedPageTurnOption = 0; int8_t pendingEmbeddedStyleOverride = -1; int8_t pendingImageRenderingOverride = -1; uint8_t pendingTextDarkness = 1; - const std::vector orientationLabels = {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, - StrId::STR_LANDSCAPE_CCW}; - const std::vector textDarknessLabels = {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, - StrId::STR_MAX_DARK}; - const std::vector imageRenderingLabels = {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, - StrId::STR_IMAGES_SUPPRESS}; - const std::vector pageTurnLabels = {I18N.get(StrId::STR_STATE_OFF), "1", "3", "6", "12"}; + + static constexpr const char* pageTurnLabels[] = {"", "1", "3", "6", "12"}; + + std::string title = "Reader Menu"; int currentPage = 0; int totalPages = 0; int bookProgressPercent = 0; diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index acd57d01..cdd31026 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -3,8 +3,6 @@ #include #include -#include - #include "KOReaderAuthActivity.h" #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" @@ -12,51 +10,58 @@ #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_USERNAME, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_PASSWORD, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_SYNC_SERVER_URL, SettingAction::None)); -void KOReaderSettingsActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - finish(); - return; - } + // Document matching: DynamicEnum toggling between Filename and Binary + menuItems.push_back(SettingInfo::DynamicEnum( + StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, + [] { return static_cast(KOREADER_STORE.getMatchMethod()); }, + [](uint8_t v) { + KOREADER_STORE.setMatchMethod(static_cast(v)); + KOREADER_STORE.saveToFile(); + })); - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - handleSelection(); - return; - } - - // Handle navigation - buttonNavigator.onNext([this] { - selectedIndex = (selectedIndex + 1) % MENU_ITEMS; - requestUpdate(); - }); - - buttonNavigator.onPrevious([this] { - selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; - requestUpdate(); - }); + // Authenticate and Register: ACTION items + menuItems.push_back(SettingInfo::Action(StrId::STR_AUTHENTICATE, SettingAction::None)); + menuItems.push_back(SettingInfo::Action(StrId::STR_REGISTER, SettingAction::None)); } -void KOReaderSettingsActivity::handleSelection() { - if (selectedIndex == 0) { - // Username +std::string KOReaderSettingsActivity::getItemValueString(int index) const { + const auto& item = menuItems[index]; + + if (item.nameId == StrId::STR_USERNAME) { + auto username = KOREADER_STORE.getUsername(); + return username.empty() ? std::string(tr(STR_NOT_SET)) : username; + } + if (item.nameId == StrId::STR_PASSWORD) { + return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); + } + if (item.nameId == StrId::STR_SYNC_SERVER_URL) { + auto serverUrl = KOREADER_STORE.getServerUrl(); + return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; + } + if (item.nameId == StrId::STR_AUTHENTICATE || item.nameId == StrId::STR_REGISTER) { + return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; + } + + return MenuListActivity::getItemValueString(index); +} + +void KOReaderSettingsActivity::onActionSelected(int index) { + const auto& item = menuItems[index]; + + if (item.nameId == StrId::STR_USERNAME) { startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_USERNAME), - KOREADER_STORE.getUsername(), - 64, // maxLength - false), // not password + KOREADER_STORE.getUsername(), 64, false), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -64,12 +69,9 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); } }); - } else if (selectedIndex == 1) { - // Password + } else if (item.nameId == StrId::STR_PASSWORD) { startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), - KOREADER_STORE.getPassword(), - 64, // maxLength - false), // show characters + KOREADER_STORE.getPassword(), 64, false), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -77,14 +79,11 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); } }); - } else if (selectedIndex == 2) { - // Sync Server URL - prefill with https:// if empty to save typing + } else if (item.nameId == StrId::STR_SYNC_SERVER_URL) { const std::string currentUrl = KOREADER_STORE.getServerUrl(); const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; startActivityForResult( - std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, - 128, // maxLength - URLs can be long - false), // not password + std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 128, false), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); @@ -93,28 +92,13 @@ void KOReaderSettingsActivity::handleSelection() { KOREADER_STORE.saveToFile(); } }); - } else if (selectedIndex == 3) { - // Document Matching - toggle between Filename and Binary - const auto current = KOREADER_STORE.getMatchMethod(); - const auto newMethod = - (current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME; - KOREADER_STORE.setMatchMethod(newMethod); - KOREADER_STORE.saveToFile(); - requestUpdate(); - } else if (selectedIndex == 4) { - // Authenticate - if (!KOREADER_STORE.hasCredentials()) { - // Can't authenticate without credentials - just show message briefly - return; - } + } else if (item.nameId == StrId::STR_AUTHENTICATE) { + if (!KOREADER_STORE.hasCredentials()) return; startActivityForResult( std::make_unique(renderer, mappedInput, KOReaderAuthActivity::Mode::LOGIN), [](const ActivityResult&) {}); - } else if (selectedIndex == 5) { - // Register - if (!KOREADER_STORE.hasCredentials()) { - return; - } + } else if (item.nameId == StrId::STR_REGISTER) { + if (!KOREADER_STORE.hasCredentials()) return; startActivityForResult( std::make_unique(renderer, mappedInput, KOReaderAuthActivity::Mode::REGISTER), [](const ActivityResult&) {}); @@ -132,33 +116,8 @@ void KOReaderSettingsActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing * 2; - GUI.drawList( - renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, static_cast(MENU_ITEMS), - static_cast(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, - nullptr, - [this](int index) { - // Draw status for each setting - if (index == 0) { - auto username = KOREADER_STORE.getUsername(); - return username.empty() ? std::string(tr(STR_NOT_SET)) : username; - } else if (index == 1) { - return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); - } else if (index == 2) { - auto serverUrl = KOREADER_STORE.getServerUrl(); - return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; - } else if (index == 3) { - return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME)) - : std::string(tr(STR_BINARY)); - } else if (index == 4) { - return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; - } else if (index == 5) { - return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; - } - return std::string(tr(STR_NOT_SET)); - }, - true); + drawMenuList(Rect{contentRect.x, contentTop, contentRect.width, contentHeight}); - // Draw help text at bottom const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index f32db8a5..2448932e 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -1,26 +1,21 @@ #pragma once -#include "activities/Activity.h" -#include "util/ButtonNavigator.h" +#include "activities/MenuListActivity.h" /** * Submenu for KOReader Sync settings. - * Shows username, password, and authenticate options. + * Shows username, password, server URL, document matching, authenticate, and register. */ -class KOReaderSettingsActivity final : public Activity { +class KOReaderSettingsActivity final : public MenuListActivity { public: - explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("KOReaderSettings", renderer, mappedInput) {} + explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput); - void onEnter() override; - void onExit() override; - void loop() override; void render(RenderLock&&) override; private: - ButtonNavigator buttonNavigator; + void buildMenuItems(); - size_t selectedIndex = 0; - - void handleSelection(); + // MenuListActivity overrides + std::string getItemValueString(int index) const override; + void onActionSelected(int index) override; }; diff --git a/src/activities/settings/SettingActionDispatch.cpp b/src/activities/settings/SettingActionDispatch.cpp new file mode 100644 index 00000000..45780107 --- /dev/null +++ b/src/activities/settings/SettingActionDispatch.cpp @@ -0,0 +1,51 @@ +#include "SettingActionDispatch.h" + +#include "ButtonRemapActivity.h" +#include "CalibreSettingsActivity.h" +#include "ClearCacheActivity.h" +#include "ClockSettingsActivity.h" +#include "DetectTimezoneActivity.h" +#include "KOReaderSettingsActivity.h" +#include "LanguageSelectActivity.h" +#include "OtaUpdateActivity.h" +#include "StatusBarSettingsActivity.h" +#include "SyncTimeActivity.h" +#include "SystemInformationActivity.h" +#include "activities/network/WifiSelectionActivity.h" +#include "activities/weather/WeatherSettingsActivity.h" + +std::unique_ptr createActivityForAction(SettingAction action, GfxRenderer& renderer, + MappedInputManager& mappedInput) { + switch (action) { + case SettingAction::RemapFrontButtons: + return std::make_unique(renderer, mappedInput); + case SettingAction::CustomiseStatusBar: + return std::make_unique(renderer, mappedInput); + case SettingAction::ClockSettings: + return std::make_unique(renderer, mappedInput); + case SettingAction::KOReaderSync: + return std::make_unique(renderer, mappedInput); + case SettingAction::OPDSBrowser: + return std::make_unique(renderer, mappedInput); + case SettingAction::Network: + return std::make_unique(renderer, mappedInput, false); + case SettingAction::ClearCache: + return std::make_unique(renderer, mappedInput); + case SettingAction::CheckForUpdates: + return std::make_unique(renderer, mappedInput); + case SettingAction::Language: + return std::make_unique(renderer, mappedInput); + case SettingAction::Weather: + return std::make_unique(renderer, mappedInput); + case SettingAction::SystemInfo: + return std::make_unique(renderer, mappedInput); + case SettingAction::SyncTime: + return std::make_unique(renderer, mappedInput); + case SettingAction::DetectTimezone: + return std::make_unique(renderer, mappedInput); + case SettingAction::Submenu: + case SettingAction::None: + return nullptr; + } + return nullptr; +} diff --git a/src/activities/settings/SettingActionDispatch.h b/src/activities/settings/SettingActionDispatch.h new file mode 100644 index 00000000..926fb875 --- /dev/null +++ b/src/activities/settings/SettingActionDispatch.h @@ -0,0 +1,13 @@ +#pragma once +#include + +#include "SettingInfo.h" + +class Activity; +class GfxRenderer; +class MappedInputManager; + +// Creates the sub-activity corresponding to the given SettingAction. +// Returns nullptr for None, Submenu, or unknown actions (caller handles those). +std::unique_ptr createActivityForAction(SettingAction action, GfxRenderer& renderer, + MappedInputManager& mappedInput); diff --git a/src/activities/settings/SettingInfo.cpp b/src/activities/settings/SettingInfo.cpp new file mode 100644 index 00000000..d1356ca4 --- /dev/null +++ b/src/activities/settings/SettingInfo.cpp @@ -0,0 +1,85 @@ +#include "SettingInfo.h" + +#include + +#include "CrossPointSettings.h" +#include "components/UITheme.h" + +std::string SettingInfo::getTitle() const { + const auto t = I18N.get(nameId); + return isSeparator ? UITheme::makeSeparatorTitle(t) : t; +} + +std::string SettingInfo::getDisplayValue() const { + if (isSeparator) return {}; + + switch (type) { + case SettingType::TOGGLE: { + bool value; + if (valuePtr) + value = SETTINGS.*(valuePtr); + else if (valueGetter) + value = valueGetter(); + else + return {}; + return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + } + case SettingType::ENUM: { + uint8_t value; + if (valuePtr) + value = SETTINGS.*(valuePtr); + else if (valueGetter) + value = valueGetter(); + else + return {}; + if (value < enumValues.size()) return std::string(I18N.get(enumValues[value])); + return {}; + } + case SettingType::VALUE: { + if (valuePtr) return std::to_string(SETTINGS.*(valuePtr)); + if (valueGetter) return std::to_string(valueGetter()); + return {}; + } + case SettingType::ACTION: + return std::string(">>"); + case SettingType::STRING: + return {}; + } + return {}; +} + +void SettingInfo::toggleValue() const { + if (isSeparator) return; + + switch (type) { + case SettingType::TOGGLE: + if (valuePtr) { + SETTINGS.*(valuePtr) = !(SETTINGS.*(valuePtr)); + } else if (valueGetter && valueSetter) { + valueSetter(!valueGetter()); + } + break; + + case SettingType::ENUM: { + const auto count = static_cast(enumValues.size()); + if (count == 0) break; + if (valuePtr) { + SETTINGS.*(valuePtr) = (SETTINGS.*(valuePtr) + 1) % count; + } else if (valueGetter && valueSetter) { + valueSetter((valueGetter() + 1) % count); + } + break; + } + + case SettingType::VALUE: + if (valuePtr) { + const auto current = static_cast(SETTINGS.*(valuePtr)); + SETTINGS.*(valuePtr) = + (current + valueRange.step > valueRange.max) ? valueRange.min : current + valueRange.step; + } + break; + + default: + break; + } +} diff --git a/src/activities/settings/SettingInfo.h b/src/activities/settings/SettingInfo.h new file mode 100644 index 00000000..108a6e34 --- /dev/null +++ b/src/activities/settings/SettingInfo.h @@ -0,0 +1,190 @@ +#pragma once +#include + +#include +#include +#include + +#include "CrossPointSettings.h" + +enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; + +enum class SettingAction { + None, + RemapFrontButtons, + CustomiseStatusBar, + ClockSettings, + KOReaderSync, + OPDSBrowser, + Network, + ClearCache, + CheckForUpdates, + Language, + SystemInfo, + DetectTimezone, + SyncTime, + Weather, + Submenu, +}; + +struct SettingInfo { + StrId nameId; + SettingType type; + uint8_t CrossPointSettings::* valuePtr = nullptr; + std::vector enumValues; + SettingAction action = SettingAction::None; + + struct ValueRange { + uint8_t min; + uint8_t max; + uint8_t step; + }; + ValueRange valueRange = {}; + + const char* key = nullptr; // JSON API key (nullptr for ACTION types) + StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping + bool obfuscated = false; // Save/load via base64 obfuscation (passwords) + + // Direct char[] string fields (for settings stored in CrossPointSettings) + size_t stringOffset = 0; + size_t stringMaxLen = 0; + + // Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore) + std::function valueGetter; + std::function valueSetter; + std::function stringGetter; + std::function stringSetter; + + SettingInfo& withObfuscated() { + obfuscated = true; + return *this; + } + + static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::TOGGLE; + s.valuePtr = ptr; + s.key = key; + s.category = category; + return s; + } + + static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector values, + const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ENUM; + s.valuePtr = ptr; + s.enumValues = std::move(values); + s.key = key; + s.category = category; + return s; + } + + static SettingInfo Action(StrId nameId, SettingAction action) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ACTION; + s.action = action; + return s; + } + + static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange, + const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::VALUE; + s.valuePtr = ptr; + s.valueRange = valueRange; + s.key = key; + s.category = category; + return s; + } + + static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::STRING; + s.stringOffset = (size_t)ptr - (size_t)&SETTINGS; + s.stringMaxLen = maxLen; + s.key = key; + s.category = category; + return s; + } + + static SettingInfo DynamicEnum(StrId nameId, std::vector values, std::function getter, + std::function setter, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ENUM; + s.enumValues = std::move(values); + s.valueGetter = std::move(getter); + s.valueSetter = std::move(setter); + s.key = key; + s.category = category; + return s; + } + + static SettingInfo DynamicString(StrId nameId, std::function getter, + std::function setter, const char* key = nullptr, + StrId category = StrId::STR_NONE_OPT) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::STRING; + s.stringGetter = std::move(getter); + s.stringSetter = std::move(setter); + s.key = key; + s.category = category; + return s; + } + + static SettingInfo Separator(StrId nameId) { + SettingInfo s; + s.nameId = nameId; + s.type = SettingType::ACTION; + s.isSeparator = true; + return s; + } + + bool isSeparator = false; + StrId subcategory = StrId::STR_NONE_OPT; // Triggers a separator row on first use and on change + StrId submenu = StrId::STR_NONE_OPT; // Routes item into a submenu; hidden from main list + + // Inserts a separator row in the parent tab when this item's subcategory first appears or changes. + SettingInfo& withSubcategory(StrId sub) { + subcategory = sub; + return *this; + } + + // Hides this item from the parent tab and places it inside a SettingsSubmenuActivity instead. + // All items sharing the same submenu StrId are grouped under one placeholder entry. + SettingInfo& withSubmenu(StrId sub) { + submenu = sub; + return *this; + } + + // For internal use by SettingsActivity: placeholder entry that launches the submenu. + static SettingInfo SubmenuEntry(StrId titleId) { + SettingInfo s; + s.nameId = titleId; + s.type = SettingType::ACTION; + s.action = SettingAction::Submenu; + return s; + } + + // Returns the localised title; separators are decorated automatically. + [[nodiscard]] std::string getTitle() const; + + // Returns the current display value string (ON/OFF for toggles, enum label, numeric value, >>). + [[nodiscard]] std::string getDisplayValue() const; + + // Toggles/cycles the underlying value for TOGGLE, ENUM, and VALUE types. + // Does nothing for ACTION and STRING types (callers handle those separately). + // Marked const because it mutates the external SETTINGS global (via valuePtr), + // not the SettingInfo itself. + void toggleValue() const; +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 16c6d43f..7331bab4 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -4,34 +4,17 @@ #include #include -#include "ButtonRemapActivity.h" -#include "CalibreSettingsActivity.h" -#include "ClearCacheActivity.h" -#include "ClockSettingsActivity.h" #include "CrossPointSettings.h" -#include "DetectTimezoneActivity.h" -#include "KOReaderSettingsActivity.h" -#include "LanguageSelectActivity.h" #include "MappedInputManager.h" -#include "OtaUpdateActivity.h" +#include "SettingActionDispatch.h" #include "SettingsList.h" #include "SettingsSubmenuActivity.h" -#include "StatusBarSettingsActivity.h" -#include "SyncTimeActivity.h" -#include "SystemInformationActivity.h" -#include "activities/network/WifiSelectionActivity.h" -#include "activities/weather/WeatherSettingsActivity.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}; -std::string SettingInfo::getTitle() const { - const auto t = I18N.get(nameId); - return isSeparator ? UITheme::makeSeparatorTitle(t) : t; -} - bool SettingsActivity::isListItemSelectable(int settingIdx) const { return settingIdx >= 0 && settingIdx < settingsCount && !(*currentSettings)[settingIdx].isSeparator; } @@ -225,82 +208,24 @@ void SettingsActivity::toggleCurrentSetting() { const auto& setting = (*currentSettings)[selectedSetting]; if (setting.isSeparator) return; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - // Toggle the boolean value using the member pointer - const bool currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = !currentValue; - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t currentValue = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast(setting.enumValues.size()); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - const int8_t currentValue = SETTINGS.*(setting.valuePtr); - if (currentValue + setting.valueRange.step > setting.valueRange.max) { - SETTINGS.*(setting.valuePtr) = setting.valueRange.min; - } else { - SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; - } - } else if (setting.type == SettingType::ACTION) { + if (setting.type == SettingType::ACTION) { auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; - switch (setting.action) { - case SettingAction::RemapFrontButtons: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::CustomiseStatusBar: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::ClockSettings: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::KOReaderSync: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::OPDSBrowser: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Network: - startActivityForResult(std::make_unique(renderer, mappedInput, false), resultHandler); - break; - case SettingAction::ClearCache: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::CheckForUpdates: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Language: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Weather: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::SystemInfo: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::SyncTime: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::DetectTimezone: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - break; - case SettingAction::Submenu: { - const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(), - [&setting](const SubmenuData& d) { return d.id == setting.nameId; }); - if (it != submenuData.cend()) { - startActivityForResult( - std::make_unique(renderer, mappedInput, setting.nameId, it->items), - resultHandler); - } - break; + if (setting.action == SettingAction::Submenu) { + const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(), + [&setting](const SubmenuData& d) { return d.id == setting.nameId; }); + if (it != submenuData.cend()) { + startActivityForResult( + std::make_unique(renderer, mappedInput, setting.nameId, it->items), resultHandler); } - case SettingAction::None: - // Do nothing - break; + } 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(); } @@ -331,24 +256,7 @@ void SettingsActivity::render(RenderLock&&) { (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)}, settingsCount, selectedSettingIndex - 1, [&settings](int index) { return settings[index].getTitle(); }, nullptr, nullptr, - [&settings](int i) { - const auto& setting = settings[i]; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - const bool value = SETTINGS.*(setting.valuePtr); - return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); - } - if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(setting.valuePtr); - return std::string(I18N.get(setting.enumValues[value])); - } - if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - return std::to_string(SETTINGS.*(setting.valuePtr)); - } - if (setting.type == SettingType::ACTION && !setting.isSeparator) { - return std::string(">>"); - } - return std::string(); - }, + [&settings](int i) { return settings[i].getDisplayValue(); }, true); // Draw help text diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 459084c2..60cf75b6 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -1,185 +1,12 @@ #pragma once #include -#include -#include #include -#include "CrossPointSettings.h" +#include "SettingInfo.h" #include "activities/Activity.h" #include "util/ButtonNavigator.h" -enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; - -enum class SettingAction { - None, - RemapFrontButtons, - CustomiseStatusBar, - ClockSettings, - KOReaderSync, - OPDSBrowser, - Network, - ClearCache, - CheckForUpdates, - Language, - SystemInfo, - DetectTimezone, - SyncTime, - Weather, - Submenu, -}; - -struct SettingInfo { - StrId nameId; - SettingType type; - uint8_t CrossPointSettings::* valuePtr = nullptr; - std::vector enumValues; - SettingAction action = SettingAction::None; - - struct ValueRange { - uint8_t min; - uint8_t max; - uint8_t step; - }; - ValueRange valueRange = {}; - - const char* key = nullptr; // JSON API key (nullptr for ACTION types) - StrId category = StrId::STR_NONE_OPT; // Category for web UI grouping - bool obfuscated = false; // Save/load via base64 obfuscation (passwords) - - // Direct char[] string fields (for settings stored in CrossPointSettings) - size_t stringOffset = 0; - size_t stringMaxLen = 0; - - // Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore) - std::function valueGetter; - std::function valueSetter; - std::function stringGetter; - std::function stringSetter; - - SettingInfo& withObfuscated() { - obfuscated = true; - return *this; - } - - static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::TOGGLE; - s.valuePtr = ptr; - s.key = key; - s.category = category; - return s; - } - - static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector values, - const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ENUM; - s.valuePtr = ptr; - s.enumValues = std::move(values); - s.key = key; - s.category = category; - return s; - } - - static SettingInfo Action(StrId nameId, SettingAction action) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ACTION; - s.action = action; - return s; - } - - static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange, - const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::VALUE; - s.valuePtr = ptr; - s.valueRange = valueRange; - s.key = key; - s.category = category; - return s; - } - - static SettingInfo String(StrId nameId, char* ptr, size_t maxLen, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::STRING; - s.stringOffset = (size_t)ptr - (size_t)&SETTINGS; - s.stringMaxLen = maxLen; - s.key = key; - s.category = category; - return s; - } - - static SettingInfo DynamicEnum(StrId nameId, std::vector values, std::function getter, - std::function setter, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ENUM; - s.enumValues = std::move(values); - s.valueGetter = std::move(getter); - s.valueSetter = std::move(setter); - s.key = key; - s.category = category; - return s; - } - - static SettingInfo DynamicString(StrId nameId, std::function getter, - std::function setter, const char* key = nullptr, - StrId category = StrId::STR_NONE_OPT) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::STRING; - s.stringGetter = std::move(getter); - s.stringSetter = std::move(setter); - s.key = key; - s.category = category; - return s; - } - - static SettingInfo Separator(StrId nameId) { - SettingInfo s; - s.nameId = nameId; - s.type = SettingType::ACTION; - s.isSeparator = true; - return s; - } - - bool isSeparator = false; - StrId subcategory = StrId::STR_NONE_OPT; // Triggers a separator row on first use and on change - StrId submenu = StrId::STR_NONE_OPT; // Routes item into a submenu; hidden from main list - [[nodiscard]] std::string getTitle() const; - - // 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; - } -}; - class SettingsActivity final : public Activity { ButtonNavigator buttonNavigator; diff --git a/src/activities/settings/SettingsSubmenuActivity.cpp b/src/activities/settings/SettingsSubmenuActivity.cpp index 6e25d106..5ac7ffff 100644 --- a/src/activities/settings/SettingsSubmenuActivity.cpp +++ b/src/activities/settings/SettingsSubmenuActivity.cpp @@ -3,123 +3,21 @@ #include #include -#include "ButtonRemapActivity.h" -#include "CalibreSettingsActivity.h" -#include "ClearCacheActivity.h" -#include "ClockSettingsActivity.h" #include "CrossPointSettings.h" -#include "KOReaderSettingsActivity.h" -#include "LanguageSelectActivity.h" #include "MappedInputManager.h" -#include "OtaUpdateActivity.h" -#include "StatusBarSettingsActivity.h" -#include "SystemInformationActivity.h" -#include "activities/network/WifiSelectionActivity.h" -#include "activities/weather/WeatherSettingsActivity.h" +#include "SettingActionDispatch.h" #include "components/UITheme.h" #include "fontIds.h" -void SettingsSubmenuActivity::onEnter() { - Activity::onEnter(); - itemCount = static_cast(items.size()); - const auto pred = UITheme::makeSelectablePredicate(itemCount, [this](int i) { return items[i].getTitle(); }); - buttonNavigator.setSelectablePredicate(pred, itemCount); - if (!pred(selectedIndex)) { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); - } - requestUpdate(); +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::onExit() { Activity::onExit(); } - -void SettingsSubmenuActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - finish(); - return; - } - - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - toggleItem(); - requestUpdate(); - return; - } - - buttonNavigator.onNextRelease([this] { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); - requestUpdate(); - }); - buttonNavigator.onPreviousRelease([this] { - selectedIndex = buttonNavigator.previousIndex(selectedIndex); - requestUpdate(); - }); - buttonNavigator.onNextContinuous([this] { - selectedIndex = buttonNavigator.nextIndex(selectedIndex); - requestUpdate(); - }); - buttonNavigator.onPreviousContinuous([this] { - selectedIndex = buttonNavigator.previousIndex(selectedIndex); - requestUpdate(); - }); -} - -void SettingsSubmenuActivity::toggleItem() { - if (selectedIndex < 0 || selectedIndex >= itemCount) return; - const auto& setting = items[selectedIndex]; - if (setting.isSeparator) return; - - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - SETTINGS.*(setting.valuePtr) = !(SETTINGS.*(setting.valuePtr)); - } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t cur = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (cur + 1) % static_cast(setting.enumValues.size()); - } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - const int8_t cur = SETTINGS.*(setting.valuePtr); - SETTINGS.*(setting.valuePtr) = (cur + setting.valueRange.step > setting.valueRange.max) - ? setting.valueRange.min - : cur + setting.valueRange.step; - } else if (setting.type == SettingType::ACTION) { - auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; - switch (setting.action) { - case SettingAction::RemapFrontButtons: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::CustomiseStatusBar: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::ClockSettings: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::KOReaderSync: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::OPDSBrowser: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::Network: - startActivityForResult(std::make_unique(renderer, mappedInput, false), resultHandler); - return; - case SettingAction::ClearCache: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::CheckForUpdates: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::Language: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::Weather: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - case SettingAction::SystemInfo: - startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - return; - default: - return; - } - } - - SETTINGS.saveToFile(); -} +void SettingsSubmenuActivity::onSettingToggled(int /*index*/) { SETTINGS.saveToFile(); } void SettingsSubmenuActivity::render(RenderLock&&) { renderer.clearScreen(); @@ -132,30 +30,7 @@ void SettingsSubmenuActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing; - - GUI.drawList( - renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, itemCount, selectedIndex, - [this](int index) { return items[index].getTitle(); }, nullptr, nullptr, - [this](int i) { - const auto& setting = items[i]; - if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { - return std::string(SETTINGS.*(setting.valuePtr) ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); - } - if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { - const uint8_t value = SETTINGS.*(setting.valuePtr); - if (value < setting.enumValues.size()) { - return std::string(I18N.get(setting.enumValues[value])); - } - } - if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) { - return std::to_string(SETTINGS.*(setting.valuePtr)); - } - if (setting.type == SettingType::ACTION && !setting.isSeparator) { - return std::string(">>"); - } - return std::string(); - }, - true); + 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); diff --git a/src/activities/settings/SettingsSubmenuActivity.h b/src/activities/settings/SettingsSubmenuActivity.h index 10d53197..6fff3691 100644 --- a/src/activities/settings/SettingsSubmenuActivity.h +++ b/src/activities/settings/SettingsSubmenuActivity.h @@ -3,28 +3,24 @@ #include -#include "activities/Activity.h" -#include "activities/settings/SettingsActivity.h" -#include "util/ButtonNavigator.h" +#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 Activity { +class SettingsSubmenuActivity final : public MenuListActivity { StrId titleId; - std::vector items; - int selectedIndex = 0; - int itemCount = 0; - ButtonNavigator buttonNavigator; - void toggleItem(); + // MenuListActivity overrides + void onActionSelected(int index) override; + void onSettingToggled(int index) override; public: explicit SettingsSubmenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, StrId titleId, std::vector items) - : Activity("SettingsSubmenu", renderer, mappedInput), titleId(titleId), items(std::move(items)) {} + : MenuListActivity("SettingsSubmenu", renderer, mappedInput), titleId(titleId) { + menuItems = std::move(items); + } - void onEnter() override; - void onExit() override; - void loop() override; void render(RenderLock&&) override; }; diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 041b1338..2f68d8f3 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -31,15 +31,14 @@ class UITheme { // navigation. Pass the same title getter you pass to drawList so rendering and navigation // always agree on which items are separators. // - // Typical usage pattern for a menu with section headers: + // Preferred approach: derive from MenuListActivity (see MenuListActivity.h) which handles + // separator skipping, navigation, and drawList automatically via SettingInfo items. // - // struct MenuItem { - // Action action; StrId labelId; bool isSeparator = false; - // static MenuItem separator(StrId label) { return {Action::NONE, label, true}; } - // std::string getTitle() const; // implemented in .cpp: makeSeparatorTitle when isSeparator - // }; - // const std::vector items = buildMenuItems(); - // // In buildMenuItems, use MenuItem::separator(StrId) for section headers. + // Manual usage (for activities that don't derive from MenuListActivity): + // + // std::vector items; + // 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(), @@ -48,7 +47,9 @@ class UITheme { // // // render: pass the same getter to drawList; separator rows are drawn automatically: // GUI.drawList(renderer, rect, items.size(), selectedIndex, - // [this](int i) { return items[i].getTitle(); }, ...); + // [this](int i) { return items[i].getTitle(); }, + // nullptr, nullptr, + // [this](int i) { return items[i].getDisplayValue(); }, true); static std::function makeSelectablePredicate(int total, std::function titleGetter); // Returns the drawable content Rect accounting for screen orientation and visible button hints. From a9c3fb8bc9dd21d4fb03c299260160d70693d354 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 20:39:26 +0200 Subject: [PATCH 10/18] Fix button press bleed through --- src/activities/MenuListActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/MenuListActivity.cpp b/src/activities/MenuListActivity.cpp index f253e157..f3704c8e 100644 --- a/src/activities/MenuListActivity.cpp +++ b/src/activities/MenuListActivity.cpp @@ -62,7 +62,7 @@ void MenuListActivity::loop() { onBackPressed(); return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { toggleCurrentItem(); return; } From 7dc6fbea0627c1363512b661ee49fe49d269118a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 20:42:28 +0200 Subject: [PATCH 11/18] format --- src/SettingsList.h | 8 +++---- src/activities/MenuListActivity.cpp | 4 +--- .../reader/EpubReaderMenuActivity.cpp | 24 +++++++++---------- src/activities/settings/SettingsActivity.cpp | 4 +--- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/SettingsList.h b/src/SettingsList.h index d8eae461..e02cbd57 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -79,18 +79,18 @@ inline const std::vector list = { 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_FONT_FAMILY), + .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), + .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), + .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), + .withSubmenu(StrId::STR_MENU_READER_FONT_SETTINGS), // Formatting settings SettingInfo::Enum( diff --git a/src/activities/MenuListActivity.cpp b/src/activities/MenuListActivity.cpp index f3704c8e..829ee706 100644 --- a/src/activities/MenuListActivity.cpp +++ b/src/activities/MenuListActivity.cpp @@ -46,9 +46,7 @@ void MenuListActivity::toggleCurrentItem() { requestUpdate(); } -std::string MenuListActivity::getItemValueString(int index) const { - return menuItems[index].getDisplayValue(); -} +std::string MenuListActivity::getItemValueString(int index) const { return menuItems[index].getDisplayValue(); } void MenuListActivity::drawMenuList(const Rect& rect) { const int count = static_cast(menuItems.size()); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 70289dde..8e1c54cf 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -62,24 +62,20 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { // 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}, + {StrId::STR_DEFAULT_VALUE, StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, [this]() -> uint8_t { return (pendingImageRenderingOverride < 0) ? 0 : (pendingImageRenderingOverride + 1); }, [this](uint8_t v) { pendingImageRenderingOverride = (v == 0) ? -1 : static_cast(v - 1); })); // Text darkness: straightforward 0-3 cycle menuItems.push_back(SettingInfo::DynamicEnum( - StrId::STR_TEXT_DARKNESS, - {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, - [this]() -> uint8_t { return pendingTextDarkness; }, - [this](uint8_t v) { pendingTextDarkness = v; })); + 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; })); + [this]() -> uint8_t { return pendingOrientation; }, [this](uint8_t v) { pendingOrientation = v; })); // --- Synchronisation (only if credentials are set) --- if (KOREADER_STORE.hasCredentials()) { @@ -158,8 +154,12 @@ void EpubReaderMenuActivity::onSettingToggled(int /*index*/) { void EpubReaderMenuActivity::onBackPressed() { ActivityResult result; result.isCancelled = true; - result.data = MenuResult{-1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride, - pendingImageRenderingOverride, pendingTextDarkness}; + result.data = MenuResult{-1, + pendingOrientation, + selectedPageTurnOption, + pendingEmbeddedStyleOverride, + pendingImageRenderingOverride, + pendingTextDarkness}; setResult(std::move(result)); finish(); } @@ -180,9 +180,7 @@ std::string EpubReaderMenuActivity::getItemValueString(int index) const { return MenuListActivity::getItemValueString(index); } -void EpubReaderMenuActivity::onEnter() { - MenuListActivity::onEnter(); -} +void EpubReaderMenuActivity::onEnter() { MenuListActivity::onEnter(); } void EpubReaderMenuActivity::render(RenderLock&&) { renderer.clearScreen(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7331bab4..37f74be7 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -255,9 +255,7 @@ void SettingsActivity::render(RenderLock&&) { contentRect.height - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2)}, settingsCount, selectedSettingIndex - 1, [&settings](int index) { return settings[index].getTitle(); }, nullptr, - nullptr, - [&settings](int i) { return settings[i].getDisplayValue(); }, - true); + nullptr, [&settings](int i) { return settings[i].getDisplayValue(); }, true); // Draw help text const auto confirmLabel = (selectedSettingIndex == 0) From f4107677f807097b478fec10170ce7eb9e76962a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 21:48:25 +0200 Subject: [PATCH 12/18] Reformat kosync settings --- lib/I18n/translations/english.yaml | 4 +- lib/I18n/translations/french.yaml | 28 +++ lib/I18n/translations/german.yaml | 17 ++ lib/I18n/translations/italian.yaml | 186 ++++++++++++++++++ lib/I18n/translations/russian.yaml | 31 +++ lib/I18n/translations/spanish.yaml | 34 ++++ .../settings/KOReaderSettingsActivity.cpp | 6 +- 7 files changed, 303 insertions(+), 3 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 10f64940..4cd522d8 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -485,4 +485,6 @@ 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" \ No newline at end of file +STR_MENU_READER_SPACING: "Spacing" +STR_MENU_KOSYNC_SERVER: "Server Settings" +STR_MENU_KOSYNC_AUTH: "Login / Register" \ No newline at end of file diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 0bf0d8b2..de9ec54c 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -461,3 +461,31 @@ STR_WEATHER_DESC_THUNDERSTORM: "Orage" STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Orage avec grêle" STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Orage avec forte grêle" STR_WEATHER_DESC_UNKNOWN: "Inconnu" + +STR_IMAGE_DITHERING: "Tramage d'image" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer diffus" +STR_UNSUPPORTED_IMAGE_FORMAT: "Format d'image non pris en charge" +STR_COULD_NOT_RENDER_IMAGE: "Impossible d'afficher l'image" +STR_FAILED_TO_SET_SLEEP_SCREEN: "Impossible de définir l'écran de veille" +STR_SET_SLEEP_SCREEN: "Définir l'écran de veille" +STR_SLEEP_SCREEN_SET: "Écran de veille mis à jour !" +STR_IMAGE_DISPLAY_BW: ">> N/B" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Niveaux de gris" +STR_READER_TOOLS: "Outils" +STR_READER_NAVIGATION: "Navigation" +STR_READER_APPEARANCE: "Apparence" +STR_MENU_SYS_SYSTEM: "Système" +STR_MENU_SYS_NETWORK: "Réseau" +STR_MENU_SYS_TOOLS: "Outils" +STR_MENU_DISP_SLEEP: "Écran de veille" +STR_MENU_DISP_BATTERY: "Symbole batterie" +STR_MENU_DISP_REFRESH: "Actualisation" +STR_MENU_READER_FONT: "Police du lecteur" +STR_MENU_READER_FONT_SETTINGS: "Paramètres police" +STR_MENU_READER_LAYOUT: "Paramètres mise en page" +STR_MENU_READER_TWEAKS: "Ajustements lecteur" +STR_MENU_READER_SPACING: "Espacement" +STR_MENU_KOSYNC_SERVER: "Paramètres serveur" +STR_MENU_KOSYNC_AUTH: "Connexion / Inscription" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 8f69dd64..4525d70f 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -349,6 +349,23 @@ STR_SLEEP_SCREEN_SET: "Standby-Bild aktualisiert!" STR_IMAGE_DISPLAY_BW: ">> S/W" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Grau" +STR_READER_TOOLS: "Werkzeuge" +STR_READER_NAVIGATION: "Navigation" +STR_READER_APPEARANCE: "Darstellung" +STR_MENU_SYS_SYSTEM: "System" +STR_MENU_SYS_NETWORK: "Netzwerk" +STR_MENU_SYS_TOOLS: "Werkzeuge" +STR_MENU_DISP_SLEEP: "Standby-Bildschirm" +STR_MENU_DISP_BATTERY: "Batteriesymbol" +STR_MENU_DISP_REFRESH: "Bildschirmaktualisierung" +STR_MENU_READER_FONT: "Leserschrift" +STR_MENU_READER_FONT_SETTINGS: "Schrifteinstellungen" +STR_MENU_READER_LAYOUT: "Layout-Einstellungen" +STR_MENU_READER_TWEAKS: "Lese-Tweaks" +STR_MENU_READER_SPACING: "Abstand" +STR_MENU_KOSYNC_SERVER: "Server-Einstellungen" +STR_MENU_KOSYNC_AUTH: "Anmelden / Registrieren" + STR_WEATHER: "Wetter" STR_WEATHER_LOCATION: "Ort" STR_WEATHER_NO_LOCATION: "Kein Ort für Wetter festgelegt" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 10c58894..ddd409d6 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -306,3 +306,189 @@ 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_SLEEP_IMAGE_PICK_MODE: "Selezione immagine standby" +STR_RANDOM: "Casuale" +STR_SEQUENTIAL: "Sequenziale" +STR_OVERLAY_WHITE: "Bianco" +STR_OVERLAY_GRAY: "Grigio" +STR_OVERLAY_BLACK: "Nero" +STR_OVERLAY_OFF: "Disattivato" +STR_OVERLAY_READING_PROGRESS: "Avanzamento lettura: Pagina %lu/%u - %.0f%%" +STR_OVERLAY_READING_PROGRESS_NO_TOTAL: "Avanzamento lettura: Pagina %lu" +STR_OVERLAY_CHAPTER_PAGE_SUFFIX: " - Pagina %d/%d - %.0f%%" +STR_SYSTEM_INFO: "Informazioni di sistema" +STR_LOAD_XTC_FAILED: "Impossibile caricare file XTC" +STR_LOAD_EPUB_FAILED: "Impossibile caricare file EPUB" +STR_FW_VERSION: "Versione FW" +STR_CHIP: "Chip" +STR_CPU: "CPU" +STR_MHZ: "MHz" +STR_FREE_RAM: "RAM libera" +STR_MIN_FREE: "Min. libera" +STR_MAX_BLOCK: "Blocco max" +STR_FLASH_USED: "Flash usato" +STR_UPTIME: "Tempo di attività" +STR_CHARGING: "In carica" +STR_GATHERING_DATA: "Raccolta dati..." +STR_READING: "Lettura..." +STR_SD_UPDATE_PROMPT: "Premere Aggiorna" +STR_UNSUPPORTED_IMAGE_FORMAT: "Formato immagine non supportato" +STR_COULD_NOT_RENDER_IMAGE: "Impossibile visualizzare l'immagine" +STR_FAILED_TO_SET_SLEEP_SCREEN: "Impossibile impostare schermo standby" +STR_SET_SLEEP_SCREEN: "Imposta standby" +STR_SLEEP_SCREEN_SET: "Schermo standby aggiornato!" +STR_IMAGE_DISPLAY_BW: ">> B/N" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gradazioni di grigio" +STR_WEATHER_MOON_INFO: "Luna" +STR_WEATHER_SUN_INFO: "Sole" +STR_READER_TOOLS: "Strumenti" +STR_READER_NAVIGATION: "Navigazione" +STR_READER_APPEARANCE: "Aspetto" +STR_MENU_SYS_SYSTEM: "Sistema" +STR_MENU_SYS_NETWORK: "Rete" +STR_MENU_SYS_TOOLS: "Strumenti" +STR_MENU_DISP_SLEEP: "Schermo standby" +STR_MENU_DISP_BATTERY: "Icona batteria" +STR_MENU_DISP_REFRESH: "Aggiorna schermo" +STR_MENU_READER_FONT: "Carattere lettore" +STR_MENU_READER_FONT_SETTINGS: "Impostazioni carattere" +STR_MENU_READER_LAYOUT: "Impostazioni layout" +STR_MENU_READER_TWEAKS: "Personalizzazioni lettore" +STR_MENU_READER_SPACING: "Spaziatura" +STR_MENU_KOSYNC_SERVER: "Impostazioni server" +STR_MENU_KOSYNC_AUTH: "Accesso / Registrazione" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index e4054460..6907d14b 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -458,3 +458,34 @@ STR_WEATHER_DESC_THUNDERSTORM: "Гроза" STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Гроза с градом" STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Гроза с сильным градом" STR_WEATHER_DESC_UNKNOWN: "Неизвестно" + +STR_TEXT_DARKNESS: "Насыщенность текста" +STR_EXTRA_DARK: "Очень тёмный" +STR_MAX_DARK: "Максимум" +STR_IMAGE_DITHERING: "Дизеринг изображения" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Диффузный Bayer" +STR_UNSUPPORTED_IMAGE_FORMAT: "Формат изображения не поддерживается" +STR_COULD_NOT_RENDER_IMAGE: "Не удалось отобразить изображение" +STR_FAILED_TO_SET_SLEEP_SCREEN: "Не удалось установить экран сна" +STR_SET_SLEEP_SCREEN: "Установить экран сна" +STR_SLEEP_SCREEN_SET: "Экран сна обновлён!" +STR_IMAGE_DISPLAY_BW: ">> Ч/Б" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Оттенки серого" +STR_READER_TOOLS: "Инструменты" +STR_READER_NAVIGATION: "Навигация" +STR_READER_APPEARANCE: "Внешний вид" +STR_MENU_SYS_SYSTEM: "Система" +STR_MENU_SYS_NETWORK: "Сеть" +STR_MENU_SYS_TOOLS: "Инструменты" +STR_MENU_DISP_SLEEP: "Экран сна" +STR_MENU_DISP_BATTERY: "Значок батареи" +STR_MENU_DISP_REFRESH: "Обновление экрана" +STR_MENU_READER_FONT: "Шрифт чтения" +STR_MENU_READER_FONT_SETTINGS: "Настройки шрифта" +STR_MENU_READER_LAYOUT: "Макет" +STR_MENU_READER_TWEAKS: "Параметры чтения" +STR_MENU_READER_SPACING: "Интервал" +STR_MENU_KOSYNC_SERVER: "Настройки сервера" +STR_MENU_KOSYNC_AUTH: "Вход / Регистрация" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index cb03c08b..af89d7eb 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -455,3 +455,37 @@ STR_WEATHER_DESC_THUNDERSTORM: "Tormenta eléctrica" STR_WEATHER_DESC_THUNDERSTORM_HAIL: "Tormenta con granizo" STR_WEATHER_DESC_THUNDERSTORM_HEAVY_HAIL: "Tormenta con granizo fuerte" STR_WEATHER_DESC_UNKNOWN: "Desconocido" + +STR_RSSI: "RSSI" +STR_NO_SIGNAL: "Sin señal" +STR_SIGNAL_QUALITY_POOR: "Mala" +STR_SIGNAL_QUALITY_WEAK: "Débil" +STR_SIGNAL_QUALITY_GOOD: "Buena" +STR_SIGNAL_QUALITY_EXCELLENT: "Excelente" +STR_IMAGE_DITHERING: "Trama de imagen" +STR_IMAGE_DITHER_BAYER: "Bayer" +STR_IMAGE_DITHER_ATKINSON: "Atkinson" +STR_IMAGE_DITHER_DIFFUSED_BAYER: "Bayer difuso" +STR_UNSUPPORTED_IMAGE_FORMAT: "Formato de imagen no compatible" +STR_COULD_NOT_RENDER_IMAGE: "No se pudo mostrar la imagen" +STR_FAILED_TO_SET_SLEEP_SCREEN: "No se pudo configurar la pantalla de suspensión" +STR_SET_SLEEP_SCREEN: "Configurar suspensión" +STR_SLEEP_SCREEN_SET: "Pantalla de suspensión actualizada!" +STR_IMAGE_DISPLAY_BW: ">> N/B" +STR_IMAGE_DISPLAY_GRAYSCALE: ">> Escala de grises" +STR_READER_TOOLS: "Herramientas" +STR_READER_NAVIGATION: "Navegación" +STR_READER_APPEARANCE: "Apariencia" +STR_MENU_SYS_SYSTEM: "Sistema" +STR_MENU_SYS_NETWORK: "Red" +STR_MENU_SYS_TOOLS: "Herramientas" +STR_MENU_DISP_SLEEP: "Pantalla de suspensión" +STR_MENU_DISP_BATTERY: "Icono de batería" +STR_MENU_DISP_REFRESH: "Refrescar pantalla" +STR_MENU_READER_FONT: "Fuente lector" +STR_MENU_READER_FONT_SETTINGS: "Ajustes de fuente" +STR_MENU_READER_LAYOUT: "Ajustes de diseño" +STR_MENU_READER_TWEAKS: "Ajustes de lectura" +STR_MENU_READER_SPACING: "Espaciado" +STR_MENU_KOSYNC_SERVER: "Ajustes del servidor" +STR_MENU_KOSYNC_AUTH: "Iniciar sesión / Registrarse" diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index cdd31026..938c59aa 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -17,9 +17,10 @@ KOReaderSettingsActivity::KOReaderSettingsActivity(GfxRenderer& renderer, Mapped 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)); - menuItems.push_back(SettingInfo::Action(StrId::STR_SYNC_SERVER_URL, SettingAction::None)); // Document matching: DynamicEnum toggling between Filename and Binary menuItems.push_back(SettingInfo::DynamicEnum( @@ -31,7 +32,8 @@ void KOReaderSettingsActivity::buildMenuItems() { })); // Authenticate and Register: ACTION items - menuItems.push_back(SettingInfo::Action(StrId::STR_AUTHENTICATE, SettingAction::None)); + 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)); } From d2b13e7a43f33abc7e44b8727f4e2b115fc545e0 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 22:19:10 +0200 Subject: [PATCH 13/18] Remove duplicate keys --- lib/I18n/translations/italian.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index ddd409d6..c8c60457 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -441,9 +441,6 @@ STR_AUTHOR: "Autore" STR_SERIES: "Serie" STR_FILE_SIZE: "Dimensione" STR_SLEEP_COVER_OVERLAY: "Overlay info standby" -STR_SLEEP_IMAGE_PICK_MODE: "Selezione immagine standby" -STR_RANDOM: "Casuale" -STR_SEQUENTIAL: "Sequenziale" STR_OVERLAY_WHITE: "Bianco" STR_OVERLAY_GRAY: "Grigio" STR_OVERLAY_BLACK: "Nero" From bec30e34032c7447383eae3bb0525f93e12591b6 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 22:25:12 +0200 Subject: [PATCH 14/18] Review fixes --- lib/I18n/translations/english.yaml | 2 +- scripts/gen_i18n.py | 24 +++++++++++++++++++++++- src/activities/settings/SettingInfo.cpp | 6 +++--- src/main.cpp | 3 --- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 4cd522d8..891beed4 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -478,7 +478,7 @@ STR_READER_APPEARANCE: "Appearance" STR_MENU_SYS_SYSTEM: "System" STR_MENU_SYS_NETWORK: "Network" STR_MENU_SYS_TOOLS: "Tools" -STR_MENU_DISP_SLEEP: "Sleepscreen" +STR_MENU_DISP_SLEEP: "Sleep Screen" STR_MENU_DISP_BATTERY: "Battery symbol" STR_MENU_DISP_REFRESH: "Screen refresh" STR_MENU_READER_FONT: "Reader Font" diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index b593a043..38a8ea71 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -281,7 +281,29 @@ def find_used_string_keys( except OSError: continue for line in text.splitlines(): - line = line.split("//", 1)[0] + quote_char = None + escaped = False + comment_index = None + for idx, ch in enumerate(line): + if escaped: + escaped = False + continue + if quote_char is None: + if ch == '"' or ch == "'": + quote_char = ch + continue + if ch == "/" and idx + 1 < len(line) and line[idx + 1] == "/": + comment_index = idx + break + else: + if ch == "\\": + escaped = True + continue + if ch == quote_char: + quote_char = None + continue + if comment_index is not None: + line = line[:comment_index] for m in pattern.finditer(line): used.add(m.group(0)) diff --git a/src/activities/settings/SettingInfo.cpp b/src/activities/settings/SettingInfo.cpp index d1356ca4..7525c1ff 100644 --- a/src/activities/settings/SettingInfo.cpp +++ b/src/activities/settings/SettingInfo.cpp @@ -73,9 +73,9 @@ void SettingInfo::toggleValue() const { case SettingType::VALUE: if (valuePtr) { - const auto current = static_cast(SETTINGS.*(valuePtr)); - SETTINGS.*(valuePtr) = - (current + valueRange.step > valueRange.max) ? valueRange.min : current + valueRange.step; + const unsigned current = SETTINGS.*(valuePtr); + SETTINGS.*(valuePtr) = static_cast( + (current + valueRange.step > valueRange.max) ? valueRange.min : current + valueRange.step); } break; diff --git a/src/main.cpp b/src/main.cpp index ceb16897..2f8894ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -204,12 +204,9 @@ void setup() { HalSystem::checkPanic(); HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info - LOG_DBG("MAIN", "System initialized, now setting up environment, millis=%lu", millis()); SETTINGS.loadFromFile(); - LOG_DBG("MAIN", "Settings loaded, now setting up clock and localization, millis=%lu", millis()); HalClock::applyTimezone(SETTINGS.timeZone); I18N.loadSettings(); - LOG_DBG("MAIN", "Localization loaded, now setting up theme and button navigation, millis=%lu", millis()); KOREADER_STORE.loadFromFile(); WEATHER_SETTINGS.loadFromFile(); UITheme::getInstance().reload(); From 475451f73c2c1ef2b2fab9adc9c9a9d421ffa2e6 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 22:26:42 +0200 Subject: [PATCH 15/18] Fix example code --- src/components/UITheme.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 2f68d8f3..19f576de 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -34,9 +34,10 @@ class UITheme { // 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): + // Manual usage (for activities that don't derive from MenuListActivity). + // Assumes `items` is a member variable (std::vector items): // - // std::vector items; + // // populate in constructor or onEnter: // items.push_back(SettingInfo::Separator(StrId::STR_MY_SECTION)); // items.push_back(SettingInfo::Toggle(StrId::STR_MY_TOGGLE, &CrossPointSettings::myFlag)); // @@ -46,10 +47,11 @@ class UITheme { // buttonNavigator.setSelectablePredicate(pred, items.size()); // // // render: pass the same getter to drawList; separator rows are drawn automatically: - // GUI.drawList(renderer, rect, items.size(), selectedIndex, - // [this](int i) { return items[i].getTitle(); }, + // 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, - // [this](int i) { return items[i].getDisplayValue(); }, true); + // [&s](int i) { return s[i].getDisplayValue(); }, true); static std::function makeSelectablePredicate(int total, std::function titleGetter); // Returns the drawable content Rect accounting for screen orientation and visible button hints. From a811c96d74b459ffbd997bf97ba9a3b72f6ec264 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 22:34:28 +0200 Subject: [PATCH 16/18] Some comments --- scripts/gen_i18n.py | 63 +++++++++++++++-------------- scripts/generate_test_epub.py | 12 +++--- src/activities/MenuListActivity.cpp | 4 +- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index 38a8ea71..bd47b17e 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -289,7 +289,7 @@ def find_used_string_keys( escaped = False continue if quote_char is None: - if ch == '"' or ch == "'": + if ch in ['"', "'"]: quote_char = ch continue if ch == "/" and idx + 1 < len(line) and line[idx + 1] == "/": @@ -413,11 +413,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 @@ -479,17 +475,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("") @@ -529,9 +527,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:") @@ -559,8 +559,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)}" @@ -594,11 +596,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) @@ -618,11 +622,10 @@ def generate_strings_cpp( "", "#include ", "", + "// Language codes", + "const char* const LANGUAGE_CODES[] = {", ] - # LANGUAGE_NAMES array - lines.append("// Language codes") - lines.append("const char* const LANGUAGE_CODES[] = {") for code in languages: _append_string_entry(lines, code) lines.append("};") @@ -686,13 +689,13 @@ def generate_strings_cpp( # Compile-time size checks lines.append("// Compile-time validation of array sizes") for code in languages: - lines.append( - f"static_assert(sizeof(i18n_strings::OFFSETS_{code}) " - f"/ sizeof(i18n_strings::OFFSETS_{code}[0]) ==" + lines.extend( + ( + f"static_assert(sizeof(i18n_strings::OFFSETS_{code}) / sizeof(i18n_strings::OFFSETS_{code}[0]) ==", + " static_cast(StrId::_COUNT),", + f' "OFFSETS_{code} size mismatch");', + ) ) - lines.append(" static_cast(StrId::_COUNT),") - lines.append(f' "OFFSETS_{code} size mismatch");') - _write_file(output_path, lines, verbose) diff --git a/scripts/generate_test_epub.py b/scripts/generate_test_epub.py index d8a7f4db..99868cf6 100644 --- a/scripts/generate_test_epub.py +++ b/scripts/generate_test_epub.py @@ -27,17 +27,17 @@ def get_font(size=20): import sys candidates = [] - if sys.platform == "win32": + if sys.platform == "darwin": + candidates = [ + "/System/Library/Fonts/Helvetica.ttc", + "/Library/Fonts/Arial.ttf", + ] + elif sys.platform == "win32": windir = os.environ.get("WINDIR", "C:\\Windows") candidates = [ os.path.join(windir, "Fonts", "arial.ttf"), os.path.join(windir, "Fonts", "calibri.ttf"), ] - elif sys.platform == "darwin": - candidates = [ - "/System/Library/Fonts/Helvetica.ttc", - "/Library/Fonts/Arial.ttf", - ] else: candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", diff --git a/src/activities/MenuListActivity.cpp b/src/activities/MenuListActivity.cpp index 829ee706..4fd19076 100644 --- a/src/activities/MenuListActivity.cpp +++ b/src/activities/MenuListActivity.cpp @@ -33,7 +33,7 @@ void MenuListActivity::handleNavigation() { void MenuListActivity::toggleCurrentItem() { if (selectedIndex < 0 || selectedIndex >= static_cast(menuItems.size())) return; - auto& item = menuItems[selectedIndex]; + const auto& item = menuItems[selectedIndex]; if (item.isSeparator) return; if (item.type == SettingType::ACTION) { @@ -41,7 +41,7 @@ void MenuListActivity::toggleCurrentItem() { return; } - item.toggleValue(); + menuItems[selectedIndex].toggleValue(); onSettingToggled(selectedIndex); requestUpdate(); } From 87a4ae4bcab864f768ec5d077dba888ba71d8b92 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 22:35:59 +0200 Subject: [PATCH 17/18] Amend password entry --- src/activities/settings/KOReaderSettingsActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 938c59aa..de554e32 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -73,7 +73,7 @@ void KOReaderSettingsActivity::onActionSelected(int index) { }); } else if (item.nameId == StrId::STR_PASSWORD) { startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), - KOREADER_STORE.getPassword(), 64, false), + KOREADER_STORE.getPassword(), 64, true), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& kb = std::get(result.data); From ed01127bf293facdc407b6ed27aec708888f9652 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 12 Apr 2026 22:55:07 +0200 Subject: [PATCH 18/18] More changes --- scripts/gen_i18n.py | 58 ++++++++++++++++++++++++++++++++++----------- src/SettingsList.h | 3 ++- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index bd47b17e..24a0575a 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -280,30 +280,60 @@ def find_used_string_keys( text = f.read_text(encoding="utf-8", errors="replace") except OSError: continue + in_block_comment = False for line in text.splitlines(): quote_char = None escaped = False - comment_index = None - for idx, ch in enumerate(line): + 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) and line[idx + 1] == "/": - comment_index = idx - break - else: - if ch == "\\": - escaped = True - continue - if ch == quote_char: - quote_char = None - continue - if comment_index is not None: - line = line[:comment_index] + 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)) diff --git a/src/SettingsList.h b/src/SettingsList.h index e02cbd57..f10ac8af 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -157,7 +157,8 @@ inline const std::vector list = { KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); KOREADER_STORE.saveToFile(); }, - "koPassword", StrId::STR_KOREADER_SYNC), + "koPassword", StrId::STR_KOREADER_SYNC) + .withObfuscated(), SettingInfo::DynamicString( StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, [](const std::string& v) {