Merge pull request #94 from jpirnay/refactor-mem

refactor: Change settings layout and fix heap corruption
This commit is contained in:
jpirnay
2026-04-17 16:54:14 +02:00
committed by GitHub
13 changed files with 273 additions and 190 deletions
+17 -3
View File
@@ -95,9 +95,23 @@ bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) {
jump["spineIndex"] = s.pendingBookmarkJump.spineIndex; jump["spineIndex"] = s.pendingBookmarkJump.spineIndex;
jump["pageNumber"] = s.pendingBookmarkJump.pageNumber; jump["pageNumber"] = s.pendingBookmarkJump.pageNumber;
String json; if (doc.overflowed()) {
serializeJson(doc, json); LOG_ERR("CPS", "JSON document overflowed while building state");
return Storage.writeFile(path, json); return false;
}
FsFile file;
if (!Storage.openFileForWrite("CPS", path, file)) {
return false;
}
const size_t expected = measureJson(doc);
const size_t written = serializeJson(doc, file);
file.flush();
if (!file.close()) {
return false;
}
return written == expected;
} }
bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) {
+8 -8
View File
@@ -151,22 +151,22 @@ inline const std::vector<SettingInfo> list = {
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- // --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString( SettingInfo::DynamicString(
StrId::STR_SYNC_SERVER_URL, [] { return KOREADER_STORE.getServerUrl(); }, StrId::STR_SYNC_SERVER_URL, [](void*) { return KOREADER_STORE.getServerUrl(); },
[](const std::string& v) { [](void*, const std::string& v) {
KOREADER_STORE.setServerUrl(v); KOREADER_STORE.setServerUrl(v);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koServerUrl", StrId::STR_KOREADER_SYNC), "koServerUrl", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicString( SettingInfo::DynamicString(
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, StrId::STR_KOREADER_USERNAME, [](void*) { return KOREADER_STORE.getUsername(); },
[](const std::string& v) { [](void*, const std::string& v) {
KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword()); KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword());
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koUsername", StrId::STR_KOREADER_SYNC), "koUsername", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicString( SettingInfo::DynamicString(
StrId::STR_KOREADER_PASSWORD, [] { return KOREADER_STORE.getPassword(); }, StrId::STR_KOREADER_PASSWORD, [](void*) { return KOREADER_STORE.getPassword(); },
[](const std::string& v) { [](void*, const std::string& v) {
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v); KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v);
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
@@ -174,8 +174,8 @@ inline const std::vector<SettingInfo> list = {
.withObfuscated(), .withObfuscated(),
SettingInfo::DynamicEnum( SettingInfo::DynamicEnum(
StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
[] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); }, [](void*) { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
[](uint8_t v) { [](void*, uint8_t v) {
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v)); KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
+69 -87
View File
@@ -99,21 +99,26 @@ int getHomeCoverRenderHeight(const HomeScreenLayout& layout) {
} }
} // namespace } // namespace
int HomeActivity::getMenuItemCount() const { // Builds the menu entry list in display order. Single source of truth for both loop() (which
int count = 4; // File Browser, Recents, File transfer, Settings // dispatches Confirm based on action) and render() (which draws labels/icons).
if (SETTINGS.useWeather) { void HomeActivity::rebuildMenuEntries() {
count++; menuEntries.clear();
} menuEntries.reserve(7);
if (!recentBooks.empty()) {
count += recentBooks.size(); menuEntries.push_back({MenuAction::FileBrowser, StrId::STR_BROWSE_FILES, Folder});
menuEntries.push_back({MenuAction::Recents, StrId::STR_MENU_RECENT_BOOKS, Recent});
if (!GLOBAL_BOOKMARKS.isEmpty()) {
menuEntries.push_back({MenuAction::GlobalBookmarks, StrId::STR_GLOBAL_BOOKMARKS, Book});
} }
if (hasOpdsUrl) { if (hasOpdsUrl) {
count++; menuEntries.push_back({MenuAction::OpdsBrowser, StrId::STR_OPDS_BROWSER, Library});
} }
if (!GLOBAL_BOOKMARKS.isEmpty()) { menuEntries.push_back({MenuAction::FileTransfer, StrId::STR_FILE_TRANSFER, Transfer});
count++; if (SETTINGS.useWeather) {
menuEntries.push_back({MenuAction::Weather, StrId::STR_WEATHER, Weather});
} }
return count; menuEntries.push_back({MenuAction::Settings, StrId::STR_SETTINGS_TITLE, Settings});
menuEntriesDirty = false;
} }
void HomeActivity::loadRecentBooks(int maxBooks) { void HomeActivity::loadRecentBooks(int maxBooks) {
@@ -207,6 +212,7 @@ void HomeActivity::onEnter() {
} }
// Trigger first update // Trigger first update
menuEntriesDirty = true;
requestUpdate(); requestUpdate();
} }
@@ -260,57 +266,39 @@ void HomeActivity::freeCoverBuffer() {
} }
void HomeActivity::loop() { void HomeActivity::loop() {
if (menuEntriesDirty) {
rebuildMenuEntries();
}
const int totalItems = static_cast<int>(recentBooks.size() + menuEntries.size());
if (firstRenderDone && !recentsLoaded && !recentsLoading) { if (firstRenderDone && !recentsLoaded && !recentsLoading) {
const auto& metrics = UITheme::getInstance().getMetrics(); const auto& metrics = UITheme::getInstance().getMetrics();
const Rect contentRect = UITheme::getContentRect(renderer, true, false); const Rect contentRect = UITheme::getContentRect(renderer, true, false);
const int menuItemCount = getMenuItemCount(); const HomeScreenLayout layout =
const HomeScreenLayout layout = computeHomeScreenLayout(metrics, contentRect.height, menuItemCount); computeHomeScreenLayout(metrics, contentRect.height, static_cast<int>(menuEntries.size()));
loadRecentCovers(getHomeCoverRenderHeight(layout)); loadRecentCovers(getHomeCoverRenderHeight(layout));
return; return;
} }
const int menuCount = getMenuItemCount(); buttonNavigator.onNext([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
buttonNavigator.onNext([this, menuCount] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
requestUpdate(); requestUpdate();
}); });
buttonNavigator.onPrevious([this, menuCount] { buttonNavigator.onPrevious([this, totalItems] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount); selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
requestUpdate(); requestUpdate();
}); });
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Calculate dynamic indices based on which options are available const int recentsCount = static_cast<int>(recentBooks.size());
int idx = 0; if (selectorIndex < recentsCount) {
int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
const bool hasGlobalBookmarks = !GLOBAL_BOOKMARKS.isEmpty();
const bool hasWeather = SETTINGS.useWeather;
const int fileBrowserIdx = idx++;
const int recentsIdx = idx++;
const int globalBookmarksIdx = hasGlobalBookmarks ? idx++ : -1;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int fileTransferIdx = idx++;
const int weatherIdx = hasWeather ? idx++ : -1;
const int settingsIdx = idx;
if (selectorIndex < recentBooks.size()) {
onSelectBook(recentBooks[selectorIndex].path); onSelectBook(recentBooks[selectorIndex].path);
} else if (menuSelectedIndex == fileBrowserIdx) { } else {
onFileBrowserOpen(); const int menuIdx = selectorIndex - recentsCount;
} else if (menuSelectedIndex == recentsIdx) { if (menuIdx < static_cast<int>(menuEntries.size())) {
onRecentsOpen(); dispatchMenuAction(menuEntries[menuIdx].action);
} else if (menuSelectedIndex == globalBookmarksIdx) { }
onGlobalBookmarksOpen();
} else if (menuSelectedIndex == opdsLibraryIdx) {
onOpdsBrowserOpen();
} else if (menuSelectedIndex == weatherIdx) {
onWeatherOpen();
} else if (menuSelectedIndex == fileTransferIdx) {
onFileTransferOpen();
} else if (menuSelectedIndex == settingsIdx) {
onSettingsOpen();
} }
} }
} }
@@ -324,37 +312,16 @@ void HomeActivity::render(RenderLock&&) {
GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.homeTopPadding}, nullptr); GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.homeTopPadding}, nullptr);
// Build menu items dynamically if (menuEntriesDirty) {
const char* weatherMenuLabel = SETTINGS.useWeather ? tr(STR_WEATHER) : tr(STR_SETTINGS_TITLE); rebuildMenuEntries();
const UIIcon weatherMenuIcon = SETTINGS.useWeather ? Weather : Settings;
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
weatherMenuLabel, tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, weatherMenuIcon, Settings};
if (!SETTINGS.useWeather) {
menuItems.erase(menuItems.begin() + 3);
menuIcons.erase(menuIcons.begin() + 3);
} }
int insertAfterRecents = 2; const int totalItems = static_cast<int>(recentBooks.size() + menuEntries.size());
if (!GLOBAL_BOOKMARKS.isEmpty()) {
menuItems.insert(menuItems.begin() + insertAfterRecents, tr(STR_GLOBAL_BOOKMARKS));
menuIcons.insert(menuIcons.begin() + insertAfterRecents, Book);
insertAfterRecents++;
}
if (hasOpdsUrl) {
menuItems.insert(menuItems.begin() + insertAfterRecents, tr(STR_OPDS_BROWSER));
menuIcons.insert(menuIcons.begin() + insertAfterRecents, Library);
}
const int totalItems = static_cast<int>(recentBooks.size() + menuItems.size());
if (selectorIndex >= totalItems) { if (selectorIndex >= totalItems) {
selectorIndex = std::max(0, totalItems - 1); selectorIndex = std::max(0, totalItems - 1);
} }
const int menuCount = static_cast<int>(menuItems.size()); const int menuCount = static_cast<int>(menuEntries.size());
const HomeScreenLayout layout = computeHomeScreenLayout(metrics, contentRect.height, menuCount); const HomeScreenLayout layout = computeHomeScreenLayout(metrics, contentRect.height, menuCount);
GUI.drawRecentBookCover(renderer, GUI.drawRecentBookCover(renderer,
@@ -367,8 +334,8 @@ void HomeActivity::render(RenderLock&&) {
Rect{contentRect.x, metrics.homeTopPadding + layout.recentTileHeight + layout.recentToMenuGap, contentRect.width, Rect{contentRect.x, metrics.homeTopPadding + layout.recentTileHeight + layout.recentToMenuGap, contentRect.width,
layout.menuHeight}, layout.menuHeight},
menuCount, selectorIndex - static_cast<int>(recentBooks.size()), menuCount, selectorIndex - static_cast<int>(recentBooks.size()),
[&menuItems](int index) { return std::string(menuItems[index]); }, [this](int index) { return std::string(I18N.get(menuEntries[index].label)); },
[&menuIcons](int index) { return menuIcons[index]; }); [this](int index) { return menuEntries[index].icon; });
const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -383,16 +350,31 @@ void HomeActivity::render(RenderLock&&) {
void HomeActivity::onSelectBook(const std::string& path) { activityManager.pushReader(path); } void HomeActivity::onSelectBook(const std::string& path) { activityManager.pushReader(path); }
void HomeActivity::onFileBrowserOpen() { activityManager.goToFileBrowser(); } void HomeActivity::dispatchMenuAction(MenuAction action) {
switch (action) {
void HomeActivity::onRecentsOpen() { activityManager.goToRecentBooks(); } case MenuAction::FileBrowser:
activityManager.goToFileBrowser();
void HomeActivity::onGlobalBookmarksOpen() { activityManager.goToGlobalBookmarks(); } break;
case MenuAction::Recents:
void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); } activityManager.goToRecentBooks();
break;
void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); } case MenuAction::GlobalBookmarks:
activityManager.goToGlobalBookmarks();
void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); } break;
case MenuAction::OpdsBrowser:
void HomeActivity::onWeatherOpen() { activityManager.goToWeather(); } activityManager.goToBrowser();
break;
case MenuAction::FileTransfer:
activityManager.goToFileTransfer();
break;
case MenuAction::Weather:
activityManager.goToWeather();
break;
case MenuAction::Settings:
activityManager.goToSettings();
break;
default:
LOG_ERR("HOME", "Unexpected menu action: %d", static_cast<int>(action));
break;
}
}
+25 -9
View File
@@ -5,12 +5,31 @@
#include "../Activity.h" #include "../Activity.h"
#include "./FileBrowserActivity.h" #include "./FileBrowserActivity.h"
#include "components/UITheme.h"
#include "util/ButtonNavigator.h" #include "util/ButtonNavigator.h"
struct RecentBook; struct RecentBook;
struct Rect; struct Rect;
class HomeActivity final : public Activity { class HomeActivity final : public Activity {
public:
enum class MenuAction {
FileBrowser,
Recents,
GlobalBookmarks,
OpdsBrowser,
FileTransfer,
Weather,
Settings,
};
private:
struct MenuEntry {
MenuAction action;
StrId label;
UIIcon icon;
};
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int selectorIndex = 0; int selectorIndex = 0;
bool recentsLoading = false; bool recentsLoading = false;
@@ -22,16 +41,13 @@ class HomeActivity final : public Activity {
size_t nextRecentCoverIndex = 0; size_t nextRecentCoverIndex = 0;
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
std::vector<RecentBook> recentBooks; std::vector<RecentBook> recentBooks;
void onSelectBook(const std::string& path); std::vector<MenuEntry> menuEntries;
void onFileBrowserOpen(); bool menuEntriesDirty = true;
void onRecentsOpen();
void onGlobalBookmarksOpen();
void onSettingsOpen();
void onFileTransferOpen();
void onOpdsBrowserOpen();
void onWeatherOpen();
int getMenuItemCount() const; void onSelectBook(const std::string& path);
void dispatchMenuAction(MenuAction action);
void rebuildMenuEntries();
bool storeCoverBuffer(); // Store frame buffer for cover image bool storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover bool restoreCoverBuffer(); // Restore frame buffer from stored cover
void freeCoverBuffer(); // Free the stored cover buffer void freeCoverBuffer(); // Free the stored cover buffer
+2 -2
View File
@@ -33,7 +33,7 @@ namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
constexpr unsigned long skipChapterMs = 700; constexpr unsigned long skipChapterMs = 700;
// pages per minute, first item is 1 to prevent division by zero if accessed // pages per minute, first item is 1 to prevent division by zero if accessed
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
void logReaderMemSnapshot(const char* stage) { void logReaderMemSnapshot(const char* stage) {
const uint32_t freeHeap = esp_get_free_heap_size(); const uint32_t freeHeap = esp_get_free_heap_size();
@@ -756,7 +756,7 @@ void EpubReaderActivity::applyTextDarkness(const uint8_t textDarkness) {
} }
void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) { void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) {
if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) { if (selectedPageTurnOption == 0 || selectedPageTurnOption >= std::size(PAGE_TURN_LABELS)) {
automaticPageTurnActive = false; automaticPageTurnActive = false;
return; return;
} }
@@ -50,44 +50,57 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa
// --- Appearance --- // --- Appearance ---
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE)); menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_APPEARANCE));
auto* self = this;
// Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2 // Embedded style: cycles default(-1) -> ON(1) -> OFF(0) via DynamicEnum indices 0/1/2
menuItems.push_back(SettingInfo::DynamicEnum( menuItems.push_back(SettingInfo::DynamicEnumCtx(
StrId::STR_EMBEDDED_STYLE, {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF}, StrId::STR_EMBEDDED_STYLE, {StrId::STR_DEFAULT_VALUE, StrId::STR_STATE_ON, StrId::STR_STATE_OFF}, self,
[this]() -> uint8_t { [](void* ctx) -> uint8_t {
if (pendingEmbeddedStyleOverride < 0) return 0; const auto* s = static_cast<const EpubReaderMenuActivity*>(ctx);
if (pendingEmbeddedStyleOverride > 0) return 1; if (s->pendingEmbeddedStyleOverride < 0) return 0;
if (s->pendingEmbeddedStyleOverride > 0) return 1;
return 2; return 2;
}, },
[this](uint8_t v) { [](void* ctx, uint8_t v) {
auto* s = static_cast<EpubReaderMenuActivity*>(ctx);
if (v == 0) if (v == 0)
pendingEmbeddedStyleOverride = -1; s->pendingEmbeddedStyleOverride = -1;
else if (v == 1) else if (v == 1)
pendingEmbeddedStyleOverride = 1; s->pendingEmbeddedStyleOverride = 1;
else else
pendingEmbeddedStyleOverride = 0; s->pendingEmbeddedStyleOverride = 0;
})); }));
// Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2) // Image rendering: cycles default(-1) -> display(0) -> placeholder(1) -> suppress(2)
menuItems.push_back(SettingInfo::DynamicEnum( menuItems.push_back(SettingInfo::DynamicEnumCtx(
StrId::STR_IMAGES, 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); }, self,
[this](uint8_t v) { pendingImageRenderingOverride = (v == 0) ? -1 : static_cast<int8_t>(v - 1); })); [](void* ctx) -> uint8_t {
const auto* s = static_cast<const EpubReaderMenuActivity*>(ctx);
return (s->pendingImageRenderingOverride < 0) ? 0 : (s->pendingImageRenderingOverride + 1);
},
[](void* ctx, uint8_t v) {
auto* s = static_cast<EpubReaderMenuActivity*>(ctx);
s->pendingImageRenderingOverride = (v == 0) ? -1 : static_cast<int8_t>(v - 1);
}));
// Text darkness: straightforward 0-3 cycle // Text darkness: straightforward 0-3 cycle
menuItems.push_back(SettingInfo::DynamicEnum( menuItems.push_back(SettingInfo::DynamicEnumCtx(
StrId::STR_TEXT_DARKNESS, {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, StrId::STR_TEXT_DARKNESS, {StrId::STR_NORMAL, StrId::STR_DARK, StrId::STR_EXTRA_DARK, StrId::STR_MAX_DARK}, self,
[this]() -> uint8_t { return pendingTextDarkness; }, [this](uint8_t v) { pendingTextDarkness = v; })); [](void* ctx) -> uint8_t { return static_cast<EpubReaderMenuActivity*>(ctx)->pendingTextDarkness; },
[](void* ctx, uint8_t v) { static_cast<EpubReaderMenuActivity*>(ctx)->pendingTextDarkness = v; }));
// Helper functions, reading ruler, auto page turn, orientation // Helper functions, reading ruler, auto page turn, orientation
menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS)); menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS));
// Auto page turn: ACTION type with custom cycling in onActionSelected // Auto page turn: ACTION type with custom cycling in onActionSelected
menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None)); menuItems.push_back(SettingInfo::Action(StrId::STR_AUTO_TURN_PAGES_PER_MIN, SettingAction::None));
// Orientation: straightforward 0-3 cycle // Orientation: straightforward 0-3 cycle
menuItems.push_back(SettingInfo::DynamicEnum( menuItems.push_back(SettingInfo::DynamicEnumCtx(
StrId::STR_ORIENTATION, StrId::STR_ORIENTATION,
{StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, {StrId::STR_PORTRAIT, StrId::STR_LANDSCAPE_CW, StrId::STR_INVERTED, StrId::STR_LANDSCAPE_CCW}, self,
[this]() -> uint8_t { return pendingOrientation; }, [this](uint8_t v) { pendingOrientation = v; })); [](void* ctx) -> uint8_t { return static_cast<EpubReaderMenuActivity*>(ctx)->pendingOrientation; },
[](void* ctx, uint8_t v) { static_cast<EpubReaderMenuActivity*>(ctx)->pendingOrientation = v; }));
// --- Synchronisation (only if credentials are set) --- // --- Synchronisation (only if credentials are set) ---
if (KOREADER_STORE.hasCredentials()) { if (KOREADER_STORE.hasCredentials()) {
@@ -19,6 +19,7 @@ const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = {
} // namespace } // namespace
void ClockSettingsActivity::buildMenuItems() { void ClockSettingsActivity::buildMenuItems() {
menuItems.reserve(6);
menuItems.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_TITLE)); menuItems.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_TITLE));
menuItems.push_back( menuItems.push_back(
SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM)); SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM));
@@ -16,6 +16,7 @@ KOReaderSettingsActivity::KOReaderSettingsActivity(GfxRenderer& renderer, Mapped
} }
void KOReaderSettingsActivity::buildMenuItems() { void KOReaderSettingsActivity::buildMenuItems() {
menuItems.reserve(6);
// Username, Password, Server URL: ACTION items with custom value display // Username, Password, Server URL: ACTION items with custom value display
menuItems.push_back(SettingInfo::Action(StrId::STR_SYNC_SERVER_URL, SettingAction::None) menuItems.push_back(SettingInfo::Action(StrId::STR_SYNC_SERVER_URL, SettingAction::None)
.withSubcategory(StrId::STR_MENU_KOSYNC_SERVER)); .withSubcategory(StrId::STR_MENU_KOSYNC_SERVER));
@@ -25,8 +26,8 @@ void KOReaderSettingsActivity::buildMenuItems() {
// Document matching: DynamicEnum toggling between Filename and Binary // Document matching: DynamicEnum toggling between Filename and Binary
menuItems.push_back(SettingInfo::DynamicEnum( menuItems.push_back(SettingInfo::DynamicEnum(
StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
[] { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); }, [](void*) { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); },
[](uint8_t v) { [](void*, uint8_t v) {
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v)); KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
})); }));
+5 -5
View File
@@ -19,7 +19,7 @@ std::string SettingInfo::getDisplayValue() const {
if (valuePtr) if (valuePtr)
value = SETTINGS.*(valuePtr); value = SETTINGS.*(valuePtr);
else if (valueGetter) else if (valueGetter)
value = valueGetter(); value = callValueGetter();
else else
return {}; return {};
return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); return std::string(value ? tr(STR_STATE_ON) : tr(STR_STATE_OFF));
@@ -29,7 +29,7 @@ std::string SettingInfo::getDisplayValue() const {
if (valuePtr) if (valuePtr)
value = SETTINGS.*(valuePtr); value = SETTINGS.*(valuePtr);
else if (valueGetter) else if (valueGetter)
value = valueGetter(); value = callValueGetter();
else else
return {}; return {};
if (value < enumValues.size()) return std::string(I18N.get(enumValues[value])); if (value < enumValues.size()) return std::string(I18N.get(enumValues[value]));
@@ -37,7 +37,7 @@ std::string SettingInfo::getDisplayValue() const {
} }
case SettingType::VALUE: { case SettingType::VALUE: {
if (valuePtr) return std::to_string(SETTINGS.*(valuePtr)); if (valuePtr) return std::to_string(SETTINGS.*(valuePtr));
if (valueGetter) return std::to_string(valueGetter()); if (valueGetter) return std::to_string(callValueGetter());
return {}; return {};
} }
case SettingType::ACTION: case SettingType::ACTION:
@@ -56,7 +56,7 @@ void SettingInfo::toggleValue() const {
if (valuePtr) { if (valuePtr) {
SETTINGS.*(valuePtr) = !(SETTINGS.*(valuePtr)); SETTINGS.*(valuePtr) = !(SETTINGS.*(valuePtr));
} else if (valueGetter && valueSetter) { } else if (valueGetter && valueSetter) {
valueSetter(!valueGetter()); callValueSetter(!callValueGetter());
} }
break; break;
@@ -66,7 +66,7 @@ void SettingInfo::toggleValue() const {
if (valuePtr) { if (valuePtr) {
SETTINGS.*(valuePtr) = (SETTINGS.*(valuePtr) + 1) % count; SETTINGS.*(valuePtr) = (SETTINGS.*(valuePtr) + 1) % count;
} else if (valueGetter && valueSetter) { } else if (valueGetter && valueSetter) {
valueSetter((valueGetter() + 1) % count); callValueSetter((callValueGetter() + 1) % count);
} }
break; break;
} }
+58 -16
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
#include <I18n.h> #include <I18n.h>
#include <functional> #include <cassert>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -49,11 +49,37 @@ struct SettingInfo {
size_t stringOffset = 0; size_t stringOffset = 0;
size_t stringMaxLen = 0; size_t stringMaxLen = 0;
// Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore) // Dynamic accessors (for settings stored outside CrossPointSettings, e.g. KOReaderCredentialStore).
std::function<uint8_t()> valueGetter; // Function pointers + opaque context avoid the heap allocation of std::function. Stateless
std::function<void(uint8_t)> valueSetter; // lambdas pass ctx=nullptr; captures must be hand-written as trampoline functions. See
std::function<std::string()> stringGetter; // DynamicEnumCtx / DynamicStringCtx factories below.
std::function<void(const std::string&)> stringSetter; using ValueGetterFn = uint8_t (*)(void*);
using ValueSetterFn = void (*)(void*, uint8_t);
using StringGetterFn = std::string (*)(void*);
using StringSetterFn = void (*)(void*, const std::string&);
void* accessorCtx = nullptr;
ValueGetterFn valueGetter = nullptr;
ValueSetterFn valueSetter = nullptr;
StringGetterFn stringGetter = nullptr;
StringSetterFn stringSetter = nullptr;
uint8_t callValueGetter() const {
assert(valueGetter && "SettingInfo::callValueGetter requires a non-null valueGetter");
return valueGetter(accessorCtx);
}
void callValueSetter(uint8_t v) const {
assert(valueSetter && "SettingInfo::callValueSetter requires a non-null valueSetter");
valueSetter(accessorCtx, v);
}
std::string callStringGetter() const {
assert(stringGetter && "SettingInfo::callStringGetter requires a non-null stringGetter");
return stringGetter(accessorCtx);
}
void callStringSetter(const std::string& v) const {
assert(stringSetter && "SettingInfo::callStringSetter requires a non-null stringSetter");
stringSetter(accessorCtx, v);
}
SettingInfo& withObfuscated() { SettingInfo& withObfuscated() {
obfuscated = true; obfuscated = true;
@@ -115,33 +141,49 @@ struct SettingInfo {
return s; return s;
} }
static SettingInfo DynamicEnum(StrId nameId, std::vector<StrId> values, std::function<uint8_t()> getter, // Stateless variant — getter/setter are free/static functions with no captured state.
std::function<void(uint8_t)> setter, const char* key = nullptr, static SettingInfo DynamicEnum(StrId nameId, std::vector<StrId> values, ValueGetterFn getter, ValueSetterFn setter,
StrId category = StrId::STR_NONE_OPT) { const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s; SettingInfo s;
s.nameId = nameId; s.nameId = nameId;
s.type = SettingType::ENUM; s.type = SettingType::ENUM;
s.enumValues = std::move(values); s.enumValues = std::move(values);
s.valueGetter = std::move(getter); s.valueGetter = getter;
s.valueSetter = std::move(setter); s.valueSetter = setter;
s.key = key; s.key = key;
s.category = category; s.category = category;
return s; return s;
} }
static SettingInfo DynamicString(StrId nameId, std::function<std::string()> getter, // Context-carrying variant — trampolines receive `ctx` as first argument and cast it back to
std::function<void(const std::string&)> setter, const char* key = nullptr, // their concrete owner type.
StrId category = StrId::STR_NONE_OPT) { static SettingInfo DynamicEnumCtx(StrId nameId, std::vector<StrId> values, void* ctx, ValueGetterFn getter,
ValueSetterFn setter, const char* key = nullptr,
StrId category = StrId::STR_NONE_OPT) {
SettingInfo s = DynamicEnum(nameId, std::move(values), getter, setter, key, category);
s.accessorCtx = ctx;
return s;
}
static SettingInfo DynamicString(StrId nameId, StringGetterFn getter, StringSetterFn setter,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s; SettingInfo s;
s.nameId = nameId; s.nameId = nameId;
s.type = SettingType::STRING; s.type = SettingType::STRING;
s.stringGetter = std::move(getter); s.stringGetter = getter;
s.stringSetter = std::move(setter); s.stringSetter = setter;
s.key = key; s.key = key;
s.category = category; s.category = category;
return s; return s;
} }
static SettingInfo DynamicStringCtx(StrId nameId, void* ctx, StringGetterFn getter, StringSetterFn setter,
const char* key = nullptr, StrId category = StrId::STR_NONE_OPT) {
SettingInfo s = DynamicString(nameId, getter, setter, key, category);
s.accessorCtx = ctx;
return s;
}
static SettingInfo Separator(StrId nameId) { static SettingInfo Separator(StrId nameId) {
SettingInfo s; SettingInfo s;
s.nameId = nameId; s.nameId = nameId;
+49 -36
View File
@@ -30,16 +30,22 @@ void SettingsActivity::onEnter() {
controlsSettings.clear(); controlsSettings.clear();
systemSettings.clear(); systemSettings.clear();
submenuData.clear(); submenuData.clear();
displaySettings.reserve(20);
readerSettings.reserve(30);
controlsSettings.reserve(8);
systemSettings.reserve(20);
submenuData.reserve(4);
StrId lastDisplaySub = StrId::STR_NONE_OPT; StrId lastDisplaySub = StrId::STR_NONE_OPT;
StrId lastReaderSub = StrId::STR_NONE_OPT; StrId lastReaderSub = StrId::STR_NONE_OPT;
StrId lastControlsSub = StrId::STR_NONE_OPT; StrId lastControlsSub = StrId::STR_NONE_OPT;
StrId lastSystemSub = StrId::STR_NONE_OPT; StrId lastSystemSub = StrId::STR_NONE_OPT;
auto addTo = [this](std::vector<SettingInfo>& vec, StrId& lastSub, SettingInfo s) { // Shared placement logic — locates submenu target or inserts separator.
// Returns the vector the caller should push into (either `vec` or a submenu's items).
auto locateTarget = [this](std::vector<SettingInfo>& vec, StrId& lastSub,
const SettingInfo& s) -> std::vector<SettingInfo>* {
if (s.submenu != StrId::STR_NONE_OPT) { 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(), auto it = std::find_if(submenuData.begin(), submenuData.end(),
[&s](const SubmenuData& d) { return d.id == s.submenu; }); [&s](const SubmenuData& d) { return d.id == s.submenu; });
if (it == submenuData.end()) { if (it == submenuData.end()) {
@@ -47,14 +53,21 @@ void SettingsActivity::onEnter() {
submenuData.push_back({s.submenu, {}}); submenuData.push_back({s.submenu, {}});
it = submenuData.end() - 1; it = submenuData.end() - 1;
} }
it->items.push_back(std::move(s)); return &it->items;
return;
} }
if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) { if (s.subcategory != StrId::STR_NONE_OPT && s.subcategory != lastSub) {
vec.push_back(SettingInfo::Separator(s.subcategory)); vec.push_back(SettingInfo::Separator(s.subcategory));
lastSub = s.subcategory; lastSub = s.subcategory;
} }
vec.push_back(std::move(s)); return &vec;
};
auto addTo = [&locateTarget](std::vector<SettingInfo>& vec, StrId& lastSub, const SettingInfo& s) {
locateTarget(vec, lastSub, s)->push_back(s);
};
auto addToMoved = [&locateTarget](std::vector<SettingInfo>& vec, StrId& lastSub, SettingInfo&& s) {
auto* target = locateTarget(vec, lastSub, s);
target->push_back(std::move(s));
}; };
for (const auto& setting : getSettingsList()) { for (const auto& setting : getSettingsList()) {
@@ -80,34 +93,34 @@ void SettingsActivity::onEnter() {
controlsSettings.insert(controlsSettings.begin(), controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
addTo(readerSettings, lastReaderSub, addToMoved(readerSettings, lastReaderSub,
SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar)); SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));
addTo(systemSettings, lastSystemSub, SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language)); addToMoved(systemSettings, lastSystemSub, SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network) std::move(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)
.withSubcategory(StrId::STR_MENU_SYS_NETWORK)); .withSubcategory(StrId::STR_MENU_SYS_NETWORK)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync) std::move(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)
.withSubcategory(StrId::STR_MENU_SYS_NETWORK)); .withSubcategory(StrId::STR_MENU_SYS_NETWORK)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser) std::move(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser)
.withSubcategory(StrId::STR_MENU_SYS_NETWORK)); .withSubcategory(StrId::STR_MENU_SYS_NETWORK)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings) std::move(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings)
.withSubcategory(StrId::STR_MENU_SYS_TOOLS)); .withSubcategory(StrId::STR_MENU_SYS_TOOLS)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather) std::move(SettingInfo::Action(StrId::STR_WEATHER_SETTINGS, SettingAction::Weather)
.withSubcategory(StrId::STR_MENU_SYS_TOOLS)); .withSubcategory(StrId::STR_MENU_SYS_TOOLS)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache) std::move(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache)
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)); .withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates) std::move(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates)
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)); .withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
addTo(systemSettings, lastSystemSub, addToMoved(systemSettings, lastSystemSub,
SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo) std::move(SettingInfo::Action(StrId::STR_SYSTEM_INFO, SettingAction::SystemInfo)
.withSubcategory(StrId::STR_MENU_SYS_SYSTEM)); .withSubcategory(StrId::STR_MENU_SYS_SYSTEM)));
// Reset selection to first category // Reset selection to first category
selectedCategoryIndex = 0; selectedCategoryIndex = 0;
@@ -212,9 +225,9 @@ void SettingsActivity::toggleCurrentSetting() {
auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); };
if (setting.action == SettingAction::Submenu) { if (setting.action == SettingAction::Submenu) {
const auto it = std::find_if(submenuData.cbegin(), submenuData.cend(), auto it = std::find_if(submenuData.begin(), submenuData.end(),
[&setting](const SubmenuData& d) { return d.id == setting.nameId; }); [&setting](const SubmenuData& d) { return d.id == setting.nameId; });
if (it != submenuData.cend()) { if (it != submenuData.end()) {
startActivityForResult( startActivityForResult(
std::make_unique<SettingsSubmenuActivity>(renderer, mappedInput, setting.nameId, it->items), resultHandler); std::make_unique<SettingsSubmenuActivity>(renderer, mappedInput, setting.nameId, it->items), resultHandler);
} }
@@ -14,6 +14,7 @@
#include "fontIds.h" #include "fontIds.h"
void WeatherSettingsActivity::buildMenuItems() { void WeatherSettingsActivity::buildMenuItems() {
menuItems.reserve(10);
menuItems.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_TITLE)); menuItems.push_back(SettingInfo::Separator(StrId::STR_SETTINGS_TITLE));
menuItems.push_back(SettingInfo::Toggle(StrId::STR_USE_WEATHER, &CrossPointSettings::useWeather, "useWeather", menuItems.push_back(SettingInfo::Toggle(StrId::STR_USE_WEATHER, &CrossPointSettings::useWeather, "useWeather",
StrId::STR_CAT_SYSTEM)); StrId::STR_CAT_SYSTEM));
+4 -4
View File
@@ -1240,7 +1240,7 @@ void CrossPointWebServer::handleGetSettings() const {
if (s.valuePtr) { if (s.valuePtr) {
doc["value"] = static_cast<int>(SETTINGS.*(s.valuePtr)); doc["value"] = static_cast<int>(SETTINGS.*(s.valuePtr));
} else if (s.valueGetter) { } else if (s.valueGetter) {
doc["value"] = static_cast<int>(s.valueGetter()); doc["value"] = static_cast<int>(s.callValueGetter());
} }
JsonArray options = doc["options"].to<JsonArray>(); JsonArray options = doc["options"].to<JsonArray>();
for (const auto& opt : s.enumValues) { for (const auto& opt : s.enumValues) {
@@ -1261,7 +1261,7 @@ void CrossPointWebServer::handleGetSettings() const {
case SettingType::STRING: { case SettingType::STRING: {
doc["type"] = "string"; doc["type"] = "string";
if (s.stringGetter) { if (s.stringGetter) {
doc["value"] = s.stringGetter(); doc["value"] = s.callStringGetter();
} else if (s.stringMaxLen > 0) { } else if (s.stringMaxLen > 0) {
doc["value"] = reinterpret_cast<const char*>(&SETTINGS) + s.stringOffset; doc["value"] = reinterpret_cast<const char*>(&SETTINGS) + s.stringOffset;
} }
@@ -1326,7 +1326,7 @@ void CrossPointWebServer::handlePostSettings() {
if (s.valuePtr) { if (s.valuePtr) {
SETTINGS.*(s.valuePtr) = static_cast<uint8_t>(val); SETTINGS.*(s.valuePtr) = static_cast<uint8_t>(val);
} else if (s.valueSetter) { } else if (s.valueSetter) {
s.valueSetter(static_cast<uint8_t>(val)); s.callValueSetter(static_cast<uint8_t>(val));
} }
applied++; applied++;
} }
@@ -1345,7 +1345,7 @@ void CrossPointWebServer::handlePostSettings() {
case SettingType::STRING: { case SettingType::STRING: {
const std::string val = doc[s.key].as<std::string>(); const std::string val = doc[s.key].as<std::string>();
if (s.stringSetter) { if (s.stringSetter) {
s.stringSetter(val); s.callStringSetter(val);
} else if (s.stringMaxLen > 0) { } else if (s.stringMaxLen > 0) {
char* ptr = reinterpret_cast<char*>(&SETTINGS) + s.stringOffset; char* ptr = reinterpret_cast<char*>(&SETTINGS) + s.stringOffset;
strncpy(ptr, val.c_str(), s.stringMaxLen - 1); strncpy(ptr, val.c_str(), s.stringMaxLen - 1);