Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
- File Browser long-press folder action for choosing a custom sleep-image folder instead of only `/.sleep` or `/sleep`.
- Expanded X3 Reading Stats, including streaks, time charts, editable dates, all-time backups, reset controls, an idle-time threshold, and the `Minimal Stats` sleep screen.
- `Reset Reading Pace` in the EPUB reader menu when Time Left is enabled, for clearing only the time-left pace estimate while keeping book reading stats.
- Added TRMNL as a sleep-screen wallpaper source with configurable server, API key, device ID, and orientation.

### Changed
- Display, Reader, and Controls settings now open list menus instead of cycling through options one by one.
Expand Down
21 changes: 18 additions & 3 deletions lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,25 @@ int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
// Convert entire source line to grayscale with alpha blending to white background.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha,
int bpp) {
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
switch (pixelType) {
case PNG_PIXEL_GRAYSCALE:
memcpy(grayLine, pPixels, width);
if (bpp >= 8) {
memcpy(grayLine, pPixels, width);
} else {
// 1/2/4-bit grayscale is packed MSB-first (8/4/2 pixels per byte); unpack each
// sample and scale it to 0..255. The old straight memcpy assumed 8-bit and
// sheared sub-8-bit images horizontally.
const int ppb = 8 / bpp;
const int mask = (1 << bpp) - 1;
for (int x = 0; x < width; x++) {
const uint8_t byte = pPixels[x / ppb];
const int shift = (ppb - 1 - (x % ppb)) * bpp;
const int sample = (byte >> shift) & mask;
grayLine[x] = (uint8_t)(sample * 255 / mask);
}
}
break;

case PNG_PIXEL_TRUECOLOR:
Expand Down Expand Up @@ -185,7 +200,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {

// Convert entire source line to grayscale (improves cache locality)
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
pDraw->iHasAlpha);
pDraw->iHasAlpha, pDraw->iBpp);

// Render scaled row using Bresenham-style integer stepping (no floating-point division)
int dstWidth = ctx->dstWidth;
Expand Down
9 changes: 9 additions & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,15 @@ STR_THEME_MINIMAL: "Minimal"
STR_THEME_DASHBOARD: "Dashboard"
STR_THEME_MINIMAL_STATS: "Minimal Stats"
STR_QUICK_RESUME: "Quick Resume"
STR_TRMNL: "TRMNL"
STR_TRMNL_SETTINGS: "TRMNL Settings"
STR_TRMNL_SERVER_URL: "TRMNL Server URL"
STR_TRMNL_API_KEY: "TRMNL API Key"
STR_TRMNL_DEVICE_ID: "TRMNL Device ID"
STR_TRMNL_ORIENTATION: "TRMNL Orientation"
STR_TRMNL_HORIZONTAL: "Horizontal"
STR_TRMNL_VERTICAL: "Vertical"
STR_TRMNL_FETCH_FAILED: "TRMNL fetch failed"
STR_RECENT_BOOKS_VIEW: "Recent Books View"
STR_LIST_VIEW: "List"
STR_GRID_VIEW: "Grid"
Expand Down
88 changes: 88 additions & 0 deletions lib/JsonParser/TrmnlDisplayJsonParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "TrmnlDisplayJsonParser.h"

#include <cstdlib>
#include <cstring>

namespace {
void safeCopy(char* dst, const size_t dstSize, const char* src, const size_t srcLen) {
const size_t n = srcLen < dstSize - 1 ? srcLen : dstSize - 1;
memcpy(dst, src, n);
dst[n] = '\0';
}
} // namespace

TrmnlDisplayJsonParser::TrmnlDisplayJsonParser()
: parser(JsonCallbacks{this, sOnKey, sOnString, sOnNumber, sOnBool, sOnNull, sOnObjectStart, sOnObjectEnd,
sOnArrayStart, sOnArrayEnd}) {
reset();
}

void TrmnlDisplayJsonParser::reset() {
parser.reset();
lastKey = LastKey::NONE;
depth = 0;
imageUrl[0] = '\0';
filename[0] = '\0';
refreshRateSeconds = 0;
imageUrlFound = false;
}

void TrmnlDisplayJsonParser::feed(const char* data, const size_t len) { parser.feed(data, len); }
bool TrmnlDisplayJsonParser::foundImageUrl() const { return imageUrlFound; }
bool TrmnlDisplayJsonParser::hasError() const { return parser.hasError(); }
const char* TrmnlDisplayJsonParser::getImageUrl() const { return imageUrl; }
const char* TrmnlDisplayJsonParser::getFilename() const { return filename; }
uint32_t TrmnlDisplayJsonParser::getRefreshRateSeconds() const { return refreshRateSeconds; }

void TrmnlDisplayJsonParser::sOnKey(void* ctx, const char* key, const size_t len) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth != 1) {
self->lastKey = LastKey::NONE;
return;
}
if (len == 9 && memcmp(key, "image_url", 9) == 0) {
self->lastKey = LastKey::IMAGE_URL;
} else if ((len == 8 && memcmp(key, "filename", 8) == 0) || (len == 10 && memcmp(key, "image_name", 10) == 0)) {
self->lastKey = LastKey::FILENAME;
} else if (len == 12 && memcmp(key, "refresh_rate", 12) == 0) {
self->lastKey = LastKey::REFRESH_RATE;
} else {
self->lastKey = LastKey::NONE;
}
}

