diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index 2b77f94..ff05c75 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -68,6 +68,7 @@ int dimBrightness = 2; // Dimming level (0-15) bool countdownEnabled = false; time_t countdownTargetTimestamp = 0; // Unix timestamp char countdownLabel[64] = ""; // Label for the countdown +bool isDramaticCountdown = true; // Default to the dramatic countdown mode // State management bool weatherCycleStarted = false; @@ -188,6 +189,7 @@ void loadConfig() { countdownObj["enabled"] = false; countdownObj["targetTimestamp"] = 0; countdownObj["label"] = ""; + countdownObj["isDramaticCountdown"] = true; File f = LittleFS.open("/config.json", "w"); if (f) { @@ -265,6 +267,7 @@ void loadConfig() { countdownEnabled = countdownObj["enabled"] | false; countdownTargetTimestamp = countdownObj["targetTimestamp"] | 0; + isDramaticCountdown = countdownObj["isDramaticCountdown"] | true; JsonVariant labelVariant = countdownObj["label"]; if (labelVariant.isNull() || !labelVariant.is()) { @@ -282,6 +285,7 @@ void loadConfig() { countdownEnabled = false; countdownTargetTimestamp = 0; strcpy(countdownLabel, ""); + isDramaticCountdown = true; Serial.println(F("[CONFIG] Countdown object not found, defaulting to disabled.")); countdownFinished = false; } @@ -505,6 +509,8 @@ void printConfigToSerial() { Serial.println(countdownTargetTimestamp); Serial.print(F("Countdown Label: ")); Serial.println(countdownLabel); + Serial.print(F("Dramatic Countdown Display: ")); + Serial.println(isDramaticCountdown ? "Yes" : "No"); Serial.println(F("========================================")); Serial.println(); } @@ -593,6 +599,7 @@ void setupWebServer() { String countdownDateStr = request->hasParam("countdownDate", true) ? request->getParam("countdownDate", true)->value() : ""; String countdownTimeStr = request->hasParam("countdownTime", true) ? request->getParam("countdownTime", true)->value() : ""; String countdownLabelStr = request->hasParam("countdownLabel", true) ? request->getParam("countdownLabel", true)->value() : ""; + bool newIsDramaticCountdown = (request->hasParam("isDramaticCountdown", true) && (request->getParam("isDramaticCountdown", true)->value() == "true" || request->getParam("isDramaticCountdown", true)->value() == "on" || request->getParam("isDramaticCountdown", true)->value() == "1")); time_t newTargetTimestamp = 0; if (newCountdownEnabled && countdownDateStr.length() > 0 && countdownTimeStr.length() > 0) { @@ -624,6 +631,7 @@ void setupWebServer() { countdownObj["enabled"] = newCountdownEnabled; countdownObj["targetTimestamp"] = newTargetTimestamp; countdownObj["label"] = countdownLabelStr; + countdownObj["isDramaticCountdown"] = newIsDramaticCountdown; size_t total = LittleFS.totalBytes(); size_t used = LittleFS.usedBytes(); @@ -919,6 +927,31 @@ void setupWebServer() { }); +server.on("/set_dramatic_countdown", HTTP_POST, [](AsyncWebServerRequest *request) { + bool enableDramaticNow = false; + if (request->hasParam("value", true)) { + String v = request->getParam("value", true)->value(); + enableDramaticNow = (v == "1" || v == "true" || v == "on"); + } + + // Check if the state has changed + if (isDramaticCountdown == enableDramaticNow) { + Serial.println(F("[WEBSERVER] Dramatic Countdown state unchanged, ignoring.")); + request->send(200, "application/json", "{\"ok\":true}"); + return; + } + + // Update the global variable + isDramaticCountdown = enableDramaticNow; + + // Call saveCountdownConfig with only the existing parameters. + // It will read the updated global variable 'isDramaticCountdown'. + saveCountdownConfig(countdownEnabled, countdownTargetTimestamp, countdownLabel); + + Serial.printf("[WEBSERVER] Set Dramatic Countdown to %d\n", isDramaticCountdown); + request->send(200, "application/json", "{\"ok\":true}"); +}); + server.begin(); Serial.println(F("[WEBSERVER] Web server started")); @@ -1376,6 +1409,7 @@ bool saveCountdownConfig(bool enabled, time_t targetTimestamp, const String &lab countdownObj["enabled"] = enabled; countdownObj["targetTimestamp"] = targetTimestamp; countdownObj["label"] = label; + countdownObj["isDramaticCountdown"] = isDramaticCountdown; doc.remove("countdownEnabled"); doc.remove("countdownDate"); doc.remove("countdownTime"); @@ -1769,9 +1803,8 @@ void loop() { long timeRemaining = countdownTargetTimestamp - now_time; // --- Countdown Finished Logic --- - // This 'if' block now handles the entire "finished" sequence (hourglass + flashing). + // This part of the code remains unchanged. if (timeRemaining <= 0 || countdownShowFinishedMessage) { - // NEW: Only show "TIMES UP" if countdown target timestamp is valid and expired time_t now = time(nullptr); if (countdownTargetTimestamp == 0 || countdownTargetTimestamp > now) { @@ -1858,165 +1891,197 @@ void loop() { } } // END of 'if (timeRemaining <= 0 || countdownShowFinishedMessage)' - // --- Normal Countdown Segments (Only if not in finished state) --- + + // --- NORMAL COUNTDOWN LOGIC --- // This 'else' block will only run if `timeRemaining > 0` and `!countdownShowFinishedMessage` else { - long days = timeRemaining / (24 * 3600); - long hours = (timeRemaining % (24 * 3600)) / 3600; - long minutes = (timeRemaining % 3600) / 60; - long seconds = timeRemaining % 60; - String currentSegmentText = ""; + // The new variable `isDramaticCountdown` toggles between the two modes + if (isDramaticCountdown) { + // --- EXISTING DRAMATIC COUNTDOWN LOGIC --- + long days = timeRemaining / (24 * 3600); + long hours = (timeRemaining % (24 * 3600)) / 3600; + long minutes = (timeRemaining % 3600) / 60; + long seconds = timeRemaining % 60; + String currentSegmentText = ""; - if (segmentStartTime == 0 || (millis() - segmentStartTime > SEGMENT_DISPLAY_DURATION)) { - segmentStartTime = millis(); - P.displayClear(); + if (segmentStartTime == 0 || (millis() - segmentStartTime > SEGMENT_DISPLAY_DURATION)) { + segmentStartTime = millis(); + P.displayClear(); - switch (countdownSegment) { - case 0: // Days - if (days > 0) { - currentSegmentText = String(days) + " " + (days == 1 ? "DAY" : "DAYS"); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); - countdownSegment++; - } else { - // Skip days if zero - countdownSegment++; - segmentStartTime = 0; - } - break; - - case 1: - { // Hours - char buf[10]; - sprintf(buf, "%02ld HRS", hours); // pad hours with 0 - currentSegmentText = String(buf); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); - countdownSegment++; - break; - } - - case 2: - { // Minutes - char buf[10]; - sprintf(buf, "%02ld MINS", minutes); // pad minutes with 0 - currentSegmentText = String(buf); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); - countdownSegment++; - break; - } - - case 3: - { - - // --- Otherwise, run countdown segments like before --- - - time_t segmentStartTime = time(nullptr); // Get fixed start time - unsigned long segmentStartMillis = millis(); // Capture start millis for delta - - long nowRemaining = countdownTargetTimestamp - segmentStartTime; - long currentSecond = nowRemaining % 60; - - char secondsBuf[10]; - sprintf(secondsBuf, "%02ld %s", currentSecond, currentSecond == 1 ? "SEC" : "SECS"); - String secondsText = String(secondsBuf); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment 3: %s\n", secondsText.c_str()); - - P.displayClear(); - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(secondsText.c_str()); - - delay(SEGMENT_DISPLAY_DURATION - 400); // Show the first seconds value slightly shorter - - unsigned long elapsed = millis() - segmentStartMillis; - long adjustedSecond = (countdownTargetTimestamp - segmentStartTime - (elapsed / 1000)) % 60; - - sprintf(secondsBuf, "%02ld %s", adjustedSecond, adjustedSecond == 1 ? "SEC" : "SECS"); - secondsText = String(secondsBuf); - - P.displayClear(); - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(secondsText.c_str()); - - delay(400); // Short burst to show the updated second clearly - - String label; - if (strlen(countdownLabel) > 0) { - label = String(countdownLabel); - label.trim(); - if (!label.startsWith("TO:") && !label.startsWith("to:")) { - label = "TO: " + label; - } - label.replace('.', ','); + switch (countdownSegment) { + case 0: // Days + if (days > 0) { + currentSegmentText = String(days) + " " + (days == 1 ? "DAY" : "DAYS"); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); + countdownSegment++; } else { - static const char *fallbackLabels[] = { - "TO: PARTY TIME!", - "TO: SHOWTIME!", - "TO: CLOCKOUT!", - "TO: BLASTOFF!", - "TO: GO TIME!", - "TO: LIFTOFF!", - "TO: THE BIG REVEAL!", - "TO: ZERO HOUR!", - "TO: THE FINAL COUNT!", - "TO: MISSION COMPLETE" - }; - int randomIndex = random(0, 10); - label = fallbackLabels[randomIndex]; + // Skip days if zero + countdownSegment++; + segmentStartTime = 0; } - - P.setTextAlignment(PA_LEFT); - P.setCharSpacing(1); - textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay); - P.displayScroll(label.c_str(), PA_LEFT, actualScrollDirection, GENERAL_SCROLL_SPEED); - - // --- THIS IS THE BLOCKING LOOP THAT REMAINS PER YOUR REQUEST --- - while (!P.displayAnimate()) { - yield(); - } - - countdownSegment++; - segmentStartTime = millis(); break; - } + case 1: + { // Hours + char buf[10]; + sprintf(buf, "%02ld HRS", hours); // pad hours with 0 + currentSegmentText = String(buf); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); + countdownSegment++; + break; + } + case 2: + { // Minutes + char buf[10]; + sprintf(buf, "%02ld MINS", minutes); // pad minutes with 0 + currentSegmentText = String(buf); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); + countdownSegment++; + break; + } + case 3: + { // Seconds & Label Scroll + time_t segmentStartTime = time(nullptr); + unsigned long segmentStartMillis = millis(); - case 4: // Exit countdown - Serial.println("[COUNTDOWN-STATIC] All countdown segments and label displayed. Advancing to Clock."); - countdownSegment = 0; - segmentStartTime = 0; + long nowRemaining = countdownTargetTimestamp - segmentStartTime; + long currentSecond = nowRemaining % 60; + char secondsBuf[10]; + sprintf(secondsBuf, "%02ld %s", currentSecond, currentSecond == 1 ? "SEC" : "SECS"); + String secondsText = String(secondsBuf); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment 3: %s\n", secondsText.c_str()); + P.displayClear(); + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + P.print(secondsText.c_str()); + delay(SEGMENT_DISPLAY_DURATION - 400); + unsigned long elapsed = millis() - segmentStartMillis; + long adjustedSecond = (countdownTargetTimestamp - segmentStartTime - (elapsed / 1000)) % 60; + sprintf(secondsBuf, "%02ld %s", adjustedSecond, adjustedSecond == 1 ? "SEC" : "SECS"); + secondsText = String(secondsBuf); + P.displayClear(); + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + P.print(secondsText.c_str()); + delay(400); + + String label; + if (strlen(countdownLabel) > 0) { + label = String(countdownLabel); + label.trim(); + if (!label.startsWith("TO:") && !label.startsWith("to:")) { + label = "TO: " + label; + } + label.replace('.', ','); + } else { + static const char *fallbackLabels[] = { + "TO: PARTY TIME!", "TO: SHOWTIME!", "TO: CLOCKOUT!", "TO: BLASTOFF!", + "TO: GO TIME!", "TO: LIFTOFF!", "TO: THE BIG REVEAL!", + "TO: ZERO HOUR!", "TO: THE FINAL COUNT!", "TO: MISSION COMPLETE" + }; + int randomIndex = random(0, 10); + label = fallbackLabels[randomIndex]; + } + + P.setTextAlignment(PA_LEFT); + P.setCharSpacing(1); + textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay); + P.displayScroll(label.c_str(), PA_LEFT, actualScrollDirection, GENERAL_SCROLL_SPEED); + + while (!P.displayAnimate()) { + yield(); + } + countdownSegment++; + segmentStartTime = millis(); + break; + } + case 4: // Exit countdown + Serial.println("[COUNTDOWN-STATIC] All segments and label displayed. Advancing to Clock."); + countdownSegment = 0; + segmentStartTime = 0; + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + advanceDisplayMode(); + yield(); + return; + + default: + Serial.println("[COUNTDOWN-ERROR] Invalid countdownSegment, resetting."); + countdownSegment = 0; + segmentStartTime = 0; + break; + } + + if (currentSegmentText.length() > 0) { P.setTextAlignment(PA_CENTER); P.setCharSpacing(1); - advanceDisplayMode(); - yield(); - return; - - default: - Serial.println("[COUNTDOWN-ERROR] Invalid countdownSegment, resetting."); - countdownSegment = 0; - segmentStartTime = 0; - break; - } - - if (currentSegmentText.length() > 0) { - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(currentSegmentText.c_str()); + P.print(currentSegmentText.c_str()); + } } + P.displayAnimate(); } - P.displayAnimate(); // This handles regular segment display updates - } // End of 'else' (Normal Countdown Segments) + // --- NEW: SINGLE-LINE COUNTDOWN LOGIC --- + else { + long days = timeRemaining / (24 * 3600); + long hours = (timeRemaining % (24 * 3600)) / 3600; + long minutes = (timeRemaining % 3600) / 60; + long seconds = timeRemaining % 60; - // Keep alignment reset just in case - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - yield(); - return; - } // End of if (displayMode == 3 && ...) + String label; + // Check if countdownLabel is empty and grab a random one if needed + if (strlen(countdownLabel) > 0) { + label = String(countdownLabel); + label.trim(); + } else { + static const char *fallbackLabels[] = { + "PARTY TIME", "SHOWTIME", "CLOCKOUT", "BLASTOFF", + "GO TIME", "LIFTOFF", "THE BIG REVEAL", + "ZERO HOUR", "THE FINAL COUNT", "MISSION COMPLETE" + }; + int randomIndex = random(0, 10); + label = fallbackLabels[randomIndex]; + } + // Format the full string + char buf[50]; + // Only show days if there are any, otherwise start with hours + if (days > 0) { + sprintf(buf, "%s IN: %ldD %02ldH %02ldM %02ldS", label.c_str(), days, hours, minutes, seconds); + } else { + sprintf(buf, "%s IN: %02ldH %02ldM %02ldS", label.c_str(), hours, minutes, seconds); + } + + String fullString = String(buf); + + // Display the full string and scroll it + P.displayClear(); + P.setTextAlignment(PA_LEFT); + P.setCharSpacing(1); + textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay); + P.displayScroll(fullString.c_str(), PA_LEFT, actualScrollDirection, GENERAL_SCROLL_SPEED); + // Blocking loop to ensure the full message scrolls + while (!P.displayAnimate()) { + yield(); + } + + // After scrolling is complete, we're done with this display mode + // Move to the next mode and exit the function. + P.setTextAlignment(PA_CENTER); + advanceDisplayMode(); + yield(); + return; + } + } + + // Keep alignment reset just in case + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + yield(); + return; +} // End of if (displayMode == 3 && ...) // --- NIGHTSCOUT Display Mode --- diff --git a/ESPTimeCast_ESP32/data/config.json b/ESPTimeCast_ESP32/data/config.json index 4f75ee5..509a440 100644 --- a/ESPTimeCast_ESP32/data/config.json +++ b/ESPTimeCast_ESP32/data/config.json @@ -28,6 +28,7 @@ "countdown": { "enabled": false, "targetTimestamp": 0, - "label": "" + "label": "", + "isDramaticCountdown": true } } \ No newline at end of file diff --git a/ESPTimeCast_ESP32/data/index.html b/ESPTimeCast_ESP32/data/index.html index a2b5741..a2d7c90 100644 --- a/ESPTimeCast_ESP32/data/index.html +++ b/ESPTimeCast_ESP32/data/index.html @@ -88,7 +88,7 @@ input[type="time"]:disabled , input[type="text"]:disabled , input[type="number"]:disabled , - input[type="date"]:disabled { + input[type="date"]:disabled{ color: rgba(255, 255, 255, 0.250); } @@ -106,6 +106,19 @@ input[type="time"]::-webkit-calendar-picker-indicator, input[type="date"]::-webk filter: invert(100%); } + +/* Enabled & checked toggle */ +.toggle-switch input[type="checkbox"]:checked + .toggle-slider { + background-color: var(--accent-color); +} + +/* Disabled toggle (regardless of checked state) */ +.toggle-switch input[type="checkbox"]:disabled + .toggle-slider { background-color: transparent !important; border: solid 1px #cccccc2e; cursor: not-allowed; } +.toggle-switch input[type="checkbox"]:disabled + .toggle-slider::before { + background-color: rgba(204, 204, 204, 0.5); +} + + input:-webkit-autofill, input:-webkit-autofill:focus, input:-webkit-autofill:hover { @@ -581,6 +594,15 @@ textarea::placeholder { +
+ +
@@ -696,6 +718,7 @@ document.getElementById('dimEndTime').value = setDimmingFieldsEnabled(!!data.dimmingEnabled); // --- NEW: Populate Countdown Fields --- + document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown); const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled); @@ -742,6 +765,12 @@ document.getElementById('dimEndTime').value = setCountdownEnabled(this.checked); // Sends command to ESP setCountdownFieldsEnabled(this.checked); // Enables/disables local fields }); + +const dramaticCountdownEl = document.getElementById('isDramaticCountdown'); +dramaticCountdownEl.addEventListener('change', function () { + setIsDramaticCountdown(this.checked); +}); + // Set initial state of fields when page loads setCountdownFieldsEnabled(countdownEnabledEl.checked); // --- END NEW --- @@ -831,6 +860,7 @@ async function submitConfig(event) { // --- NEW: Countdown Form Data --- formData.set('countdownEnabled', document.getElementById('countdownEnabled').checked ? 'true' : 'false'); + formData.set('isDramaticCountdown', document.getElementById('isDramaticCountdown').checked ? 'true' : 'false'); // Date and Time inputs are already handled by formData if they have a 'name' attribute // 'countdownDate' and 'countdownTime' are collected automatically // Also apply the same validation for the label when submitting @@ -1162,6 +1192,7 @@ function setCountdownFieldsEnabled(enabled) { document.getElementById('countdownLabel').disabled = !enabled; document.getElementById('countdownDate').disabled = !enabled; document.getElementById('countdownTime').disabled = !enabled; + document.getElementById('isDramaticCountdown').disabled = !enabled; } // Existing function to send countdown enable/disable command to ESP @@ -1172,6 +1203,15 @@ function setCountdownEnabled(val) { body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false }); } + +function setIsDramaticCountdown(val) { + fetch('/set_dramatic_countdown', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false + }); +} + // --- END Countdown Controls Logic --- diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index c4e5f10..c6f687e 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -68,6 +68,7 @@ int dimBrightness = 2; // Dimming level (0-15) bool countdownEnabled = false; time_t countdownTargetTimestamp = 0; // Unix timestamp char countdownLabel[64] = ""; // Label for the countdown +bool isDramaticCountdown = true; // Default to the dramatic countdown mode // State management bool weatherCycleStarted = false; @@ -188,6 +189,7 @@ void loadConfig() { countdownObj["enabled"] = false; countdownObj["targetTimestamp"] = 0; countdownObj["label"] = ""; + countdownObj["isDramaticCountdown"] = true; File f = LittleFS.open("/config.json", "w"); if (f) { @@ -265,6 +267,8 @@ void loadConfig() { countdownEnabled = countdownObj["enabled"] | false; countdownTargetTimestamp = countdownObj["targetTimestamp"] | 0; + isDramaticCountdown = countdownObj["isDramaticCountdown"] | true; + JsonVariant labelVariant = countdownObj["label"]; if (labelVariant.isNull() || !labelVariant.is()) { @@ -282,6 +286,7 @@ void loadConfig() { countdownEnabled = false; countdownTargetTimestamp = 0; strcpy(countdownLabel, ""); + isDramaticCountdown = true; Serial.println(F("[CONFIG] Countdown object not found, defaulting to disabled.")); countdownFinished = false; } @@ -427,7 +432,7 @@ void setupTime() { } if (serverOk) { - configTime(0, 0, ntpServer1, ntpServer2); // safe to call now + configTime(0, 0, ntpServer1, ntpServer2); // safe to call now setenv("TZ", ianaToPosix(timeZone), 1); tzset(); ntpState = NTP_SYNCING; @@ -437,7 +442,7 @@ void setupTime() { } else { Serial.println(F("[TIME] NTP server lookup failed — skipping sync")); ntpSyncSuccessful = false; - ntpState = NTP_IDLE; // or custom NTP_ERROR state + ntpState = NTP_IDLE; // or custom NTP_ERROR state // Trigger your error display here if desired } } @@ -505,6 +510,8 @@ void printConfigToSerial() { Serial.println(countdownTargetTimestamp); Serial.print(F("Countdown Label: ")); Serial.println(countdownLabel); + Serial.print(F("Dramatic Countdown Display: ")); + Serial.println(isDramaticCountdown ? "Yes" : "No"); Serial.println(F("========================================")); Serial.println(); } @@ -593,6 +600,7 @@ void setupWebServer() { String countdownDateStr = request->hasParam("countdownDate", true) ? request->getParam("countdownDate", true)->value() : ""; String countdownTimeStr = request->hasParam("countdownTime", true) ? request->getParam("countdownTime", true)->value() : ""; String countdownLabelStr = request->hasParam("countdownLabel", true) ? request->getParam("countdownLabel", true)->value() : ""; + bool newIsDramaticCountdown = (request->hasParam("isDramaticCountdown", true) && (request->getParam("isDramaticCountdown", true)->value() == "true" || request->getParam("isDramaticCountdown", true)->value() == "on" || request->getParam("isDramaticCountdown", true)->value() == "1")); time_t newTargetTimestamp = 0; if (newCountdownEnabled && countdownDateStr.length() > 0 && countdownTimeStr.length() > 0) { @@ -624,6 +632,7 @@ void setupWebServer() { countdownObj["enabled"] = newCountdownEnabled; countdownObj["targetTimestamp"] = newTargetTimestamp; countdownObj["label"] = countdownLabelStr; + countdownObj["isDramaticCountdown"] = newIsDramaticCountdown; FSInfo fs_info; LittleFS.info(fs_info); @@ -919,6 +928,30 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); +server.on("/set_dramatic_countdown", HTTP_POST, [](AsyncWebServerRequest *request) { + bool enableDramaticNow = false; + if (request->hasParam("value", true)) { + String v = request->getParam("value", true)->value(); + enableDramaticNow = (v == "1" || v == "true" || v == "on"); + } + + // Check if the state has changed + if (isDramaticCountdown == enableDramaticNow) { + Serial.println(F("[WEBSERVER] Dramatic Countdown state unchanged, ignoring.")); + request->send(200, "application/json", "{\"ok\":true}"); + return; + } + + // Update the global variable + isDramaticCountdown = enableDramaticNow; + + // Call saveCountdownConfig with only the existing parameters. + // It will read the updated global variable 'isDramaticCountdown'. + saveCountdownConfig(countdownEnabled, countdownTargetTimestamp, countdownLabel); + + Serial.printf("[WEBSERVER] Set Dramatic Countdown to %d\n", isDramaticCountdown); + request->send(200, "application/json", "{\"ok\":true}"); +}); server.begin(); @@ -1275,69 +1308,6 @@ void setup() { lastColonBlink = millis(); } - - -// void advanceDisplayMode() { -// int oldMode = displayMode; -// String ntpField = String(ntpServer2); -// bool nightscoutConfigured = ntpField.startsWith("https://"); - -// if (displayMode == 0) { // Clock -> ... -// if (weatherAvailable && (strlen(openWeatherApiKey) == 32) && (strlen(openWeatherCity) > 0) && (strlen(openWeatherCountry) > 0)) { -// displayMode = 1; -// Serial.println(F("[DISPLAY] Switching to display mode: WEATHER (from Clock)")); -// } else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) { -// displayMode = 3; -// Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Clock, weather skipped)")); -// } else if (nightscoutConfigured) { -// displayMode = 4; // Clock -> Nightscout (if weather & countdown are skipped) -// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Clock, weather & countdown skipped)")); -// } else { -// displayMode = 0; -// Serial.println(F("[DISPLAY] Staying in CLOCK (from Clock)")); -// } -// } else if (displayMode == 1) { // Weather -> ... -// if (showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) { -// displayMode = 2; -// Serial.println(F("[DISPLAY] Switching to display mode: DESCRIPTION (from Weather)")); -// } else if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) { -// displayMode = 3; -// Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Weather)")); -// } else if (nightscoutConfigured) { -// displayMode = 4; // Weather -> Nightscout (if description & countdown are skipped) -// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Weather, description & countdown skipped)")); -// } else { -// displayMode = 0; -// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Weather)")); -// } -// } else if (displayMode == 2) { // Weather Description -> ... -// if (countdownEnabled && !countdownFinished && ntpSyncSuccessful) { -// displayMode = 3; -// Serial.println(F("[DISPLAY] Switching to display mode: COUNTDOWN (from Description)")); -// } else if (nightscoutConfigured) { -// displayMode = 4; // Description -> Nightscout (if countdown is skipped) -// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Description, countdown skipped)")); -// } else { -// displayMode = 0; -// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Description)")); -// } -// } else if (displayMode == 3) { // Countdown -> Nightscout -// if (nightscoutConfigured) { -// displayMode = 4; -// Serial.println(F("[DISPLAY] Switching to display mode: NIGHTSCOUT (from Countdown)")); -// } else { -// displayMode = 0; -// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Countdown)")); -// } -// } else if (displayMode == 4) { // Nightscout -> Clock -// displayMode = 0; -// Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Nightscout)")); -// } - -// // --- Common cleanup/reset logic remains the same --- -// lastSwitch = millis(); -// } - void advanceDisplayMode() { int oldMode = displayMode; String ntpField = String(ntpServer2); @@ -1435,6 +1405,7 @@ bool saveCountdownConfig(bool enabled, time_t targetTimestamp, const String &lab countdownObj["enabled"] = enabled; countdownObj["targetTimestamp"] = targetTimestamp; countdownObj["label"] = label; + countdownObj["isDramaticCountdown"] = isDramaticCountdown; doc.remove("countdownEnabled"); doc.remove("countdownDate"); doc.remove("countdownTime"); @@ -1826,9 +1797,8 @@ void loop() { long timeRemaining = countdownTargetTimestamp - now_time; // --- Countdown Finished Logic --- - // This 'if' block now handles the entire "finished" sequence (hourglass + flashing). + // This part of the code remains unchanged. if (timeRemaining <= 0 || countdownShowFinishedMessage) { - // NEW: Only show "TIMES UP" if countdown target timestamp is valid and expired time_t now = time(nullptr); if (countdownTargetTimestamp == 0 || countdownTargetTimestamp > now) { @@ -1915,327 +1885,360 @@ void loop() { } } // END of 'if (timeRemaining <= 0 || countdownShowFinishedMessage)' - // --- Normal Countdown Segments (Only if not in finished state) --- + + // --- NORMAL COUNTDOWN LOGIC --- // This 'else' block will only run if `timeRemaining > 0` and `!countdownShowFinishedMessage` else { - long days = timeRemaining / (24 * 3600); - long hours = (timeRemaining % (24 * 3600)) / 3600; - long minutes = (timeRemaining % 3600) / 60; - long seconds = timeRemaining % 60; - String currentSegmentText = ""; + // The new variable `isDramaticCountdown` toggles between the two modes + if (isDramaticCountdown) { + // --- EXISTING DRAMATIC COUNTDOWN LOGIC --- + long days = timeRemaining / (24 * 3600); + long hours = (timeRemaining % (24 * 3600)) / 3600; + long minutes = (timeRemaining % 3600) / 60; + long seconds = timeRemaining % 60; + String currentSegmentText = ""; - if (segmentStartTime == 0 || (millis() - segmentStartTime > SEGMENT_DISPLAY_DURATION)) { - segmentStartTime = millis(); - P.displayClear(); + if (segmentStartTime == 0 || (millis() - segmentStartTime > SEGMENT_DISPLAY_DURATION)) { + segmentStartTime = millis(); + P.displayClear(); - switch (countdownSegment) { - case 0: // Days - if (days > 0) { - currentSegmentText = String(days) + " " + (days == 1 ? "DAY" : "DAYS"); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); - countdownSegment++; - } else { - // Skip days if zero - countdownSegment++; - segmentStartTime = 0; - } - break; - - case 1: - { // Hours - char buf[10]; - sprintf(buf, "%02ld HRS", hours); // pad hours with 0 - currentSegmentText = String(buf); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); - countdownSegment++; - break; - } - - case 2: - { // Minutes - char buf[10]; - sprintf(buf, "%02ld MINS", minutes); // pad minutes with 0 - currentSegmentText = String(buf); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); - countdownSegment++; - break; - } - - case 3: - { - - // --- Otherwise, run countdown segments like before --- - - time_t segmentStartTime = time(nullptr); // Get fixed start time - unsigned long segmentStartMillis = millis(); // Capture start millis for delta - - long nowRemaining = countdownTargetTimestamp - segmentStartTime; - long currentSecond = nowRemaining % 60; - - char secondsBuf[10]; - sprintf(secondsBuf, "%02ld %s", currentSecond, currentSecond == 1 ? "SEC" : "SECS"); - String secondsText = String(secondsBuf); - Serial.printf("[COUNTDOWN-STATIC] Displaying segment 3: %s\n", secondsText.c_str()); - - P.displayClear(); - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(secondsText.c_str()); - - delay(SEGMENT_DISPLAY_DURATION - 400); // Show the first seconds value slightly shorter - - unsigned long elapsed = millis() - segmentStartMillis; - long adjustedSecond = (countdownTargetTimestamp - segmentStartTime - (elapsed / 1000)) % 60; - - sprintf(secondsBuf, "%02ld %s", adjustedSecond, adjustedSecond == 1 ? "SEC" : "SECS"); - secondsText = String(secondsBuf); - - P.displayClear(); - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(secondsText.c_str()); - - delay(400); // Short burst to show the updated second clearly - - String label; - if (strlen(countdownLabel) > 0) { - label = String(countdownLabel); - label.trim(); - if (!label.startsWith("TO:") && !label.startsWith("to:")) { - label = "TO: " + label; - } - label.replace('.', ','); + switch (countdownSegment) { + case 0: // Days + if (days > 0) { + currentSegmentText = String(days) + " " + (days == 1 ? "DAY" : "DAYS"); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); + countdownSegment++; } else { - static const char *fallbackLabels[] = { - "TO: PARTY TIME!", - "TO: SHOWTIME!", - "TO: CLOCKOUT!", - "TO: BLASTOFF!", - "TO: GO TIME!", - "TO: LIFTOFF!", - "TO: THE BIG REVEAL!", - "TO: ZERO HOUR!", - "TO: THE FINAL COUNT!", - "TO: MISSION COMPLETE" - }; - int randomIndex = random(0, 10); - label = fallbackLabels[randomIndex]; + // Skip days if zero + countdownSegment++; + segmentStartTime = 0; } - - P.setTextAlignment(PA_LEFT); - P.setCharSpacing(1); - textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay); - P.displayScroll(label.c_str(), PA_LEFT, actualScrollDirection, GENERAL_SCROLL_SPEED); - - // --- THIS IS THE BLOCKING LOOP THAT REMAINS PER YOUR REQUEST --- - while (!P.displayAnimate()) { - yield(); - } - - countdownSegment++; - segmentStartTime = millis(); break; - } + case 1: + { // Hours + char buf[10]; + sprintf(buf, "%02ld HRS", hours); // pad hours with 0 + currentSegmentText = String(buf); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); + countdownSegment++; + break; + } + case 2: + { // Minutes + char buf[10]; + sprintf(buf, "%02ld MINS", minutes); // pad minutes with 0 + currentSegmentText = String(buf); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment %d: %s\n", countdownSegment, currentSegmentText.c_str()); + countdownSegment++; + break; + } + case 3: + { // Seconds & Label Scroll + time_t segmentStartTime = time(nullptr); + unsigned long segmentStartMillis = millis(); - case 4: // Exit countdown - Serial.println("[COUNTDOWN-STATIC] All countdown segments and label displayed. Advancing to Clock."); - countdownSegment = 0; - segmentStartTime = 0; + long nowRemaining = countdownTargetTimestamp - segmentStartTime; + long currentSecond = nowRemaining % 60; + char secondsBuf[10]; + sprintf(secondsBuf, "%02ld %s", currentSecond, currentSecond == 1 ? "SEC" : "SECS"); + String secondsText = String(secondsBuf); + Serial.printf("[COUNTDOWN-STATIC] Displaying segment 3: %s\n", secondsText.c_str()); + P.displayClear(); + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + P.print(secondsText.c_str()); + delay(SEGMENT_DISPLAY_DURATION - 400); + unsigned long elapsed = millis() - segmentStartMillis; + long adjustedSecond = (countdownTargetTimestamp - segmentStartTime - (elapsed / 1000)) % 60; + sprintf(secondsBuf, "%02ld %s", adjustedSecond, adjustedSecond == 1 ? "SEC" : "SECS"); + secondsText = String(secondsBuf); + P.displayClear(); + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + P.print(secondsText.c_str()); + delay(400); + + String label; + if (strlen(countdownLabel) > 0) { + label = String(countdownLabel); + label.trim(); + if (!label.startsWith("TO:") && !label.startsWith("to:")) { + label = "TO: " + label; + } + label.replace('.', ','); + } else { + static const char *fallbackLabels[] = { + "TO: PARTY TIME!", "TO: SHOWTIME!", "TO: CLOCKOUT!", "TO: BLASTOFF!", + "TO: GO TIME!", "TO: LIFTOFF!", "TO: THE BIG REVEAL!", + "TO: ZERO HOUR!", "TO: THE FINAL COUNT!", "TO: MISSION COMPLETE" + }; + int randomIndex = random(0, 10); + label = fallbackLabels[randomIndex]; + } + + P.setTextAlignment(PA_LEFT); + P.setCharSpacing(1); + textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay); + P.displayScroll(label.c_str(), PA_LEFT, actualScrollDirection, GENERAL_SCROLL_SPEED); + + while (!P.displayAnimate()) { + yield(); + } + countdownSegment++; + segmentStartTime = millis(); + break; + } + case 4: // Exit countdown + Serial.println("[COUNTDOWN-STATIC] All segments and label displayed. Advancing to Clock."); + countdownSegment = 0; + segmentStartTime = 0; + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + advanceDisplayMode(); + yield(); + return; + + default: + Serial.println("[COUNTDOWN-ERROR] Invalid countdownSegment, resetting."); + countdownSegment = 0; + segmentStartTime = 0; + break; + } + + if (currentSegmentText.length() > 0) { P.setTextAlignment(PA_CENTER); P.setCharSpacing(1); - advanceDisplayMode(); - yield(); - return; - - default: - Serial.println("[COUNTDOWN-ERROR] Invalid countdownSegment, resetting."); - countdownSegment = 0; - segmentStartTime = 0; - break; - } - - if (currentSegmentText.length() > 0) { - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(currentSegmentText.c_str()); + P.print(currentSegmentText.c_str()); + } } + P.displayAnimate(); } - P.displayAnimate(); // This handles regular segment display updates - } // End of 'else' (Normal Countdown Segments) + // --- NEW: SINGLE-LINE COUNTDOWN LOGIC --- + else { + long days = timeRemaining / (24 * 3600); + long hours = (timeRemaining % (24 * 3600)) / 3600; + long minutes = (timeRemaining % 3600) / 60; + long seconds = timeRemaining % 60; + + String label; + // Check if countdownLabel is empty and grab a random one if needed + if (strlen(countdownLabel) > 0) { + label = String(countdownLabel); + label.trim(); + } else { + static const char *fallbackLabels[] = { + "PARTY TIME", "SHOWTIME", "CLOCKOUT", "BLASTOFF", + "GO TIME", "LIFTOFF", "THE BIG REVEAL", + "ZERO HOUR", "THE FINAL COUNT", "MISSION COMPLETE" + }; + int randomIndex = random(0, 10); + label = fallbackLabels[randomIndex]; + } + + // Format the full string + char buf[50]; + // Only show days if there are any, otherwise start with hours + if (days > 0) { + sprintf(buf, "%s IN: %ldD %02ldH %02ldM %02ldS", label.c_str(), days, hours, minutes, seconds); + } else { + sprintf(buf, "%s IN: %02ldH %02ldM %02ldS", label.c_str(), hours, minutes, seconds); + } + + String fullString = String(buf); + + // Display the full string and scroll it + P.displayClear(); + P.setTextAlignment(PA_LEFT); + P.setCharSpacing(1); + textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay); + P.displayScroll(fullString.c_str(), PA_LEFT, actualScrollDirection, GENERAL_SCROLL_SPEED); + + // Blocking loop to ensure the full message scrolls + while (!P.displayAnimate()) { + yield(); + } + + // After scrolling is complete, we're done with this display mode + // Move to the next mode and exit the function. + P.setTextAlignment(PA_CENTER); + advanceDisplayMode(); + yield(); + return; + } + } + + // Keep alignment reset just in case + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(1); + yield(); + return; +} // End of if (displayMode == 3 && ...) + + +// --- NIGHTSCOUT Display Mode --- +if (displayMode == 4) { + String ntpField = String(ntpServer2); + + // These static variables will retain their values between calls to this block + static unsigned long lastNightscoutFetchTime = 0; + const unsigned long NIGHTSCOUT_FETCH_INTERVAL = 150000; // 2.5 minutes + static int currentGlucose = -1; + static String currentDirection = "?"; + + // Check if it's time to fetch new data or if we have no data yet + if (currentGlucose == -1 || millis() - lastNightscoutFetchTime >= NIGHTSCOUT_FETCH_INTERVAL) { + WiFiClientSecure client; + client.setInsecure(); + HTTPClient https; + https.begin(client, ntpField); + + https.setTimeout(5000); // This sets both the connection and response timeout. + + Serial.print("[HTTPS] Nightscout fetch initiated...\n"); + int httpCode = https.GET(); + + if (httpCode == HTTP_CODE_OK) { + String payload = https.getString(); + StaticJsonDocument<1024> doc; + DeserializationError error = deserializeJson(doc, payload); + + if (!error && doc.is() && doc.size() > 0) { + JsonObject firstReading = doc[0].as(); + currentGlucose = firstReading["glucose"] | firstReading["sgv"] | -1; + currentDirection = firstReading["direction"] | "?"; + + Serial.printf("Nightscout data fetched: mg/dL %d %s\n", currentGlucose, currentDirection.c_str()); + } else { + Serial.println("Failed to parse Nightscout JSON"); + } + } else { + Serial.printf("[HTTPS] GET failed, error: %s\n", https.errorToString(httpCode).c_str()); + } + + https.end(); + lastNightscoutFetchTime = millis(); // Update the timestamp + } + + // Display the data we have, which is now stored in static variables + if (currentGlucose != -1) { + char arrow; + if (currentDirection == "Flat") arrow = 139; + else if (currentDirection == "SingleUp") arrow = 134; + else if (currentDirection == "DoubleUp") arrow = 135; + else if (currentDirection == "SingleDown") arrow = 136; + else if (currentDirection == "DoubleDown") arrow = 137; + else if (currentDirection == "FortyFiveUp") arrow = 138; + else if (currentDirection == "FortyFiveDown") arrow = 140; + else arrow = '?'; + + String displayText = String(currentGlucose) + String(arrow); - // Keep alignment reset just in case P.setTextAlignment(PA_CENTER); P.setCharSpacing(1); - yield(); + P.print(displayText.c_str()); + + delay(weatherDuration); + advanceDisplayMode(); return; - } // End of if (displayMode == 3 && ...) - - - - // --- NIGHTSCOUT Display Mode --- - if (displayMode == 4) { - String ntpField = String(ntpServer2); - - // These static variables will retain their values between calls to this block - static unsigned long lastNightscoutFetchTime = 0; - const unsigned long NIGHTSCOUT_FETCH_INTERVAL = 150000; // 2.5 minutes - static int currentGlucose = -1; - static String currentDirection = "?"; - - // Check if it's time to fetch new data or if we have no data yet - if (currentGlucose == -1 || millis() - lastNightscoutFetchTime >= NIGHTSCOUT_FETCH_INTERVAL) { - WiFiClientSecure client; - client.setInsecure(); - HTTPClient https; - https.begin(client, ntpField); - - https.setTimeout(5000); // This sets both the connection and response timeout. - - Serial.print("[HTTPS] Nightscout fetch initiated...\n"); - int httpCode = https.GET(); - - if (httpCode == HTTP_CODE_OK) { - String payload = https.getString(); - StaticJsonDocument<1024> doc; - DeserializationError error = deserializeJson(doc, payload); - - if (!error && doc.is() && doc.size() > 0) { - JsonObject firstReading = doc[0].as(); - currentGlucose = firstReading["glucose"] | firstReading["sgv"] | -1; - currentDirection = firstReading["direction"] | "?"; - - Serial.printf("Nightscout data fetched: mg/dL %d %s\n", currentGlucose, currentDirection.c_str()); - } else { - Serial.println("Failed to parse Nightscout JSON"); - } - } else { - Serial.printf("[HTTPS] GET failed, error: %s\n", https.errorToString(httpCode).c_str()); - } - - https.end(); - lastNightscoutFetchTime = millis(); // Update the timestamp - } - - // Display the data we have, which is now stored in static variables - if (currentGlucose != -1) { - char arrow; - if (currentDirection == "Flat") arrow = 139; - else if (currentDirection == "SingleUp") arrow = 134; - else if (currentDirection == "DoubleUp") arrow = 135; - else if (currentDirection == "SingleDown") arrow = 136; - else if (currentDirection == "DoubleDown") arrow = 137; - else if (currentDirection == "FortyFiveUp") arrow = 138; - else if (currentDirection == "FortyFiveDown") arrow = 140; - else arrow = '?'; - - String displayText = String(currentGlucose) + String(arrow); - - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(1); - P.print(displayText.c_str()); - - delay(weatherDuration); - advanceDisplayMode(); - return; - } else { - // If no data is available after the first fetch attempt, show an error and advance - P.setTextAlignment(PA_CENTER); - P.setCharSpacing(0); - P.print(F("?)")); - delay(2000); // Wait 2 seconds before advancing - advanceDisplayMode(); - return; - } - } - - //DATE Display Mode - else if (displayMode == 5 && showDate) { - - // --- VALID DATE CHECK --- - if (timeinfo.tm_year < 120 || timeinfo.tm_mday <= 0 || timeinfo.tm_mon < 0 || timeinfo.tm_mon > 11) { - advanceDisplayMode(); - return; // skip drawing - } - // ------------------------- - String dateString; - - // Get localized month names - const char *const *months = getMonthsOfYear(language); - String monthAbbr = String(months[timeinfo.tm_mon]).substring(0, 5); - monthAbbr.toLowerCase(); - - // Add spaces between day digits - String dayString = String(timeinfo.tm_mday); - String spacedDay = ""; - for (size_t i = 0; i < dayString.length(); i++) { - spacedDay += dayString[i]; - if (i < dayString.length() - 1) spacedDay += " "; - } - - // Function to check if day should come first for given language - auto isDayFirst = [](const String &lang) { - // Languages with DD-MM order - const char *dayFirstLangs[] = { - "af", // Afrikaans - "cs", // Czech - "da", // Danish - "de", // German - "eo", // Esperanto - "es", // Spanish - "et", // Estonian - "fi", // Finnish - "fr", // French - "hr", // Croatian - "hu", // Hungarian - "it", // Italian - "lt", // Lithuanian - "lv", // Latvian - "nl", // Dutch - "no", // Norwegian - "pl", // Polish - "pt", // Portuguese - "ro", // Romanian - "sk", // Slovak - "sl", // Slovenian - "sr", // Serbian - "sv", // Swedish - "sw", // Swahili - "tr" // Turkish - }; - for (auto lf : dayFirstLangs) { - if (lang.equalsIgnoreCase(lf)) { - return true; - } - } - return false; - }; - - String langForDate = String(language); - - if (langForDate == "ja") { - // Japanese: month number (spaced digits) + day + symbol - String spacedMonth = ""; - String monthNum = String(timeinfo.tm_mon + 1); - dateString = monthAbbr + " " + spacedDay + " ±"; - - } else { - if (isDayFirst(language)) { - dateString = spacedDay + " " + monthAbbr; - } else { - dateString = monthAbbr + " " + spacedDay; - } - } - + } else { + // If no data is available after the first fetch attempt, show an error and advance P.setTextAlignment(PA_CENTER); P.setCharSpacing(0); - P.print(dateString); + P.print(F("?)")); + delay(2000); // Wait 2 seconds before advancing + advanceDisplayMode(); + return; + } +} - if (millis() - lastSwitch > weatherDuration) { - advanceDisplayMode(); +//DATE Display Mode +else if (displayMode == 5 && showDate) { + + // --- VALID DATE CHECK --- + if (timeinfo.tm_year < 120 || timeinfo.tm_mday <= 0 || timeinfo.tm_mon < 0 || timeinfo.tm_mon > 11) { + advanceDisplayMode(); + return; // skip drawing + } + // ------------------------- + String dateString; + + // Get localized month names + const char *const *months = getMonthsOfYear(language); + String monthAbbr = String(months[timeinfo.tm_mon]).substring(0, 5); + monthAbbr.toLowerCase(); + + // Add spaces between day digits + String dayString = String(timeinfo.tm_mday); + String spacedDay = ""; + for (size_t i = 0; i < dayString.length(); i++) { + spacedDay += dayString[i]; + if (i < dayString.length() - 1) spacedDay += " "; + } + + // Function to check if day should come first for given language + auto isDayFirst = [](const String &lang) { + // Languages with DD-MM order + const char *dayFirstLangs[] = { + "af", // Afrikaans + "cs", // Czech + "da", // Danish + "de", // German + "eo", // Esperanto + "es", // Spanish + "et", // Estonian + "fi", // Finnish + "fr", // French + "hr", // Croatian + "hu", // Hungarian + "it", // Italian + "lt", // Lithuanian + "lv", // Latvian + "nl", // Dutch + "no", // Norwegian + "pl", // Polish + "pt", // Portuguese + "ro", // Romanian + "sk", // Slovak + "sl", // Slovenian + "sr", // Serbian + "sv", // Swedish + "sw", // Swahili + "tr" // Turkish + }; + for (auto lf : dayFirstLangs) { + if (lang.equalsIgnoreCase(lf)) { + return true; + } + } + return false; + }; + + String langForDate = String(language); + + if (langForDate == "ja") { + // Japanese: month number (spaced digits) + day + symbol + String spacedMonth = ""; + String monthNum = String(timeinfo.tm_mon + 1); + dateString = monthAbbr + " " + spacedDay + " ±"; + + } else { + if (isDayFirst(language)) { + dateString = spacedDay + " " + monthAbbr; + } else { + dateString = monthAbbr + " " + spacedDay; } } - yield(); + + P.setTextAlignment(PA_CENTER); + P.setCharSpacing(0); + P.print(dateString); + + if (millis() - lastSwitch > weatherDuration) { + advanceDisplayMode(); + } +} +yield(); } \ No newline at end of file diff --git a/ESPTimeCast_ESP8266/data/config.json b/ESPTimeCast_ESP8266/data/config.json index 4f75ee5..509a440 100644 --- a/ESPTimeCast_ESP8266/data/config.json +++ b/ESPTimeCast_ESP8266/data/config.json @@ -28,6 +28,7 @@ "countdown": { "enabled": false, "targetTimestamp": 0, - "label": "" + "label": "", + "isDramaticCountdown": true } } \ No newline at end of file diff --git a/ESPTimeCast_ESP8266/data/index.html b/ESPTimeCast_ESP8266/data/index.html index a2b5741..a2d7c90 100644 --- a/ESPTimeCast_ESP8266/data/index.html +++ b/ESPTimeCast_ESP8266/data/index.html @@ -88,7 +88,7 @@ input[type="time"]:disabled , input[type="text"]:disabled , input[type="number"]:disabled , - input[type="date"]:disabled { + input[type="date"]:disabled{ color: rgba(255, 255, 255, 0.250); } @@ -106,6 +106,19 @@ input[type="time"]::-webkit-calendar-picker-indicator, input[type="date"]::-webk filter: invert(100%); } + +/* Enabled & checked toggle */ +.toggle-switch input[type="checkbox"]:checked + .toggle-slider { + background-color: var(--accent-color); +} + +/* Disabled toggle (regardless of checked state) */ +.toggle-switch input[type="checkbox"]:disabled + .toggle-slider { background-color: transparent !important; border: solid 1px #cccccc2e; cursor: not-allowed; } +.toggle-switch input[type="checkbox"]:disabled + .toggle-slider::before { + background-color: rgba(204, 204, 204, 0.5); +} + + input:-webkit-autofill, input:-webkit-autofill:focus, input:-webkit-autofill:hover { @@ -581,6 +594,15 @@ textarea::placeholder {
+
+ +
@@ -696,6 +718,7 @@ document.getElementById('dimEndTime').value = setDimmingFieldsEnabled(!!data.dimmingEnabled); // --- NEW: Populate Countdown Fields --- + document.getElementById('isDramaticCountdown').checked = !!(data.countdown && data.countdown.isDramaticCountdown); const countdownEnabledEl = document.getElementById('countdownEnabled'); // Get reference countdownEnabledEl.checked = !!(data.countdown && data.countdown.enabled); @@ -742,6 +765,12 @@ document.getElementById('dimEndTime').value = setCountdownEnabled(this.checked); // Sends command to ESP setCountdownFieldsEnabled(this.checked); // Enables/disables local fields }); + +const dramaticCountdownEl = document.getElementById('isDramaticCountdown'); +dramaticCountdownEl.addEventListener('change', function () { + setIsDramaticCountdown(this.checked); +}); + // Set initial state of fields when page loads setCountdownFieldsEnabled(countdownEnabledEl.checked); // --- END NEW --- @@ -831,6 +860,7 @@ async function submitConfig(event) { // --- NEW: Countdown Form Data --- formData.set('countdownEnabled', document.getElementById('countdownEnabled').checked ? 'true' : 'false'); + formData.set('isDramaticCountdown', document.getElementById('isDramaticCountdown').checked ? 'true' : 'false'); // Date and Time inputs are already handled by formData if they have a 'name' attribute // 'countdownDate' and 'countdownTime' are collected automatically // Also apply the same validation for the label when submitting @@ -1162,6 +1192,7 @@ function setCountdownFieldsEnabled(enabled) { document.getElementById('countdownLabel').disabled = !enabled; document.getElementById('countdownDate').disabled = !enabled; document.getElementById('countdownTime').disabled = !enabled; + document.getElementById('isDramaticCountdown').disabled = !enabled; } // Existing function to send countdown enable/disable command to ESP @@ -1172,6 +1203,15 @@ function setCountdownEnabled(val) { body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false }); } + +function setIsDramaticCountdown(val) { + fetch('/set_dramatic_countdown', { + method: 'POST', + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "value=" + (val ? 1 : 0) // Send 1 for true, 0 for false + }); +} + // --- END Countdown Controls Logic ---