From 3a386826fc7eec89b50beda371dcef0ad3adc2a5 Mon Sep 17 00:00:00 2001
From: Roman Bazalevskiy <rvb@rvb.name>
Date: Fri, 18 Nov 2022 19:21:43 +0300
Subject: [PATCH] =?utf8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?=
 =?utf8?q?=D0=B8=D0=B5/=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE?=
 =?utf8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5/=D1=81=D0=B1=D1=80=D0=BE?=
 =?utf8?q?=D1=81=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA=20?=
 =?utf8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=B2=D0=B5=D0=B1-=D0=B8=D0=BD?=
 =?utf8?q?=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=utf8
Content-Transfer-Encoding: 8bit

---
 config.cpp         |  1 +
 data/web/script.js | 32 +++++++++++++++++++++++++++--
 data/web/style.css | 26 +++++++++++++++++++----
 panel.cpp          |  6 ++++--
 ui.yml             | 11 +++++++++-
 web.cpp            | 51 +++++++++++++++++++++++++++++++++++++++++++---
 6 files changed, 115 insertions(+), 12 deletions(-)

diff --git a/config.cpp b/config.cpp
index 0835802..9c46b95 100644
--- a/config.cpp
+++ b/config.cpp
@@ -556,6 +556,7 @@ char* distConfig PROGMEM =
 
 void reset() {
   messageModal(F("Сбрасываю настройки"));
+  delay(2000);
   if (File f = LittleFS.open(F("/config.txt"),"w")) {
     f.print(distConfig);
     f.close();
diff --git a/data/web/script.js b/data/web/script.js
index cb4fa7e..ee43439 100644
--- a/data/web/script.js
+++ b/data/web/script.js
@@ -251,6 +251,25 @@ function sendTime(id) {
   }
 }
 
+function downloadFile(url, name) {
+  const a = document.createElement('a')
+  a.href = url
+  a.download = name?name:url.split('/').pop()
+  document.body.appendChild(a)
+  a.click()
+  document.body.removeChild(a)
+}
+
+function uploadConfig() {
+  var elem = document.getElementById('_config_file')
+  if (elem.value) {
+    var data = elem.files[0]
+    console.log(data)
+    fetch('/config/put', {method:'PUT',body:data});
+    elem.value = null
+  }
+}
+
 function elementHTML(element) {
   var value
   if (parameters[element.id] || !isNaN(parameters[element.id])) {
@@ -326,8 +345,8 @@ function elementHTML(element) {
       now.setMinutes(now.getMinutes() - now.getTimezoneOffset())
       value = now.toISOString().slice(0, -1);
       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
-        +'<div class="timesetter"><input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
-        + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div></div>'
+        +'<input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
+        + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div>'
     case 'text':
       return '<div class="pure-u-1 pure-u-md-1-3"><h2 id="_ui_element_'+ element.id +'" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</h2></div>'
     case 'number':
@@ -340,6 +359,13 @@ function elementHTML(element) {
         + '<input data-ui_class="range" type="range" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
+    case 'config':
+      return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
+        + '<div align="center"><input type="button" class="pure-button row-button" value="Сохранить..." onclick="downloadFile(\'/config/get\',\'config.json\')">'
+        + '<label for="_config_file" class="pure-button row-button">Восстановить...</label>'
+        + '<input type="button" class="pure-button row-button" value="Настройки по умолчанию" onclick="confirm(\'Вы точно хотите сбросить настройки?\')?sendAction(\'reset\'):console.log(\'Не надо так не надо...\')">'
+        + '<input type="file" id="_config_file" onchange="uploadConfig()" style="visibility:hidden">'
+        + '</div></div>'
     case 'table':
     default:
       return '<div class="pure-u-1 pure-u-md-1-3"><table class="texttable" cellpadding="5" border="0" align="center"><tbody><tr><td class="value-name" align="right">' 
@@ -432,9 +458,11 @@ GetUI()
 function initES() {
   if (!!window.EventSource) {
     var source = new EventSource('/events');
+    openMsg('Соединение установлено')
 
     source.onerror = function(e) {
       if (source.readyState == 2) {
+        openMsg('Соединение прервано')
         setTimeout(initES, 5000);
       }
     };
diff --git a/data/web/style.css b/data/web/style.css
index fad4817..243c659 100644
--- a/data/web/style.css
+++ b/data/web/style.css
@@ -1388,10 +1388,6 @@ label.switch.socket input[type=checkbox]:checked + span.slider:after {
 
 /* Time setter */
 
-.timesetter {
-  display: inline;
-}
-
 .inline-input.inline-input.inline-input {
   display: inline-block;
   width: calc(100% - 5em)
@@ -1468,6 +1464,28 @@ input[type=number] {
   padding-right: 1em;
 }
 
+/* Config */
+
+input[type=file]::file-selector-button {
+    display: none;
+}
+
+input[type=file]::-webkit-file-upload-button {
+    display: block;
+    width: 0;
+    height: 0;
+    margin-left: -100%;
+}
+
+input[type=file]::-ms-browse {
+    display: none;
+}
+
+.row-button.row-button.row-button {
+  display: inline-block;
+  margin: 0 2em 0 2em
+}
+
 /* fonts */
 
 @font-face {
diff --git a/panel.cpp b/panel.cpp
index a02b52a..fc07261 100644
--- a/panel.cpp
+++ b/panel.cpp
@@ -133,6 +133,7 @@ void drawScroll() {
 
 void message(const char* str, int priority) {
   if (priority > currentPriority) return;
+  currentPriority = priority;
   utf8rus(str, msgBuf, 255);
   Panel->displayClear();
   Panel->setFont(RomanCyrillic);
@@ -143,6 +144,7 @@ void message(const char* str, int priority) {
 
 void message(const __FlashStringHelper*  str, int priority) {
   if (priority > currentPriority) return;
+  currentPriority = priority;
   char buf[256];
   strncpy_P(buf, (PGM_P)str, 255);
   utf8rus(buf, msgBuf, 255);
@@ -155,12 +157,12 @@ void message(const __FlashStringHelper*  str, int priority) {
 
 void messageModal(const char* str) {
   message(str);
-  while (!(Panel->displayAnimate())) { delay(50); }
+  while (!(Panel->displayAnimate())) { delay(10); }
 }
 
 void messageModal(const __FlashStringHelper*  str) {
   message(str);
-  while (!(Panel->displayAnimate())) { delay(50); }
+  while (!(Panel->displayAnimate())) { delay(10); }
 }
 
 void scroll(const char* str, bool force) {
diff --git a/ui.yml b/ui.yml
index b54720d..ec261c2 100644
--- a/ui.yml
+++ b/ui.yml
@@ -1,6 +1,6 @@
 project:
   name: WiFi Clock
-  version: 0.1.2
+  version: 0.2.0
   contacts:
     - mailto:rvb@rvb.name
     - tg:rvbglas
@@ -412,6 +412,9 @@ pages:
     title: Погода
     icon: "&#61673;"
     elements:
+      - id: enable_weather
+        label: Использовать погодный сервис
+        type: checkbox
       - id: weather_url
         label: URL погодного сервиса
         type: input
@@ -469,6 +472,12 @@ pages:
         type: button
         label: Сменить пароль
       - type: hr
+      - type: text
+        value: Конфигурация
+      - id: _config
+        type: config
+        label: Сохранение и восстановление настроек
+      - type: hr
       - type: text
         value: Синхронизация времени
       - id: _timeset
diff --git a/web.cpp b/web.cpp
index 7d017b5..13e7916 100644
--- a/web.cpp
+++ b/web.cpp
@@ -3,6 +3,8 @@
 #include <ESPAsyncWebServer.h>
 #include <StreamString.h>
 #include <Ticker.h>
+#include "ArduinoJson.h"
+#include "AsyncJson.h"
 
 bool isApEnabled = false;
 bool isWebStarted = false;
@@ -144,7 +146,11 @@ void setupWeb() {
     }
     if(request->hasParam("name")) {
       const char* action = request->getParam("name")->value().c_str();
-      if (strcmp(action,"restart") == 0) {
+      if (strcmp(action,"reset") == 0) {
+          millisScheduled = millis();
+          actionScheduled = "reset";
+          request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Сбрасываю настройки и перезагружаюсь\"}");
+      } else if (strcmp(action,"restart") == 0) {
         if (pendingWiFi) {
           request->send(200,"application/json", "{\"result\":\"FAILED\",\"message\":\"Не применены настройки WiFi\", \"page\":\"wifi\"}");
         } else if (pendingAuth) {
@@ -181,7 +187,7 @@ void setupWeb() {
           unsigned long timestamp = atoi(request->getParam("timestamp")->value().c_str());
           if (timestamp) {
             timeval tv = { timestamp, 0 };
-            settimeofday(&tv, nullptr);  
+            settimeofday(&tv, nullptr);
             if (isRTCEnabled) {
               Serial.println(F("Время установлено вручную"));
               RTC.adjust(DateTime(timestamp));
@@ -302,6 +308,42 @@ void setupWeb() {
     }
   });
 
+  AsyncCallbackJsonWebHandler* configUploadHandler = new AsyncCallbackJsonWebHandler("/config/put", [](AsyncWebServerRequest *request, JsonVariant &json) {
+    cfg.clear();
+    // first - set values
+    for( JsonPair kv : json.as<JsonObject>() ) {
+      const char* name = kv.key().c_str();
+      if (kv.value().is<bool>()) {
+        cfg.setValue(name, kv.value().as<bool>());
+      } else if (kv.value().is<int>()) {
+        cfg.setValue(name, kv.value().as<int>());
+      } else if (kv.value().is<double>()) {
+        cfg.setValue(name, kv.value().as<double>());
+      } else if (kv.value().is<char*>()) {
+        cfg.setValue(name, kv.value().as<char*>());
+      } else {
+        Serial.print(F("Неопознанный тип значения параметра ")); Serial.print(name); Serial.print(": "); Serial.println(kv.value().as<String>().c_str());
+        cfg.clear();
+        setupConfig();
+        request->send(500, "text/plain", "Unknown parameter type");
+      }
+    }
+    // second - handle all changes
+    for( JsonPair kv : json.as<JsonObject>() ) {
+      apply(kv.key().c_str());
+    }
+    pendingWiFi = false;
+    pendingAuth = false;
+    saveConfig();
+    message(F("Применены сохраненные настройки"),5);
+    reportMessage(F("Применены сохраненные настройки"));
+    millisScheduled = millis() + 10000;
+    actionScheduled = "restart";
+    request->send(200,"application/json", "{\"result\":\"OK\",\"message\":\"Настройки восстановлены из резервной копии\"}");
+  });
+
+  server.addHandler(configUploadHandler).setAuthentication(auth_user,auth_pwd);
+
   server.serveStatic("ui", LittleFS, "/ui.json").setAuthentication(auth_user,auth_pwd);
 
   server.serveStatic("/", LittleFS, "/web/").setDefaultFile("index.html").setAuthentication(auth_user,auth_pwd);
@@ -334,7 +376,10 @@ void tickWeb() {
   if (actionScheduled && millis()>millisScheduled+300) {
     Serial.print(F("Запланированная операция ")); Serial.println(actionScheduled);
     //
-    if (strcmp(actionScheduled,"restart") == 0) {
+    if (strcmp(actionScheduled,"reset") == 0) {
+      server.end();
+      reset();
+    } else if (strcmp(actionScheduled,"restart") == 0) {
       server.end();
       reboot();
     } else if (strcmp(actionScheduled,"auth") == 0) {
-- 
2.34.1