void TrmnlDisplayJsonParser::sOnString(void* ctx, const char* value, const size_t len) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth == 1 && self->lastKey == LastKey::IMAGE_URL) {
safeCopy(self->imageUrl, sizeof(self->imageUrl), value, len);
self->imageUrlFound = self->imageUrl[0] != '\0';
} else if (self->depth == 1 && self->lastKey == LastKey::FILENAME) {
safeCopy(self->filename, sizeof(self->filename), value, len);
}
self->lastKey = LastKey::NONE;
}

void TrmnlDisplayJsonParser::sOnNumber(void* ctx, const char* value, const size_t /*len*/) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth == 1 && self->lastKey == LastKey::REFRESH_RATE) {
self->refreshRateSeconds = static_cast<uint32_t>(strtoul(value, nullptr, 10));
}
self->lastKey = LastKey::NONE;
}

void TrmnlDisplayJsonParser::sOnBool(void* ctx, bool /*value*/) {
static_cast<TrmnlDisplayJsonParser*>(ctx)->lastKey = LastKey::NONE;
}
void TrmnlDisplayJsonParser::sOnNull(void* ctx) { static_cast<TrmnlDisplayJsonParser*>(ctx)->lastKey = LastKey::NONE; }
void TrmnlDisplayJsonParser::sOnObjectStart(void* ctx) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
self->depth++;
self->lastKey = LastKey::NONE;
}
void TrmnlDisplayJsonParser::sOnObjectEnd(void* ctx) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth > 0) self->depth--;
self->lastKey = LastKey::NONE;
}
void TrmnlDisplayJsonParser::sOnArrayStart(void* ctx) { sOnObjectStart(ctx); }
void TrmnlDisplayJsonParser::sOnArrayEnd(void* ctx) { sOnObjectEnd(ctx); }
40 changes: 40 additions & 0 deletions lib/JsonParser/TrmnlDisplayJsonParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <cstddef>
#include <cstdint>

#include "StreamingJsonParser.h"

class TrmnlDisplayJsonParser {
public:
TrmnlDisplayJsonParser();
void reset();
void feed(const char* data, size_t len);

bool foundImageUrl() const;
bool hasError() const;
const char* getImageUrl() const;
const char* getFilename() const;
uint32_t getRefreshRateSeconds() const;

private:
enum class LastKey : uint8_t { NONE, IMAGE_URL, FILENAME, REFRESH_RATE };

static void sOnKey(void* ctx, const char* key, size_t len);
static void sOnString(void* ctx, const char* value, size_t len);
static void sOnNumber(void* ctx, const char* value, size_t len);
static void sOnBool(void* ctx, bool value);
static void sOnNull(void* ctx);
static void sOnObjectStart(void* ctx);
static void sOnObjectEnd(void* ctx);
static void sOnArrayStart(void* ctx);
static void sOnArrayEnd(void* ctx);

StreamingJsonParser parser;
LastKey lastKey = LastKey::NONE;
uint16_t depth = 0;
char imageUrl[512] = "";
char filename[128] = "";
uint32_t refreshRateSeconds = 0;
bool imageUrlFound = false;
};
1 change: 1 addition & 0 deletions src/CrossPointSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ constexpr uint8_t SLEEP_SCREEN_STORAGE_ORDER[] = {
static_cast<uint8_t>(CrossPointSettings::QUICK_RESUME),
static_cast<uint8_t>(CrossPointSettings::MINIMAL_STATS_SLEEP),
static_cast<uint8_t>(CrossPointSettings::DASHBOARD_SLEEP),
static_cast<uint8_t>(CrossPointSettings::TRMNL),
};
constexpr uint8_t SLEEP_SCREEN_STORAGE_ORDER_COUNT =
sizeof(SLEEP_SCREEN_STORAGE_ORDER) / sizeof(SLEEP_SCREEN_STORAGE_ORDER[0]);
Expand Down
6 changes: 6 additions & 0 deletions src/CrossPointSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CrossPointSettings {
QUICK_RESUME = 9,
MINIMAL_STATS_SLEEP = 10,
DASHBOARD_SLEEP = 11,
TRMNL = 12,
SLEEP_SCREEN_MODE_COUNT
};
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
Expand All @@ -40,6 +41,7 @@ class CrossPointSettings {
INVERTED_BLACK_AND_WHITE = 2,
SLEEP_SCREEN_COVER_FILTER_COUNT
};
enum TRMNL_ORIENTATION { TRMNL_LANDSCAPE = 0, TRMNL_PORTRAIT = 1, TRMNL_ORIENTATION_COUNT };

