-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathCrossPointSettings.cpp
More file actions
362 lines (327 loc) · 13 KB
/
Copy pathCrossPointSettings.cpp
File metadata and controls
362 lines (327 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
#include "CrossPointSettings.h"
#include <I18n.h>
#include <Logging.h>
#include <ObfuscationUtils.h>
#include <algorithm>
#include <cstring>
#include <iterator>
#include <string>
#include "I18nKeys.h"
#include "ReaderFontSizes.h"
#include "SettingsList.h"
#include "fontIds.h"
namespace {
// Stack buffer for "<key>_obf" key construction — avoids a std::string
// allocation per obfuscated setting on every save and load.
constexpr size_t OBF_KEY_BUF = 64;
// Null-terminated copy into a fixed-size settings field.
void copyToField(char* dest, const char* src, const size_t maxLen) {
strncpy(dest, src, maxLen - 1);
dest[maxLen - 1] = '\0';
}
} // namespace
void CrossPointSettings::validateFrontButtonMapping(CrossPointSettings& settings) {
const uint8_t mapping[] = {settings.frontButtonBack, settings.frontButtonConfirm, settings.frontButtonLeft,
settings.frontButtonRight};
for (size_t i = 0; i < 4; i++) {
for (size_t j = i + 1; j < 4; j++) {
if (mapping[i] == mapping[j]) {
settings.frontButtonBack = FRONT_HW_BACK;
settings.frontButtonConfirm = FRONT_HW_CONFIRM;
settings.frontButtonLeft = FRONT_HW_LEFT;
settings.frontButtonRight = FRONT_HW_RIGHT;
return;
}
}
}
}
uint8_t CrossPointSettings::sleepTimeoutEnumToMinutes(const uint8_t legacyValue) {
switch (legacyValue) {
case SLEEP_1_MIN:
return 1;
case SLEEP_5_MIN:
return 5;
case SLEEP_15_MIN:
return 15;
case SLEEP_30_MIN:
return 30;
case SLEEP_10_MIN:
default:
return 10;
}
}
void CrossPointSettings::toJson(JsonDocument& doc) const {
const CrossPointSettings& s = *this;
for (const auto& info : getSettingsList()) {
if (!info.key) continue;
// Dynamic entries (KOReader etc.) are stored in their own files — skip.
if (!info.valuePtr && !info.stringOffset) continue;
if (info.stringOffset) {
const char* strPtr = (const char*)&s + info.stringOffset;
if (info.obfuscated) {
char obfKey[OBF_KEY_BUF];
snprintf(obfKey, sizeof(obfKey), "%s_obf", info.key);
doc[obfKey] = obfuscation::obfuscateToBase64(strPtr);
} else {
doc[info.key] = strPtr;
}
} else {
doc[info.key] = s.*(info.valuePtr);
}
}
// Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList.
doc["frontButtonBack"] = frontButtonBack;
doc["frontButtonConfirm"] = frontButtonConfirm;
doc["frontButtonLeft"] = frontButtonLeft;
doc["frontButtonRight"] = frontButtonRight;
// Font family and size — both use dynamic getter/setters in SettingsList (the
// option lists depend on the SD font registry), so the generic loop skips them.
doc["fontFamily"] = fontFamily;
doc["fontSize"] = fontPointSize;
// SD card font family name — not in SettingsList, save manually
if (sdFontFamilyName[0] != '\0') {
doc["sdFontFamilyName"] = sdFontFamilyName;
}
// Dictionary folder name — uses dynamic getter/setter in SettingsList, save manually
if (dictionaryName[0] != '\0') {
doc["dictionaryName"] = dictionaryName;
}
// Language -- managed by LanguageSelectActivity, not in SettingsList.
// Stored as ISO code string ("EN", "DE", ...) for stability across enum reorders.
doc["language"] = (language < getLanguageCount()) ? LANGUAGE_CODES[language] : "EN";
}
bool CrossPointSettings::fromJson(JsonVariantConst doc) {
CrossPointSettings& s = *this;
bool needsResave = false;
auto clamp = [](uint8_t val, uint8_t maxVal, uint8_t def) -> uint8_t { return val < maxVal ? val : def; };
for (const auto& info : getSettingsList()) {
if (!info.key) continue;
// Dynamic entries (KOReader etc.) are stored in their own files — skip.
if (!info.valuePtr && !info.stringOffset) continue;
if (info.stringOffset) {
// destPtr starts out holding the struct-initializer default; it stays that
// way unless the document actually carries a value for this key.
char* destPtr = (char*)&s + info.stringOffset;
if (info.stringMaxLen == 0) {
LOG_ERR("CPS", "Misconfigured SettingInfo: stringMaxLen is 0 for key '%s'", info.key);
destPtr[0] = '\0';
needsResave = true;
continue;
}
bool loaded = false;
if (info.obfuscated) {
char obfKey[OBF_KEY_BUF];
snprintf(obfKey, sizeof(obfKey), "%s_obf", info.key);
bool ok = false;
bool tooLong = false;
const std::string decoded =
obfuscation::deobfuscateFromBase64(doc[obfKey] | "", info.stringMaxLen - 1, &ok, &tooLong);
if (tooLong) {
LOG_ERR("CPS", "Oversized obfuscated value for key '%s'", info.key);
needsResave = true;
}
if (ok && !decoded.empty()) {
copyToField(destPtr, decoded.c_str(), info.stringMaxLen);
loaded = true;
}
}
if (!loaded) {
// Read as const char*, never `| std::string(...)`: ArduinoJson's
// std::string converter drags a per-TU copy of the serializer into
// flash. See the note in PersistableStore.h.
const char* raw = doc[info.key].is<const char*>() ? doc[info.key].as<const char*>() : nullptr;
if (raw) {
// Obfuscated field recovered from a legacy plaintext value -> resave.
if (info.obfuscated && strcmp(raw, destPtr) != 0) needsResave = true;
copyToField(destPtr, raw, info.stringMaxLen);
}
}
} else {
const uint8_t fieldDefault = s.*(info.valuePtr); // struct-initializer default, read before we overwrite it
uint8_t v = doc[info.key] | fieldDefault;
if (info.type == SettingType::ENUM) {
v = clamp(v, (uint8_t)info.enumValues.size(), fieldDefault);
} else if (info.type == SettingType::TOGGLE) {
v = clamp(v, (uint8_t)2, fieldDefault);
} else if (info.type == SettingType::VALUE) {
if (v < info.valueRange.min)
v = info.valueRange.min;
else if (v > info.valueRange.max)
v = info.valueRange.max;
}
s.*(info.valuePtr) = v;
}
}
if (doc["sleepTimeoutMinutes"].isNull() && !doc["sleepTimeout"].isNull()) {
const uint8_t legacyValue =
clamp(doc["sleepTimeout"] | (uint8_t)SLEEP_10_MIN, SLEEP_TIMEOUT_COUNT, (uint8_t)SLEEP_10_MIN);
sleepTimeoutMinutes = sleepTimeoutEnumToMinutes(legacyValue);
needsResave = true;
}
// Front button remap — managed by RemapFrontButtons sub-activity, not in SettingsList.
frontButtonBack = clamp(doc["frontButtonBack"] | (uint8_t)FRONT_HW_BACK, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_BACK);
frontButtonConfirm =
clamp(doc["frontButtonConfirm"] | (uint8_t)FRONT_HW_CONFIRM, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_CONFIRM);
frontButtonLeft = clamp(doc["frontButtonLeft"] | (uint8_t)FRONT_HW_LEFT, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_LEFT);
frontButtonRight =
clamp(doc["frontButtonRight"] | (uint8_t)FRONT_HW_RIGHT, FRONT_BUTTON_HARDWARE_COUNT, FRONT_HW_RIGHT);
validateFrontButtonMapping(s);
// Reader font size — an actual point size since 1.5. Files written by 1.4 and
// earlier hold the old SMALL/MEDIUM/LARGE/EXTRA_LARGE slot in 0..3; no font is
// renderable at those sizes, so the range is unambiguous and folds to the
// point sizes those slots used to mean. Drop this once 1.4 upgrades are done.
uint8_t storedFontSize = doc["fontSize"] | DEFAULT_FONT_POINT_SIZE;
if (storedFontSize <= LEGACY_FONT_SIZE_MAX) {
storedFontSize = 12 + storedFontSize * 2; // 0,1,2,3 -> 12,14,16,18
needsResave = true;
}
fontPointSize = storedFontSize;
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0;
fontFamily = clamp(storedFontFamily, BUILTIN_FONT_COUNT, 0);
// SD card font family name — not in SettingsList, load manually
const char* sfn = doc["sdFontFamilyName"] | "";
strncpy(sdFontFamilyName, sfn, sizeof(sdFontFamilyName) - 1);
sdFontFamilyName[sizeof(sdFontFamilyName) - 1] = '\0';
if (storedFontFamily == LEGACY_OPENDYSLEXIC && sdFontFamilyName[0] == '\0') {
fontFamily = NOTOSERIF;
strncpy(sdFontFamilyName, "OpenDyslexic", sizeof(sdFontFamilyName) - 1);
sdFontFamilyName[sizeof(sdFontFamilyName) - 1] = '\0';
needsResave = true;
} else if (storedFontFamily >= BUILTIN_FONT_COUNT) {
needsResave = true;
}
// Dictionary folder name — uses dynamic getter/setter in SettingsList, load manually
copyToField(dictionaryName, doc["dictionaryName"] | "", sizeof(dictionaryName));
// Language -- stored as code string for stability across enum reorders.
if (doc["language"].is<const char*>()) {
language = static_cast<uint8_t>(I18n::languageFromCode(doc["language"].as<const char*>()));
}
if (needsResave) {
LOG_DBG("CPS", "Resaving settings to update format");
requestResave();
}
LOG_DBG("CPS", "Settings loaded from file");
return true;
}
CrossPointSettings::StatusBarSpec CrossPointSettings::statusBarSpec() const {
StatusBarSpec spec;
spec.showChapterPageCount = statusBarChapterPageCount != 0;
spec.showBookProgressPercent = statusBarBookProgressPercentage != 0;
spec.titleMode = statusBarTitle;
spec.showBattery = statusBarBattery != 0;
spec.showBatteryPercent = hideBatteryPercentage == HIDE_NEVER;
spec.clockMode = statusBarClock;
spec.clock12h = clockFormat == 1;
spec.clockUtcOffsetQ = clockUtcOffsetQ;
spec.progressBarMode = statusBarProgressBar;
spec.progressBarHeightPx =
statusBarProgressBar != HIDE_PROGRESS ? static_cast<uint8_t>((statusBarProgressBarThickness + 1) * 2) : 0;
spec.xtcMode = xtcStatusBarMode;
return spec;
}
ReaderRenderSpec CrossPointSettings::readerRenderSpec(const uint16_t viewportWidth,
const uint16_t viewportHeight) const {
ReaderRenderSpec spec;
spec.fontId = getReaderFontId();
spec.lineCompression = getReaderLineCompression();
spec.extraParagraphSpacing = extraParagraphSpacing != 0;
spec.paragraphAlignment = paragraphAlignment;
spec.viewportWidth = viewportWidth;
spec.viewportHeight = viewportHeight;
spec.hyphenationEnabled = hyphenationEnabled != 0;
spec.embeddedStyle = embeddedStyle != 0;
spec.imageRendering = imageRendering;
spec.focusReadingEnabled = focusReadingEnabled != 0;
return spec;
}
float CrossPointSettings::getReaderLineCompression() const {
// SD card fonts use same compression as Bookerly (the most neutral values)
if (sdFontFamilyName[0] != '\0') {
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default:
return 1.0f;
case WIDE:
return 1.1f;
}
}
switch (fontFamily) {
case NOTOSERIF:
default:
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default:
return 1.0f;
case WIDE:
return 1.1f;
}
case NOTOSANS:
switch (lineSpacing) {
case TIGHT:
return 0.90f;
case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
}
}
unsigned long CrossPointSettings::getSleepTimeoutMs() const {
if (sleepTimeoutMinutes >= SLEEP_TIMEOUT_NEVER_MINUTES) return 0UL;
const uint8_t minutes =
std::clamp(sleepTimeoutMinutes, MIN_SLEEP_TIMEOUT_MINUTES, static_cast<uint8_t>(SLEEP_TIMEOUT_NEVER_MINUTES - 1));
return static_cast<unsigned long>(minutes) * 60UL * 1000UL;
}
int CrossPointSettings::getRefreshFrequency() const {
switch (refreshFrequency) {
case REFRESH_1:
return 1;
case REFRESH_5:
return 5;
case REFRESH_10:
return 10;
case REFRESH_15:
default:
return 15;
case REFRESH_30:
return 30;
}
}
void CrossPointSettings::clearSdFontFamily() {
sdFontFamilyName[0] = '\0';
fontPointSize =
snapToNearestPointSize(BUILTIN_READER_POINT_SIZES, std::size(BUILTIN_READER_POINT_SIZES), fontPointSize);
saveToFile();
}
int CrossPointSettings::getReaderFontId() const {
// Check SD card font first
if (sdFontFamilyName[0] != '\0' && sdFontIdResolver) {
int id = sdFontIdResolver(sdFontResolverCtx, sdFontFamilyName, fontPointSize);
if (id != 0) return id;
// Fall through to built-in if SD font not found
}
// A built-in family only exists at BUILTIN_READER_POINT_SIZES, so a size
// carried over from an SD family may not be one of them. ensureLoaded()
// normally persists the snap; snap again here (without allocating — this runs
// in the page render loop) so rendering is correct even before it has run.
const uint8_t pt =
snapToNearestPointSize(BUILTIN_READER_POINT_SIZES, std::size(BUILTIN_READER_POINT_SIZES), fontPointSize);
const bool sans = (fontFamily == NOTOSANS);
switch (pt) {
case 12:
return sans ? NOTOSANS_12_FONT_ID : NOTOSERIF_12_FONT_ID;
case 16:
return sans ? NOTOSANS_16_FONT_ID : NOTOSERIF_16_FONT_ID;
case 18:
return sans ? NOTOSANS_18_FONT_ID : NOTOSERIF_18_FONT_ID;
case 14:
default:
return sans ? NOTOSANS_14_FONT_ID : NOTOSERIF_14_FONT_ID;
}
}