feat: blurred letterbox, portrait pairs, overlay metadata, button fixes
- Letterbox: extend edge pixels outward with blur + fade to solid color - Portrait: detect and pair portraits side-by-side, fallback to solo - Metadata: add overlay mode (corner badge), time field, custom date/time format strings (strftime), font size 1.5x, region-aware for pairs - Buttons: swap G9/G10 to match physical layout, double-press random button for deep sleep, SLP indicator drawn before sleep - Boot: remove redundant 'Loading photos' screen (saves 17s refresh) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -84,7 +84,6 @@ void DisplayManager::showImage(const ProcessedImage& img) {
|
||||
_power->enableEPDPower();
|
||||
|
||||
Serial.println("[display] Writing framebuffer to e-ink...");
|
||||
unsigned long start = millis();
|
||||
|
||||
// Write pixel by pixel using palette-mapped colors
|
||||
_display->startWrite();
|
||||
@@ -95,13 +94,7 @@ void DisplayManager::showImage(const ProcessedImage& img) {
|
||||
}
|
||||
}
|
||||
_display->endWrite();
|
||||
|
||||
triggerRefresh();
|
||||
|
||||
unsigned long elapsed = millis() - start;
|
||||
Serial.printf("[display] Refresh complete in %lu ms\n", elapsed);
|
||||
|
||||
_power->disableEPDPower();
|
||||
// Do NOT refresh here — caller should add metadata overlay then call refresh()
|
||||
}
|
||||
|
||||
void DisplayManager::showMessage(const char* title, const char* body) {
|
||||
@@ -121,14 +114,43 @@ void DisplayManager::showMessage(const char* title, const char* body) {
|
||||
_power->disableEPDPower();
|
||||
}
|
||||
|
||||
void DisplayManager::showMetadata(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos) {
|
||||
if (metaFlags == 0) return; // No metadata to show
|
||||
void DisplayManager::showMetadata(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos,
|
||||
const String& dateFmt, const String& timeFmt) {
|
||||
showMetadataInRegion(info, metaFlags, pos, 0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, dateFmt, timeFmt);
|
||||
}
|
||||
|
||||
void DisplayManager::showMetadataInRegion(const AssetInfo& info, uint8_t metaFlags, MetaPosition pos,
|
||||
uint16_t regionX, uint16_t regionY,
|
||||
uint16_t regionW, uint16_t regionH,
|
||||
const String& dateFmt, const String& timeFmt) {
|
||||
if (metaFlags == 0) return;
|
||||
|
||||
// Build metadata string
|
||||
String metaText = "";
|
||||
|
||||
if ((metaFlags & META_DATE) && info.dateTime.length() > 0) {
|
||||
// Extract just the date portion (YYYY-MM-DD)
|
||||
// Parse ISO datetime (YYYY-MM-DDTHH:MM:SS) into struct tm for strftime
|
||||
if (((metaFlags & META_DATE) || (metaFlags & META_TIME)) && info.dateTime.length() >= 19) {
|
||||
struct tm t = {};
|
||||
sscanf(info.dateTime.c_str(), "%d-%d-%dT%d:%d:%d",
|
||||
&t.tm_year, &t.tm_mon, &t.tm_mday,
|
||||
&t.tm_hour, &t.tm_min, &t.tm_sec);
|
||||
t.tm_year -= 1900;
|
||||
t.tm_mon -= 1;
|
||||
|
||||
char buf[64];
|
||||
if ((metaFlags & META_DATE) && (metaFlags & META_TIME)) {
|
||||
String combinedFmt = dateFmt + " " + timeFmt;
|
||||
strftime(buf, sizeof(buf), combinedFmt.c_str(), &t);
|
||||
metaText += buf;
|
||||
} else if (metaFlags & META_DATE) {
|
||||
strftime(buf, sizeof(buf), dateFmt.c_str(), &t);
|
||||
metaText += buf;
|
||||
} else {
|
||||
strftime(buf, sizeof(buf), timeFmt.c_str(), &t);
|
||||
metaText += buf;
|
||||
}
|
||||
} else if ((metaFlags & META_DATE) && info.dateTime.length() >= 10) {
|
||||
// Fallback if datetime is too short for full parse
|
||||
metaText += info.dateTime.substring(0, 10);
|
||||
}
|
||||
if ((metaFlags & META_LOCATION) && info.city.length() > 0) {
|
||||
@@ -143,7 +165,7 @@ void DisplayManager::showMetadata(const AssetInfo& info, uint8_t metaFlags, Meta
|
||||
}
|
||||
}
|
||||
if ((metaFlags & META_ALBUM)) {
|
||||
// Album name would need to be passed separately — skip for now
|
||||
// Album name would need to be passed separately
|
||||
}
|
||||
if ((metaFlags & META_CAMERA) && info.camera.length() > 0) {
|
||||
if (metaText.length() > 0) metaText += " | ";
|
||||
@@ -152,13 +174,113 @@ void DisplayManager::showMetadata(const AssetInfo& info, uint8_t metaFlags, Meta
|
||||
|
||||
if (metaText.length() == 0) return;
|
||||
|
||||
// Draw semi-transparent bar with text
|
||||
uint16_t barY = (pos == MetaPosition::Top) ? 0 : (DISPLAY_HEIGHT - 30);
|
||||
_display->fillRect(0, barY, DISPLAY_WIDTH, 30, TFT_BLACK);
|
||||
_display->setTextSize(1.5f);
|
||||
_display->setTextColor(TFT_WHITE);
|
||||
_display->setTextDatum(middle_center);
|
||||
|
||||
switch (pos) {
|
||||
case MetaPosition::CaptionBottom:
|
||||
case MetaPosition::CaptionTop: {
|
||||
// Bar spanning the region width, positioned at region top or bottom
|
||||
uint16_t barH = 36;
|
||||
uint16_t barY = (pos == MetaPosition::CaptionTop)
|
||||
? regionY
|
||||
: regionY + regionH - barH;
|
||||
_display->fillRect(regionX, barY, regionW, barH, TFT_BLACK);
|
||||
_display->setTextDatum(middle_center);
|
||||
_display->drawString(metaText.c_str(), regionX + regionW / 2, barY + barH / 2);
|
||||
break;
|
||||
}
|
||||
case MetaPosition::OverlayTopLeft:
|
||||
case MetaPosition::OverlayTopRight:
|
||||
case MetaPosition::OverlayBottomLeft:
|
||||
case MetaPosition::OverlayBottomRight: {
|
||||
static constexpr uint16_t MARGIN = 8;
|
||||
static constexpr uint16_t PAD_X = 8;
|
||||
static constexpr uint16_t PAD_Y = 5;
|
||||
static constexpr uint16_t TEXT_H = 20;
|
||||
|
||||
uint16_t textW = _display->textWidth(metaText.c_str());
|
||||
uint16_t badgeW = min((uint16_t)(textW + PAD_X * 2), regionW);
|
||||
uint16_t badgeH = TEXT_H + PAD_Y * 2;
|
||||
|
||||
uint16_t bx, by;
|
||||
switch (pos) {
|
||||
case MetaPosition::OverlayTopLeft:
|
||||
bx = regionX + MARGIN;
|
||||
by = regionY + MARGIN;
|
||||
break;
|
||||
case MetaPosition::OverlayTopRight:
|
||||
bx = regionX + regionW - badgeW - MARGIN;
|
||||
by = regionY + MARGIN;
|
||||
break;
|
||||
case MetaPosition::OverlayBottomLeft:
|
||||
bx = regionX + MARGIN;
|
||||
by = regionY + regionH - badgeH - MARGIN;
|
||||
break;
|
||||
case MetaPosition::OverlayBottomRight:
|
||||
bx = regionX + regionW - badgeW - MARGIN;
|
||||
by = regionY + regionH - badgeH - MARGIN;
|
||||
break;
|
||||
default:
|
||||
bx = regionX + MARGIN;
|
||||
by = regionY + MARGIN;
|
||||
break;
|
||||
}
|
||||
|
||||
_display->fillRect(bx, by, badgeW, badgeH, TFT_BLACK);
|
||||
_display->setTextDatum(middle_left);
|
||||
_display->drawString(metaText.c_str(), bx + PAD_X, by + badgeH / 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayManager::showSleepIndicator(MetaPosition metaPos) {
|
||||
_power->enableEPDPower();
|
||||
|
||||
// Pick a corner opposite to metadata position
|
||||
static constexpr uint16_t MARGIN = 6;
|
||||
uint16_t tx, ty;
|
||||
lgfx::textdatum_t datum;
|
||||
|
||||
switch (metaPos) {
|
||||
case MetaPosition::OverlayTopLeft:
|
||||
case MetaPosition::CaptionTop:
|
||||
tx = DISPLAY_WIDTH - MARGIN;
|
||||
ty = DISPLAY_HEIGHT - MARGIN;
|
||||
datum = bottom_right;
|
||||
break;
|
||||
case MetaPosition::OverlayTopRight:
|
||||
tx = MARGIN;
|
||||
ty = DISPLAY_HEIGHT - MARGIN;
|
||||
datum = bottom_left;
|
||||
break;
|
||||
case MetaPosition::OverlayBottomLeft:
|
||||
tx = DISPLAY_WIDTH - MARGIN;
|
||||
ty = MARGIN;
|
||||
datum = top_right;
|
||||
break;
|
||||
case MetaPosition::OverlayBottomRight:
|
||||
case MetaPosition::CaptionBottom:
|
||||
default:
|
||||
tx = MARGIN;
|
||||
ty = MARGIN;
|
||||
datum = top_left;
|
||||
break;
|
||||
}
|
||||
|
||||
_display->setTextSize(1);
|
||||
_display->drawString(metaText.c_str(), DISPLAY_WIDTH / 2, barY + 15);
|
||||
_display->setTextColor(TFT_BLACK);
|
||||
_display->setTextDatum(datum);
|
||||
_display->drawString("SLP", tx, ty);
|
||||
}
|
||||
|
||||
void DisplayManager::refresh() {
|
||||
unsigned long start = millis();
|
||||
triggerRefresh();
|
||||
unsigned long elapsed = millis() - start;
|
||||
Serial.printf("[display] Refresh complete in %lu ms\n", elapsed);
|
||||
_power->disableEPDPower();
|
||||
}
|
||||
|
||||
void DisplayManager::triggerRefresh() {
|
||||
|
||||
Reference in New Issue
Block a user