// Status bar enum - legacy
enum STATUS_BAR_MODE {
Expand Down Expand Up @@ -387,6 +389,10 @@ class CrossPointSettings {
char opdsServerUrl[128] = "";
char opdsUsername[64] = "";
char opdsPassword[64] = "";
char trmnlServerUrl[160] = "";
char trmnlApiKey[128] = "";
char trmnlDeviceId[32] = "";
uint8_t trmnlOrientation = TRMNL_LANDSCAPE;
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior
Expand Down
17 changes: 15 additions & 2 deletions src/SettingsList.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ inline SettingInfo buildSleepScreenSetting() {
StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{StrId::STR_NONE_OPT, StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER,
StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY, StrId::STR_READING_STATS, StrId::STR_THEME_MINIMAL,
StrId::STR_THEME_MINIMAL_STATS, StrId::STR_THEME_DASHBOARD, StrId::STR_QUICK_RESUME},
StrId::STR_THEME_MINIMAL_STATS, StrId::STR_THEME_DASHBOARD, StrId::STR_TRMNL, StrId::STR_QUICK_RESUME},
"sleepScreen", StrId::STR_CAT_DISPLAY);
s.withEnumRawValues({
static_cast<uint8_t>(CrossPointSettings::BLANK),
Expand All @@ -254,7 +254,9 @@ inline SettingInfo buildSleepScreenSetting() {
static_cast<uint8_t>(CrossPointSettings::MINIMAL_SLEEP),
static_cast<uint8_t>(CrossPointSettings::MINIMAL_STATS_SLEEP),
static_cast<uint8_t>(CrossPointSettings::DASHBOARD_SLEEP),
static_cast<uint8_t>(CrossPointSettings::TRMNL),
static_cast<uint8_t>(CrossPointSettings::QUICK_RESUME),
static_cast<uint8_t>(CrossPointSettings::TRMNL),
});
return s;
}
Expand Down Expand Up @@ -587,6 +589,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
add(SettingInfo::Toggle(StrId::STR_TRACK_READING_STATS, &CrossPointSettings::trackReadingStats, "trackReadingStats",
StrId::STR_CAT_SYSTEM));
#endif
add(SettingInfo::String(StrId::STR_TRMNL_SERVER_URL, SETTINGS.trmnlServerUrl, sizeof(SETTINGS.trmnlServerUrl),
"trmnlServerUrl", StrId::STR_TRMNL_SETTINGS));
add(SettingInfo::String(StrId::STR_TRMNL_API_KEY, SETTINGS.trmnlApiKey, sizeof(SETTINGS.trmnlApiKey),
"trmnlApiKey", StrId::STR_TRMNL_SETTINGS)
.withObfuscated());
add(SettingInfo::String(StrId::STR_TRMNL_DEVICE_ID, SETTINGS.trmnlDeviceId, sizeof(SETTINGS.trmnlDeviceId),
"trmnlDeviceId", StrId::STR_TRMNL_SETTINGS));
add(SettingInfo::Enum(StrId::STR_TRMNL_ORIENTATION, &CrossPointSettings::trmnlOrientation,
{StrId::STR_TRMNL_HORIZONTAL, StrId::STR_TRMNL_VERTICAL}, "trmnlOrientation",
StrId::STR_TRMNL_SETTINGS));

// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
add(SettingInfo::DynamicString(
Expand Down Expand Up @@ -914,11 +926,12 @@ inline std::vector<SettingInfo> buildDisplaySleepSettingsList(const std::vector<

inline std::vector<SettingInfo> buildSystemSettingsParentList(const std::vector<SettingInfo>& allSettings) {
std::vector<SettingInfo> systemSettings;
systemSettings.reserve(8);
systemSettings.reserve(9);
systemSettings.push_back(SettingInfo::Submenu(StrId::STR_SYSTEM_DEVICE, SettingAction::SystemDevice));
systemSettings.push_back(SettingInfo::Submenu(StrId::STR_SYSTEM_FILES_CACHE, SettingAction::SystemFilesCache));
systemSettings.push_back(SettingInfo::Submenu(StrId::STR_READING_STATS, SettingAction::SystemReadingStats));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
systemSettings.push_back(SettingInfo::Action(StrId::STR_TRMNL_SETTINGS, SettingAction::TrmnlSettings));
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
Expand Down
Loading