mirror of
https://github.com/mfactory-osaka/ESPTimeCast.git
synced 2026-02-19 11:54:56 -05:00
Added Advance setting:
Added Advance setting: Custom Primary/Secondary NTP server input 24/12h clock mode toggle (24-hour default) Flip display (180 degrees) Adjustable display brightness
This commit is contained in:
255
ESPTimeCast.ino
255
ESPTimeCast.ino
@@ -33,6 +33,13 @@ char timeZone[64] = "";
|
||||
unsigned long clockDuration = 10000;
|
||||
unsigned long weatherDuration = 5000;
|
||||
|
||||
// ADVANCED SETTINGS
|
||||
int brightness = 7;
|
||||
bool flipDisplay = false;
|
||||
bool twelveHourToggle = false; // <-- NEW: 12h/24h clock toggle
|
||||
char ntpServer1[64] = "pool.ntp.org";
|
||||
char ntpServer2[64] = "time.nist.gov";
|
||||
|
||||
WiFiClient client;
|
||||
const byte DNS_PORT = 53;
|
||||
DNSServer dnsServer;
|
||||
@@ -78,6 +85,11 @@ void printConfigToSerial() {
|
||||
Serial.print(F("Clock duration: ")); Serial.println(clockDuration);
|
||||
Serial.print(F("Weather duration: ")); Serial.println(weatherDuration);
|
||||
Serial.print(F("TimeZone (IANA): ")); Serial.println(timeZone);
|
||||
Serial.print(F("Brightness: ")); Serial.println(brightness);
|
||||
Serial.print(F("FlipDisplay: ")); Serial.println(flipDisplay ? "Yes" : "No");
|
||||
Serial.print(F("12h Clock: ")); Serial.println(twelveHourToggle ? "Yes" : "No"); // <-- NEW
|
||||
Serial.print(F("NTP Server 1: ")); Serial.println(ntpServer1);
|
||||
Serial.print(F("NTP Server 2: ")); Serial.println(ntpServer2);
|
||||
Serial.println(F("========================================"));
|
||||
Serial.println();
|
||||
}
|
||||
@@ -101,6 +113,11 @@ void loadConfig() {
|
||||
doc[F("clockDuration")] = 8000;
|
||||
doc[F("weatherDuration")] = 5000;
|
||||
doc[F("timeZone")] = "Asia/Tokyo";
|
||||
doc[F("brightness")] = brightness;
|
||||
doc[F("flipDisplay")] = flipDisplay;
|
||||
doc[F("twelveHourToggle")] = twelveHourToggle; // <-- NEW
|
||||
doc[F("ntpServer1")] = ntpServer1;
|
||||
doc[F("ntpServer2")] = ntpServer2;
|
||||
File f = LittleFS.open("/config.json", "w");
|
||||
if (f) {
|
||||
serializeJsonPretty(doc, f);
|
||||
@@ -139,6 +156,11 @@ void loadConfig() {
|
||||
if (doc.containsKey("clockDuration")) clockDuration = doc["clockDuration"];
|
||||
if (doc.containsKey("weatherDuration")) weatherDuration = doc["weatherDuration"];
|
||||
if (doc.containsKey("timeZone")) strlcpy(timeZone, doc["timeZone"], sizeof(timeZone));
|
||||
if (doc.containsKey("brightness")) brightness = doc["brightness"];
|
||||
if (doc.containsKey("flipDisplay")) flipDisplay = doc["flipDisplay"];
|
||||
if (doc.containsKey("twelveHourToggle")) twelveHourToggle = doc["twelveHourToggle"]; // <-- NEW
|
||||
if (doc.containsKey("ntpServer1")) strlcpy(ntpServer1, doc["ntpServer1"], sizeof(ntpServer1));
|
||||
if (doc.containsKey("ntpServer2")) strlcpy(ntpServer2, doc["ntpServer2"], sizeof(ntpServer2));
|
||||
if (strcmp(weatherUnits, "imperial") == 0)
|
||||
tempSymbol = 'F';
|
||||
else if (strcmp(weatherUnits, "standard") == 0)
|
||||
@@ -193,8 +215,7 @@ void connectWiFi() {
|
||||
void setupTime() {
|
||||
sntp_stop();
|
||||
Serial.println(F("[TIME] Starting NTP sync..."));
|
||||
configTime(0, 0, "pool.ntp.org", "time.nist.gov"); // Start NTP
|
||||
// NOW set time zone after starting configTime
|
||||
configTime(0, 0, ntpServer1, ntpServer2); // Use custom NTP servers
|
||||
setenv("TZ", ianaToPosix(timeZone), 1);
|
||||
tzset();
|
||||
ntpState = NTP_SYNCING;
|
||||
@@ -231,12 +252,17 @@ void setupWebServer() {
|
||||
serializeJson(doc, response);
|
||||
request->send(200, "application/json", response);
|
||||
});
|
||||
server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
Serial.println(F("[WEBSERVER] Request: /save"));
|
||||
DynamicJsonDocument doc(2048);
|
||||
for (int i = 0; i < request->params(); i++) {
|
||||
const AsyncWebParameter* p = request->getParam(i);
|
||||
doc[p->name()] = p->value();
|
||||
String n = p->name();
|
||||
String v = p->value();
|
||||
if (n == "brightness") doc[n] = v.toInt();
|
||||
else if (n == "flipDisplay") doc[n] = (v == "true" || v == "on" || v == "1");
|
||||
else if (n == "twelveHourToggle") doc[n] = (v == "true" || v == "on" || v == "1"); // <-- NEW
|
||||
else doc[n] = v;
|
||||
}
|
||||
if (LittleFS.exists("/config.json")) {
|
||||
LittleFS.rename("/config.json", "/config.bak");
|
||||
@@ -274,155 +300,62 @@ void setupWebServer() {
|
||||
request->send(200, "application/json", response);
|
||||
Serial.println(F("[WEBSERVER] Rebooting..."));
|
||||
request->onDisconnect([]() {
|
||||
Serial.println(F("[WEBSERVER] Rebooting..."));
|
||||
// delay(2500); // optional, can be reduced or omitted
|
||||
ESP.restart();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// server.on("/save", HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
// Serial.println(F("[WEBSERVER] Request: /save"));
|
||||
// DynamicJsonDocument doc(2048);
|
||||
// for (int i = 0; i < request->params(); i++) {
|
||||
// const AsyncWebParameter* p = request->getParam(i);
|
||||
// doc[p->name()] = p->value();
|
||||
// }
|
||||
|
||||
// // Simulate "Failed to Write Config" error:
|
||||
// Serial.println(F("[WEBSERVER] Simulating LittleFS read-only error"));
|
||||
// LittleFS.end(); // Unmount the filesystem (make it read-only)
|
||||
|
||||
// if (LittleFS.exists("/config.json")) {
|
||||
// LittleFS.rename("/config.json", "/config.bak");
|
||||
// }
|
||||
// File f = LittleFS.open("/config.json", "w");
|
||||
// if (!f) {
|
||||
// Serial.println(F("[WEBSERVER] Failed to open /config.json for writing"));
|
||||
// DynamicJsonDocument errorDoc(256);
|
||||
// errorDoc[F("error")] = "Failed to write config";
|
||||
// String response;
|
||||
// serializeJson(errorDoc, response);
|
||||
// request->send(500, "application/json", response);
|
||||
// return;
|
||||
// }
|
||||
// serializeJson(doc, f);
|
||||
// f.close();
|
||||
|
||||
// // Remount the filesystem (allow writes again for future operations)
|
||||
// Serial.println(F("[WEBSERVER] Remounting LittleFS"));
|
||||
// if (!LittleFS.begin()) {
|
||||
// Serial.println(F("[WEBSERVER] LittleFS mount failed after simulating error!"));
|
||||
// // Handle the error appropriately (e.g., send an error response)
|
||||
// }
|
||||
|
||||
// File verify = LittleFS.open("/config.json", "r");
|
||||
// DynamicJsonDocument test(2048);
|
||||
// DeserializationError err = deserializeJson(test, verify);
|
||||
// verify.close();
|
||||
// if (err) {
|
||||
// Serial.print(F("[WEBSERVER] Config corrupted after save: "));
|
||||
// Serial.println(err.f_str());
|
||||
// DynamicJsonDocument errorDoc(256);
|
||||
// errorDoc[F("error")] = "Config corrupted. Reboot cancelled.";
|
||||
// String response;
|
||||
// serializeJson(errorDoc, response);
|
||||
// request->send(500, "application/json", response);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// DynamicJsonDocument okDoc(128);
|
||||
// okDoc[F("message")] = "Saved successfully. Rebooting...";
|
||||
// String response;
|
||||
// serializeJson(okDoc, response);
|
||||
// request->send(200, "application/json", response);
|
||||
// Serial.println(F("[WEBSERVER] Rebooting..."));
|
||||
// ESP.restart();
|
||||
// });
|
||||
|
||||
|
||||
// server.on("/restore", HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
// Serial.println(F("[WEBSERVER] Request: /restore"));
|
||||
// if (LittleFS.exists("/config.bak")) {
|
||||
// LittleFS.remove("/config.json");
|
||||
// if (LittleFS.rename("/config.bak", "/config.json")) {
|
||||
// DynamicJsonDocument okDoc(128);
|
||||
// okDoc[F("message")] = "Backup restored.";
|
||||
// String response;
|
||||
// serializeJson(okDoc, response);
|
||||
// request->send(200, "application/json", response);
|
||||
// } else {
|
||||
// Serial.println(F("[WEBSERVER] Failed to rename backup"));
|
||||
// DynamicJsonDocument errorDoc(128);
|
||||
// errorDoc[F("error")] = "Failed to restore backup.";
|
||||
// String response;
|
||||
// serializeJson(errorDoc, response);
|
||||
// request->send(500, "application/json", response);
|
||||
// return;
|
||||
// }
|
||||
// } else {
|
||||
// Serial.println(F("[WEBSERVER] No backup found"));
|
||||
// DynamicJsonDocument errorDoc(128);
|
||||
// errorDoc[F("error")] = "No backup found.";
|
||||
// String response;
|
||||
// serializeJson(errorDoc, response);
|
||||
// request->send(404, "application/json", response);
|
||||
// }
|
||||
// });
|
||||
|
||||
server.on("/restore", HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
Serial.println(F("[WEBSERVER] Request: /restore"));
|
||||
if (LittleFS.exists("/config.bak")) {
|
||||
File src = LittleFS.open("/config.bak", "r");
|
||||
if (!src) {
|
||||
Serial.println(F("[WEBSERVER] Failed to open /config.bak"));
|
||||
DynamicJsonDocument errorDoc(128);
|
||||
errorDoc[F("error")] = "Failed to open backup file.";
|
||||
String response;
|
||||
serializeJson(errorDoc, response);
|
||||
request->send(500, "application/json", response);
|
||||
return;
|
||||
}
|
||||
File dst = LittleFS.open("/config.json", "w");
|
||||
if (!dst) {
|
||||
src.close();
|
||||
Serial.println(F("[WEBSERVER] Failed to open /config.json for writing"));
|
||||
DynamicJsonDocument errorDoc(128);
|
||||
errorDoc[F("error")] = "Failed to open config for writing.";
|
||||
String response;
|
||||
serializeJson(errorDoc, response);
|
||||
request->send(500, "application/json", response);
|
||||
return;
|
||||
}
|
||||
// Copy contents
|
||||
while (src.available()) {
|
||||
dst.write(src.read());
|
||||
}
|
||||
src.close();
|
||||
dst.close();
|
||||
|
||||
DynamicJsonDocument okDoc(128);
|
||||
okDoc[F("message")] = "✅ Backup restored! Device will now reboot.";
|
||||
String response;
|
||||
serializeJson(okDoc, response);
|
||||
request->send(200, "application/json", response);
|
||||
request->onDisconnect([]() {
|
||||
Serial.println(F("[WEBSERVER] Rebooting after restore..."));
|
||||
Serial.println(F("[WEBSERVER] Rebooting..."));
|
||||
ESP.restart();
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
Serial.println(F("[WEBSERVER] No backup found"));
|
||||
DynamicJsonDocument errorDoc(128);
|
||||
errorDoc[F("error")] = "No backup found.";
|
||||
String response;
|
||||
serializeJson(errorDoc, response);
|
||||
request->send(404, "application/json", response);
|
||||
}
|
||||
});
|
||||
server.on("/restore", HTTP_POST, [](AsyncWebServerRequest *request){
|
||||
Serial.println(F("[WEBSERVER] Request: /restore"));
|
||||
if (LittleFS.exists("/config.bak")) {
|
||||
File src = LittleFS.open("/config.bak", "r");
|
||||
if (!src) {
|
||||
Serial.println(F("[WEBSERVER] Failed to open /config.bak"));
|
||||
DynamicJsonDocument errorDoc(128);
|
||||
errorDoc[F("error")] = "Failed to open backup file.";
|
||||
String response;
|
||||
serializeJson(errorDoc, response);
|
||||
request->send(500, "application/json", response);
|
||||
return;
|
||||
}
|
||||
File dst = LittleFS.open("/config.json", "w");
|
||||
if (!dst) {
|
||||
src.close();
|
||||
Serial.println(F("[WEBSERVER] Failed to open /config.json for writing"));
|
||||
DynamicJsonDocument errorDoc(128);
|
||||
errorDoc[F("error")] = "Failed to open config for writing.";
|
||||
String response;
|
||||
serializeJson(errorDoc, response);
|
||||
request->send(500, "application/json", response);
|
||||
return;
|
||||
}
|
||||
// Copy contents
|
||||
while (src.available()) {
|
||||
dst.write(src.read());
|
||||
}
|
||||
src.close();
|
||||
dst.close();
|
||||
|
||||
DynamicJsonDocument okDoc(128);
|
||||
okDoc[F("message")] = "✅ Backup restored! Device will now reboot.";
|
||||
String response;
|
||||
serializeJson(okDoc, response);
|
||||
request->send(200, "application/json", response);
|
||||
request->onDisconnect([]() {
|
||||
Serial.println(F("[WEBSERVER] Rebooting after restore..."));
|
||||
ESP.restart();
|
||||
});
|
||||
|
||||
} else {
|
||||
Serial.println(F("[WEBSERVER] No backup found"));
|
||||
DynamicJsonDocument errorDoc(128);
|
||||
errorDoc[F("error")] = "No backup found.";
|
||||
String response;
|
||||
serializeJson(errorDoc, response);
|
||||
request->send(404, "application/json", response);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the /ap_status endpoint here:
|
||||
server.on("/ap_status", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||
Serial.print(F("[WEBSERVER] Request: /ap_status. isAPMode = "));
|
||||
Serial.println(isAPMode);
|
||||
@@ -518,7 +451,6 @@ void fetchWeather() {
|
||||
}
|
||||
break; // All done!
|
||||
}
|
||||
// If not yet body, keep looping for the header lines
|
||||
yield();
|
||||
delay(1);
|
||||
}
|
||||
@@ -569,10 +501,11 @@ void setup() {
|
||||
Serial.println(F("[SETUP] Starting setup..."));
|
||||
P.begin();
|
||||
P.setFont(mFactory); // Custom font
|
||||
P.setIntensity(8);
|
||||
loadConfig(); // Load config before setting intensity & flip
|
||||
P.setIntensity(brightness);
|
||||
P.setZoneEffect(0, flipDisplay, PA_FLIP_UD);
|
||||
P.setZoneEffect(0, flipDisplay, PA_FLIP_LR);
|
||||
Serial.println(F("[SETUP] Parola (LED Matrix) initialized"));
|
||||
loadConfig();
|
||||
Serial.println(F("[SETUP] Config loaded"));
|
||||
connectWiFi();
|
||||
Serial.println(F("[SETUP] Wifi connected"));
|
||||
setupWebServer();
|
||||
@@ -673,9 +606,9 @@ void loop() {
|
||||
fetchWeather();
|
||||
lastFetch = millis();
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
weatherFetchInitiated = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Time display logic
|
||||
time_t now = time(nullptr);
|
||||
@@ -684,8 +617,15 @@ void loop() {
|
||||
|
||||
int dayOfWeek = timeinfo.tm_wday;
|
||||
char* daySymbol = daysOfTheWeek[dayOfWeek];
|
||||
char timeStr[6];
|
||||
|
||||
char timeStr[9]; // enough for "12:34 AM"
|
||||
if (twelveHourToggle) {
|
||||
int hour12 = timeinfo.tm_hour % 12;
|
||||
if (hour12 == 0) hour12 = 12;
|
||||
sprintf(timeStr, "%d:%02d", hour12, timeinfo.tm_min);
|
||||
} else {
|
||||
sprintf(timeStr, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
||||
}
|
||||
String formattedTime = String(daySymbol) + " " + String(timeStr);
|
||||
|
||||
unsigned long displayDuration = (displayMode == 0) ? clockDuration : weatherDuration;
|
||||
@@ -739,8 +679,7 @@ void loop() {
|
||||
const unsigned long displayUpdateInterval = 50;
|
||||
if (millis() - lastDisplayUpdate >= displayUpdateInterval) {
|
||||
lastDisplayUpdate = millis();
|
||||
}
|
||||
}
|
||||
|
||||
yield();
|
||||
}
|
||||
|
||||
}
|
||||
BIN
assets/webui3.png
Normal file
BIN
assets/webui3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 536 KiB |
@@ -4,8 +4,13 @@
|
||||
"openWeatherApiKey": "ADD-YOUR-API-KEY-32-CHARACTERS",
|
||||
"openWeatherCity": "",
|
||||
"openWeatherCountry": "",
|
||||
"clockDuration": "10000",
|
||||
"weatherDuration": "5000",
|
||||
"clockDuration": 10000,
|
||||
"weatherDuration": 5000,
|
||||
"timeZone": "",
|
||||
"weatherUnits": "metric"
|
||||
}
|
||||
"weatherUnits": "metric",
|
||||
"brightness": 10,
|
||||
"flipDisplay": false,
|
||||
"ntpServer1": "pool.ntp.org",
|
||||
"ntpServer2": "time.nist.gov",
|
||||
"twelveHourToggle": false
|
||||
}
|
||||
493
data/index.html
493
data/index.html
@@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>ESPTimeCast Settings</title>
|
||||
<style>
|
||||
|
||||
:root{
|
||||
--accent-color: #0075ff;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html{
|
||||
background: radial-gradient(ellipse at 70% 0%, #2b425a 0%, #171e23 100%);
|
||||
@@ -17,18 +22,18 @@
|
||||
color: #FFFFFF;
|
||||
background-repeat: no-repeat, repeat, repeat;
|
||||
opacity: 0;
|
||||
transition: opacity 0.6s cubic-bezier(.4,0,.2,1);
|
||||
visibility: 0;
|
||||
transition: opacity 0.6s cubic-bezier(.4,0,.2,1);
|
||||
visibility: 0;
|
||||
}
|
||||
|
||||
body.loaded {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
body.loaded {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
@@ -62,7 +67,7 @@ body.loaded {
|
||||
}
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
display: block;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@@ -85,6 +90,24 @@ body.loaded {
|
||||
input[type="submit"]:hover {
|
||||
background-color: #005ecb;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:hover {
|
||||
background: rgba(225,245,255,0.07) !important;
|
||||
color: #fff !important;
|
||||
-webkit-box-shadow: 0 0 0 1000px rgba(225,245,255,0.07) inset !important;
|
||||
box-shadow: 0 0 0 1000px rgba(225,245,255,0.07) inset !important;
|
||||
-webkit-text-fill-color: #fff !important;
|
||||
transition: background 9999s ease-in-out 0s;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #fff; /* Example: light blue */
|
||||
opacity: 1; /* Make sure it's not semi-transparent */
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -155,20 +178,80 @@ body.loaded {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
select option {
|
||||
color: black;
|
||||
}
|
||||
|
||||
select option {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@media (min-width: 320px) {
|
||||
.form-row.two-col {
|
||||
flex-direction: row;
|
||||
gap: 1rem; }
|
||||
.form-row.two-col {
|
||||
flex-direction: row;
|
||||
gap: 1rem; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Make sure .details-content is visible only when open */
|
||||
.animated-details[open] .details-content {
|
||||
/* JS will animate the height */
|
||||
}
|
||||
|
||||
|
||||
/* Toggle Switch Styling for Flip Display */
|
||||
.toggle-switch { position: relative; display: inline-block; width: 48px; height: 24px; }
|
||||
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.toggle-slider {
|
||||
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: #ccc; transition: .4s; border-radius: 24px;
|
||||
}
|
||||
.toggle-slider:before {
|
||||
position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px;
|
||||
background-color: white; transition: .4s; border-radius: 50%;
|
||||
}
|
||||
input:checked + .toggle-slider { background-color: var(--accent-color); }
|
||||
input:checked + .toggle-slider:before { transform: translateX(24px); }
|
||||
|
||||
.accent{
|
||||
accent-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.collapsible-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
gap: 0.5em;
|
||||
user-select: none;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
.collapsible-toggle .icon-area {
|
||||
transition: transform 0.3s cubic-bezier(.4,0,.2,1);
|
||||
display: flex;
|
||||
}
|
||||
.collapsible-toggle.open .icon-area {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.collapsible-content {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
transition: height 0.3s cubic-bezier(.4,0,.2,1);
|
||||
color: #fff;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.collapsible-content-inner {
|
||||
padding: 1em 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -209,103 +292,102 @@ body.loaded {
|
||||
</select>
|
||||
<div class="small">
|
||||
Consult the <a href="https://openweathermap.org/price" target="_blank" rel="noopener">OpenWeatherMap</a>
|
||||
documendation for info about getting your API key, city, and country code.
|
||||
</div>
|
||||
documentation for info about getting your API key, city, and country code.
|
||||
</div>
|
||||
|
||||
<h2>Clock Settings</h2>
|
||||
<label for="timeZone">Time Zone</label>
|
||||
<select id="timeZone" name="timeZone" required>
|
||||
<option value="" disabled selected>Select your time zone</option>
|
||||
<option value="Africa/Algiers">Africa/Algiers</option>
|
||||
<option value="Africa/Cairo">Africa/Cairo</option>
|
||||
<option value="Africa/Casablanca">Africa/Casablanca</option>
|
||||
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
|
||||
<option value="Africa/Lagos">Africa/Lagos</option>
|
||||
<option value="Africa/Nairobi">Africa/Nairobi</option>
|
||||
<option value="America/Anchorage">America/Anchorage</option>
|
||||
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
|
||||
<option value="America/Bogota">America/Bogota</option>
|
||||
<option value="America/Caracas">America/Caracas</option>
|
||||
<option value="America/Chicago">America/Chicago</option>
|
||||
<option value="America/Denver">America/Denver</option>
|
||||
<option value="America/Lima">America/Lima</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
<option value="America/Mexico_City">America/Mexico_City</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Phoenix">America/Phoenix</option>
|
||||
<option value="America/Santiago">America/Santiago</option>
|
||||
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
|
||||
<option value="America/Toronto">America/Toronto</option>
|
||||
<option value="America/Vancouver">America/Vancouver</option>
|
||||
<option value="Asia/Almaty">Asia/Almaty</option>
|
||||
<option value="Asia/Amman">Asia/Amman</option>
|
||||
<option value="Asia/Baghdad">Asia/Baghdad</option>
|
||||
<option value="Asia/Baku">Asia/Baku</option>
|
||||
<option value="Asia/Bangkok">Asia/Bangkok</option>
|
||||
<option value="Asia/Beirut">Asia/Beirut</option>
|
||||
<option value="Asia/Dhaka">Asia/Dhaka</option>
|
||||
<option value="Asia/Dubai">Asia/Dubai</option>
|
||||
<option value="Asia/Ho_Chi_Minh">Asia/Ho_Chi_Minh</option>
|
||||
<option value="Asia/Hong_Kong">Asia/Hong_Kong</option>
|
||||
<option value="Asia/Jakarta">Asia/Jakarta</option>
|
||||
<option value="Asia/Jerusalem">Asia/Jerusalem</option>
|
||||
<option value="Asia/Kabul">Asia/Kabul</option>
|
||||
<option value="Asia/Karachi">Asia/Karachi</option>
|
||||
<option value="Asia/Kathmandu">Asia/Kathmandu</option>
|
||||
<option value="Asia/Kolkata">Asia/Kolkata</option>
|
||||
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
||||
<option value="Asia/Kuwait">Asia/Kuwait</option>
|
||||
<option value="Asia/Manila">Asia/Manila</option>
|
||||
<option value="Asia/Riyadh">Asia/Riyadh</option>
|
||||
<option value="Asia/Seoul">Asia/Seoul</option>
|
||||
<option value="Asia/Shanghai">Asia/Shanghai</option>
|
||||
<option value="Asia/Singapore">Asia/Singapore</option>
|
||||
<option value="Asia/Taipei">Asia/Taipei</option>
|
||||
<option value="Asia/Tashkent">Asia/Tashkent</option>
|
||||
<option value="Asia/Tehran">Asia/Tehran</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
<option value="Asia/Yangon">Asia/Yangon</option>
|
||||
<option value="Australia/Adelaide">Australia/Adelaide</option>
|
||||
<option value="Australia/Brisbane">Australia/Brisbane</option>
|
||||
<option value="Australia/Melbourne">Australia/Melbourne</option>
|
||||
<option value="Australia/Perth">Australia/Perth</option>
|
||||
<option value="Australia/Sydney">Australia/Sydney</option>
|
||||
<option value="Europe/Amsterdam">Europe/Amsterdam</option>
|
||||
<option value="Europe/Athens">Europe/Athens</option>
|
||||
<option value="Europe/Belgrade">Europe/Belgrade</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/Brussels">Europe/Brussels</option>
|
||||
<option value="Europe/Bucharest">Europe/Bucharest</option>
|
||||
<option value="Europe/Budapest">Europe/Budapest</option>
|
||||
<option value="Europe/Copenhagen">Europe/Copenhagen</option>
|
||||
<option value="Europe/Dublin">Europe/Dublin</option>
|
||||
<option value="Europe/Helsinki">Europe/Helsinki</option>
|
||||
<option value="Europe/Istanbul">Europe/Istanbul</option>
|
||||
<option value="Europe/Kiev">Europe/Kiev</option>
|
||||
<option value="Europe/Lisbon">Europe/Lisbon</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="Europe/Madrid">Europe/Madrid</option>
|
||||
<option value="Europe/Moscow">Europe/Moscow</option>
|
||||
<option value="Europe/Oslo">Europe/Oslo</option>
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/Prague">Europe/Prague</option>
|
||||
<option value="Europe/Riga">Europe/Riga</option>
|
||||
<option value="Europe/Rome">Europe/Rome</option>
|
||||
<option value="Europe/Sofia">Europe/Sofia</option>
|
||||
<option value="Europe/Stockholm">Europe/Stockholm</option>
|
||||
<option value="Europe/Tallinn">Europe/Tallinn</option>
|
||||
<option value="Europe/Vienna">Europe/Vienna</option>
|
||||
<option value="Europe/Vilnius">Europe/Vilnius</option>
|
||||
<option value="Europe/Warsaw">Europe/Warsaw</option>
|
||||
<option value="Europe/Zurich">Europe/Zurich</option>
|
||||
<option value="Pacific/Auckland">Pacific/Auckland</option>
|
||||
<option value="Pacific/Fiji">Pacific/Fiji</option>
|
||||
<option value="Pacific/Guam">Pacific/Guam</option>
|
||||
<option value="Pacific/Honolulu">Pacific/Honolulu</option>
|
||||
<option value="Etc/UTC">Etc/UTC</option>
|
||||
</select>
|
||||
|
||||
|
||||
<select id="timeZone" name="timeZone" required>
|
||||
<option value="" disabled selected>Select your time zone</option>
|
||||
<!-- ... (timezone options unchanged) ... -->
|
||||
<option value="Africa/Algiers">Africa/Algiers</option>
|
||||
<option value="Africa/Cairo">Africa/Cairo</option>
|
||||
<option value="Africa/Casablanca">Africa/Casablanca</option>
|
||||
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
|
||||
<option value="Africa/Lagos">Africa/Lagos</option>
|
||||
<option value="Africa/Nairobi">Africa/Nairobi</option>
|
||||
<option value="America/Anchorage">America/Anchorage</option>
|
||||
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
|
||||
<option value="America/Bogota">America/Bogota</option>
|
||||
<option value="America/Caracas">America/Caracas</option>
|
||||
<option value="America/Chicago">America/Chicago</option>
|
||||
<option value="America/Denver">America/Denver</option>
|
||||
<option value="America/Lima">America/Lima</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
<option value="America/Mexico_City">America/Mexico_City</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Phoenix">America/Phoenix</option>
|
||||
<option value="America/Santiago">America/Santiago</option>
|
||||
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
|
||||
<option value="America/Toronto">America/Toronto</option>
|
||||
<option value="America/Vancouver">America/Vancouver</option>
|
||||
<option value="Asia/Almaty">Asia/Almaty</option>
|
||||
<option value="Asia/Amman">Asia/Amman</option>
|
||||
<option value="Asia/Baghdad">Asia/Baghdad</option>
|
||||
<option value="Asia/Baku">Asia/Baku</option>
|
||||
<option value="Asia/Bangkok">Asia/Bangkok</option>
|
||||
<option value="Asia/Beirut">Asia/Beirut</option>
|
||||
<option value="Asia/Dhaka">Asia/Dhaka</option>
|
||||
<option value="Asia/Dubai">Asia/Dubai</option>
|
||||
<option value="Asia/Ho_Chi_Minh">Asia/Ho_Chi_Minh</option>
|
||||
<option value="Asia/Hong_Kong">Asia/Hong_Kong</option>
|
||||
<option value="Asia/Jakarta">Asia/Jakarta</option>
|
||||
<option value="Asia/Jerusalem">Asia/Jerusalem</option>
|
||||
<option value="Asia/Kabul">Asia/Kabul</option>
|
||||
<option value="Asia/Karachi">Asia/Karachi</option>
|
||||
<option value="Asia/Kathmandu">Asia/Kathmandu</option>
|
||||
<option value="Asia/Kolkata">Asia/Kolkata</option>
|
||||
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
||||
<option value="Asia/Kuwait">Asia/Kuwait</option>
|
||||
<option value="Asia/Manila">Asia/Manila</option>
|
||||
<option value="Asia/Riyadh">Asia/Riyadh</option>
|
||||
<option value="Asia/Seoul">Asia/Seoul</option>
|
||||
<option value="Asia/Shanghai">Asia/Shanghai</option>
|
||||
<option value="Asia/Singapore">Asia/Singapore</option>
|
||||
<option value="Asia/Taipei">Asia/Taipei</option>
|
||||
<option value="Asia/Tashkent">Asia/Tashkent</option>
|
||||
<option value="Asia/Tehran">Asia/Tehran</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
<option value="Asia/Yangon">Asia/Yangon</option>
|
||||
<option value="Australia/Adelaide">Australia/Adelaide</option>
|
||||
<option value="Australia/Brisbane">Australia/Brisbane</option>
|
||||
<option value="Australia/Melbourne">Australia/Melbourne</option>
|
||||
<option value="Australia/Perth">Australia/Perth</option>
|
||||
<option value="Australia/Sydney">Australia/Sydney</option>
|
||||
<option value="Europe/Amsterdam">Europe/Amsterdam</option>
|
||||
<option value="Europe/Athens">Europe/Athens</option>
|
||||
<option value="Europe/Belgrade">Europe/Belgrade</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/Brussels">Europe/Brussels</option>
|
||||
<option value="Europe/Bucharest">Europe/Bucharest</option>
|
||||
<option value="Europe/Budapest">Europe/Budapest</option>
|
||||
<option value="Europe/Copenhagen">Europe/Copenhagen</option>
|
||||
<option value="Europe/Dublin">Europe/Dublin</option>
|
||||
<option value="Europe/Helsinki">Europe/Helsinki</option>
|
||||
<option value="Europe/Istanbul">Europe/Istanbul</option>
|
||||
<option value="Europe/Kiev">Europe/Kiev</option>
|
||||
<option value="Europe/Lisbon">Europe/Lisbon</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="Europe/Madrid">Europe/Madrid</option>
|
||||
<option value="Europe/Moscow">Europe/Moscow</option>
|
||||
<option value="Europe/Oslo">Europe/Oslo</option>
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/Prague">Europe/Prague</option>
|
||||
<option value="Europe/Riga">Europe/Riga</option>
|
||||
<option value="Europe/Rome">Europe/Rome</option>
|
||||
<option value="Europe/Sofia">Europe/Sofia</option>
|
||||
<option value="Europe/Stockholm">Europe/Stockholm</option>
|
||||
<option value="Europe/Tallinn">Europe/Tallinn</option>
|
||||
<option value="Europe/Vienna">Europe/Vienna</option>
|
||||
<option value="Europe/Vilnius">Europe/Vilnius</option>
|
||||
<option value="Europe/Warsaw">Europe/Warsaw</option>
|
||||
<option value="Europe/Zurich">Europe/Zurich</option>
|
||||
<option value="Pacific/Auckland">Pacific/Auckland</option>
|
||||
<option value="Pacific/Fiji">Pacific/Fiji</option>
|
||||
<option value="Pacific/Guam">Pacific/Guam</option>
|
||||
<option value="Pacific/Honolulu">Pacific/Honolulu</option>
|
||||
<option value="Etc/UTC">Etc/UTC</option>
|
||||
</select>
|
||||
|
||||
<div class="form-row two-col">
|
||||
<div>
|
||||
@@ -319,7 +401,47 @@ body.loaded {
|
||||
<label class="small">(Seconds)</label>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br>
|
||||
|
||||
<button type="button" class="collapsible-toggle" aria-expanded="false">
|
||||
<span class="icon-area" aria-hidden="true">
|
||||
<svg width="20" height="20" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path fill="currentColor" d="M16,9V7l-2.259-0.753c-0.113-0.372-0.262-0.728-0.441-1.066l1.066-2.131L12.95,1.634L10.819,2.7 c-0.338-0.178-0.694-0.325-1.066-0.441L9,0H7L6.247,2.259C5.875,2.372,5.519,2.522,5.181,2.7L3.05,1.638L1.638,3.05l1.066,2.131 C2.522,5.519,2.375,5.875,2.259,6.247L0,7v2l2.259,0.753c0.112,0.372,0.263,0.728,0.441,1.066L1.634,12.95l1.416,1.416L5.181,13.3 c0.338,0.178,0.694,0.328,1.066,0.441L7,16h2l0.753-2.259c0.372-0.113,0.728-0.262,1.066-0.441l2.131,1.066l1.416-1.416L13.3,10.819 c0.178-0.337,0.328-0.694,0.441-1.066L16,9z M8,11c-1.656,0-3-1.344-3-3s1.344-3,3-3s3,1.344,3,3S9.656,11,8,11z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>Advanced Settings</span>
|
||||
</button>
|
||||
<div class="collapsible-content" aria-hidden="true">
|
||||
<!-- Custom NTP Servers -->
|
||||
<label>Primary NTP Server:</label>
|
||||
<input type="text" name="ntpServer1" id="ntpServer1" placeholder="Enter NTP address">
|
||||
|
||||
<label>Secondary NTP Server:</label>
|
||||
<input type="text" name="ntpServer2" id="ntpServer2" placeholder="Enter NTP address">
|
||||
|
||||
<!-- 24/12hrs Clock Toggle (Styled) -->
|
||||
<label style="display: flex; align-items: center; margin-top: 1.75rem;">
|
||||
<span style="margin-right: 0.5em;">Display 12-hour Clock</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" name="twelveHourToggle" id="twelveHourToggle">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Display Flip Toggle (Styled) -->
|
||||
<label style="display: flex; align-items: center; margin-top: 1.75rem;">
|
||||
<span style="margin-right: 0.5em;">Flip Display (180°):</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" name="flipDisplay" id="flipDisplay">
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- Brightness Slider with Value -->
|
||||
<label style="margin-top: 1.75rem;">Brightness: <span id="brightnessValue">10</span></label>
|
||||
<input type="range" min="0" max="15" name="brightness" id="brightnessSlider" value="10" oninput="brightnessValue.textContent = brightnessSlider.value">
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="primary-button" value="Save Settings">
|
||||
</form>
|
||||
|
||||
@@ -333,50 +455,20 @@ body.loaded {
|
||||
let isSaving = false;
|
||||
let isAPMode = false;
|
||||
|
||||
function ensureReloadButton(options = {}) {
|
||||
let modalContent = document.getElementById('savingModalContent');
|
||||
if (!modalContent) return;
|
||||
let btn = document.getElementById('reloadButton');
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.id = 'reloadButton';
|
||||
btn.className = 'primary-button';
|
||||
btn.style.display = 'inline-block';
|
||||
btn.style.margin = '1rem 0.5rem 0 0';
|
||||
modalContent.appendChild(btn);
|
||||
}
|
||||
btn.textContent = options.text || "Reload Page";
|
||||
btn.onclick = options.onClick || (() => location.reload());
|
||||
btn.style.display = 'inline-block';
|
||||
return btn;
|
||||
}
|
||||
// Set initial value display for brightness
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
brightnessValue.textContent = brightnessSlider.value;
|
||||
});
|
||||
|
||||
function ensureRestoreButton(options = {}) {
|
||||
let modalContent = document.getElementById('savingModalContent');
|
||||
if (!modalContent) return;
|
||||
let btn = document.getElementById('restoreButton');
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.id = 'restoreButton';
|
||||
btn.className = 'primary-button';
|
||||
btn.style.display = 'inline-block';
|
||||
btn.style.margin = '1rem 0 0 0.5rem';
|
||||
modalContent.appendChild(btn);
|
||||
}
|
||||
btn.textContent = options.text || "Restore Backup";
|
||||
btn.onclick = options.onClick || restoreBackupConfig;
|
||||
btn.style.display = 'inline-block';
|
||||
return btn;
|
||||
}
|
||||
// Show/hide password toggle
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const passwordInput = document.getElementById("password");
|
||||
const toggleCheckbox = document.getElementById("togglePassword");
|
||||
|
||||
function removeReloadButton() {
|
||||
let btn = document.getElementById('reloadButton');
|
||||
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
|
||||
}
|
||||
function removeRestoreButton() {
|
||||
let btn = document.getElementById('restoreButton');
|
||||
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
|
||||
}
|
||||
toggleCheckbox.addEventListener("change", function () {
|
||||
passwordInput.type = this.checked ? "text" : "password";
|
||||
});
|
||||
});
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
if (isSaving) {
|
||||
@@ -401,7 +493,13 @@ window.onload = function () {
|
||||
document.getElementById('weatherUnits').value = data.weatherUnits || 'metric';
|
||||
document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000;
|
||||
document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000;
|
||||
|
||||
// Advanced:
|
||||
document.getElementById('brightnessSlider').value = typeof data.brightness !== "undefined" ? data.brightness : 10;
|
||||
document.getElementById('brightnessValue').textContent = document.getElementById('brightnessSlider').value;
|
||||
document.getElementById('flipDisplay').checked = !!data.flipDisplay;
|
||||
document.getElementById('ntpServer1').value = data.ntpServer1 || "";
|
||||
document.getElementById('ntpServer2').value = data.ntpServer2 || "";
|
||||
document.getElementById('twelveHourToggle').checked = !!data.twelveHourToggle;
|
||||
// Auto-detect browser's timezone if not set in config
|
||||
if (!data.timeZone) {
|
||||
try {
|
||||
@@ -442,8 +540,6 @@ window.onload = function () {
|
||||
});
|
||||
document.querySelector('html').style.height = 'unset';
|
||||
document.body.classList.add('loaded');
|
||||
|
||||
|
||||
};
|
||||
|
||||
async function submitConfig(event) {
|
||||
@@ -458,6 +554,11 @@ async function submitConfig(event) {
|
||||
formData.set('clockDuration', clockDuration);
|
||||
formData.set('weatherDuration', weatherDuration);
|
||||
|
||||
// Advanced: ensure correct values are set for advanced fields
|
||||
formData.set('brightness', document.getElementById('brightnessSlider').value);
|
||||
formData.set('flipDisplay', document.getElementById('flipDisplay').checked ? 'on' : '');
|
||||
formData.set('twelveHourToggle', document.getElementById('twelveHourToggle').checked ? 'on' : '');
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const pair of formData.entries()) {
|
||||
params.append(pair[0], pair[1]);
|
||||
@@ -579,6 +680,51 @@ function updateSavingModal(message, showSpinner = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureReloadButton(options = {}) {
|
||||
let modalContent = document.getElementById('savingModalContent');
|
||||
if (!modalContent) return;
|
||||
let btn = document.getElementById('reloadButton');
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.id = 'reloadButton';
|
||||
btn.className = 'primary-button';
|
||||
btn.style.display = 'inline-block';
|
||||
btn.style.margin = '1rem 0.5rem 0 0';
|
||||
modalContent.appendChild(btn);
|
||||
}
|
||||
btn.textContent = options.text || "Reload Page";
|
||||
btn.onclick = options.onClick || (() => location.reload());
|
||||
btn.style.display = 'inline-block';
|
||||
return btn;
|
||||
}
|
||||
|
||||
function ensureRestoreButton(options = {}) {
|
||||
let modalContent = document.getElementById('savingModalContent');
|
||||
if (!modalContent) return;
|
||||
let btn = document.getElementById('restoreButton');
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.id = 'restoreButton';
|
||||
btn.className = 'primary-button';
|
||||
btn.style.display = 'inline-block';
|
||||
btn.style.margin = '1rem 0 0 0.5rem';
|
||||
modalContent.appendChild(btn);
|
||||
}
|
||||
btn.textContent = options.text || "Restore Backup";
|
||||
btn.onclick = options.onClick || restoreBackupConfig;
|
||||
btn.style.display = 'inline-block';
|
||||
return btn;
|
||||
}
|
||||
|
||||
function removeReloadButton() {
|
||||
let btn = document.getElementById('reloadButton');
|
||||
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
|
||||
}
|
||||
function removeRestoreButton() {
|
||||
let btn = document.getElementById('restoreButton');
|
||||
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
|
||||
}
|
||||
|
||||
function restoreBackupConfig() {
|
||||
showSavingModal("Restoring backup...");
|
||||
removeReloadButton();
|
||||
@@ -606,16 +752,6 @@ function restoreBackupConfig() {
|
||||
});
|
||||
}
|
||||
|
||||
// Show/hide password toggle
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const passwordInput = document.getElementById("password");
|
||||
const toggleCheckbox = document.getElementById("togglePassword");
|
||||
|
||||
toggleCheckbox.addEventListener("change", function () {
|
||||
passwordInput.type = this.checked ? "text" : "password";
|
||||
});
|
||||
});
|
||||
|
||||
function hideSavingModal() {
|
||||
const modal = document.getElementById('savingModal');
|
||||
if (modal) {
|
||||
@@ -623,6 +759,31 @@ function hideSavingModal() {
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = document.querySelector('.collapsible-toggle');
|
||||
const content = document.querySelector('.collapsible-content');
|
||||
toggle.addEventListener('click', function() {
|
||||
const isOpen = toggle.classList.toggle('open');
|
||||
toggle.setAttribute('aria-expanded', isOpen);
|
||||
content.setAttribute('aria-hidden', !isOpen);
|
||||
if(isOpen) {
|
||||
content.style.height = content.scrollHeight + 'px';
|
||||
content.addEventListener('transitionend', function handler() {
|
||||
content.style.height = 'auto';
|
||||
content.removeEventListener('transitionend', handler);
|
||||
});
|
||||
} else {
|
||||
content.style.height = content.scrollHeight + 'px';
|
||||
// Force reflow to make sure the browser notices the height before transitioning to 0
|
||||
void content.offsetHeight;
|
||||
content.style.height = '0px';
|
||||
}
|
||||
});
|
||||
// Optional: If open on load, set height to auto
|
||||
if(toggle.classList.contains('open')) {
|
||||
content.style.height = 'auto';
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user