mirror of
https://github.com/mfactory-osaka/ESPTimeCast.git
synced 2026-02-19 11:54:56 -05:00
Added scrolling countdown
Added scrolling countdown
This commit is contained in:
@@ -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<const char *>()) {
|
||||
@@ -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 ---
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"countdown": {
|
||||
"enabled": false,
|
||||
"targetTimestamp": 0,
|
||||
"label": ""
|
||||
"label": "",
|
||||
"isDramaticCountdown": true
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span style="margin-right: 0.5em;">Dramatic Countdown:</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="isDramaticCountdown" name="isDramaticCountdown">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row two-col">
|
||||
<div class="form-group">
|
||||
<label for="countdownDate">Countdown Date:</label>
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
|
||||
@@ -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<const char *>()) {
|
||||
@@ -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<JsonArray>() && doc.size() > 0) {
|
||||
JsonObject firstReading = doc[0].as<JsonObject>();
|
||||
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<JsonArray>() && doc.size() > 0) {
|
||||
JsonObject firstReading = doc[0].as<JsonObject>();
|
||||
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();
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"countdown": {
|
||||
"enabled": false,
|
||||
"targetTimestamp": 0,
|
||||
"label": ""
|
||||
"label": "",
|
||||
"isDramaticCountdown": true
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span style="margin-right: 0.5em;">Dramatic Countdown:</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="isDramaticCountdown" name="isDramaticCountdown">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row two-col">
|
||||
<div class="form-group">
|
||||
<label for="countdownDate">Countdown Date:</label>
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user