Book cover sleep screen (#89)
## Summary * Fix issue with 2-bit bmp rendering * Add support generate book cover BMP from JPG and use as sleep screen ## Additional Context * It does not support other image formats beyond JPG at this point * Something is cooked with my JpegToBmpConverter logic, it generates weird interlaced looking images for some JPGs | Book 1 | Book 2| | --- | --- | |  |  |
This commit is contained in:
@@ -59,7 +59,11 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con
|
|||||||
### 3.5 Settings
|
### 3.5 Settings
|
||||||
|
|
||||||
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
||||||
- **White Sleep Screen**: Whether to use the white screen or black (inverted) default sleep screen
|
- **Sleep Screen**: Which sleep screen to display when the device sleeps, options are:
|
||||||
|
- "Dark" (default) - The default dark sleep screen
|
||||||
|
- "Light" - The same default sleep screen, on a white background
|
||||||
|
- "Custom" - Custom images from the SD card, see [3.6 Sleep Screen](#36-sleep-screen) below for more information
|
||||||
|
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||||
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled,
|
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled,
|
||||||
paragraphs will not have vertical space between them, but will have first word indentation.
|
paragraphs will not have vertical space between them, but will have first word indentation.
|
||||||
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
|
- **Short Power Button Click**: Whether to trigger the power button on a short press or a long press.
|
||||||
@@ -69,7 +73,12 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
|||||||
You can customize the sleep screen by placing custom images in specific locations on the SD card:
|
You can customize the sleep screen by placing custom images in specific locations on the SD card:
|
||||||
|
|
||||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
|
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
|
||||||
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.png` file, and one will be randomly selected each time the device sleeps.
|
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images
|
||||||
|
inside. If images are found in this directory, they will take priority over the `sleep.png` file, and one will be
|
||||||
|
randomly selected each time the device sleeps.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> For best results:
|
> For best results:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
#include <JpegToBmpConverter.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
@@ -218,7 +219,45 @@ const std::string& Epub::getPath() const { return filepath; }
|
|||||||
|
|
||||||
const std::string& Epub::getTitle() const { return title; }
|
const std::string& Epub::getTitle() const { return title; }
|
||||||
|
|
||||||
const std::string& Epub::getCoverImageItem() const { return coverImageItem; }
|
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||||
|
|
||||||
|
bool Epub::generateCoverBmp() const {
|
||||||
|
// Already generated, return true
|
||||||
|
if (SD.exists(getCoverBmpPath().c_str())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverImageItem.empty()) {
|
||||||
|
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverImageItem.substr(coverImageItem.length() - 4) == ".jpg" ||
|
||||||
|
coverImageItem.substr(coverImageItem.length() - 5) == ".jpeg") {
|
||||||
|
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
|
||||||
|
File coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_WRITE, true);
|
||||||
|
readItemContentsToStream(coverImageItem, coverJpg, 1024);
|
||||||
|
coverJpg.close();
|
||||||
|
|
||||||
|
coverJpg = SD.open((getCachePath() + "/.cover.jpg").c_str(), FILE_READ);
|
||||||
|
File coverBmp = SD.open(getCoverBmpPath().c_str(), FILE_WRITE, true);
|
||||||
|
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
||||||
|
coverJpg.close();
|
||||||
|
coverBmp.close();
|
||||||
|
SD.remove((getCachePath() + "/.cover.jpg").c_str());
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis());
|
||||||
|
SD.remove(getCoverBmpPath().c_str());
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no");
|
||||||
|
return success;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::string normalisePath(const std::string& path) {
|
std::string normalisePath(const std::string& path) {
|
||||||
std::vector<std::string> components;
|
std::vector<std::string> components;
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ class Epub {
|
|||||||
const std::string& getCachePath() const;
|
const std::string& getCachePath() const;
|
||||||
const std::string& getPath() const;
|
const std::string& getPath() const;
|
||||||
const std::string& getTitle() const;
|
const std::string& getTitle() const;
|
||||||
const std::string& getCoverImageItem() const;
|
std::string getCoverBmpPath() const;
|
||||||
|
bool generateCoverBmp() const;
|
||||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer) const {
|
|||||||
int bitShift = 6;
|
int bitShift = 6;
|
||||||
|
|
||||||
// Helper lambda to pack 2bpp color into the output stream
|
// Helper lambda to pack 2bpp color into the output stream
|
||||||
auto packPixel = [&](uint8_t lum) {
|
auto packPixel = [&](const uint8_t lum) {
|
||||||
uint8_t color = (lum >> 6); // Simple 2-bit reduction: 0-255 -> 0-3
|
uint8_t color = (lum >> 6); // Simple 2-bit reduction: 0-255 -> 0-3
|
||||||
currentOutByte |= (color << bitShift);
|
currentOutByte |= (color << bitShift);
|
||||||
if (bitShift == 0) {
|
if (bitShift == 0) {
|
||||||
@@ -140,38 +140,49 @@ BmpReaderError Bitmap::readRow(uint8_t* data, uint8_t* rowBuffer) const {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uint8_t lum;
|
||||||
|
|
||||||
switch (bpp) {
|
switch (bpp) {
|
||||||
case 8: {
|
case 32: {
|
||||||
|
const uint8_t* p = rowBuffer;
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
packPixel(paletteLum[rowBuffer[x]]);
|
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||||
|
packPixel(lum);
|
||||||
|
p += 4;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 24: {
|
case 24: {
|
||||||
const uint8_t* p = rowBuffer;
|
const uint8_t* p = rowBuffer;
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
||||||
packPixel(lum);
|
packPixel(lum);
|
||||||
p += 3;
|
p += 3;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 8: {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
packPixel(paletteLum[rowBuffer[x]]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
|
||||||
|
packPixel(lum);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 1: {
|
case 1: {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
uint8_t lum = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 0xFF : 0x00;
|
lum = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 0xFF : 0x00;
|
||||||
packPixel(lum);
|
packPixel(lum);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 32: {
|
default:
|
||||||
const uint8_t* p = rowBuffer;
|
return BmpReaderError::UnsupportedBpp;
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
|
|
||||||
packPixel(lum);
|
|
||||||
p += 4;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush remaining bits if width is not a multiple of 4
|
// Flush remaining bits if width is not a multiple of 4
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
std::ofstream outputFile(SETTINGS_FILE);
|
std::ofstream outputFile(SETTINGS_FILE);
|
||||||
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
serialization::writePod(outputFile, SETTINGS_FILE_VERSION);
|
||||||
serialization::writePod(outputFile, SETTINGS_COUNT);
|
serialization::writePod(outputFile, SETTINGS_COUNT);
|
||||||
serialization::writePod(outputFile, whiteSleepScreen);
|
serialization::writePod(outputFile, sleepScreen);
|
||||||
serialization::writePod(outputFile, extraParagraphSpacing);
|
serialization::writePod(outputFile, extraParagraphSpacing);
|
||||||
serialization::writePod(outputFile, shortPwrBtn);
|
serialization::writePod(outputFile, shortPwrBtn);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
@@ -54,7 +54,7 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
// load settings that exist
|
// load settings that exist
|
||||||
uint8_t settingsRead = 0;
|
uint8_t settingsRead = 0;
|
||||||
do {
|
do {
|
||||||
serialization::readPod(inputFile, whiteSleepScreen);
|
serialization::readPod(inputFile, sleepScreen);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
serialization::readPod(inputFile, extraParagraphSpacing);
|
serialization::readPod(inputFile, extraParagraphSpacing);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ class CrossPointSettings {
|
|||||||
CrossPointSettings(const CrossPointSettings&) = delete;
|
CrossPointSettings(const CrossPointSettings&) = delete;
|
||||||
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
|
CrossPointSettings& operator=(const CrossPointSettings&) = delete;
|
||||||
|
|
||||||
|
// Should match with SettingsActivity text
|
||||||
|
enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3 };
|
||||||
|
|
||||||
// Sleep screen settings
|
// Sleep screen settings
|
||||||
uint8_t whiteSleepScreen = 0;
|
uint8_t sleepScreen = DARK;
|
||||||
// Text rendering settings
|
// Text rendering settings
|
||||||
uint8_t extraParagraphSpacing = 1;
|
uint8_t extraParagraphSpacing = 1;
|
||||||
// Duration of the power button press
|
// Duration of the power button press
|
||||||
|
|||||||
@@ -1,16 +1,45 @@
|
|||||||
#include "SleepActivity.h"
|
#include "SleepActivity.h"
|
||||||
|
|
||||||
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "CrossPointState.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "images/CrossLarge.h"
|
#include "images/CrossLarge.h"
|
||||||
|
|
||||||
void SleepActivity::onEnter() {
|
void SleepActivity::onEnter() {
|
||||||
renderPopup("Entering Sleep...");
|
renderPopup("Entering Sleep...");
|
||||||
|
|
||||||
|
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM) {
|
||||||
|
return renderCustomSleepScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::COVER) {
|
||||||
|
return renderCoverSleepScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDefaultSleepScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SleepActivity::renderPopup(const char* message) const {
|
||||||
|
const int textWidth = renderer.getTextWidth(READER_FONT_ID, message);
|
||||||
|
constexpr int margin = 20;
|
||||||
|
const int x = (renderer.getScreenWidth() - textWidth - margin * 2) / 2;
|
||||||
|
constexpr int y = 117;
|
||||||
|
const int w = textWidth + margin * 2;
|
||||||
|
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
|
||||||
|
// renderer.clearScreen();
|
||||||
|
renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false);
|
||||||
|
renderer.drawText(READER_FONT_ID, x + margin, y + margin, message);
|
||||||
|
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SleepActivity::renderCustomSleepScreen() const {
|
||||||
// Check if we have a /sleep directory
|
// Check if we have a /sleep directory
|
||||||
auto dir = SD.open("/sleep");
|
auto dir = SD.open("/sleep");
|
||||||
if (dir && dir.isDirectory()) {
|
if (dir && dir.isDirectory()) {
|
||||||
@@ -28,31 +57,31 @@ void SleepActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filename.substr(filename.length() - 4) != ".bmp") {
|
if (filename.substr(filename.length() - 4) != ".bmp") {
|
||||||
Serial.printf("[%lu] [Slp] Skipping non-.bmp file name: %s\n", millis(), file.name());
|
Serial.printf("[%lu] [SLP] Skipping non-.bmp file name: %s\n", millis(), file.name());
|
||||||
file.close();
|
file.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [Slp] Skipping invalid BMP file: %s\n", millis(), file.name());
|
Serial.printf("[%lu] [SLP] Skipping invalid BMP file: %s\n", millis(), file.name());
|
||||||
file.close();
|
file.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
files.emplace_back(filename);
|
files.emplace_back(filename);
|
||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
int numFiles = files.size();
|
const auto numFiles = files.size();
|
||||||
if (numFiles > 0) {
|
if (numFiles > 0) {
|
||||||
// Generate a random number between 1 and numFiles
|
// Generate a random number between 1 and numFiles
|
||||||
int randomFileIndex = random(numFiles);
|
const auto randomFileIndex = random(numFiles);
|
||||||
auto filename = "/sleep/" + files[randomFileIndex];
|
const auto filename = "/sleep/" + files[randomFileIndex];
|
||||||
auto file = SD.open(filename.c_str());
|
auto file = SD.open(filename.c_str());
|
||||||
if (file) {
|
if (file) {
|
||||||
Serial.printf("[%lu] [Slp] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
|
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
|
||||||
delay(100);
|
delay(100);
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
renderCustomSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap);
|
||||||
dir.close();
|
dir.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -67,8 +96,8 @@ void SleepActivity::onEnter() {
|
|||||||
if (file) {
|
if (file) {
|
||||||
Bitmap bitmap(file);
|
Bitmap bitmap(file);
|
||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
Serial.printf("[%lu] [Slp] Loading: /sleep.bmp\n", millis());
|
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis());
|
||||||
renderCustomSleepScreen(bitmap);
|
renderBitmapSleepScreen(bitmap);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,41 +105,27 @@ void SleepActivity::onEnter() {
|
|||||||
renderDefaultSleepScreen();
|
renderDefaultSleepScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SleepActivity::renderPopup(const char* message) const {
|
|
||||||
const int textWidth = renderer.getTextWidth(READER_FONT_ID, message);
|
|
||||||
constexpr int margin = 20;
|
|
||||||
const int x = (GfxRenderer::getScreenWidth() - textWidth - margin * 2) / 2;
|
|
||||||
constexpr int y = 117;
|
|
||||||
const int w = textWidth + margin * 2;
|
|
||||||
const int h = renderer.getLineHeight(READER_FONT_ID) + margin * 2;
|
|
||||||
// renderer.clearScreen();
|
|
||||||
renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false);
|
|
||||||
renderer.drawText(READER_FONT_ID, x + margin, y + margin, message);
|
|
||||||
renderer.drawRect(x + 5, y + 5, w - 10, h - 10);
|
|
||||||
renderer.displayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SleepActivity::renderDefaultSleepScreen() const {
|
void SleepActivity::renderDefaultSleepScreen() const {
|
||||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
||||||
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD);
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
|
||||||
|
|
||||||
// Apply white screen if enabled in settings
|
// Make sleep screen dark unless light is selected in settings
|
||||||
if (!SETTINGS.whiteSleepScreen) {
|
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) {
|
||||||
renderer.invertScreen();
|
renderer.invertScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SleepActivity::renderCustomSleepScreen(const Bitmap& bitmap) const {
|
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
|
||||||
int x, y;
|
int x, y;
|
||||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
|
||||||
// image will scale, make sure placement is right
|
// image will scale, make sure placement is right
|
||||||
@@ -153,3 +168,26 @@ void SleepActivity::renderCustomSleepScreen(const Bitmap& bitmap) const {
|
|||||||
renderer.setRenderMode(GfxRenderer::BW);
|
renderer.setRenderMode(GfxRenderer::BW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SleepActivity::renderCoverSleepScreen() const {
|
||||||
|
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
|
if (!lastEpub.load()) {
|
||||||
|
Serial.println("[SLP] Failed to load last epub");
|
||||||
|
return renderDefaultSleepScreen();
|
||||||
|
}
|
||||||
|
if (!lastEpub.generateCoverBmp()) {
|
||||||
|
Serial.println("[SLP] Failed to generate cover bmp");
|
||||||
|
return renderDefaultSleepScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto file = SD.open(lastEpub.getCoverBmpPath().c_str(), FILE_READ);
|
||||||
|
if (file) {
|
||||||
|
Bitmap bitmap(file);
|
||||||
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
|
renderBitmapSleepScreen(bitmap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDefaultSleepScreen();
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class SleepActivity final : public Activity {
|
|||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void renderDefaultSleepScreen() const;
|
|
||||||
void renderCustomSleepScreen(const Bitmap& bitmap) const;
|
|
||||||
void renderPopup(const char* message) const;
|
void renderPopup(const char* message) const;
|
||||||
|
void renderDefaultSleepScreen() const;
|
||||||
|
void renderCustomSleepScreen() const;
|
||||||
|
void renderCoverSleepScreen() const;
|
||||||
|
void renderBitmapSleepScreen(const Bitmap& bitmap) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
|
namespace {
|
||||||
const SettingInfo SettingsActivity::settingsList[settingsCount] = {
|
constexpr int settingsCount = 3;
|
||||||
{"White Sleep Screen", SettingType::TOGGLE, &CrossPointSettings::whiteSleepScreen},
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing},
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn}};
|
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||||
|
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
|
||||||
|
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}}};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void SettingsActivity::taskTrampoline(void* param) {
|
void SettingsActivity::taskTrampoline(void* param) {
|
||||||
auto* self = static_cast<SettingsActivity*>(param);
|
auto* self = static_cast<SettingsActivity*>(param);
|
||||||
@@ -81,15 +84,18 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
|
|
||||||
const auto& setting = settingsList[selectedSettingIndex];
|
const auto& setting = settingsList[selectedSettingIndex];
|
||||||
|
|
||||||
// Only toggle if it's a toggle type and has a value pointer
|
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
||||||
if (setting.type != SettingType::TOGGLE || setting.valuePtr == nullptr) {
|
// Toggle the boolean value using the member pointer
|
||||||
|
const bool currentValue = SETTINGS.*(setting.valuePtr);
|
||||||
|
SETTINGS.*(setting.valuePtr) = !currentValue;
|
||||||
|
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
|
||||||
|
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
|
||||||
|
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
|
||||||
|
} else {
|
||||||
|
// Only toggle if it's a toggle type and has a value pointer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle the boolean value using the member pointer
|
|
||||||
bool currentValue = SETTINGS.*(setting.valuePtr);
|
|
||||||
SETTINGS.*(setting.valuePtr) = !currentValue;
|
|
||||||
|
|
||||||
// Save settings when they change
|
// Save settings when they change
|
||||||
SETTINGS.saveToFile();
|
SETTINGS.saveToFile();
|
||||||
}
|
}
|
||||||
@@ -129,8 +135,13 @@ void SettingsActivity::render() const {
|
|||||||
|
|
||||||
// Draw value based on setting type
|
// Draw value based on setting type
|
||||||
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
|
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
|
||||||
bool value = SETTINGS.*(settingsList[i].valuePtr);
|
const bool value = SETTINGS.*(settingsList[i].valuePtr);
|
||||||
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
|
renderer.drawText(UI_FONT_ID, pageWidth - 80, settingY, value ? "ON" : "OFF");
|
||||||
|
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
||||||
|
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
||||||
|
auto valueText = settingsList[i].enumValues[value];
|
||||||
|
const auto width = renderer.getTextWidth(UI_FONT_ID, valueText.c_str());
|
||||||
|
renderer.drawText(UI_FONT_ID, pageWidth - 50 - width, settingY, valueText.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,14 @@
|
|||||||
|
|
||||||
class CrossPointSettings;
|
class CrossPointSettings;
|
||||||
|
|
||||||
enum class SettingType { TOGGLE };
|
enum class SettingType { TOGGLE, ENUM };
|
||||||
|
|
||||||
// Structure to hold setting information
|
// Structure to hold setting information
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
const char* name; // Display name of the setting
|
const char* name; // Display name of the setting
|
||||||
SettingType type; // Type of setting
|
SettingType type; // Type of setting
|
||||||
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE)
|
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings (for TOGGLE/ENUM)
|
||||||
|
std::vector<std::string> enumValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsActivity final : public Activity {
|
class SettingsActivity final : public Activity {
|
||||||
@@ -28,10 +29,6 @@ class SettingsActivity final : public Activity {
|
|||||||
int selectedSettingIndex = 0; // Currently selected setting
|
int selectedSettingIndex = 0; // Currently selected setting
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
// Static settings list
|
|
||||||
static constexpr int settingsCount = 3; // Number of settings
|
|
||||||
static const SettingInfo settingsList[settingsCount];
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void render() const;
|
void render() const;
|
||||||
|
|||||||
Reference in New Issue
Block a user