mirror of
https://github.com/mfactory-osaka/ESPTimeCast.git
synced 2026-04-03 03:00:24 -04:00
Captive portal will now open automatically in most devices. Some user were concerned that their API key was visible so it is now obfuscated. Pinout has been updated to better fit all the Wemos boards.
1339 lines
69 KiB
HTML
1339 lines
69 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="ru">Russian</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="-1" max="15" name="brightness" id="brightnessSlider" value="10"
|
||
oninput="brightnessValue.textContent = (this.value == -1 ? 'Off' : this.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="-1" max="15" name="dimming_brightness" id="dimBrightness" value="2"
|
||
oninput="dimmingBrightnessValue.textContent = (this.value == -1 ? 'Off' : this.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 () {
|
||
if (this.checked) {
|
||
// Show password as text
|
||
passwordInput.type = "text";
|
||
|
||
// Only clear if it's the masked placeholder
|
||
if (passwordInput.value === "********") {
|
||
passwordInput.value = "";
|
||
passwordInput.placeholder = "Enter new password";
|
||
}
|
||
} else {
|
||
// Hide password as dots
|
||
passwordInput.type = "password";
|
||
|
||
// Remove placeholder only if it was set by show-password toggle
|
||
if (passwordInput.placeholder === "Enter new password") {
|
||
passwordInput.placeholder = "";
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
|
||
|
||
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 || '';
|
||
const apiInput = document.getElementById('openWeatherApiKey');
|
||
if (data.openWeatherApiKey && data.openWeatherApiKey.trim() !== '') {
|
||
apiInput.value = MASK;
|
||
hasSavedKey = true; // mark it as having a saved key
|
||
} else {
|
||
apiInput.value = '';
|
||
hasSavedKey = false;
|
||
}
|
||
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 == -1 ? 'Off' : 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 == -1 ? 'Off' : document.getElementById('dimBrightness').value);
|
||
setDimmingFieldsEnabled(!!data.dimmingEnabled);
|
||
// --- 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 = '';
|
||
}
|
||
// 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 = '';
|
||
}
|
||
// 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);
|
||
// 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);
|
||
|
||
let apiKeyToSend = apiInput.value;
|
||
|
||
// If the user left the masked key untouched, skip sending it
|
||
if (apiKeyToSend === MASK && hasSavedKey) {
|
||
formData.delete('openWeatherApiKey');
|
||
} else {
|
||
formData.set('openWeatherApiKey', apiKeyToSend);
|
||
}
|
||
|
||
// 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]);
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
if (isAPMode) {
|
||
showSavingModal("");
|
||
updateSavingModal(
|
||
"✅ Settings saved successfully!<br><br>" +
|
||
"Rebooting the device now...<br><br>" +
|
||
"Your device will connect to your Wi-Fi.<br>" +
|
||
"Its new IP address will appear on the display for future access.",
|
||
true // show spinner
|
||
);
|
||
} else{
|
||
showSavingModal("");
|
||
};
|
||
|
||
await new Promise(resolve => setTimeout(resolve, isAPMode ? 5000 : 0));
|
||
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) {
|
||
setTimeout(() => {
|
||
document.getElementById('configForm').style.display = 'none';
|
||
document.querySelector('.footer').style.display = 'none';
|
||
document.querySelector('html').style.height = '100vh';
|
||
document.body.style.height = '100vh';
|
||
updateSavingModal(
|
||
"✅ All done!<br>You can now close this tab safely.<br><br>" +
|
||
"Your device has rebooted and is now connected to your Wi-Fi.<br>" +
|
||
"Check the display for the current IP address.",
|
||
false // stop spinner
|
||
);
|
||
}, 5000);
|
||
return;
|
||
} else {
|
||
showSavingModal("");
|
||
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 has rebooted and is now connected to your Wi-Fi.<br>" +
|
||
"Check the display for the current IP address.", 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."
|
||
);
|
||
});
|
||
}
|
||
|
||
|
||
// --- OpenWeather API Key field UX ---
|
||
const MASK_LENGTH = 32;
|
||
const MASK = '*'.repeat(MASK_LENGTH);
|
||
const apiInput = document.getElementById('openWeatherApiKey');
|
||
let hasSavedKey = false;
|
||
|
||
// --- Initialize the field after config load ---
|
||
if (apiInput.value && apiInput.value.trim() !== '') {
|
||
apiInput.value = MASK; // show mask
|
||
hasSavedKey = true;
|
||
} else {
|
||
apiInput.value = '';
|
||
hasSavedKey = false;
|
||
}
|
||
|
||
// --- Detect user clearing intent ---
|
||
apiInput.addEventListener('input', () => {
|
||
apiInput.dataset.clearing = apiInput.value === '' ? 'true' : 'false';
|
||
});
|
||
|
||
// --- Handle Delete/Backspace when focused but empty ---
|
||
apiInput.addEventListener('keydown', (e) => {
|
||
if ((e.key === 'Backspace' || e.key === 'Delete') && apiInput.value === '') {
|
||
apiInput.dataset.clearing = 'true';
|
||
}
|
||
});
|
||
|
||
// --- Focus handler: clear mask for editing ---
|
||
apiInput.addEventListener('focus', () => {
|
||
if (apiInput.value === MASK) apiInput.value = '';
|
||
});
|
||
|
||
// --- Blur handler: restore mask if user didn’t clear the field ---
|
||
apiInput.addEventListener('blur', () => {
|
||
if (apiInput.value === '') {
|
||
if (hasSavedKey && apiInput.dataset.clearing !== 'true') {
|
||
apiInput.value = MASK; // remask
|
||
} else {
|
||
hasSavedKey = false; // user cleared the key
|
||
apiInput.dataset.clearing = 'false';
|
||
apiInput.value = ''; // leave blank
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |