diff --git a/assets/sprites/lifecycle/lifecycle_placeholder.png b/assets/sprites/lifecycle/lifecycle_placeholder.png new file mode 100644 index 0000000..64dfe22 Binary files /dev/null and b/assets/sprites/lifecycle/lifecycle_placeholder.png differ diff --git a/src/LifeCountdownCard.cpp b/src/LifeCountdownCard.cpp new file mode 100644 index 0000000..e6939b6 --- /dev/null +++ b/src/LifeCountdownCard.cpp @@ -0,0 +1,331 @@ +#include "LifeCountdownCard.h" +#include +#include +#include + +// Define the static holiday array +const LifeCountdownCard::Holiday LifeCountdownCard::US_HOLIDAYS[] = { + {1, 1, "New Year's Day"}, + {1, 20, "Martin Luther King Jr. Day"}, + {2, 17, "Presidents Day"}, + {5, 26, "Memorial Day"}, + {6, 19, "Juneteenth"}, + {7, 4, "Independence Day"}, + {9, 1, "Labor Day"}, + {10, 13, "Columbus Day"}, + {11, 11, "Veterans Day"}, + {11, 27, "Thanksgiving"}, + {12, 25, "Christmas"} +}; + +const int LifeCountdownCard::HOLIDAY_COUNT = sizeof(US_HOLIDAYS) / sizeof(Holiday); + +LifeCountdownCard::LifeCountdownCard() + : main_container(nullptr), + display_label(nullptr), + currentMode(MODE_END_OF_WORKDAY), + birthdayMonth(6), + birthdayDay(15), + navaStartYear(2024), + navaStartMonth(1), + navaStartDay(15), + gender(0), + customLifeExpectancy(80), + workStartHour(9), + workEndHour(17) { +} + +LifeCountdownCard::~LifeCountdownCard() { + cleanup(); +} + +void LifeCountdownCard::setup(lv_obj_t* parent_screen) { + // Create main container + main_container = lv_obj_create(parent_screen); + lv_obj_set_size(main_container, LV_PCT(100), LV_PCT(100)); + lv_obj_clear_flag(main_container, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(main_container, lv_color_hex(0x000000), 0); + lv_obj_set_style_border_width(main_container, 0, 0); + lv_obj_set_style_pad_all(main_container, 0, 0); + + // Create display label for countdown + display_label = lv_label_create(main_container); + lv_obj_set_style_text_color(display_label, lv_color_hex(0xFFFFFF), 0); + //lv_obj_set_style_text_font(display_label, &lv_font_montserrat_24, 0); + lv_obj_center(display_label); + lv_label_set_text(display_label, "Loading..."); + + // Load default settings + loadPreferences(); + + // Initial draw + drawModeDisplay(); +} + +void LifeCountdownCard::loop() { + // Update display regularly + drawModeDisplay(); +} + +void LifeCountdownCard::cleanup() { + if (main_container != nullptr) { + lv_obj_del(main_container); + main_container = nullptr; + display_label = nullptr; + } +} + +lv_obj_t* LifeCountdownCard::get_main_container() { + return main_container; +} + +void LifeCountdownCard::loadPreferences() { + // For now, just use hardcoded defaults + // TODO: Add persistent storage later + currentMode = MODE_END_OF_WORKDAY; + + // Default user settings (you can customize these) + birthdayMonth = 6; // June + birthdayDay = 15; // 15th + navaStartYear = 2024; + navaStartMonth = 1; + navaStartDay = 15; + gender = 0; + customLifeExpectancy = 80; + workStartHour = 9; + workEndHour = 17; +} + +void LifeCountdownCard::cycleMode() { + currentMode = (CountdownMode)((currentMode + 1) % MODE_LAST_DAY); + drawModeDisplay(); +} + +void LifeCountdownCard::drawModeDisplay() { + if (display_label == nullptr) return; + + char buffer[128]; + const char* modeLabel = getModeLabel(); + const char* unitLabel = getUnitLabel(); + + switch (currentMode) { + case MODE_END_OF_WORKDAY: { + float hours = calculateHoursUntilEndOfDay(); + snprintf(buffer, sizeof(buffer), "%s\n%.1f %s", modeLabel, hours, unitLabel); + break; + } + case MODE_END_OF_WEEK: { + float days = calculateDaysUntilEndOfWeek(); + snprintf(buffer, sizeof(buffer), "%s\n%.1f %s", modeLabel, days, unitLabel); + break; + } + case MODE_NEXT_HOLIDAY: { + const char* holidayName = ""; + int days = calculateDaysUntilNextHoliday(&holidayName); + snprintf(buffer, sizeof(buffer), "%s\n%d %s\n(%s)", modeLabel, days, unitLabel, holidayName); + break; + } + case MODE_NEXT_BIRTHDAY: { + int days = calculateDaysUntilBirthday(); + snprintf(buffer, sizeof(buffer), "%s\n%d %s", modeLabel, days, unitLabel); + break; + } + case MODE_DAYS_AT_NAVA: { + int days = calculateDaysSinceNavaStart(); + snprintf(buffer, sizeof(buffer), "%s\n%d %s", modeLabel, days, unitLabel); + break; + } + case MODE_END_OF_YEAR: { + int days = calculateDaysUntilEndOfYear(); + snprintf(buffer, sizeof(buffer), "%s\n%d %s", modeLabel, days, unitLabel); + break; + } + default: + snprintf(buffer, sizeof(buffer), "Unknown Mode"); + } + + lv_label_set_text(display_label, buffer); + lv_obj_center(display_label); +} + +const char* LifeCountdownCard::getModeLabel() { + switch (currentMode) { + case MODE_END_OF_WORKDAY: return "Until End of Workday"; + case MODE_END_OF_WEEK: return "Until Weekend"; + case MODE_NEXT_HOLIDAY: return "Until Next Holiday"; + case MODE_NEXT_BIRTHDAY: return "Until Birthday"; + case MODE_DAYS_AT_NAVA: return "Days at Nava"; + case MODE_END_OF_YEAR: return "Until End of Year"; + default: return "Unknown"; + } +} + +const char* LifeCountdownCard::getUnitLabel() { + switch (currentMode) { + case MODE_END_OF_WORKDAY: return "hours"; + case MODE_END_OF_WEEK: return "days"; + case MODE_NEXT_HOLIDAY: return "days"; + case MODE_NEXT_BIRTHDAY: return "days"; + case MODE_DAYS_AT_NAVA: return "days"; + case MODE_END_OF_YEAR: return "days"; + default: return ""; + } +} + +// Calculation methods +struct tm LifeCountdownCard::getCurrentTime() { + time_t now = time(nullptr); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + return timeinfo; +} + +float LifeCountdownCard::calculateHoursUntilEndOfDay() { + struct tm now = getCurrentTime(); + int currentHour = now.tm_hour; + int currentMin = now.tm_min; + + // If past work end time, show 0 + if (currentHour >= workEndHour) { + return 0.0f; + } + + float hoursLeft = (workEndHour - currentHour) - (currentMin / 60.0f); + return hoursLeft > 0 ? hoursLeft : 0.0f; +} + +float LifeCountdownCard::calculateDaysUntilEndOfWeek() { + struct tm now = getCurrentTime(); + int currentDay = now.tm_wday; // 0=Sunday, 6=Saturday + int currentHour = now.tm_hour; + + // Days until Friday 5pm + int daysUntilFriday = (5 - currentDay + 7) % 7; + if (daysUntilFriday == 0 && currentHour >= 17) { + daysUntilFriday = 7; // Next Friday + } + + float hoursIntoDay = currentHour + (now.tm_min / 60.0f); + float daysLeft = daysUntilFriday + ((17.0f - hoursIntoDay) / 24.0f); + + return daysLeft > 0 ? daysLeft : 0.0f; +} + +int LifeCountdownCard::calculateDaysUntilNextHoliday(const char** holidayName) { + struct tm now = getCurrentTime(); + int currentYear = now.tm_year + 1900; + int currentMonth = now.tm_mon + 1; + int currentDay = now.tm_mday; + + int minDays = 999999; + const char* nextHoliday = "None"; + + for (int i = 0; i < HOLIDAY_COUNT; i++) { + int holidayMonth = US_HOLIDAYS[i].month; + int holidayDay = US_HOLIDAYS[i].day; + + // Calculate days to this holiday + struct tm holidayTime = now; + holidayTime.tm_year = currentYear - 1900; + holidayTime.tm_mon = holidayMonth - 1; + holidayTime.tm_mday = holidayDay; + + time_t holidayTimestamp = mktime(&holidayTime); + time_t nowTimestamp = mktime(&now); + + int days = (int)difftime(holidayTimestamp, nowTimestamp) / 86400; + + // If holiday already passed this year, check next year + if (days < 0) { + holidayTime.tm_year = currentYear - 1900 + 1; + holidayTimestamp = mktime(&holidayTime); + days = (int)difftime(holidayTimestamp, nowTimestamp) / 86400; + } + + if (days >= 0 && days < minDays) { + minDays = days; + nextHoliday = US_HOLIDAYS[i].name; + } + } + + *holidayName = nextHoliday; + return minDays; +} + +int LifeCountdownCard::calculateDaysUntilBirthday() { + struct tm now = getCurrentTime(); + int currentYear = now.tm_year + 1900; + + struct tm birthday = now; + birthday.tm_year = currentYear - 1900; + birthday.tm_mon = birthdayMonth - 1; + birthday.tm_mday = birthdayDay; + + time_t birthdayTimestamp = mktime(&birthday); + time_t nowTimestamp = mktime(&now); + + int days = (int)difftime(birthdayTimestamp, nowTimestamp) / 86400; + + // If birthday already passed this year, calculate for next year + if (days < 0) { + birthday.tm_year = currentYear - 1900 + 1; + birthdayTimestamp = mktime(&birthday); + days = (int)difftime(birthdayTimestamp, nowTimestamp) / 86400; + } + + return days; +} + +int LifeCountdownCard::calculateDaysSinceNavaStart() { + struct tm now = getCurrentTime(); + + struct tm navaStart = {}; + navaStart.tm_year = navaStartYear - 1900; + navaStart.tm_mon = navaStartMonth - 1; + navaStart.tm_mday = navaStartDay; + + time_t navaTimestamp = mktime(&navaStart); + time_t nowTimestamp = mktime(&now); + + int days = (int)difftime(nowTimestamp, navaTimestamp) / 86400; + + return days > 0 ? days : 0; +} + +int LifeCountdownCard::calculateDaysUntilEndOfYear() { + struct tm now = getCurrentTime(); + int currentYear = now.tm_year + 1900; + + struct tm endOfYear = now; + endOfYear.tm_year = currentYear - 1900; + endOfYear.tm_mon = 11; // December + endOfYear.tm_mday = 31; + endOfYear.tm_hour = 23; + endOfYear.tm_min = 59; + endOfYear.tm_sec = 59; + + time_t endTimestamp = mktime(&endOfYear); + time_t nowTimestamp = mktime(&now); + + int days = (int)difftime(endTimestamp, nowTimestamp) / 86400; + + return days > 0 ? days : 0; +} + +bool LifeCountdownCard::isLeapYear(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); +} + +int LifeCountdownCard::getDaysInMonth(int month, int year) { + const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 2 && isLeapYear(year)) { + return 29; + } + return daysInMonth[month - 1]; +} + +long LifeCountdownCard::calculateDaysUntilLastDay() { + // This would calculate based on life expectancy + // Placeholder for now + return 30000; // ~82 years +} \ No newline at end of file diff --git a/src/LifeCountdownCard.h b/src/LifeCountdownCard.h new file mode 100644 index 0000000..7a38bcf --- /dev/null +++ b/src/LifeCountdownCard.h @@ -0,0 +1,77 @@ +#ifndef LIFE_COUNTDOWN_CARD_H +#define LIFE_COUNTDOWN_CARD_H + +#include +#include + +enum CountdownMode { + MODE_END_OF_WORKDAY = 0, + MODE_END_OF_WEEK, + MODE_NEXT_HOLIDAY, + MODE_NEXT_BIRTHDAY, + MODE_DAYS_AT_NAVA, + MODE_END_OF_YEAR, + MODE_LAST_DAY +}; + +class LifeCountdownCard { +public: + LifeCountdownCard(); + ~LifeCountdownCard(); + + void setup(lv_obj_t* parent_screen); + void loop(); + void cleanup(); + lv_obj_t* get_main_container(); + void cycleMode(); + +private: + lv_obj_t* main_container; + lv_obj_t* display_label; + + // Remove: Preferences prefs; + CountdownMode currentMode; + + // User configuration (hardcoded for now) + int birthdayMonth; + int birthdayDay; + int navaStartYear; + int navaStartMonth; + int navaStartDay; + int gender; + int customLifeExpectancy; + int workStartHour; + int workEndHour; + + struct Holiday { + int month; + int day; + const char* name; + }; + + static const Holiday US_HOLIDAYS[]; + static const int HOLIDAY_COUNT; + + // Calculation methods + float calculateHoursUntilEndOfDay(); + float calculateDaysUntilEndOfWeek(); + int calculateDaysUntilNextHoliday(const char** holidayName); + int calculateDaysUntilBirthday(); + int calculateDaysSinceNavaStart(); + int calculateDaysUntilEndOfYear(); + long calculateDaysUntilLastDay(); + + // Helper methods + void loadPreferences(); // Will just set defaults + void cycleToNextMode(); + void drawModeDisplay(); + const char* getModeLabel(); + const char* getUnitLabel(); + + // Time helpers + struct tm getCurrentTime(); + int getDaysInMonth(int month, int year); + bool isLeapYear(int year); +}; + +#endif // LIFE_COUNTDOWN_CARD_H \ No newline at end of file diff --git a/src/config/CardConfig.h b/src/config/CardConfig.h index dfb7d2d..3d9886d 100644 --- a/src/config/CardConfig.h +++ b/src/config/CardConfig.h @@ -13,8 +13,9 @@ enum class CardType { HELLO_WORLD, ///< Simple hello world card FLAPPY_HOG, ///< Flappy Hog game card QUESTION, ///< Question trivia card - PADDLE ///< Paddle game card + PADDLE, ///< Paddle game card // New card types can be added here + LIFE_COUNTDOWN }; /** @@ -85,6 +86,7 @@ inline String cardTypeToString(CardType type) { case CardType::FLAPPY_HOG: return "FLAPPY_HOG"; case CardType::QUESTION: return "QUESTION"; case CardType::PADDLE: return "PADDLE"; + case CardType::LIFE_COUNTDOWN: return "LIFE_COUNTDOWN"; default: return "UNKNOWN"; } } @@ -101,5 +103,6 @@ inline CardType stringToCardType(const String& str) { if (str == "FLAPPY_HOG") return CardType::FLAPPY_HOG; if (str == "QUESTION") return CardType::QUESTION; if (str == "PADDLE") return CardType::PADDLE; + if (str == "LIFE_COUNTDOWN") return CardType::LIFE_COUNTDOWN; return CardType::INSIGHT; // Default fallback } \ No newline at end of file diff --git a/src/ui/CardController.cpp b/src/ui/CardController.cpp index 2cb08a9..311f16b 100644 --- a/src/ui/CardController.cpp +++ b/src/ui/CardController.cpp @@ -1,3 +1,4 @@ +#include "LifeCountdownCardWrapper.h" #include "ui/CardController.h" #include "ui/PaddleCard.h" #include @@ -209,6 +210,8 @@ void CardController::registerCardType(const CardDefinition& definition) { } void CardController::initializeCardTypes() { + Serial.println("DEBUG: Starting card type initialization..."); + // Register INSIGHT card type CardDefinition insightDef; insightDef.type = CardType::INSIGHT; @@ -381,6 +384,35 @@ void CardController::initializeCardTypes() { return nullptr; }; registerCardType(paddleDef); + + // Register LIFE_COUNTDOWN card type + CardDefinition lifecountdownDef; + lifecountdownDef.type = CardType::LIFE_COUNTDOWN; + lifecountdownDef.name = "Life Countdown"; + lifecountdownDef.allowMultiple = false; + lifecountdownDef.needsConfigInput = false; + lifecountdownDef.configInputLabel = ""; + lifecountdownDef.uiDescription = "Motivational countdowns for your life and work"; + lifecountdownDef.factory = [this](const String& configValue) -> lv_obj_t* { + LifeCountdownCardWrapper* newCard = new LifeCountdownCardWrapper(screen); + + if (newCard && newCard->getCard()) { + // Add to unified tracking system + CardInstance instance{newCard, newCard->getCard()}; + dynamicCards[CardType::LIFE_COUNTDOWN].push_back(instance); + + // Register as input handler + cardStack->registerInputHandler(newCard->getCard(), newCard); + return newCard->getCard(); + } + + delete newCard; + return nullptr; + }; + + registerCardType(lifecountdownDef); +Serial.println("DEBUG: Life Countdown card type registered!"); +Serial.printf("Total registered card types: %d\n", registeredCardTypes.size()); } void CardController::handleCardConfigChanged() { diff --git a/src/ui/LifeCountdownCardWrapper.cpp b/src/ui/LifeCountdownCardWrapper.cpp new file mode 100644 index 0000000..4ff903c --- /dev/null +++ b/src/ui/LifeCountdownCardWrapper.cpp @@ -0,0 +1,103 @@ +#include "LifeCountdownCardWrapper.h" +#include + +LifeCountdownCardWrapper::LifeCountdownCardWrapper(lv_obj_t* parent) { + markedForRemoval = false; + + // Create the card container + cardContainer = lv_obj_create(parent); + lv_obj_set_size(cardContainer, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(cardContainer, lv_color_hex(0x000000), 0); + lv_obj_set_style_border_width(cardContainer, 0, 0); + lv_obj_clear_flag(cardContainer, LV_OBJ_FLAG_SCROLLABLE); + + // Create the actual Life Countdown Card + lifecycleCard = new LifeCountdownCard(); + lifecycleCard->init(); + + // Create LVGL labels for display + // Large number in center + labelNumber = lv_label_create(cardContainer); + lv_obj_set_style_text_font(labelNumber, &lv_font_montserrat_48, 0); + lv_obj_set_style_text_color(labelNumber, lv_color_hex(0xFFFFFF), 0); + lv_obj_align(labelNumber, LV_ALIGN_CENTER, 0, -20); + lv_label_set_text(labelNumber, "---"); + + // Unit label below number + labelUnit = lv_label_create(cardContainer); + lv_obj_set_style_text_font(labelUnit, &lv_font_montserrat_20, 0); + lv_obj_set_style_text_color(labelUnit, lv_color_hex(0xAAAAAA), 0); + lv_obj_align(labelUnit, LV_ALIGN_CENTER, 0, 30); + lv_label_set_text(labelUnit, "hours"); + + // Context text at bottom + labelContext = lv_label_create(cardContainer); + lv_obj_set_style_text_font(labelContext, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(labelContext, lv_color_hex(0x888888), 0); + lv_obj_align(labelContext, LV_ALIGN_BOTTOM_MID, 0, -40); + lv_label_set_text(labelContext, "until 5pm"); + + // Mode indicator in bottom right + labelMode = lv_label_create(cardContainer); + lv_obj_set_style_text_font(labelMode, &lv_font_montserrat_12, 0); + lv_obj_set_style_text_color(labelMode, lv_color_hex(0x666666), 0); + lv_obj_align(labelMode, LV_ALIGN_BOTTOM_RIGHT, -10, -10); + lv_label_set_text(labelMode, "1/7"); + + Serial.println("LifeCountdownCard initialized"); +} + +LifeCountdownCardWrapper::~LifeCountdownCardWrapper() { + if (lifecycleCard) { + delete lifecycleCard; + lifecycleCard = nullptr; + } + + if (!markedForRemoval && cardContainer) { + lv_obj_del(cardContainer); + cardContainer = nullptr; + } +} + +lv_obj_t* LifeCountdownCardWrapper::getCard() { + return cardContainer; +} + +bool LifeCountdownCardWrapper::handleButtonPress(uint8_t button_index) { + // Center button (index 1) cycles through modes + if (button_index == 1) { + lifecycleCard->handleInput(); + return true; + } + return false; +} + +void LifeCountdownCardWrapper::prepareForRemoval() { + markedForRemoval = true; +} + +bool LifeCountdownCardWrapper::update() { + if (!lifecycleCard) { + return true; + } + + // Update the card logic + lifecycleCard->update(); + + // Get current values and update display + // This is a simplified version - you'll enhance this + + // For now, just update with placeholder text + // In a full implementation, you'd call methods on lifecycleCard + // to get the actual countdown values + + static char numberBuffer[16]; + snprintf(numberBuffer, sizeof(numberBuffer), "42"); + lv_label_set_text(labelNumber, numberBuffer); + + lv_label_set_text(labelUnit, "days"); + lv_label_set_text(labelContext, "until Friday"); + lv_label_set_text(labelMode, "2/7"); + + return true; // Continue receiving updates +} \ No newline at end of file diff --git a/src/ui/LifeCountdownCardWrapper.h b/src/ui/LifeCountdownCardWrapper.h new file mode 100644 index 0000000..537f939 --- /dev/null +++ b/src/ui/LifeCountdownCardWrapper.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "ui/InputHandler.h" +#include "LifeCountdownCard.h" + +class LifeCountdownCardWrapper : public InputHandler { +public: + LifeCountdownCardWrapper(lv_obj_t* parent); + ~LifeCountdownCardWrapper(); + + lv_obj_t* getCard(); + bool handleButtonPress(uint8_t button_index) override; + void prepareForRemoval() override; + bool update() override; + +private: + LifeCountdownCard* countdownGame; + lv_obj_t* cardContainer; + bool markedForRemoval; +}; \ No newline at end of file