mirror of
https://github.com/mfactory-osaka/ESPTimeCast.git
synced 2026-02-19 11:54:56 -05:00
v2
Added support for DST New Web UI Various fixes in main .ino
This commit is contained in:
906
ESPTimeCast.ino
906
ESPTimeCast.ino
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
assets/webui2.png
Normal file
BIN
assets/webui2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
@@ -2,10 +2,10 @@
|
||||
"ssid": "",
|
||||
"password": "",
|
||||
"openWeatherApiKey": "ADD-YOUR-API-KEY-32-CHARACTERS",
|
||||
"openWeatherCity": "Osaka",
|
||||
"openWeatherCountry": "JP",
|
||||
"openWeatherCity": "",
|
||||
"openWeatherCountry": "",
|
||||
"clockDuration": "10000",
|
||||
"weatherDuration": "5000",
|
||||
"utcOffsetInSeconds": 32400,
|
||||
"timeZone": "",
|
||||
"weatherUnits": "metric"
|
||||
}
|
||||
|
||||
435
data/index.html
435
data/index.html
@@ -6,14 +6,29 @@
|
||||
<title>ESPTimeCast Settings</title>
|
||||
<style>
|
||||
* { 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;
|
||||
background-color: #121212; color: #FFFFFF;
|
||||
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: 0;
|
||||
}
|
||||
|
||||
body.loaded {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
@@ -24,20 +39,30 @@
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h2:first-of-type{
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.logo svg{
|
||||
filter: drop-shadow(0px 0px 0.5rem gray);
|
||||
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-color: #1e1e1e; padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.4);
|
||||
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;
|
||||
color: #bbb;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
display: block;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@@ -45,8 +70,10 @@
|
||||
input[type="password"],
|
||||
input[type="number"], select {
|
||||
width: 100%; padding: 0.75rem;
|
||||
border: none; border-radius: 8px;
|
||||
background-color: #2c2c2c; color: #ffffff;
|
||||
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="submit"] {
|
||||
@@ -69,7 +96,8 @@
|
||||
flex: 1;
|
||||
}
|
||||
.primary-button {
|
||||
background: linear-gradient(135deg, #007aff, #005ecb); color: white;
|
||||
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;
|
||||
@@ -85,22 +113,24 @@
|
||||
.note {
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#savingModal {
|
||||
backdrop-filter: blur(5px);
|
||||
position: fixed; top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background-color: rgba(18, 18, 18, 0.85);
|
||||
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 {
|
||||
margin: 1.5rem; background-color: #1e1e1e;
|
||||
padding: 2rem 2.5rem; border-radius: 12px;
|
||||
text-align: center; color: white;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
|
||||
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);
|
||||
@@ -113,12 +143,23 @@
|
||||
.footer{
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
a{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.small{
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
select option {
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
@@ -142,7 +183,7 @@
|
||||
<label for="password">Password</label>
|
||||
<div style="position: relative;">
|
||||
<input type="password" id="password" name="password" required />
|
||||
<label style="display: block; font-size: 0.8rem; color: #aaa; margin-top: 0.25rem;">
|
||||
<label class="small">
|
||||
<input type="checkbox" id="togglePassword" style="margin-right: 0.3rem;" />
|
||||
Show Password
|
||||
</label>
|
||||
@@ -166,49 +207,102 @@
|
||||
<option value="imperial">Imperial (°F)</option>
|
||||
<option value="standard">Standard (K)</option>
|
||||
</select>
|
||||
<div class="small">
|
||||
Consult the <a href="https://openweathermap.org/price" target="_blank" rel="noopener">OpenWeatherMap</a>
|
||||
documendation for info about getting your API key, city, and country code.
|
||||
</div>
|
||||
|
||||
<h2>Clock Settings</h2>
|
||||
<label for="utcOffsetInHours">UTC Offset (default +09:00 for Japan)</label>
|
||||
<select id="utcOffsetInHours" name="utcOffsetInHours" required>
|
||||
<option value="-12">−12:00 United States (Baker Island)</option>
|
||||
<option value="-11">−11:00 American Samoa, Niue</option>
|
||||
<option value="-10">−10:00 United States (Hawaii), French Polynesia</option>
|
||||
<option value="-9.5">−09:30 French Polynesia (Marquesas Islands)</option>
|
||||
<option value="-9">−09:00 United States (Alaska)</option>
|
||||
<option value="-8">−08:00 United States (California), Canada (British Columbia)</option>
|
||||
<option value="-7">−07:00 United States (Arizona), Mexico</option>
|
||||
<option value="-6">−06:00 United States (Central), Guatemala, Honduras</option>
|
||||
<option value="-5">−05:00 United States (Eastern), Colombia, Peru, Cuba</option>
|
||||
<option value="-4.5">−04:30 Venezuela</option>
|
||||
<option value="-4">−04:00 Bolivia, Paraguay, Canada (Atlantic)</option>
|
||||
<option value="-3.5">−03:30 Canada (Newfoundland)</option>
|
||||
<option value="-3">−03:00 Argentina, Brazil (East), Uruguay</option>
|
||||
<option value="-2">−02:00 South Georgia & South Sandwich Islands</option>
|
||||
<option value="-1">−01:00 Cape Verde, Azores (Portugal)</option>
|
||||
<option value="0">±00:00 United Kingdom, Ireland, Portugal</option>
|
||||
<option value="1">+01:00 Germany, France, Spain, Nigeria</option>
|
||||
<option value="2">+02:00 South Africa, Egypt, Greece</option>
|
||||
<option value="3">+03:00 Russia (West), Saudi Arabia, Kenya</option>
|
||||
<option value="3.5">+03:30 Iran</option>
|
||||
<option value="4">+04:00 United Arab Emirates, Azerbaijan</option>
|
||||
<option value="4.5">+04:30 Afghanistan</option>
|
||||
<option value="5">+05:00 Pakistan, Uzbekistan</option>
|
||||
<option value="5.5">+05:30 India, Sri Lanka</option>
|
||||
<option value="5.75">+05:45 Nepal</option>
|
||||
<option value="6">+06:00 Bangladesh, Kazakhstan (East)</option>
|
||||
<option value="6.5">+06:30 Myanmar</option>
|
||||
<option value="7">+07:00 Thailand, Vietnam, Indonesia (West)</option>
|
||||
<option value="8">+08:00 China, Malaysia, Singapore, Australia (West)</option>
|
||||
<option value="8.75">+08:45 Australia (Eucla)</option>
|
||||
<option value="9">+09:00 Japan, South Korea</option>
|
||||
<option value="9.5">+09:30 Australia (Northern Territory, South Australia)</option>
|
||||
<option value="10">+10:00 Australia (Queensland, New South Wales), Papua New Guinea</option>
|
||||
<option value="10.5">+10:30 Australia (Lord Howe Island)</option>
|
||||
<option value="11">+11:00 Solomon Islands, New Caledonia</option>
|
||||
<option value="12">+12:00 New Zealand, Fiji</option>
|
||||
<option value="12.75">+12:45 New Zealand (Chatham Islands)</option>
|
||||
<option value="13">+13:00 Tonga, Samoa</option>
|
||||
<option value="14">+14:00 Kiribati (Line Islands)</option>
|
||||
<label for="timeZone">Time Zone</label>
|
||||
<select id="timeZone" name="timeZone" required>
|
||||
<option value="" disabled selected>Select your time zone</option>
|
||||
<option value="Africa/Algiers">Africa/Algiers</option>
|
||||
<option value="Africa/Cairo">Africa/Cairo</option>
|
||||
<option value="Africa/Casablanca">Africa/Casablanca</option>
|
||||
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
|
||||
<option value="Africa/Lagos">Africa/Lagos</option>
|
||||
<option value="Africa/Nairobi">Africa/Nairobi</option>
|
||||
<option value="America/Anchorage">America/Anchorage</option>
|
||||
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
|
||||
<option value="America/Bogota">America/Bogota</option>
|
||||
<option value="America/Caracas">America/Caracas</option>
|
||||
<option value="America/Chicago">America/Chicago</option>
|
||||
<option value="America/Denver">America/Denver</option>
|
||||
<option value="America/Lima">America/Lima</option>
|
||||
<option value="America/Los_Angeles">America/Los_Angeles</option>
|
||||
<option value="America/Mexico_City">America/Mexico_City</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="America/Phoenix">America/Phoenix</option>
|
||||
<option value="America/Santiago">America/Santiago</option>
|
||||
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
|
||||
<option value="America/Toronto">America/Toronto</option>
|
||||
<option value="America/Vancouver">America/Vancouver</option>
|
||||
<option value="Asia/Almaty">Asia/Almaty</option>
|
||||
<option value="Asia/Amman">Asia/Amman</option>
|
||||
<option value="Asia/Baghdad">Asia/Baghdad</option>
|
||||
<option value="Asia/Baku">Asia/Baku</option>
|
||||
<option value="Asia/Bangkok">Asia/Bangkok</option>
|
||||
<option value="Asia/Beirut">Asia/Beirut</option>
|
||||
<option value="Asia/Dhaka">Asia/Dhaka</option>
|
||||
<option value="Asia/Dubai">Asia/Dubai</option>
|
||||
<option value="Asia/Ho_Chi_Minh">Asia/Ho_Chi_Minh</option>
|
||||
<option value="Asia/Hong_Kong">Asia/Hong_Kong</option>
|
||||
<option value="Asia/Jakarta">Asia/Jakarta</option>
|
||||
<option value="Asia/Jerusalem">Asia/Jerusalem</option>
|
||||
<option value="Asia/Kabul">Asia/Kabul</option>
|
||||
<option value="Asia/Karachi">Asia/Karachi</option>
|
||||
<option value="Asia/Kathmandu">Asia/Kathmandu</option>
|
||||
<option value="Asia/Kolkata">Asia/Kolkata</option>
|
||||
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
||||
<option value="Asia/Kuwait">Asia/Kuwait</option>
|
||||
<option value="Asia/Manila">Asia/Manila</option>
|
||||
<option value="Asia/Riyadh">Asia/Riyadh</option>
|
||||
<option value="Asia/Seoul">Asia/Seoul</option>
|
||||
<option value="Asia/Shanghai">Asia/Shanghai</option>
|
||||
<option value="Asia/Singapore">Asia/Singapore</option>
|
||||
<option value="Asia/Taipei">Asia/Taipei</option>
|
||||
<option value="Asia/Tashkent">Asia/Tashkent</option>
|
||||
<option value="Asia/Tehran">Asia/Tehran</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
<option value="Asia/Yangon">Asia/Yangon</option>
|
||||
<option value="Australia/Adelaide">Australia/Adelaide</option>
|
||||
<option value="Australia/Brisbane">Australia/Brisbane</option>
|
||||
<option value="Australia/Melbourne">Australia/Melbourne</option>
|
||||
<option value="Australia/Perth">Australia/Perth</option>
|
||||
<option value="Australia/Sydney">Australia/Sydney</option>
|
||||
<option value="Europe/Amsterdam">Europe/Amsterdam</option>
|
||||
<option value="Europe/Athens">Europe/Athens</option>
|
||||
<option value="Europe/Belgrade">Europe/Belgrade</option>
|
||||
<option value="Europe/Berlin">Europe/Berlin</option>
|
||||
<option value="Europe/Brussels">Europe/Brussels</option>
|
||||
<option value="Europe/Bucharest">Europe/Bucharest</option>
|
||||
<option value="Europe/Budapest">Europe/Budapest</option>
|
||||
<option value="Europe/Copenhagen">Europe/Copenhagen</option>
|
||||
<option value="Europe/Dublin">Europe/Dublin</option>
|
||||
<option value="Europe/Helsinki">Europe/Helsinki</option>
|
||||
<option value="Europe/Istanbul">Europe/Istanbul</option>
|
||||
<option value="Europe/Kiev">Europe/Kiev</option>
|
||||
<option value="Europe/Lisbon">Europe/Lisbon</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="Europe/Madrid">Europe/Madrid</option>
|
||||
<option value="Europe/Moscow">Europe/Moscow</option>
|
||||
<option value="Europe/Oslo">Europe/Oslo</option>
|
||||
<option value="Europe/Paris">Europe/Paris</option>
|
||||
<option value="Europe/Prague">Europe/Prague</option>
|
||||
<option value="Europe/Riga">Europe/Riga</option>
|
||||
<option value="Europe/Rome">Europe/Rome</option>
|
||||
<option value="Europe/Sofia">Europe/Sofia</option>
|
||||
<option value="Europe/Stockholm">Europe/Stockholm</option>
|
||||
<option value="Europe/Tallinn">Europe/Tallinn</option>
|
||||
<option value="Europe/Vienna">Europe/Vienna</option>
|
||||
<option value="Europe/Vilnius">Europe/Vilnius</option>
|
||||
<option value="Europe/Warsaw">Europe/Warsaw</option>
|
||||
<option value="Europe/Zurich">Europe/Zurich</option>
|
||||
<option value="Pacific/Auckland">Pacific/Auckland</option>
|
||||
<option value="Pacific/Fiji">Pacific/Fiji</option>
|
||||
<option value="Pacific/Guam">Pacific/Guam</option>
|
||||
<option value="Pacific/Honolulu">Pacific/Honolulu</option>
|
||||
<option value="Etc/UTC">Etc/UTC</option>
|
||||
</select>
|
||||
|
||||
|
||||
@@ -217,12 +311,12 @@
|
||||
<div>
|
||||
<label for="clockDuration">Clock Duration</label>
|
||||
<input type="number" id="clockDuration" name="clockDuration" min="1" required />
|
||||
<label style="display: block; font-size: 0.8rem; color: #aaa; margin-top: 0.25rem;">(Seconds)</label>
|
||||
<label class="small">(Seconds)</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="weatherDuration">Weather Duration</label>
|
||||
<input type="number" id="weatherDuration" name="weatherDuration" min="1" required />
|
||||
<label style="display: block; font-size: 0.8rem; color: #aaa; margin-top: 0.25rem;">(Seconds)</label>
|
||||
<label class="small">(Seconds)</label>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br>
|
||||
@@ -239,6 +333,51 @@
|
||||
let isSaving = false;
|
||||
let isAPMode = false;
|
||||
|
||||
function ensureReloadButton(options = {}) {
|
||||
let modalContent = document.getElementById('savingModalContent');
|
||||
if (!modalContent) return;
|
||||
let btn = document.getElementById('reloadButton');
|
||||
if (!btn) {
|
||||
btn = document.createElement('button');
|
||||
btn.id = 'reloadButton';
|
||||
btn.className = 'primary-button';
|
||||
btn.style.display = 'inline-block';
|
||||
btn.style.margin = '1rem 0.5rem 0 0';
|
||||
modalContent.appendChild(btn);
|
||||
}
|
||||
btn.textContent = options.text || "Reload Page";
|
||||
btn.onclick = options.onClick || (() => location.reload());
|
||||
btn.style.display = 'inline-block';
|
||||
return btn;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
if (isSaving) {
|
||||
return "Settings are being saved. Leaving now may interrupt the process.";
|
||||
@@ -250,7 +389,6 @@ window.onload = function () {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
isAPMode = (data.mode === "ap");
|
||||
// console.log("✅ Config data:", data);
|
||||
if (isAPMode) {
|
||||
document.querySelector('.footer').style.display = 'none';
|
||||
}
|
||||
@@ -263,18 +401,52 @@ window.onload = function () {
|
||||
document.getElementById('weatherUnits').value = data.weatherUnits || 'metric';
|
||||
document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000;
|
||||
document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000;
|
||||
document.getElementById('utcOffsetInHours').value = (data.utcOffsetInSeconds || 0) / 3600;
|
||||
|
||||
// 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);
|
||||
document.getElementById('restoreButton').style.display = 'inline-block';
|
||||
|
||||
// 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');
|
||||
|
||||
|
||||
};
|
||||
|
||||
function submitConfig(event) {
|
||||
async function submitConfig(event) {
|
||||
event.preventDefault();
|
||||
isSaving = true;
|
||||
|
||||
@@ -286,10 +458,6 @@ function submitConfig(event) {
|
||||
formData.set('clockDuration', clockDuration);
|
||||
formData.set('weatherDuration', weatherDuration);
|
||||
|
||||
const utcOffsetInHours = parseFloat(formData.get('utcOffsetInHours')) || 0;
|
||||
formData.set('utcOffsetInSeconds', Math.round(utcOffsetInHours * 3600));
|
||||
formData.delete('utcOffsetInHours'); // backend expects only utcOffsetInSeconds
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const pair of formData.entries()) {
|
||||
params.append(pair[0], pair[1]);
|
||||
@@ -297,6 +465,17 @@ function submitConfig(event) {
|
||||
|
||||
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
|
||||
@@ -309,56 +488,63 @@ function submitConfig(event) {
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
isSaving = false;
|
||||
if (isAPMode) {
|
||||
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
|
||||
.then(json => {
|
||||
isSaving = false;
|
||||
removeReloadButton();
|
||||
removeRestoreButton();
|
||||
if (isAPMode) {
|
||||
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('configForm').style.display = 'none';
|
||||
// Optionally update modal text:
|
||||
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
|
||||
}, 5000);
|
||||
return;
|
||||
setTimeout(() => {
|
||||
document.querySelector('html').style.height = '100%';
|
||||
document.getElementById('configForm').style.display = 'none';
|
||||
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
|
||||
}, 5000);
|
||||
return;
|
||||
|
||||
} else {
|
||||
updateSavingModal("✅ Configuration saved successfully.<br><br><br>Device will reboot", false);
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
isSaving = false;
|
||||
} else {
|
||||
updateSavingModal("✅ Configuration saved successfully.<br><br><br>Device will reboot", false);
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
isSaving = false;
|
||||
|
||||
if (isAPMode && err.message.includes("Failed to fetch")) {
|
||||
console.warn("Expected disconnect in AP mode after reboot.");
|
||||
|
||||
// Ensure modal is visible
|
||||
showSavingModal(""); // Create or show modal if needed
|
||||
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('configForm').style.display = 'none';
|
||||
// Optionally update modal text:
|
||||
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
|
||||
}, 5000);
|
||||
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);
|
||||
const restoreBtn = document.getElementById('restoreButton');
|
||||
restoreBtn.textContent = "Reload Page";
|
||||
restoreBtn.onclick = () => location.reload();
|
||||
restoreBtn.style.display = 'inline-block';
|
||||
});
|
||||
if (isAPMode && err.message.includes("Failed to fetch")) {
|
||||
console.warn("Expected disconnect in AP mode after reboot.");
|
||||
showSavingModal("");
|
||||
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
|
||||
setTimeout(() => {
|
||||
document.getElementById('configForm').style.display = 'none';
|
||||
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", 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) {
|
||||
@@ -382,13 +568,21 @@ function showSavingModal(message) {
|
||||
}
|
||||
|
||||
function updateSavingModal(message, showSpinner = false) {
|
||||
document.getElementById('savingModalText').innerHTML = message;
|
||||
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 restoreBackupConfig() {
|
||||
showSavingModal("Restoring backup...");
|
||||
document.getElementById('restoreButton').remove();
|
||||
removeReloadButton();
|
||||
removeRestoreButton();
|
||||
|
||||
fetch('/restore', { method: 'POST' })
|
||||
.then(response => {
|
||||
@@ -398,12 +592,17 @@ function restoreBackupConfig() {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
updateSavingModal("✅ Backup restored! Reloading...");
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
|
||||
3, 62, 40, 56, // 98 - 'b'
|
||||
3, 56, 68, 68, // 99 - 'c'
|
||||
3, 56, 40, 62, // 100 - 'd'
|
||||
3, 60, 44, 44, // 101 - 'e'
|
||||
3, 124, 84, 68, // 101 - 'e'
|
||||
3, 8, 62, 10, // 102 - 'f'
|
||||
3, 56, 84, 116, // 103 - 'g'
|
||||
3, 62, 8, 56, // 104 - 'h'
|
||||
@@ -112,14 +112,14 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
|
||||
2, 32, 58, // 106 - 'j'
|
||||
3, 60, 16, 40, // 107 - 'k'
|
||||
1, 60, // 108 - 'l'
|
||||
3, 56, 24, 56, // 109 - 'm'
|
||||
5, 124, 4, 124, 4, 120, // 109 - 'm'
|
||||
3, 124, 4, 120, // 110 - 'n'
|
||||
3, 56, 40, 56, // 111 - 'o'
|
||||
3, 120, 40, 56, // 112 - 'p'
|
||||
3, 56, 68, 56, // 111 - 'o'
|
||||
3, 124, 20, 8, // 112 - 'p'
|
||||
3, 56, 40, 120, // 113 - 'q'
|
||||
3, 56, 8, 24, // 114 - 'r'
|
||||
3, 72, 84, 36, // 115 - 's'
|
||||
3, 11, 123, 11, // 116 - 't'
|
||||
3, 4, 124, 4, // 116 - 't'
|
||||
3, 56, 32, 56, // 117 - 'u'
|
||||
3, 24, 32, 24, // 118 - 'v'
|
||||
3, 56, 48, 56, // 119 - 'w'
|
||||
|
||||
109
tz_lookup.h
Normal file
109
tz_lookup.h
Normal file
@@ -0,0 +1,109 @@
|
||||
#ifndef TZ_LOOKUP_H
|
||||
#define TZ_LOOKUP_H
|
||||
|
||||
typedef struct {
|
||||
const char* iana;
|
||||
const char* posix;
|
||||
} TimeZoneMapping;
|
||||
|
||||
const TimeZoneMapping tz_mappings[] = {
|
||||
{"Etc/UTC", "UTC0"},
|
||||
{"Europe/London", "GMT0BST,M3.5.0/1,M10.5.0/2"},
|
||||
{"Europe/Dublin", "IST-1GMT0,M10.5.0,M3.5.0/1"},
|
||||
{"Europe/Lisbon", "WET0WEST,M3.5.0/1,M10.5.0"},
|
||||
{"Europe/Paris", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Berlin", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Rome", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Madrid", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Amsterdam", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Brussels", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Zurich", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Vienna", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Prague", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Warsaw", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Budapest", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Athens", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Istanbul", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Moscow", "MSK-3"},
|
||||
{"Europe/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Bucharest", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Kiev", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Belgrade", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Copenhagen", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Stockholm", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Oslo", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||
{"Europe/Riga", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Tallinn", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Europe/Vilnius", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||
{"Africa/Cairo", "EET-2"},
|
||||
{"Africa/Johannesburg", "SAST-2"},
|
||||
{"Africa/Lagos", "WAT-1"},
|
||||
{"Africa/Nairobi", "EAT-3"},
|
||||
{"Africa/Casablanca", "WET0"},
|
||||
{"Africa/Algiers", "CET-1"},
|
||||
{"Asia/Jerusalem", "IST-2IDT,M3.4.4/26,M10.5.0"},
|
||||
{"Asia/Dubai", "GST-4"},
|
||||
{"Asia/Kolkata", "IST-5:30"},
|
||||
{"Asia/Karachi", "PKT-5"},
|
||||
{"Asia/Dhaka", "BDT-6"},
|
||||
{"Asia/Ho_Chi_Minh", "ICT-7"},
|
||||
{"Asia/Bangkok", "ICT-7"},
|
||||
{"Asia/Jakarta", "WIB-7"},
|
||||
{"Asia/Singapore", "SGT-8"},
|
||||
{"Asia/Kuala_Lumpur", "MYT-8"},
|
||||
{"Asia/Shanghai", "CST-8"},
|
||||
{"Asia/Hong_Kong", "HKT-8"},
|
||||
{"Asia/Taipei", "CST-8"},
|
||||
{"Asia/Seoul", "KST-9"},
|
||||
{"Asia/Tokyo", "JST-9"},
|
||||
{"Asia/Manila", "PHT-8"},
|
||||
{"Asia/Yangon", "MMT-6:30"},
|
||||
{"Asia/Kathmandu", "NPT-5:45"},
|
||||
{"Asia/Almaty", "ALMT-6"},
|
||||
{"Asia/Baku", "AZT-4AZST,M3.5.0/5,M10.5.0/6"},
|
||||
{"Asia/Tashkent", "UZT-5"},
|
||||
{"Asia/Tehran", "IRST-3:30IRDT,J79/24,J263/24"},
|
||||
{"Asia/Baghdad", "AST-3"},
|
||||
{"Asia/Riyadh", "AST-3"},
|
||||
{"Asia/Kuwait", "AST-3"},
|
||||
{"Asia/Amman", "EET-2EEST,J80/24,J273/1"},
|
||||
{"Asia/Beirut", "EET-2EEST,M3.5.0/0,M10.5.0/0"},
|
||||
{"Asia/Kabul", "AFT-4:30"},
|
||||
{"Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3"},
|
||||
{"Australia/Brisbane", "AEST-10"},
|
||||
{"Australia/Perth", "AWST-8"},
|
||||
{"Australia/Adelaide", "ACST-9:30ACDT,M10.1.0,M4.1.0/3"},
|
||||
{"Australia/Melbourne", "AEST-10AEDT,M10.1.0,M4.1.0/3"},
|
||||
{"Pacific/Auckland", "NZST-12NZDT,M9.5.0,M4.1.0/3"},
|
||||
{"Pacific/Fiji", "FJT-12FJST,M11.2.0,M1/1"},
|
||||
{"Pacific/Honolulu", "HST10"},
|
||||
{"Pacific/Guam", "ChST-10"},
|
||||
{"America/Anchorage", "AKDT9AKST,M3.2.0,M11.1.0"},
|
||||
{"America/Los_Angeles", "PDT8PST,M3.2.0,M11.1.0"},
|
||||
{"America/Vancouver", "PDT8PST,M3.2.0,M11.1.0"},
|
||||
{"America/Denver", "MDT7MST,M3.2.0,M11.1.0"},
|
||||
{"America/Phoenix", "MST7"},
|
||||
{"America/Chicago", "CDT6CST,M3.2.0,M11.1.0"},
|
||||
{"America/Mexico_City", "CDT6CST,M4.1.0,M10.5.0"},
|
||||
{"America/Toronto", "EDT5EST,M3.2.0,M11.1.0"},
|
||||
{"America/New_York", "EDT5EST,M3.2.0,M11.1.0"},
|
||||
{"America/Caracas", "VET4"},
|
||||
{"America/Bogota", "COT5"},
|
||||
{"America/Lima", "PET5"},
|
||||
{"America/Santiago", "CLST4CLT,M9.1.1,M4.2.7"},
|
||||
{"America/Argentina/Buenos_Aires", "ART3"},
|
||||
{"America/Sao_Paulo", "BRT3BRST,M10.3.0/0,M2.3.0/0"}
|
||||
};
|
||||
|
||||
#define TZ_MAPPINGS_COUNT (sizeof(tz_mappings)/sizeof(tz_mappings[0]))
|
||||
|
||||
inline const char* ianaToPosix(const char* iana) {
|
||||
for (size_t i = 0; i < TZ_MAPPINGS_COUNT; i++) {
|
||||
if (strcmp(iana, tz_mappings[i].iana) == 0)
|
||||
return tz_mappings[i].posix;
|
||||
}
|
||||
return "UTC0"; // fallback
|
||||
}
|
||||
|
||||
#endif // TZ_LOOKUP_H
|
||||
Reference in New Issue
Block a user