mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
593 lines
25 KiB
C++
593 lines
25 KiB
C++
#include "cardboy/apps/lockscreen_app.hpp"
|
|
|
|
#include "cardboy/apps/menu_app.hpp"
|
|
#include "cardboy/gfx/font16x8.hpp"
|
|
#include "cardboy/sdk/app_framework.hpp"
|
|
#include "cardboy/sdk/app_system.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace apps {
|
|
|
|
namespace {
|
|
|
|
using cardboy::sdk::AppButtonEvent;
|
|
using cardboy::sdk::AppContext;
|
|
using cardboy::sdk::AppTimeoutEvent;
|
|
using cardboy::sdk::AppTimerEvent;
|
|
|
|
constexpr std::uint32_t kRefreshIntervalMs = 100;
|
|
constexpr std::uint32_t kSlowRefreshMs = 1000;
|
|
constexpr std::uint32_t kFastRefreshMs = 20;
|
|
constexpr std::uint32_t kUnlockHoldMs = 1500;
|
|
|
|
using Framebuffer = typename AppContext::Framebuffer;
|
|
using Clock = typename AppContext::Clock;
|
|
|
|
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowUpGlyph{
|
|
0b00010000, 0b00111000, 0b01111100, 0b11111110, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
|
|
0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00000000, 0b00000000};
|
|
|
|
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowDownGlyph{
|
|
0b00000000, 0b00000000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000, 0b00010000,
|
|
0b00010000, 0b00010000, 0b00010000, 0b11111110, 0b01111100, 0b00111000, 0b00010000, 0b00000000};
|
|
|
|
struct TimeSnapshot {
|
|
bool hasWallTime = false;
|
|
int hour24 = 0;
|
|
int minute = 0;
|
|
int second = 0;
|
|
int year = 0;
|
|
int month = 0;
|
|
int day = 0;
|
|
int weekday = 0;
|
|
std::uint64_t uptimeSeconds = 0;
|
|
};
|
|
|
|
class LockscreenApp final : public cardboy::sdk::IApp {
|
|
public:
|
|
explicit LockscreenApp(AppContext& ctx) :
|
|
context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock), notificationCenter(ctx.notificationCenter()) {}
|
|
|
|
void onStart() override {
|
|
cancelRefreshTimer();
|
|
lastSnapshot = {};
|
|
holdActive = false;
|
|
holdProgressMs = 0;
|
|
dirty = true;
|
|
lastNotificationInteractionMs = clock.millis();
|
|
lastRefreshMs = clock.millis();
|
|
refreshNotifications();
|
|
const auto snap = captureTime();
|
|
renderIfNeeded(snap);
|
|
lastSnapshot = snap;
|
|
rescheduleRefreshTimer(kSlowRefreshMs);
|
|
}
|
|
|
|
void onStop() override { cancelRefreshTimer(); }
|
|
|
|
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
|
|
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
|
|
[this](const AppTimerEvent& timer) {
|
|
if (timer.handle == refreshTimer) {
|
|
const std::uint32_t now = clock.millis();
|
|
const std::uint32_t elapsed = now - lastRefreshMs;
|
|
lastRefreshMs = now;
|
|
advanceHoldProgress(elapsed);
|
|
updateDisplay();
|
|
}
|
|
},
|
|
[](const AppTimeoutEvent&) { /* ignore */ }));
|
|
return std::nullopt;
|
|
}
|
|
|
|
private:
|
|
static constexpr std::size_t kMaxDisplayedNotifications = 5;
|
|
static constexpr std::uint32_t kNotificationHideMs = 8000;
|
|
AppContext& context;
|
|
Framebuffer& framebuffer;
|
|
Clock& clock;
|
|
cardboy::sdk::INotificationCenter* notificationCenter = nullptr;
|
|
std::uint32_t lastNotificationRevision = 0;
|
|
std::vector<cardboy::sdk::INotificationCenter::Notification> notifications;
|
|
std::size_t selectedNotification = 0;
|
|
|
|
bool dirty = false;
|
|
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
|
TimeSnapshot lastSnapshot{};
|
|
bool holdActive = false;
|
|
std::uint32_t holdProgressMs = 0;
|
|
std::uint32_t lastNotificationInteractionMs = 0;
|
|
std::uint32_t lastRefreshMs = 0;
|
|
|
|
void cancelRefreshTimer() {
|
|
if (refreshTimer == cardboy::sdk::kInvalidAppTimer)
|
|
return;
|
|
if (auto* timer = context.timer())
|
|
timer->cancelTimer(refreshTimer);
|
|
refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
|
}
|
|
|
|
void rescheduleRefreshTimer(std::uint32_t intervalMs) {
|
|
cancelRefreshTimer();
|
|
if (auto* timer = context.timer())
|
|
refreshTimer = timer->scheduleTimer(intervalMs, true);
|
|
}
|
|
|
|
static bool comboPressed(const cardboy::sdk::InputState& state) { return state.a && state.select; }
|
|
|
|
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {
|
|
const bool upPressed = button.current.up && !button.previous.up;
|
|
const bool downPressed = button.current.down && !button.previous.down;
|
|
bool navPressed = false;
|
|
|
|
if (!notifications.empty() && (upPressed || downPressed)) {
|
|
const std::size_t count = notifications.size();
|
|
lastNotificationInteractionMs = clock.millis();
|
|
if (count > 1) {
|
|
if (upPressed && selectedNotification > 0) {
|
|
selectedNotification--;
|
|
navPressed = true;
|
|
} else if (downPressed && selectedNotification < count - 1) {
|
|
selectedNotification++;
|
|
navPressed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool deletePressed = button.current.b && !button.previous.b;
|
|
if (deletePressed && notificationCenter && !notifications.empty()) {
|
|
const std::size_t index = std::min<std::size_t>(selectedNotification, notifications.size() - 1);
|
|
const auto& note = notifications[index];
|
|
std::size_t preferredIndex = index;
|
|
if (index + 1 < notifications.size())
|
|
preferredIndex = index + 1;
|
|
else if (index > 0)
|
|
preferredIndex = index - 1;
|
|
if (note.externalId != 0)
|
|
notificationCenter->removeByExternalId(note.externalId);
|
|
else
|
|
notificationCenter->removeById(note.id);
|
|
selectedNotification = preferredIndex;
|
|
lastNotificationInteractionMs = clock.millis();
|
|
dirty = true;
|
|
refreshNotifications();
|
|
}
|
|
|
|
const bool comboNow = comboPressed(button.current);
|
|
updateHoldState(comboNow);
|
|
if (navPressed)
|
|
dirty = true;
|
|
updateDisplay();
|
|
}
|
|
|
|
void refreshNotifications() {
|
|
if (!notificationCenter) {
|
|
if (!notifications.empty() || lastNotificationRevision != 0) {
|
|
notifications.clear();
|
|
selectedNotification = 0;
|
|
lastNotificationRevision = 0;
|
|
dirty = true;
|
|
}
|
|
return;
|
|
}
|
|
const std::uint32_t revision = notificationCenter->revision();
|
|
if (revision == lastNotificationRevision)
|
|
return;
|
|
lastNotificationRevision = revision;
|
|
|
|
const std::uint64_t previousId =
|
|
(selectedNotification < notifications.size()) ? notifications[selectedNotification].id : 0;
|
|
|
|
auto latest = notificationCenter->recent(kMaxDisplayedNotifications);
|
|
notifications = std::move(latest);
|
|
|
|
if (notifications.empty()) {
|
|
selectedNotification = 0;
|
|
} else if (previousId != 0) {
|
|
auto it = std::find_if(notifications.begin(), notifications.end(),
|
|
[previousId](const auto& note) { return note.id == previousId; });
|
|
if (it != notifications.end()) {
|
|
selectedNotification = static_cast<std::size_t>(std::distance(notifications.begin(), it));
|
|
} else {
|
|
selectedNotification = 0;
|
|
}
|
|
} else {
|
|
selectedNotification = 0;
|
|
}
|
|
|
|
lastNotificationInteractionMs = clock.millis();
|
|
dirty = true;
|
|
}
|
|
|
|
void updateHoldState(bool comboNow) {
|
|
const bool wasActive = holdActive;
|
|
if (comboNow) {
|
|
if (!holdActive) {
|
|
holdActive = true;
|
|
holdProgressMs = 0;
|
|
lastRefreshMs = clock.millis();
|
|
dirty = true;
|
|
}
|
|
} else {
|
|
if (holdActive || holdProgressMs != 0) {
|
|
holdActive = false;
|
|
holdProgressMs = 0;
|
|
dirty = true;
|
|
}
|
|
}
|
|
if (wasActive != holdActive) {
|
|
rescheduleRefreshTimer(holdActive ? kFastRefreshMs : kSlowRefreshMs);
|
|
}
|
|
}
|
|
|
|
void advanceHoldProgress(std::uint32_t elapsedMs) {
|
|
if (holdActive) {
|
|
const std::uint32_t next = std::min<std::uint32_t>(holdProgressMs + elapsedMs, kUnlockHoldMs);
|
|
if (next != holdProgressMs) {
|
|
holdProgressMs = next;
|
|
dirty = true;
|
|
}
|
|
if (holdProgressMs >= kUnlockHoldMs) {
|
|
holdActive = false;
|
|
context.requestAppSwitchByName(kMenuAppName);
|
|
}
|
|
} else if (holdProgressMs != 0) {
|
|
holdProgressMs = 0;
|
|
dirty = true;
|
|
}
|
|
}
|
|
|
|
void updateDisplay() {
|
|
refreshNotifications();
|
|
const auto snap = captureTime();
|
|
if (!sameSnapshot(snap, lastSnapshot))
|
|
dirty = true;
|
|
renderIfNeeded(snap);
|
|
lastSnapshot = snap;
|
|
}
|
|
|
|
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
|
|
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second &&
|
|
a.day == b.day && a.month == b.month && a.year == b.year;
|
|
}
|
|
|
|
TimeSnapshot captureTime() const {
|
|
TimeSnapshot snap{};
|
|
snap.uptimeSeconds = clock.millis() / 1000ULL;
|
|
|
|
std::time_t raw = 0;
|
|
if (std::time(&raw) != static_cast<std::time_t>(-1) && raw > 0) {
|
|
std::tm tm{};
|
|
if (localtime_r(&raw, &tm) != nullptr) {
|
|
snap.hasWallTime = true;
|
|
snap.hour24 = tm.tm_hour;
|
|
snap.minute = tm.tm_min;
|
|
snap.second = tm.tm_sec;
|
|
snap.year = tm.tm_year + 1900;
|
|
snap.month = tm.tm_mon + 1;
|
|
snap.day = tm.tm_mday;
|
|
snap.weekday = tm.tm_wday;
|
|
return snap;
|
|
}
|
|
}
|
|
|
|
snap.hasWallTime = false;
|
|
snap.hour24 = static_cast<int>((snap.uptimeSeconds / 3600ULL) % 24ULL);
|
|
snap.minute = static_cast<int>((snap.uptimeSeconds / 60ULL) % 60ULL);
|
|
snap.second = static_cast<int>(snap.uptimeSeconds % 60ULL);
|
|
return snap;
|
|
}
|
|
|
|
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) {
|
|
const int width = font16x8::measureText(text, scale, letterSpacing);
|
|
const int x = (fb.width() - width) / 2;
|
|
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
|
|
}
|
|
|
|
static void drawRectOutline(Framebuffer& fb, int x, int y, int w, int h) {
|
|
if (w <= 0 || h <= 0)
|
|
return;
|
|
for (int dx = 0; dx < w; ++dx) {
|
|
fb.drawPixel(x + dx, y, true);
|
|
fb.drawPixel(x + dx, y + h - 1, true);
|
|
}
|
|
for (int dy = 0; dy < h; ++dy) {
|
|
fb.drawPixel(x, y + dy, true);
|
|
fb.drawPixel(x + w - 1, y + dy, true);
|
|
}
|
|
}
|
|
|
|
static void fillRect(Framebuffer& fb, int x, int y, int w, int h) {
|
|
if (w <= 0 || h <= 0)
|
|
return;
|
|
for (int dy = 0; dy < h; ++dy) {
|
|
for (int dx = 0; dx < w; ++dx) {
|
|
fb.drawPixel(x + dx, y + dy, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void drawArrowGlyph(Framebuffer& fb, int x, int y,
|
|
const std::array<std::uint8_t, font16x8::kGlyphHeight>& glyph, int scale = 1) {
|
|
if (scale <= 0)
|
|
return;
|
|
for (int row = 0; row < font16x8::kGlyphHeight; ++row) {
|
|
const std::uint8_t rowBits = glyph[row];
|
|
for (int col = 0; col < font16x8::kGlyphWidth; ++col) {
|
|
const auto mask = static_cast<std::uint8_t>(1u << (font16x8::kGlyphWidth - 1 - col));
|
|
if ((rowBits & mask) == 0)
|
|
continue;
|
|
for (int sx = 0; sx < scale; ++sx) {
|
|
for (int sy = 0; sy < scale; ++sy) {
|
|
fb.drawPixel(x + col * scale + sx, y + row * scale + sy, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void drawArrow(Framebuffer& fb, int x, int y, bool up, int scale = 1) {
|
|
const auto& glyph = up ? kArrowUpGlyph : kArrowDownGlyph;
|
|
drawArrowGlyph(fb, x, y, glyph, scale);
|
|
}
|
|
|
|
static std::string truncateWithEllipsis(std::string_view text, int maxWidth, int scale, int letterSpacing) {
|
|
if (font16x8::measureText(text, scale, letterSpacing) <= maxWidth)
|
|
return std::string(text);
|
|
|
|
std::string result(text.begin(), text.end());
|
|
const std::string ellipsis = "...";
|
|
while (!result.empty()) {
|
|
result.pop_back();
|
|
std::string candidate = result + ellipsis;
|
|
if (font16x8::measureText(candidate, scale, letterSpacing) <= maxWidth)
|
|
return candidate;
|
|
}
|
|
return ellipsis;
|
|
}
|
|
|
|
static std::vector<std::string> wrapText(std::string_view text, int maxWidth, int scale, int letterSpacing,
|
|
int maxLines) {
|
|
std::vector<std::string> lines;
|
|
if (text.empty() || maxWidth <= 0 || maxLines <= 0)
|
|
return lines;
|
|
|
|
std::string current;
|
|
std::string word;
|
|
bool truncated = false;
|
|
|
|
auto flushCurrent = [&]() {
|
|
if (current.empty())
|
|
return;
|
|
if (lines.size() < static_cast<std::size_t>(maxLines)) {
|
|
lines.push_back(current);
|
|
} else {
|
|
truncated = true;
|
|
}
|
|
current.clear();
|
|
};
|
|
|
|
for (std::size_t i = 0; i <= text.size(); ++i) {
|
|
char ch = (i < text.size()) ? text[i] : ' ';
|
|
const bool isBreak = (ch == ' ' || ch == '\n' || ch == '\r' || i == text.size());
|
|
if (!isBreak) {
|
|
word.push_back(ch);
|
|
continue;
|
|
}
|
|
|
|
if (!word.empty()) {
|
|
std::string candidate = current.empty() ? word : current + " " + word;
|
|
if (!current.empty() && font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) {
|
|
flushCurrent();
|
|
if (lines.size() >= static_cast<std::size_t>(maxLines)) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
candidate = word;
|
|
}
|
|
|
|
if (font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) {
|
|
std::string shortened = truncateWithEllipsis(word, maxWidth, scale, letterSpacing);
|
|
flushCurrent();
|
|
if (lines.size() < static_cast<std::size_t>(maxLines)) {
|
|
lines.push_back(shortened);
|
|
} else {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
current.clear();
|
|
} else {
|
|
current = candidate;
|
|
}
|
|
word.clear();
|
|
}
|
|
|
|
if (ch == '\n' || ch == '\r') {
|
|
flushCurrent();
|
|
if (lines.size() >= static_cast<std::size_t>(maxLines)) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
flushCurrent();
|
|
if (lines.size() > static_cast<std::size_t>(maxLines)) {
|
|
truncated = true;
|
|
lines.resize(maxLines);
|
|
}
|
|
if (truncated && !lines.empty()) {
|
|
lines.back() = truncateWithEllipsis(lines.back(), maxWidth, scale, letterSpacing);
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
static std::string formatDate(const TimeSnapshot& snap) {
|
|
static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
|
|
if (!snap.hasWallTime)
|
|
return "UPTIME MODE";
|
|
const char* weekday = (snap.weekday >= 0 && snap.weekday < 7) ? kWeekdays[snap.weekday] : "";
|
|
char buffer[32];
|
|
std::snprintf(buffer, sizeof(buffer), "%s %04d-%02d-%02d", weekday, snap.year, snap.month, snap.day);
|
|
return buffer;
|
|
}
|
|
|
|
void renderIfNeeded(const TimeSnapshot& snap) {
|
|
if (!dirty)
|
|
return;
|
|
dirty = false;
|
|
|
|
framebuffer.frameReady();
|
|
|
|
const int scaleTime = 4;
|
|
const int scaleSeconds = 2;
|
|
const int scaleSmall = 1;
|
|
const int textLineHeight = font16x8::kGlyphHeight * scaleSmall;
|
|
|
|
const int cardMarginTop = 4;
|
|
const int cardMarginSide = 8;
|
|
const int cardPadding = 6;
|
|
const int cardLineSpacing = 4;
|
|
int cardHeight = 0;
|
|
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
|
|
|
|
const std::uint32_t nowMs = clock.millis();
|
|
const bool hasNotifications = !notifications.empty();
|
|
const bool showNotificationDetails =
|
|
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
|
|
|
|
if (hasNotifications) {
|
|
const auto& note = notifications[selectedNotification];
|
|
if (showNotificationDetails) {
|
|
std::string title = note.title.empty() ? std::string("Notification") : note.title;
|
|
title = truncateWithEllipsis(title, cardWidth - cardPadding * 2, scaleSmall, 1);
|
|
|
|
auto bodyLines = wrapText(note.body, cardWidth - cardPadding * 2, scaleSmall, 1, 4);
|
|
|
|
cardHeight = cardPadding * 2 + textLineHeight;
|
|
if (!bodyLines.empty()) {
|
|
cardHeight += cardLineSpacing;
|
|
cardHeight += static_cast<int>(bodyLines.size()) * textLineHeight;
|
|
if (bodyLines.size() > 1)
|
|
cardHeight += (static_cast<int>(bodyLines.size()) - 1) * cardLineSpacing;
|
|
}
|
|
|
|
if (notifications.size() > 1) {
|
|
cardHeight = std::max(cardHeight, cardPadding * 2 + textLineHeight * 2 + cardLineSpacing + 8);
|
|
}
|
|
|
|
drawRectOutline(framebuffer, cardMarginSide, cardMarginTop, cardWidth, cardHeight);
|
|
if (cardWidth > 2 && cardHeight > 2)
|
|
drawRectOutline(framebuffer, cardMarginSide + 1, cardMarginTop + 1, cardWidth - 2, cardHeight - 2);
|
|
|
|
font16x8::drawText(framebuffer, cardMarginSide + cardPadding, cardMarginTop + cardPadding, title,
|
|
scaleSmall, true, 1);
|
|
|
|
if (notifications.size() > 1) {
|
|
char counter[16];
|
|
std::snprintf(counter, sizeof(counter), "%zu/%zu", selectedNotification + 1, notifications.size());
|
|
const int counterWidth = font16x8::measureText(counter, scaleSmall, 1);
|
|
const int counterX = cardMarginSide + cardWidth - cardPadding - counterWidth;
|
|
font16x8::drawText(framebuffer, counterX, cardMarginTop + cardPadding, counter, scaleSmall, true,
|
|
1);
|
|
const int arrowWidth = font16x8::kGlyphWidth * scaleSmall;
|
|
const int arrowSpacing = std::max(1, scaleSmall);
|
|
const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing;
|
|
const int arrowX = counterX + (counterWidth - arrowsTotalWide) / 2;
|
|
const int arrowY = cardMarginTop + cardPadding + textLineHeight + 1;
|
|
if (selectedNotification > 0)
|
|
drawArrow(framebuffer, arrowX, arrowY, true, scaleSmall);
|
|
if (selectedNotification < notifications.size() - 1)
|
|
drawArrow(framebuffer, arrowX + arrowWidth + arrowSpacing, arrowY, false, scaleSmall);
|
|
const int arrowHeight = font16x8::kGlyphHeight * scaleSmall;
|
|
cardHeight = std::max(cardHeight, arrowY + arrowHeight - cardMarginTop);
|
|
}
|
|
|
|
if (!bodyLines.empty()) {
|
|
int bodyY = cardMarginTop + cardPadding + textLineHeight + cardLineSpacing;
|
|
for (const auto& line: bodyLines) {
|
|
font16x8::drawText(framebuffer, cardMarginSide + cardPadding, bodyY, line, scaleSmall, true, 1);
|
|
bodyY += textLineHeight + cardLineSpacing;
|
|
}
|
|
}
|
|
} else {
|
|
cardHeight = textLineHeight + cardPadding * 2;
|
|
char summary[32];
|
|
if (notifications.size() == 1)
|
|
std::snprintf(summary, sizeof(summary), "1 NOTIFICATION");
|
|
else
|
|
std::snprintf(summary, sizeof(summary), "%zu NOTIFICATIONS", notifications.size());
|
|
const int summaryWidth = font16x8::measureText(summary, scaleSmall, 1);
|
|
const int summaryX = (framebuffer.width() - summaryWidth) / 2;
|
|
const int summaryY = cardMarginTop;
|
|
font16x8::drawText(framebuffer, summaryX, summaryY, summary, scaleSmall, true, 1);
|
|
}
|
|
}
|
|
|
|
const int defaultTimeY = (framebuffer.height() - font16x8::kGlyphHeight * scaleTime) / 2 - 8;
|
|
const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16;
|
|
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
|
|
const int timeY = std::clamp(defaultTimeY, minTimeY, maxTimeY);
|
|
|
|
char hoursMinutes[6];
|
|
std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute);
|
|
const int mainW = font16x8::measureText(hoursMinutes, scaleTime, 0);
|
|
const int timeX = (framebuffer.width() - mainW) / 2;
|
|
const int secX = timeX + mainW + 12;
|
|
const int secY = timeY + font16x8::kGlyphHeight * scaleTime - font16x8::kGlyphHeight * scaleSeconds;
|
|
char secs[3];
|
|
std::snprintf(secs, sizeof(secs), "%02d", snap.second);
|
|
|
|
font16x8::drawText(framebuffer, timeX, timeY, hoursMinutes, scaleTime, true, 0);
|
|
font16x8::drawText(framebuffer, secX, secY, secs, scaleSeconds, true, 0);
|
|
|
|
const std::string dateLine = formatDate(snap);
|
|
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 16, dateLine, scaleSmall, 1);
|
|
|
|
const std::string instruction = "HOLD A+SELECT TO UNLOCK";
|
|
const int instructionWidth = font16x8::measureText(instruction, scaleSmall, 1);
|
|
const int barHeight = 18;
|
|
const int barY = framebuffer.height() - 30;
|
|
const int textY = barY + (barHeight - textLineHeight) / 2 + 1;
|
|
drawCenteredText(framebuffer, textY, instruction, scaleSmall, 1);
|
|
|
|
const int barWidth = framebuffer.width() - 64;
|
|
const int barX = 32;
|
|
|
|
if (holdActive || holdProgressMs > 0) {
|
|
drawRectOutline(framebuffer, barX, barY, barWidth, barHeight);
|
|
const int innerWidth = barWidth - 2;
|
|
const int innerHeight = barHeight - 2;
|
|
const float ratio = std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
|
|
const int fillWidth = static_cast<int>(ratio * innerWidth + 0.5f);
|
|
if (fillWidth > 0)
|
|
fillRect(framebuffer, barX + 1, barY + 1, fillWidth, innerHeight);
|
|
}
|
|
|
|
framebuffer.sendFrame();
|
|
}
|
|
};
|
|
|
|
class LockscreenAppFactory final : public cardboy::sdk::IAppFactory {
|
|
public:
|
|
const char* name() const override { return kLockscreenAppName; }
|
|
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
|
|
return std::make_unique<LockscreenApp>(context);
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
std::unique_ptr<cardboy::sdk::IAppFactory> createLockscreenAppFactory() {
|
|
return std::make_unique<LockscreenAppFactory>();
|
|
}
|
|
|
|
} // namespace apps
|