Files
ESPTimeCast/ESPTimeCast_ESP32/data/index.html
M-Factory 1fd35d235f Added seconds to clock without the days of the week
Added seconds to clock without the days of the week.
Blinking colon toggle changed to Animated seconds.
Correct days of the week for German language.
Geolocation changed to ip-api.com
2025-08-29 22:05:34 +09:00

1260 lines
66 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<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%);
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
margin: 0;
padding: 2rem 1rem;
color: #FFFFFF;
background-repeat: no-repeat, repeat, repeat;
opacity: 0;
transition: opacity 0.6s cubic-bezier(.4,0,.2,1);
visibility: hidden;
height: 100%;
line-height: 1.5;
}
body.loaded {
visibility: visible;
opacity: 1;
}
body.modal-open {
overflow: hidden;
}
h1 {
text-align: center;
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #ffffff;
}
h2{
margin-top: 2rem;
margin-bottom: 0;
}
h2:first-of-type{
margin-top: 1.5rem;
}
.logo svg{
filter: drop-shadow(0px 0px 0.5rem #1ec7fa);
width: 100%;
height: auto;
color: #c2f0ff;
margin: 0.5rem 0;
}
form {
display: flex; flex-direction: column;
max-width: 500px; margin: 0 auto;
background: linear-gradient(120deg, rgba(45,65,90,0.72) 0%, rgba(53,133,183,0.38) 100%);
padding: 1.5rem;
border-radius: 24px;
box-shadow: 0 10px 36px 0 rgba(40,170,255,0.11), 0 2px 8px 0 rgba(44, 70, 110, 0.08);
border: 1.5px solid rgba(180, 230, 255, 0.10)
}
label {
font-size: 0.9rem;
display: block;
margin-top: 0.75rem;
}
input[type="text"],
input[type="time"],
input[type="password"],
input[type="date"],
input[type="number"], select {
width: 100%; padding: 0.75rem;
border: 1.5px solid rgba(180, 230, 255, 0.08);
border-radius: 8px;
background-color: rgba(225,245,255,0.07);
color: #ffffff;
font-size: 1rem; appearance: none;
}
input[type="time"]:disabled ,
input[type="text"]:disabled ,
input[type="number"]:disabled ,
input[type="date"]:disabled{
color: rgba(255, 255, 255, 0.250);
}
input[type="submit"] {
background-color: #007aff; color: white;
padding: 0.9rem; font-size: 1rem;
border: none; border-radius: 8px;
cursor: pointer; transition: background-color 0.2s ease-in-out;
}
input[type="submit"]:hover {
background-color: #005ecb;
}
input[type="time"]::-webkit-calendar-picker-indicator, input[type="date"]::-webkit-calendar-picker-indicator{
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 {
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: hwb(0 100% 0% / 0.39); /* Example: light blue */
opacity: 1; /* Make sure it's not semi-transparent */
}
.form-row {
display: flex;
flex-direction: column;
}
.form-row.two-col {
flex-direction: column;
}
.form-row.two-col > div {
flex: 1;
}
.primary-button {
background: linear-gradient(90deg, #3e99bc, #47add4 85%);
color: white;
padding: 0.9rem; font-size: 1rem; font-weight: 600;
border: none; border-radius: 8px;
cursor: pointer; text-align: center;
transition: background 0.25s, transform 0.15s ease-in-out;
}
.primary-button:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 122, 255, 0.35);
}
.primary-button:active {
transform: scale(0.97);
}
.note {
font-size: 0.85rem;
text-align: center;
margin-top: 1rem;
}
#savingModal {
backdrop-filter: blur(5px);
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
background: radial-gradient(ellipse at 70% 0%, hsl(210.64deg 35.34% 26.08% / 60%) 0%, #171e2399 100%);
display: none; justify-content: center; align-items: center;
z-index: 1000;
}
#savingModalContent {
background: linear-gradient(120deg, rgb(45 65 90) 0%, rgb(53 133 183 / 44%) 100%);
border-radius: 12px;
box-shadow: 0 10px 36px 0 rgba(40, 170, 255, 0.11), 0 2px 8px 0 rgba(44, 70, 110, 0.08);
border: 1.5px solid rgba(180, 230, 255, 0.10);
margin: 1.5rem;
padding: 2rem 2.5rem;
text-align: center;
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.2);
border-top: 4px solid #007aff;
border-radius: 50%;
width: 36px; height: 36px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
.footer{
font-size: 0.8rem;
text-align: center;
margin-top: 1rem;
}
a{
color: white;
}
.small{
display: block;
font-size: 0.8rem;
margin-top: 0.25rem;
}
select option {
color: black;
}
.geo-disabled{
opacity: 0.5;
background: transparent;
border: 0.1rem white solid;
cursor: not-allowed;
}
.geo-disabled:hover{
transform: none;
box-shadow: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
#openWeatherCountry{
margin-top: 0.75rem;
}
@media (min-width: 361px) {
.form-row.two-col {
flex-direction: row;
gap: 1rem; }
#openWeatherCountry{
margin-top: 0;
}
}
/* 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>
<form id="configForm" onsubmit="submitConfig(event)">
<div class="logo">
<svg viewBox="0 0 132.291 14.322" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path style="fill:currentColor; stroke-width:1" d="M326.491 337.688c-.233.003-.476.121-.657.38-.333.475-.326.56.074.96.603.604 1.332.33 1.332-.499 0-.529-.36-.846-.749-.841m4.23.009c-.698 0-1.057.903-.557 1.403.144.143.4.261.57.261.38 0 .832-.43.832-.79 0-.368-.49-.874-.845-.874m35.71 0c-.697 0-1.055.903-.556 1.403.144.143.4.261.57.261.38 0 .833-.43.833-.79 0-.368-.49-.874-.846-.874m-118.103.008a.7.7 0 0 0-.504.192c-.28.28-.248.892.062 1.203.337.337.716.332 1.098-.014.378-.342.387-.69.03-1.085a.93.93 0 0 0-.686-.296m22.402 0a.7.7 0 0 0-.505.192c-.28.28-.248.892.062 1.203.337.337.716.332 1.099-.014.377-.342.386-.69.028-1.085a.93.93 0 0 0-.684-.296m15.342 0a.7.7 0 0 0-.504.192c-.28.28-.248.892.062 1.203.337.336.717.332 1.099-.014.377-.342.387-.69.029-1.085a.93.93 0 0 0-.686-.296m-28.564 0c-.248-.009-.496.112-.672.362-.333.476-.326.56.074.961.45.45.935.419 1.264-.083.252-.385.252-.447 0-.832-.172-.262-.419-.399-.666-.408m15.445 0c-.248-.009-.496.112-.671.362-.334.476-.327.56.073.961.451.45.935.419 1.264-.083.252-.385.252-.447 0-.832-.172-.262-.419-.399-.666-.408m-30.884.001a.76.76 0 0 0-.663.343c-.27.386-.199.783.2 1.104.403.325.827.246 1.125-.208.252-.385.252-.447 0-.832-.17-.258-.416-.395-.662-.407m37.744 0a.76.76 0 0 0-.663.343c-.27.386-.199.783.2 1.104.402.325.827.246 1.125-.208.252-.385.252-.447 0-.832-.17-.258-.416-.395-.662-.407m-39.949.002c-.232.003-.445.108-.554.31-.242.452-.215.778.089 1.082.337.336.716.332 1.098-.014.378-.342.387-.69.03-1.085a.9.9 0 0 0-.663-.293m15.444 0c-.232.003-.447.108-.555.31-.242.452-.215.778.09 1.082.336.337.715.332 1.097-.014.378-.342.387-.69.03-1.085a.9.9 0 0 0-.662-.293m73.302.001q-.279.004-.567.263c-.377.342-.387.69-.029 1.085.152.167.409.304.57.304.698 0 1.057-.903.558-1.402q-.255-.253-.532-.25m-40.331.005a.75.75 0 0 0-.648.343c-.341.488-.335.569.078.982.436.436 1.004.358 1.269-.176.298-.6-.186-1.133-.699-1.149m-42.007.01a.8.8 0 0 0-.55.235c-.336.337-.331.716.015 1.098.494.546 1.389.206 1.389-.528 0-.488-.425-.813-.854-.805m22.4 0a.8.8 0 0 0-.548.235c-.337.337-.332.716.014 1.098.494.546 1.388.206 1.388-.528 0-.488-.424-.813-.853-.805m15.344 0a.8.8 0 0 0-.55.235c-.336.337-.331.716.015 1.098.494.546 1.388.206 1.388-.528 0-.488-.424-.813-.853-.805m-39.875.001a.8.8 0 0 0-.505.217c-.41.37-.43.71-.063 1.115.554.611 1.456.203 1.362-.616-.053-.458-.413-.733-.794-.716m15.443 0a.8.8 0 0 0-.505.217c-.41.37-.429.71-.063 1.115.554.611 1.456.203 1.362-.616-.053-.458-.413-.733-.794-.716m6.958 0a.8.8 0 0 0-.505.217c-.41.37-.429.71-.063 1.115.554.611 1.455.203 1.362-.616-.053-.458-.414-.733-.794-.716m15.343 0a.8.8 0 0 0-.505.217c-.41.37-.43.71-.063 1.115.554.611 1.456.203 1.362-.616-.053-.458-.413-.733-.794-.716m11.157 0a.8.8 0 0 0-.504.217c-.41.37-.43.71-.063 1.115.553.611 1.455.203 1.361-.616-.052-.458-.413-.733-.794-.716m-53.124 2.112c-.608-.001-1.077.752-.505 1.383.355.393.643.385 1.056-.028.4-.401.422-.72.071-1.07-.2-.2-.419-.285-.622-.285m126.508.01a.8.8 0 0 0-.464.13c-.43.27-.424.976.012 1.282.476.334.52.332.978-.04.613-.496.123-1.348-.526-1.372m-42.091.013c-.728 0-1.005.782-.481 1.36.151.168.376.304.5.304.275 0 .889-.594.889-.86 0-.332-.533-.804-.908-.804m8.487 0c-.728 0-1.004.782-.48 1.36.15.168.375.304.498.304.276 0 .89-.594.89-.86 0-.332-.533-.804-.908-.804m-66.164.008a.97.97 0 0 0-.698.253c-.35.351-.33.67.072 1.07.4.4.719.422 1.07.071.31-.31.341-.923.061-1.203a.74.74 0 0 0-.505-.19m8.487 0c-.253-.008-.523.079-.697.253-.35.351-.33.67.07 1.07.402.4.72.422 1.07.071.311-.31.343-.923.063-1.203a.75.75 0 0 0-.506-.19m-21.896.075c-.482 0-.601.077-.741.475-.144.41-.113.52.222.79.496.403.726.398 1.106-.022.242-.268.273-.432.147-.79-.132-.375-.259-.453-.734-.453m8.487 0c-.481 0-.601.077-.741.475-.144.41-.113.52.222.79.497.403.726.398 1.106-.022.243-.268.273-.432.147-.79-.132-.375-.259-.453-.734-.453m22.301 0c-.481 0-.6.077-.741.475-.144.41-.113.52.222.79.497.403.726.398 1.106-.022.243-.268.273-.432.147-.79-.132-.375-.258-.453-.734-.453m-17.528 1.934c-.14-.001-.265.1-.477.313-.401.4-.422.719-.072 1.07.31.31.923.341 1.203.061.318-.318.232-.923-.17-1.205-.223-.156-.36-.238-.484-.24m8.487 0c-.14-.001-.264.1-.477.313-.4.4-.422.719-.071 1.07.31.31.923.341 1.203.061.317-.318.232-.923-.17-1.205-.224-.156-.361-.238-.485-.24m18.014 0c-.141-.001-.265.1-.478.313-.4.4-.421.719-.07 1.07.31.31.922.341 1.202.061.318-.318.232-.923-.17-1.205-.223-.156-.36-.238-.484-.24m4.794 0c-.14-.001-.264.1-.477.313-.4.4-.422.719-.072 1.07.31.31.923.341 1.203.061.318-.318.233-.923-.17-1.205-.223-.156-.36-.238-.484-.24m15.452 0c-.14-.001-.265.1-.477.313-.401.4-.422.719-.072 1.07.31.31.923.341 1.203.061.318-.317.232-.923-.17-1.205-.223-.156-.36-.238-.484-.24m51.004 0c-.14-.001-.264.1-.477.313-.4.4-.422.719-.071 1.07.31.31.923.341 1.203.061.317-.317.232-.923-.17-1.205-.224-.156-.36-.238-.485-.24m-111.01.025c-.113 0-.224.063-.417.19-.446.291-.552.894-.216 1.229.11.11.394.2.632.2.877 0 1.153-.947.416-1.43-.193-.126-.304-.19-.416-.19m30.787 0c-.111 0-.223.063-.416.19-.445.291-.552.894-.216 1.229.11.11.394.2.632.2.878 0 1.153-.947.416-1.43-.192-.126-.304-.19-.416-.19m18.121.01c-.111 0-.223.063-.416.19-.446.292-.551.894-.216 1.23.257.257 1.007.257 1.265 0 .335-.336.229-.938-.217-1.23-.192-.127-.304-.19-.416-.19m44.047.003c-.275-.001-.55.175-.712.53-.138.303-.122.474.068.745.135.194.425.352.643.352.56 0 .944-.588.714-1.092-.162-.355-.437-.534-.713-.535m-42.108.012c-.525.016-.892.648-.53 1.202.326.496.979.513 1.314.034.289-.411.132-.855-.402-1.141a.75.75 0 0 0-.382-.095m37.717 0c-.525.016-.892.648-.53 1.202.326.496.979.513 1.315.034.288-.411.131-.855-.403-1.141a.76.76 0 0 0-.382-.095m15.462.005a.6.6 0 0 0-.267.075c-.47.252-.574.672-.28 1.122.278.423.888.52 1.215.193.5-.5-.049-1.421-.668-1.39m2.217.002h-.059a.64.64 0 0 0-.27.078.854.854 0 0 0-.358 1.17c.204.382.977.465 1.302.14.488-.488-.015-1.366-.615-1.388m-119.55 0c-.595.03-1.092.902-.606 1.388.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m77.636 0c-.596.03-1.092.902-.607 1.388.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m6.806 0c-.597.03-1.093.902-.607 1.388.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m15.46 0c-.597.03-1.093.902-.607 1.388.11.11.406.2.66.2.833 0 1.04-1.105.282-1.51a.64.64 0 0 0-.336-.078m4.211 0a.63.63 0 0 0-.336.078.854.854 0 0 0-.359 1.17c.204.382.977.465 1.302.14.486-.486-.01-1.357-.607-1.388m9.07 0c-.597.03-1.093.902-.607 1.388.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m2.163 0c-.596.03-1.093.902-.607 1.388.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m11.131 0c-.596.03-1.093.902-.607 1.388.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m2.164 0c-.597.03-1.093.902-.607 1.388.11.11.406.2.66.2.832 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m2.009.001a.6.6 0 0 0-.29.072c-.632.338-.582 1.112.097 1.497.398.226.94-.15 1.007-.699.055-.458-.386-.871-.814-.87m-70.624.002c-.602.027-1.102.894-.612 1.385.278.278 1.05.25 1.296-.048.275-.33.084-1.028-.344-1.257a.65.65 0 0 0-.34-.08m15.452 0c-.603.027-1.103.894-.612 1.385.278.278 1.05.25 1.296-.048.275-.33.084-1.028-.344-1.257a.65.65 0 0 0-.34-.08m-60.08 2.084c-.61 0-1.002 1.023-.56 1.464s1.464.05 1.464-.56c0-.308-.596-.904-.904-.904m15.445 0c-.61 0-1.002 1.023-.56 1.464s1.464.05 1.464-.56c0-.308-.596-.904-.904-.904m31.295 0c-.61 0-1.002 1.023-.56 1.464.42.42 1.464.011 1.464-.575 0-.296-.603-.89-.904-.89m22.258 0c-.61 0-1.002 1.023-.56 1.464.42.42 1.464.011 1.464-.575 0-.296-.603-.89-.904-.89m21.884 0c-.357 0-.861.496-.861.846 0 .162.117.412.261.556s.4.262.57.262c.38 0 .833-.43.833-.79 0-.348-.483-.874-.803-.874m20.252 0c-.357 0-.861.496-.861.846 0 .162.117.412.261.556s.4.262.57.262c.38 0 .833-.43.833-.79 0-.348-.483-.874-.803-.874m-28.696.01c-.232.001-.476.12-.66.383-.333.476-.327.56.074.962.562.562 1.331.289 1.331-.472 0-.545-.357-.874-.745-.872m-97.915.005c-.159 0-.3.082-.465.247-.349.348-.332 1.012.03 1.223.964.561 1.908-.592 1.003-1.226-.234-.163-.41-.245-.568-.244m6.472 0c-.182-.01-.368.077-.57.26-.377.341-.39.78-.034 1.137.718.718 1.83-.321 1.148-1.074-.185-.205-.363-.312-.544-.322m22.4 0c-.181-.01-.367.077-.57.26-.376.341-.389.78-.033 1.137.718.718 1.83-.321 1.148-1.074-.185-.205-.363-.312-.545-.322m15.344 0c-.182-.01-.368.077-.57.26-.377.341-.39.78-.034 1.137.718.718 1.83-.321 1.148-1.074-.185-.205-.363-.312-.544-.322m27.088.028c-.583.002-1.021.64-.644 1.216.329.502.982.532 1.312.062.292-.418.181-.939-.25-1.17a.9.9 0 0 0-.418-.108m-4.882.007a.76.76 0 0 0-.479.206c-.4.362-.414.702-.043 1.112.33.365 1.02.337 1.264-.053.384-.612-.16-1.292-.742-1.265m44.674 0a.76.76 0 0 0-.478.206c-.4.362-.414.702-.044 1.112.331.365 1.022.337 1.265-.053.384-.612-.16-1.292-.743-1.265m-106.757.003c-.267.003-.547.153-.728.476-.228.408-.222.477.062.79.557.616 1.425.356 1.425-.425 0-.538-.367-.846-.758-.84m15.443 0c-.268.003-.547.153-.727.476-.228.408-.223.477.061.79.557.616 1.425.356 1.425-.425 0-.538-.367-.846-.759-.84m6.958 0c-.268.003-.547.153-.728.476-.228.408-.222.477.062.79.557.616 1.425.356 1.425-.425 0-.538-.367-.846-.759-.84m26.501 0c-.268.003-.547.153-.728.476-.228.408-.223.477.062.79.556.616 1.425.356 1.425-.425 0-.538-.367-.846-.759-.84m4.795 0c-.268.003-.548.153-.728.476-.228.408-.223.477.061.79.557.616 1.425.356 1.425-.425 0-.538-.367-.846-.758-.84m21.806 0a.85.85 0 0 0-.782.468c-.136.254-.115.43.087.738.485.74 1.423.44 1.423-.454 0-.469-.35-.74-.728-.752m-77.674 0c-.603-.007-1.03.684-.62 1.267.132.19.42.344.638.344.84 0 1.096-1.18.33-1.529a.9.9 0 0 0-.348-.081m15.443 0c-.603-.007-1.029.684-.62 1.267.132.19.42.344.638.344.84 0 1.096-1.18.33-1.529a.9.9 0 0 0-.348-.081m15.445 0c-.603-.007-1.029.684-.62 1.267.133.19.42.344.638.344.84 0 1.096-1.18.33-1.529a.9.9 0 0 0-.348-.081m38.282 2.098c-.233 0-.477.12-.66.382-.334.476-.327.56.073.962.562.562 1.331.289 1.331-.472 0-.545-.357-.874-.744-.872m8.487 0c-.233 0-.476.12-.66.382-.334.476-.327.56.073.962.563.562 1.332.289 1.332-.472 0-.545-.358-.874-.745-.872m37.71 0c-.233 0-.476.12-.66.382-.334.476-.327.56.074.962.562.562 1.33.289 1.33-.472 0-.545-.356-.874-.744-.872m-39.967.015a.68.68 0 0 0-.46.197c-.299.3-.243 1.09.092 1.285.48.28.85.203 1.138-.237.253-.385.253-.446 0-.832a.9.9 0 0 0-.77-.413m20.118 0a.68.68 0 0 0-.46.197c-.3.3-.243 1.09.091 1.285.481.28.85.203 1.139-.237.252-.385.252-.446 0-.832a.9.9 0 0 0-.77-.413m17.592 0a.68.68 0 0 0-.46.197c-.3.3-.243 1.09.092 1.285.48.28.85.203 1.139-.237.252-.385.252-.446 0-.832a.9.9 0 0 0-.771-.413m-115.3.004a.7.7 0 0 0-.505.192c-.28.28-.249.893.062 1.203.337.337.716.332 1.098-.014.378-.342.387-.689.03-1.084a.93.93 0 0 0-.686-.297m66.51.004q-.278.004-.566.263c-.378.342-.387.69-.03 1.085.152.167.409.304.571.304.698 0 1.056-.903.557-1.403q-.255-.251-.532-.249m6.964 0q-.278.004-.566.263c-.377.342-.387.69-.029 1.085.152.167.408.304.57.304.698 0 1.057-.903.558-1.403q-.255-.251-.532-.249m-11.356.008a.67.67 0 0 0-.399.154c-.336.28-.304 1.104.051 1.31.45.262.793.213 1.134-.164.284-.314.29-.383.062-.79-.19-.34-.532-.526-.848-.51m22.257 0a.67.67 0 0 0-.398.154c-.336.28-.304 1.104.05 1.31.45.262.794.213 1.134-.164.285-.314.29-.383.062-.79-.19-.34-.532-.526-.848-.51m-57.727.005c-.16 0-.286.069-.453.22-.441.4-.43.837.032 1.16.45.316.643.324 1.027.043.46-.336.297-1.153-.269-1.35a1 1 0 0 0-.337-.073m86.64.002a.8.8 0 0 0-.55.235c-.336.337-.331.716.015 1.098.494.546 1.388.206 1.388-.527 0-.489-.424-.814-.853-.806m-91.415 0a.8.8 0 0 0-.55.235c-.336.337-.331.716.015 1.098.494.546 1.388.206 1.388-.527 0-.489-.424-.814-.853-.805m22.3 0a.8.8 0 0 0-.548.235c-.337.337-.332.716.014 1.098.494.546 1.388.206 1.388-.527 0-.489-.424-.814-.853-.805m31.405 0a.8.8 0 0 0-.55.235c-.336.337-.331.716.015 1.098.494.546 1.388.206 1.388-.527 0-.489-.424-.814-.853-.805m-22.378.002a.8.8 0 0 0-.504.217c-.41.37-.43.71-.063 1.114.553.612 1.455.204 1.361-.616-.052-.458-.413-.733-.794-.715m4.804.004a.76.76 0 0 0-.514.212c-.41.371-.429.71-.063 1.115.341.377.78.39 1.137.034.56-.562.05-1.362-.56-1.361m48.345 0c-.602-.026-1.104.72-.574 1.305.357.394.794.418 1.147.065.354-.354.33-.79-.064-1.147a.8.8 0 0 0-.51-.223m20.252 0c-.603-.026-1.104.72-.573 1.305.356.394.793.418 1.146.065.354-.354.33-.79-.064-1.147a.8.8 0 0 0-.51-.223m-113.172 2.13a.81.81 0 0 0-.764.56c-.143.407-.112.518.223.79.214.173.44.315.5.315.318 0 .851-.567.851-.904 0-.5-.404-.771-.81-.76m84.414 0c-.406-.01-.81.26-.81.76 0 .213.166.503.37.646.467.327.51.325.98-.057.335-.27.367-.382.224-.79a.81.81 0 0 0-.764-.559m8.487 0c-.406-.01-.81.26-.81.76 0 .213.166.503.37.646.467.327.51.325.98-.057.336-.27.367-.382.224-.79a.81.81 0 0 0-.764-.559m20.252 0c-.406-.01-.81.26-.81.76 0 .213.166.503.37.646.467.327.51.325.98-.057.336-.27.367-.382.224-.79a.81.81 0 0 0-.764-.559m-68.625 0c-.762 0-1.034.77-.472 1.332.405.405.63.417 1.027.057.615-.556.282-1.388-.555-1.388m4.278 0c-.778 0-1.038.949-.386 1.406.203.142.428.259.499.259.201 0 .795-.643.795-.86 0-.333-.533-.804-.908-.804m4.209 0c-.762 0-1.034.77-.472 1.332.405.405.63.417 1.027.057.615-.556.282-1.388-.555-1.388m4.912 0c-.777 0-1.067.955-.444 1.457.142.114.341.208.444.208.262 0 .685-.366.796-.688.137-.397-.336-.976-.796-.976m13.137 0c-.778 0-1.039.949-.387 1.406.204.142.428.259.5.259.2 0 .795-.643.795-.86 0-.333-.533-.804-.908-.804m8.487 0c-.778 0-1.039.949-.386 1.406.203.142.428.259.499.259.2 0 .795-.643.795-.86 0-.333-.533-.804-.908-.804m26.626 0c-.762 0-1.034.77-.472 1.332.4.4.485.407.961.074.7-.49.382-1.405-.49-1.405m-92.79.009a.97.97 0 0 0-.698.253c-.35.35-.33.67.072 1.07.404.405.629.417 1.027.057.347-.314.398-.895.104-1.189-.122-.122-.308-.185-.505-.191m26.5 0a.97.97 0 0 0-.697.253c-.35.35-.33.67.071 1.07.405.405.63.417 1.027.057.348-.314.4-.895.105-1.189-.122-.122-.309-.185-.505-.191m-53.286.003a.7.7 0 0 0-.535.188c-.318.318-.232.924.17 1.206.204.142.429.259.5.259.22 0 .795-.653.795-.903 0-.42-.484-.737-.93-.75m104.176.015h-.059a.64.64 0 0 0-.27.078.854.854 0 0 0-.358 1.17c.204.382.977.465 1.302.14.488-.488-.015-1.365-.615-1.388m-82.312.057c-.481 0-.601.077-.741.475-.144.409-.113.52.222.79.214.174.448.316.52.316.07 0 .304-.142.518-.315.336-.272.367-.382.223-.79-.14-.4-.26-.476-.742-.476m22.301 0c-.481 0-.6.077-.741.475-.144.409-.113.52.222.79.215.174.448.316.52.316.07 0 .304-.142.518-.315.336-.272.367-.382.223-.79-.14-.4-.26-.476-.742-.476m-39.93 1.933c-.14 0-.263.1-.476.313-.4.401-.422.72-.072 1.07.31.31.923.342 1.203.062.318-.317.233-.923-.17-1.205-.223-.157-.36-.238-.484-.24m15.444 0c-.14 0-.264.1-.477.313-.401.401-.422.72-.072 1.07.31.31.923.342 1.203.062.318-.317.232-.923-.17-1.205-.223-.157-.36-.238-.484-.24m6.958 0c-.14 0-.265.1-.477.313-.401.401-.422.72-.072 1.07.31.31.923.342 1.203.062.318-.317.232-.923-.17-1.205-.223-.157-.36-.238-.484-.24m26.5 0c-.14 0-.264.1-.477.313-.4.401-.421.72-.07 1.07.31.31.922.342 1.202.062.318-.317.232-.923-.17-1.205-.223-.157-.36-.238-.484-.24m-50.958.017c-.276 0-.55.175-.712.53-.138.303-.123.474.067.745.136.194.425.352.644.352.56 0 .943-.587.714-1.091-.162-.356-.438-.535-.713-.536m15.443 0c-.276 0-.551.175-.712.53-.139.303-.123.474.067.745.136.194.425.352.644.352.559 0 .943-.587.714-1.091-.162-.356-.438-.535-.713-.536m68.972.004q-.09 0-.193.033c-.578.183-.773.702-.45 1.196.333.507 1.015.485 1.364-.044.211-.32.195-.403-.157-.812-.223-.26-.382-.37-.564-.373m15.46 0q-.09 0-.194.033c-.578.183-.773.702-.449 1.196.333.508 1.014.485 1.364-.044.21-.32.194-.403-.158-.812-.223-.26-.381-.37-.564-.373m15.444 0q-.09 0-.193.033c-.578.183-.773.702-.449 1.196.332.508 1.014.485 1.363-.044.211-.32.195-.403-.157-.812-.223-.26-.382-.37-.564-.373m-111.077.004c-.111 0-.223.064-.416.19-.445.292-.552.895-.216 1.23.257.258 1.007.258 1.265 0 .335-.335.23-.938-.217-1.23-.192-.126-.304-.19-.416-.19m37.744 0c-.111 0-.223.064-.416.19-.445.292-.552.895-.216 1.23.257.258 1.007.258 1.265 0 .335-.335.23-.938-.217-1.23-.192-.126-.304-.19-.416-.19m31.404 0c-.111 0-.223.064-.416.19-.446.292-.551.895-.216 1.23.316.317 1.097.243 1.292-.122.272-.508.205-.814-.244-1.108-.192-.126-.304-.19-.416-.19m55.202.006a.6.6 0 0 0-.192.03c-.554.175-.749.701-.435 1.18.285.434.89.536 1.222.204.471-.47.01-1.406-.595-1.414m-68.54.007a.7.7 0 0 0-.449.191c-.373.338-.436.913-.134 1.216.325.324 1.098.242 1.302-.14.33-.617-.174-1.283-.718-1.267m15.453 0a.7.7 0 0 0-.45.191c-.373.338-.436.913-.134 1.216.325.324 1.098.242 1.302-.14.33-.617-.174-1.283-.718-1.267m22.265 0a.7.7 0 0 0-.45.191c-.373.338-.436.913-.133 1.216.324.324 1.097.242 1.301-.14.33-.617-.174-1.283-.718-1.267m15.444 0a.7.7 0 0 0-.449.191c-.373.338-.436.913-.134 1.216.325.324 1.098.242 1.302-.14.33-.617-.174-1.283-.719-1.267m13.295 0a.7.7 0 0 0-.45.191c-.373.338-.436.913-.133 1.216.324.324 1.097.242 1.301-.14.33-.617-.174-1.283-.718-1.267m-128.616.02c-.596.03-1.093.9-.607 1.387.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m8.487 0c-.596.03-1.093.9-.607 1.387.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m6.956 0c-.596.03-1.093.9-.607 1.387.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m42.465 0a.63.63 0 0 0-.336.077.854.854 0 0 0-.359 1.17c.204.382.977.465 1.301.14.486-.486-.01-1.357-.606-1.388m8.487 0a.63.63 0 0 0-.336.077.854.854 0 0 0-.359 1.17c.205.382.977.465 1.302.14.485-.486-.01-1.357-.607-1.388m6.964 0a.63.63 0 0 0-.336.077.854.854 0 0 0-.358 1.17c.204.382.977.465 1.301.14.486-.486-.01-1.357-.607-1.388m15.294 0a.63.63 0 0 0-.337.077.854.854 0 0 0-.358 1.17c.204.382.977.465 1.301.14.486-.486-.01-1.357-.606-1.388m2.112 0c-.597.03-1.093.9-.607 1.387.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078m15.46 0c-.597.03-1.093.9-.607 1.387.11.11.406.2.66.2.832 0 1.04-1.105.282-1.51a.64.64 0 0 0-.336-.078m4.845 0a.63.63 0 0 0-.336.077.854.854 0 0 0-.359 1.17c.204.382.977.465 1.301.14.486-.486-.01-1.357-.606-1.388m2.112 0c-.597.03-1.093.9-.607 1.387.11.11.407.2.66.2.833 0 1.04-1.105.283-1.51a.64.64 0 0 0-.336-.078" transform="translate(-239.14 -337.688)"/></svg>
</div>
<h2>WiFi Settings</h2>
<label for="ssid">SSID</label>
<input type="text" id="ssid" name="ssid" required />
<label for="password">Password</label>
<div style="position: relative;">
<input type="password" id="password" name="password" required />
<label class="small">
<input type="checkbox" id="togglePassword" style="margin-right: 0.3rem;" />
Show Password
</label>
</div>
<h2>Weather Settings</h2>
<label for="openWeatherApiKey">OpenWeather API Key</label>
<input type="text" id="openWeatherApiKey" name="openWeatherApiKey" placeholder="ADD-YOUR-API-KEY-32-CHARACTERS"/>
<div class="small">Required to fetch weather data. <a href="https://home.openweathermap.org/users/sign_up" target="_blank">Get your API key here</a>.</div>
<label>Location</label>
<div class="form-row two-col">
<input type="text" id="openWeatherCity" name="openWeatherCity" placeholder="City / Zip / Lat."/>
<input type="text" id="openWeatherCountry" name="openWeatherCountry" placeholder="Country Code / Long."/>
</div>
<button type="button" class="primary-button" id="geo-button" onclick="getLocation()" style="margin-top: 1rem;">Get My Location</button>
<div class="small">
<strong>Location format examples:</strong> City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023
</div>
<div class="geo-note" style="display: none;">
<br>
<span class="small"><strong>Note:</strong> External links and the "Get My Location" button require internet access.</span>
<span class="small">They won't work while the device is in AP Mode - connect to Wi-Fi first.</span>
</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/Cairo">Africa/Cairo</option>
<option value="Africa/Casablanca">Africa/Casablanca</option>
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
<option value="America/Anchorage">America/Anchorage</option>
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
<option value="America/Chicago">America/Chicago</option>
<option value="America/Denver">America/Denver</option>
<option value="America/Guatemala">America/Guatemala</option>
<option value="America/Halifax">America/Halifax</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/St_Johns">America/St_Johns</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/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/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/Manila">Asia/Manila</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/Tokyo">Asia/Tokyo</option>
<option value="Asia/Ulaanbaatar">Asia/Ulaanbaatar</option>
<option value="Asia/Yekaterinburg">Asia/Yekaterinburg</option>
<option value="Atlantic/Azores">Atlantic/Azores</option>
<option value="Atlantic/Reykjavik">Atlantic/Reykjavik</option>
<option value="Australia/Adelaide">Australia/Adelaide</option>
<option value="Australia/Brisbane">Australia/Brisbane</option>
<option value="Australia/Darwin">Australia/Darwin</option>
<option value="Australia/Hobart">Australia/Hobart</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/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/Rome">Europe/Rome</option>
<option value="Europe/Stockholm">Europe/Stockholm</option>
<option value="Europe/Warsaw">Europe/Warsaw</option>
<option value="Pacific/Auckland">Pacific/Auckland</option>
<option value="Pacific/Chatham">Pacific/Chatham</option>
<option value="Pacific/Fiji">Pacific/Fiji</option>
<option value="Pacific/Guam">Pacific/Guam</option>
<option value="Pacific/Honolulu">Pacific/Honolulu</option>
<option value="Pacific/Port_Moresby">Pacific/Port_Moresby</option>
<option value="Pacific/Tahiti">Pacific/Tahiti</option>
<option value="UTC">UTC</option>
<option value="Etc/GMT+1">Etc/GMT+1</option>
<option value="Etc/GMT-1">Etc/GMT-1</option>
</select>
<label for="language">Language (Day & Weather)</label>
<select id="language" name="language" onchange="setLanguage(this.value)">
<option value="" disabled selected>Select language</option>
<option value="af">Afrikaans</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl">Dutch</option>
<option value="en">English</option>
<option value="eo">Esperanto</option>
<option value="et">Estonian</option>
<option value="fi">Finnish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="hu">Hungarian</option>
<option value="it">Italian</option>
<option value="ga">Irish</option>
<option value="ja">Japanese</option>
<option value="lv">Latvian</option>
<option value="lt">Lithuanian</option>
<option value="no">Norwegian</option>
<option value="pl">Polish</option>
<option value="pt">Portuguese</option>
<option value="ro">Romanian</option>
<option value="sr">Serbian</option>
<option value="sk">Slovak</option>
<option value="sl">Slovenian</option>
<option value="es">Spanish</option>
<option value="sv">Swedish</option>
<option value="sw">Swahili</option>
<option value="tr">Turkish</option>
</select>
<div class="form-row two-col">
<div>
<label for="clockDuration">Clock Duration</label>
<input type="number" id="clockDuration" name="clockDuration" min="1" required />
<label class="small">(Seconds)</label>
</div>
<div>
<label for="weatherDuration">Weather Duration</label>
<input type="number" id="weatherDuration" name="weatherDuration" min="1" required />
<label class="small">(Seconds)</label>
</div>
</div>
<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.344-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">
<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">
<div class="toggles" style="padding: 0 1rem;">
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Show Day of Week:</span>
<span class="toggle-switch">
<input type="checkbox" id="showDayOfWeek" name="showDayOfWeek" onchange="setShowDayOfWeek(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Animated Seconds:</span>
<span class="toggle-switch">
<input type="checkbox" id="colonBlinkEnabled" name="colonBlinkEnabled" onchange="setColonBlink(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Show Date:</span>
<span class="toggle-switch">
<input type="checkbox" id="showDate" name="showDate" onchange="setShowDate(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Display 12-hour Clock:</span>
<span class="toggle-switch">
<input type="checkbox" id="twelveHourToggle" name="twelveHourToggle" onchange="setTwelveHour(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Use Imperial Units (°F):</span>
<span class="toggle-switch">
<input type="checkbox" id="weatherUnits" name="weatherUnits" onchange="setWeatherUnits(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Show Humidity:</span>
<span class="toggle-switch">
<input type="checkbox" id="showHumidity" name="showHumidity" onchange="setShowHumidity(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Show Weather Description:</span>
<span class="toggle-switch">
<input type="checkbox" id="showWeatherDescription" name="showWeatherDescription" onchange="setShowWeatherDescription(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="display: flex; align-items: center; margin-top: 1.75rem; justify-content: space-between;">
<span style="margin-right: 0.5em;">Flip Display (180°):</span>
<span class="toggle-switch">
<input type="checkbox" id="flipDisplay" name="flipDisplay" onchange="setFlipDisplay(this.checked)">
<span class="toggle-slider"></span>
</span>
</label>
<label style="margin-top: 1.75rem;">Brightness: <span id="brightnessValue">10</span></label>
<input style="width: 100%;" type="range" min="0" max="15" name="brightness" id="brightnessSlider" value="10"
oninput="brightnessValue.textContent = brightnessSlider.value; setBrightnessLive(this.value);">
<br><br><br>
<label style="display: flex; align-items: center; justify-content: space-between;">
<span style="margin-right: 0.5em;">Enable Dimming:</span>
<span class="toggle-switch">
<input type="checkbox" id="dimmingEnabled" name="dimmingEnabled">
<span class="toggle-slider"></span>
</span>
</label>
<div class="form-row two-col">
<div>
<label for="dimStartTime">Dimming Start Time:</label>
<input type="time" id="dimStartTime" value="18:00">
</div>
<div>
<label for="dimEndTime">Dimming End Time:</label>
<input type="time" id="dimEndTime" value="08:00">
</div>
</div>
<label style="margin-top: 1.75rem;" for="dimBrightness">Dimming Brightness: <span id="dimmingBrightnessValue">2</span></label>
<input style="width: 100%;" type="range" min="0" max="15" name="dimming_brightness" id="dimBrightness" value="2"
oninput="dimmingBrightnessValue.textContent = dimBrightness.value; ">
<br><br><br>
<div class="form-group">
<label style="display: flex; align-items: center; justify-content: space-between;">
<span style="margin-right: 0.5em;">Enable Countdown:</span>
<span class="toggle-switch">
<input type="checkbox" id="countdownEnabled" name="countdownEnabled">
<span class="toggle-slider"></span>
</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>
<input type="date" id="countdownDate" name="countdownDate" class="form-control">
</div>
<div class="form-group">
<label for="countdownTime">Countdown Time:</label>
<input type="time" id="countdownTime" name="countdownTime" class="form-control">
</div>
</div>
<div class="form-group">
<label for="countdownLabel">Countdown Label (Optional):</label>
<input type="text" id="countdownLabel" name="countdownLabel" class="form-control"
placeholder="e.g., BIRTHDAY, VACATION" maxlength="24"
pattern="[A-Z0-9 :!'.\-]*"
title="Only uppercase letters, numbers, space, and : ! ' - . allowed">
<div class="small">Only uppercase letters, numbers, space, and : ! ' - . allowed</div>
</div>
</div>
</div>
<input type="submit" class="primary-button" value="Save Settings">
</form>
<div class="footer">
Project by: <a href="https://www.instagram.com/mfactory.osaka" target="_blank" rel="noopener noreferrer">M-Factory</a><br>
</div>
<div id="savingMessage"></div>
<script>
let isSaving = false;
let isAPMode = false;
// Set initial value display for brightness
document.addEventListener('DOMContentLoaded', function() {
brightnessValue.textContent = brightnessSlider.value;
});
// 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";
});
});
window.onbeforeunload = function () {
if (isSaving) {
return "Settings are being saved. Leaving now may interrupt the process.";
}
};
window.onload = function () {
fetch('/config.json')
.then(response => response.json())
.then(data => {
isAPMode = (data.mode === "ap");
if (isAPMode) {
document.querySelector('.geo-note').style.display = 'block';
document.getElementById('geo-button').classList.add('geo-disabled');
document.getElementById('geo-button').disabled = true;
}
document.getElementById('ssid').value = data.ssid || '';
document.getElementById('password').value = data.password || '';
document.getElementById('openWeatherApiKey').value = data.openWeatherApiKey || '';
document.getElementById('openWeatherCity').value = data.openWeatherCity || '';
document.getElementById('openWeatherCountry').value = data.openWeatherCountry || '';
document.getElementById('weatherUnits').checked = (data.weatherUnits === "imperial");
document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000;
document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000;
document.getElementById('language').value = data.language || '';
// 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;
document.getElementById('showDayOfWeek').checked = !!data.showDayOfWeek;
document.getElementById('showDate').checked = !!data.showDate;
document.getElementById('showHumidity').checked = !!data.showHumidity;
document.getElementById('colonBlinkEnabled').checked = !!data.colonBlinkEnabled;
document.getElementById('showWeatherDescription').checked = !!data.showWeatherDescription;
// Dimming controls
const dimmingEnabledEl = document.getElementById('dimmingEnabled');
const isDimming = (data.dimmingEnabled === true || data.dimmingEnabled === "true" || data.dimmingEnabled === 1);
dimmingEnabledEl.checked = isDimming;
// Defer field enabling until checkbox state is rendered
setTimeout(() => {
setDimmingFieldsEnabled(dimmingEnabledEl.checked);
}, 0);
dimmingEnabledEl.addEventListener('change', function () {
setDimmingFieldsEnabled(this.checked);
});
document.getElementById('dimStartTime').value =
(data.dimStartHour !== undefined ? String(data.dimStartHour).padStart(2, '0') : "18") + ":" +
(data.dimStartMinute !== undefined ? String(data.dimStartMinute).padStart(2, '0') : "00");
document.getElementById('dimEndTime').value =
(data.dimEndHour !== undefined ? String(data.dimEndHour).padStart(2, '0') : "08") + ":" +
(data.dimEndMinute !== undefined ? String(data.dimEndMinute).padStart(2, '0') : "00");
document.getElementById('dimBrightness').value = (data.dimBrightness !== undefined ? data.dimBrightness : 2);
// Then update the span's text content with that value
document.getElementById('dimmingBrightnessValue').textContent = document.getElementById('dimBrightness').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);
if (data.countdown && data.countdown.targetTimestamp) {
// Convert Unix timestamp (seconds) to milliseconds for JavaScript Date object
const targetDate = new Date(data.countdown.targetTimestamp * 1000);
const year = targetDate.getFullYear();
// Month is 0-indexed in JS, so add 1
const month = (targetDate.getMonth() + 1).toString().padStart(2, '0');
const day = targetDate.getDate().toString().padStart(2, '0');
const hours = targetDate.getHours().toString().padStart(2, '0');
const minutes = targetDate.getMinutes().toString().padStart(2, '0');
document.getElementById('countdownDate').value = `${year}-${month}-${day}`;
document.getElementById('countdownTime').value = `${hours}:${minutes}`;
} else {
// Clear fields if no target timestamp is set
document.getElementById('countdownDate').value = '';
document.getElementById('countdownTime').value = '';
}
// --- END NEW ---
// --- NEW: Countdown Label Input Validation ---
const countdownLabelInput = document.getElementById('countdownLabel');
countdownLabelInput.addEventListener('input', function() {
// Convert to uppercase and remove any characters that are not A-Z or space
// Note: The `maxlength` attribute in HTML handles the length limit.
this.value = this.value.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, '');
});
// Set initial value for countdownLabel from config.json (apply validation on load too)
if (data.countdown && data.countdown.label) {
countdownLabelInput.value = data.countdown.label.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, '');
} else {
countdownLabelInput.value = '';
}
// --- END NEW ---
// --- NEW: Countdown Toggle Event Listener & Field Enabling ---
// If you're using onchange="setCountdownEnabled(this.checked)" directly in HTML,
// you would add setCountdownFieldsEnabled(this.checked) there as well.
// If you are using addEventListener, keep this:
countdownEnabledEl.addEventListener('change', function() {
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 ---
// Auto-detect browser's timezone if not set in config
if (!data.timeZone) {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (
tz &&
document.getElementById('timeZone').querySelector(`[value="${tz}"]`)
) {
document.getElementById('timeZone').value = tz;
} else {
document.getElementById('timeZone').value = '';
}
} catch (e) {
document.getElementById('timeZone').value = '';
}
} else {
document.getElementById('timeZone').value = data.timeZone;
}
})
.catch(err => {
console.error('Failed to load config:', err);
showSavingModal("");
updateSavingModal("⚠️ Failed to load configuration.", false);
// Show appropriate button for load error
removeReloadButton();
removeRestoreButton();
const errorMsg = (err.message || "").toLowerCase();
if (
errorMsg.includes("config corrupted") ||
errorMsg.includes("failed to write config") ||
errorMsg.includes("restore")
) {
ensureRestoreButton();
} else {
ensureReloadButton();
}
});
document.querySelector('html').style.height = 'unset';
document.body.classList.add('loaded');
};
async function submitConfig(event) {
event.preventDefault();
isSaving = true;
const form = document.getElementById('configForm');
const formData = new FormData(form);
const clockDuration = parseInt(formData.get('clockDuration')) * 1000;
const weatherDuration = parseInt(formData.get('weatherDuration')) * 1000;
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' : '');
formData.set('showDayOfWeek', document.getElementById('showDayOfWeek').checked ? 'on' : '');
formData.set('showDate', document.getElementById('showDate').checked ? 'on' : '');
formData.set('showHumidity', document.getElementById('showHumidity').checked ? 'on' : '');
formData.set('colonBlinkEnabled', document.getElementById('colonBlinkEnabled').checked ? 'on' : '');
//dimming
formData.set('dimmingEnabled', document.getElementById('dimmingEnabled').checked ? 'true' : 'false');
const dimStart = document.getElementById('dimStartTime').value; // "18:45"
const dimEnd = document.getElementById('dimEndTime').value; // "08:30"
// Parse hour and minute
if (dimStart) {
const [startHour, startMin] = dimStart.split(":").map(x => parseInt(x, 10));
formData.set('dimStartHour', startHour);
formData.set('dimStartMinute', startMin);
}
if (dimEnd) {
const [endHour, endMin] = dimEnd.split(":").map(x => parseInt(x, 10));
formData.set('dimEndHour', endHour);
formData.set('dimEndMinute', endMin);
}
formData.set('dimBrightness', document.getElementById('dimBrightness').value);
formData.set('showWeatherDescription', document.getElementById('showWeatherDescription').checked ? 'on' : '');
formData.set('weatherUnits', document.getElementById('weatherUnits').checked ? 'imperial' : 'metric');
// --- 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
const finalCountdownLabel = document.getElementById('countdownLabel').value.toUpperCase().replace(/[^A-Z0-9 :!'.\-]/g, '');
formData.set('countdownLabel', finalCountdownLabel);
// --- END NEW ---
const params = new URLSearchParams();
for (const pair of formData.entries()) {
params.append(pair[0], pair[1]);
}
showSavingModal("Saving...");
// Check AP mode status
let isAPMode = false;
try {
const apStatusResponse = await fetch('/ap_status');
const apStatusData = await apStatusResponse.json();
isAPMode = apStatusData.isAP;
} catch (error) {
console.error("Error fetching AP status:", error);
// Handle error appropriately (e.g., assume not in AP mode)
}
fetch('/save', {
method: 'POST',
body: params
})
.then(response => {
if (!response.ok) {
return response.json().then(json => {
throw new Error(`Server error ${response.status}: ${json.error}`);
});
}
return response.json();
})
.then(json => {
isSaving = false;
removeReloadButton();
removeRestoreButton();
if (isAPMode) {
updateSavingModal("✅ Settings saved successfully!<br><br>Rebooting the device now... ", false);
setTimeout(() => {
document.getElementById('configForm').style.display = 'none';
document.querySelector('.footer').style.display = 'none';
document.querySelector('html').style.height = '100vh';
document.body.style.height = '100vh';
document.getElementById('configForm').style.display = 'none';
updateSavingModal("✅ All done!<br>You can now close this tab safely.<br><br>Your device is now rebooting and connecting to your Wi-Fi. Its new IP address will appear on the display for future access.", false);
}, 5000);
return;
} else {
updateSavingModal("✅ Configuration saved successfully.<br><br>Device will reboot", false);
setTimeout(() => location.href = location.href.split('#')[0], 3000);
}
})
.catch(err => {
isSaving = false;
if (isAPMode && err.message.includes("Failed to fetch")) {
console.warn("Expected disconnect in AP mode after reboot.");
showSavingModal("");
updateSavingModal("✅ Settings saved successfully!<br><br>Rebooting the device now... ", false);
setTimeout(() => {
document.getElementById('configForm').style.display = 'none';
updateSavingModal("✅ All done!<br>You can now close this tab safely.<br><br>Your device is now rebooting and connecting to your Wi-Fi. Its new IP address will appear on the display for future access.", false);
}, 5000);
removeReloadButton();
removeRestoreButton();
return;
}
console.error('Save error:', err);
let friendlyMessage = "⚠️ Something went wrong while saving the configuration.";
if (err.message.includes("Failed to fetch")) {
friendlyMessage = "⚠️ Cannot connect to the device.<br>Is it powered on and connected?";
}
updateSavingModal(`${friendlyMessage}<br><br>Details: ${err.message}`, false);
// Show only one action button, based on error content
removeReloadButton();
removeRestoreButton();
const errorMsg = (err.message || "").toLowerCase();
if (
errorMsg.includes("config corrupted") ||
errorMsg.includes("failed to write config") ||
errorMsg.includes("restore")
) {
ensureRestoreButton();
} else {
ensureReloadButton();
}
});
}
function showSavingModal(message) {
let modal = document.getElementById('savingModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'savingModal';
modal.innerHTML = `
<div id="savingModalContent">
<div class="spinner"></div>
<div id="savingModalText">${message}</div>
</div>
`;
document.body.appendChild(modal);
} else {
document.getElementById('savingModalText').innerHTML = message;
document.querySelector('#savingModal .spinner').style.display = 'block';
}
modal.style.display = 'flex';
document.body.classList.add('modal-open');
}
function updateSavingModal(message, showSpinner = false) {
let modalText = document.getElementById('savingModalText');
modalText.innerHTML = message;
document.querySelector('#savingModal .spinner').style.display = showSpinner ? 'block' : 'none';
// Remove reload/restore buttons if no longer needed
if (message.includes("saved successfully") || message.includes("Backup restored") || message.includes("All done!")) {
removeReloadButton();
removeRestoreButton();
}
}
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();
removeRestoreButton();
fetch('/restore', { method: 'POST' })
.then(response => {
if (!response.ok) {
throw new Error("Server returned an error");
}
return response.json();
})
.then(data => {
updateSavingModal("✅ Backup restored! Device will now reboot.");
setTimeout(() => location.reload(), 1500);
})
.catch(err => {
console.error("Restore error:", err);
updateSavingModal(`❌ Failed to restore backup: ${err.message}`, false);
// Show only one button, for backup restore failures show reload.
removeReloadButton();
removeRestoreButton();
ensureReloadButton();
});
}
function hideSavingModal() {
const modal = document.getElementById('savingModal');
if (modal) {
modal.style.display = 'none';
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';
}
let brightnessDebounceTimeout = null;
function setBrightnessLive(val) {
// Cancel the previous timeout if it exists
if (brightnessDebounceTimeout) {
clearTimeout(brightnessDebounceTimeout);
}
// Set a new timeout
brightnessDebounceTimeout = setTimeout(() => {
fetch('/set_brightness', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + encodeURIComponent(val)
})
.then(res => res.json())
.catch(e => {}); // Optionally handle errors
}, 150); // 150ms debounce, adjust as needed
}
function setFlipDisplay(val) {
fetch('/set_flip', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setTwelveHour(val) {
fetch('/set_twelvehour', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setShowDayOfWeek(val) {
fetch('/set_dayofweek', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setShowDate(val) {
fetch('/set_showdate', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setColonBlink(val) {
fetch('/set_colon_blink', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setShowHumidity(val) {
fetch('/set_humidity', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setLanguage(val) {
fetch('/set_language', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + encodeURIComponent(val)
});
}
function setShowWeatherDescription(val) {
fetch('/set_weatherdesc', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
function setWeatherUnits(val) {
fetch('/set_units', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "value=" + (val ? 1 : 0)
});
}
// --- Countdown Controls Logic ---
// NEW: Function to enable/disable countdown specific fields
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
function setCountdownEnabled(val) {
fetch('/set_countdown_enabled', {
method: 'POST',
headers: { "Content-Type": "application/x-www-form-urlencoded" },
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 ---
// --- Dimming Controls Logic ---
function setDimmingFieldsEnabled(enabled) {
document.getElementById('dimStartTime').disabled = !enabled;
document.getElementById('dimEndTime').disabled = !enabled;
document.getElementById('dimBrightness').disabled = !enabled;
}
function getLocation() {
fetch('http://ip-api.com/json/')
.then(response => response.json())
.then(data => {
// Update your latitude/longitude fields
document.getElementById('openWeatherCity').value = data.lat;
document.getElementById('openWeatherCountry').value = data.lon;
// Determine the label to show on the button
const button = document.getElementById('geo-button');
let label = data.city;
if (!label) label = data.regionName;
if (!label) label = data.country;
if (!label) label = "Location Found";
button.textContent = "Location: " + label;
button.disabled = true;
button.classList.add('geo-disabled');
console.log("Location fetched via ip-api.com. Free service: http://ip-api.com/");
})
.catch(error => {
alert(
"Failed to guess your location.\n\n" +
"This may happen if:\n" +
"- You are using an AdBlocker\n" +
"- There is a network issue\n" +
"- The service might be temporarily down.\n\n" +
"You can visit https://openweathermap.org/find to manually search for your city and get latitude/longitude."
);
});
}
</script>
</body>
</html>