Browse Source

Improved webUI UX

fastled
Xose Pérez 7 years ago
parent
commit
74148bb6e6
5 changed files with 3782 additions and 3643 deletions
  1. BIN
      code/espurna/data/index.html.gz
  2. +3566
    -3543
      code/espurna/static/index.html.gz.h
  3. +1
    -1
      code/html/custom.css
  4. +200
    -84
      code/html/custom.js
  5. +15
    -15
      code/html/index.html

BIN
code/espurna/data/index.html.gz View File


+ 3566
- 3543
code/espurna/static/index.html.gz.h
File diff suppressed because it is too large
View File


+ 1
- 1
code/html/custom.css View File

@ -29,7 +29,7 @@
}
.pure-button {
color: white;
padding: 8px 12px;
padding: 8px 8px;
border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}


+ 200
- 84
code/html/custom.js View File

@ -5,6 +5,32 @@ var useWhite = false;
var messages = [];
var webhost;
var numChanged = 0;
var numReset = 0;
var numReconnect = 0;
var numReload = 0;
// -----------------------------------------------------------------------------
// Messages
// -----------------------------------------------------------------------------
function initMessages() {
messages[01] = "Remote update started";
messages[02] = "OTA update started";
messages[03] = "Error parsing data!";
messages[04] = "The file does not look like a valid configuration backup or is corrupted";
messages[05] = "Changes saved. You should reboot your board now";
messages[06] = "Home Assistant auto-discovery message sent";
messages[07] = "Passwords do not match!";
messages[08] = "Changes saved";
messages[09] = "No changes detected";
messages[10] = "Session expired, please reload page...";
}
// -----------------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------------
// http://www.the-art-of-web.com/javascript/validate-password/
function checkPassword(str) {
// at least one number, one lowercase and one uppercase letter
@ -13,6 +39,10 @@ function checkPassword(str) {
return re.test(str);
}
function zeroPad(number, positions) {
return ("0".repeat(positions) + number).slice(-positions);
}
function validateForm(form) {
// password
@ -42,8 +72,59 @@ function valueSet(data, name, value) {
data.push({'name': name, 'value': value});
}
function zeroPad(number, positions) {
return ("0".repeat(positions) + number).slice(-positions);
function randomString(length, chars) {
var mask = '';
if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz';
if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (chars.indexOf('#') > -1) mask += '0123456789';
if (chars.indexOf('@') > -1) mask += 'ABCDEF';
if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\';
var result = '';
for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))];
return result;
}
function generateAPIKey() {
var apikey = randomString(16, '@#');
$("input[name=\"apiKey\"]").val(apikey);
return false;
}
function forgetCredentials() {
$.ajax({
'method': 'GET',
'url': '/',
'async': false,
'username': "logmeout",
'password': "123456",
'headers': { "Authorization": "Basic xxx" }
}).done(function(data) {
return false;
// If we don't get an error, we actually got an error as we expect an 401!
}).fail(function(){
// We expect to get an 401 Unauthorized error! In this case we are successfully
// logged out and we redirect the user.
return true;
});
}
function getJson(str) {
try {
return JSON.parse(str);
} catch (e) {
return false;
}
}
// -----------------------------------------------------------------------------
// Actions
// -----------------------------------------------------------------------------
function doReload(milliseconds) {
milliseconds = (typeof milliseconds == 'undefined') ? 0 : parseInt(milliseconds);
setTimeout(function() {
window.location.reload();
}, milliseconds);
}
function doUpdate() {
@ -71,6 +152,21 @@ function doUpdate() {
.prop("checked", false)
.iphoneStyle("refresh");
numChanged = 0;
setTimeout(function() {
if (numReset > 0) {
var response = window.confirm("You have to reset the board for the changes to take effect, do you want to do it now?");
if (response == true) doReset(false);
} else if (numReconnect > 0) {
var response = window.confirm("You have to reset the wifi connection for the changes to take effect, do you want to do it now?");
if (response == true) doReconnect(false);
} else if (numReload > 0) {
var response = window.confirm("You have to reload the page to see the latest changes, do you want to do it now?");
if (response == true) doReload();
}
numReset = numReconnect = numReload = 0;
}, 1000);
}
return false;
@ -108,9 +204,7 @@ function doUpgrade() {
$("#upgrade-progress").hide();
if (data == 'OK') {
alert("Firmware image uploaded, board rebooting. This page will be refreshed in 5 seconds.");
setTimeout(function() {
window.location.reload();
}, 5000);
doReload(5000);
} else {
alert("There was an error trying to upload the new image, please try again (" + data + ").");
}
@ -146,18 +240,44 @@ function doUpdatePassword() {
return false;
}
function doReset() {
var response = window.confirm("Are you sure you want to reset the device?");
if (response == false) return false;
function doReset(ask) {
ask = (typeof ask == 'undefined') ? true : ask;
if (numChanged > 0) {
var response = window.confirm("Some changes have not been saved yet, do you want to save them first?");
if (response == true) return doUpdate();
}
if (ask) {
var response = window.confirm("Are you sure you want to reset the device?");
if (response == false) return false;
}
websock.send(JSON.stringify({'action': 'reset'}));
doReload(5000);
return false;
}
function doReconnect() {
var response = window.confirm("Are you sure you want to disconnect from the current WIFI network?");
if (response == false) return false;
function doReconnect(ask) {
ask = (typeof ask == 'undefined') ? true : ask;
if (numChanged > 0) {
var response = window.confirm("Some changes have not been saved yet, do you want to save them first?");
if (response == true) return doUpdate();
}
if (ask) {
var response = window.confirm("Are you sure you want to disconnect from the current WIFI network?");
if (response == false) return false;
}
websock.send(JSON.stringify({'action': 'reconnect'}));
doReload(5000);
return false;
}
function doToggle(element, value) {
@ -166,7 +286,7 @@ function doToggle(element, value) {
return false;
}
function backupSettings() {
function doBackup() {
document.getElementById('downloader').src = webhost + 'config';
return false;
}
@ -196,7 +316,7 @@ function onFileUpload(event) {
}
function restoreSettings() {
function doRestore() {
if (typeof window.FileReader !== 'function') {
alert("The file API isn't supported on this browser yet.");
} else {
@ -205,23 +325,9 @@ function restoreSettings() {
return false;
}
function randomString(length, chars) {
var mask = '';
if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz';
if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if (chars.indexOf('#') > -1) mask += '0123456789';
if (chars.indexOf('@') > -1) mask += 'ABCDEF';
if (chars.indexOf('!') > -1) mask += '~`!@#$%^&*()_+-={}[]:";\'<>?,./|\\';
var result = '';
for (var i = length; i > 0; --i) result += mask[Math.round(Math.random() * (mask.length - 1))];
return result;
}
function doGenerateAPIKey() {
var apikey = randomString(16, '@#');
$("input[name=\"apiKey\"]").val(apikey);
return false;
}
// -----------------------------------------------------------------------------
// Visualization
// -----------------------------------------------------------------------------
function showPanel() {
$(".panel").hide();
@ -236,6 +342,10 @@ function toggleMenu() {
$("#menuLink").toggleClass('active');
}
// -----------------------------------------------------------------------------
// Templates
// -----------------------------------------------------------------------------
function createRelays(count) {
var current = $("#relays > div").length;
@ -270,7 +380,7 @@ function createIdxs(count) {
for (var id=0; id<count; id++) {
var line = $(template).clone();
$(line).find("input").each(function() {
$(this).attr("data", id).attr("tabindex", 40+id);
$(this).attr("data", id).attr("tabindex", 40+id).attr("original", "");
});
if (count > 1) $(".id", line).html(" " + id);
line.appendTo("#idxs");
@ -300,7 +410,7 @@ function addNetwork() {
var template = $("#networkTemplate").children();
var line = $(template).clone();
$(line).find("input").each(function() {
$(this).attr("tabindex", tabindex++);
$(this).attr("tabindex", tabindex++).attr("original", "");
});
$(line).find(".button-del-network").on('click', delNetwork);
$(line).find(".button-more-network").on('click', moreNetwork);
@ -405,9 +515,6 @@ function initColorsExtras() {
});
}
function initChannels(num) {
// check if already initialized
@ -495,23 +602,9 @@ function rfbSend() {
websock.send(JSON.stringify({'action': 'rfbsend', 'data' : {'id' : input.attr("data_id"), 'status': input.attr("data_status"), 'data': input.val()}}));
}
function forgetCredentials() {
$.ajax({
'method': 'GET',
'url': '/',
'async': false,
'username': "logmeout",
'password': "123456",
'headers': { "Authorization": "Basic xxx" }
}).done(function(data) {
return false;
// If we don't get an error, we actually got an error as we expect an 401!
}).fail(function(){
// We expect to get an 401 Unauthorized error! In this case we are successfully
// logged out and we redirect the user.
return true;
});
}
// -----------------------------------------------------------------------------
// Processing
// -----------------------------------------------------------------------------
function processData(data) {
@ -543,9 +636,7 @@ function processData(data) {
if (data.action == "reload") {
if (password) forgetCredentials();
setTimeout(function() {
window.location.reload();
}, 1000);
doReload(1000);
}
if (data.action == "rfbLearn") {
@ -570,7 +661,7 @@ function processData(data) {
for (var i in nodes) {
var node = nodes[i];
var element = $("input[name=rfbcode][data_id=" + node["id"] + "][data_status=" + node["status"] + "]");
if (element.length) element.val(node["data"]);
if (element.length) element.val(node["data"]).attr("original", node["data"]);
}
return;
}
@ -596,6 +687,7 @@ function processData(data) {
$("[name='animation']").val(data[key]);
return;
}
if (key == "anim_speed") {
initColorsExtras();
var slider = $("#animSpeed");
@ -651,7 +743,7 @@ function processData(data) {
var wifi = data.wifi[i];
Object.keys(wifi).forEach(function(key) {
var element = $("input[name=" + key + "]", line);
if (element.length) element.val(wifi[key]);
if (element.length) element.val(wifi[key]).attr("original", wifi[key]);
});
}
@ -686,7 +778,7 @@ function processData(data) {
for (var i in idxs) {
var element = $(".dczRelayIdx[data=" + i + "]");
if (element.length > 0) element.val(idxs[i]);
if (element.length > 0) element.val(idxs[i]).attr("original", idxs[i]);
}
return;
@ -732,7 +824,7 @@ function processData(data) {
} else {
var pre = element.attr("pre") || "";
var post = element.attr("post") || "";
element.val(pre + data[key] + post);
element.val(pre + data[key] + post).attr("original", data[key]);
}
return;
}
@ -749,7 +841,7 @@ function processData(data) {
// Look for SELECTs
var element = $("select[name=" + key + "]");
if (element.length > 0) {
element.val(data[key]);
element.val(data[key]).attr("original", data[key]);
return;
}
@ -757,19 +849,51 @@ function processData(data) {
// Auto generate an APIKey if none defined yet
if ($("input[name='apiKey']").val() == "") {
doGenerateAPIKey();
generateAPIKey();
}
}
function getJson(str) {
try {
return JSON.parse(str);
} catch (e) {
return false;
function hasChanged() {
var newValue, originalValue;
if ($(this).attr('type') == 'checkbox') {
newValue = $(this).prop("checked")
originalValue = $(this).attr("original") == "true";
} else {
newValue = $(this).val();
originalValue = $(this).attr("original");
}
var hasChanged = $(this).attr("hasChanged") || 0;
var action = $(this).attr("action");
if (typeof originalValue == 'undefined') return;
if (action == 'none') return;
if (newValue != originalValue) {
if (hasChanged == 0) {
++numChanged;
if (action == "reconnect") ++numReconnect;
if (action == "reset") ++numReset;
if (action == "reload") ++numReload;
$(this).attr("hasChanged", 1);
}
} else {
if (hasChanged == 1) {
--numChanged;
if (action == "reconnect") --numReconnect;
if (action == "reset") --numReset;
if (action == "reload") --numReload;
$(this).attr("hasChanged", 0);
}
}
}
// -----------------------------------------------------------------------------
// Init & connect
// -----------------------------------------------------------------------------
function connect(host) {
if (typeof host === 'undefined') {
@ -799,33 +923,24 @@ function connect(host) {
};
}
function initMessages() {
messages[01] = "Remote update started";
messages[02] = "OTA update started";
messages[03] = "Error parsing data!";
messages[04] = "The file does not look like a valid configuration backup or is corrupted";
messages[05] = "Changes saved. You should reboot your board now";
messages[06] = "Home Assistant auto-discovery message sent";
messages[07] = "Passwords do not match!";
messages[08] = "Changes saved";
messages[09] = "No changes detected";
messages[10] = "Session expired, please reload page...";
}
function init() {
initMessages();
$("#menuLink").on('click', toggleMenu);
$(".pure-menu-link").on('click', showPanel);
$('progress').attr({ value: 0, max: 100 });
$(".button-update").on('click', doUpdate);
$(".button-update-password").on('click', doUpdatePassword);
$(".button-reset").on('click', doReset);
$(".button-reconnect").on('click', doReconnect);
$(".button-settings-backup").on('click', backupSettings);
$(".button-settings-restore").on('click', restoreSettings);
$(".button-settings-backup").on('click', doBackup);
$(".button-settings-restore").on('click', doRestore);
$('#uploader').on('change', onFileUpload);
$(".button-apikey").on('click', doGenerateAPIKey);
$(".button-upgrade").on('click', doUpgrade);
$(".button-apikey").on('click', generateAPIKey);
$(".button-upgrade-browse").on('click', function() {
$("input[name='upgrade']")[0].click();
return false;
@ -834,8 +949,6 @@ function init() {
var fileName = $(this).val();
$("input[name='filename']").val(fileName.replace(/^.*[\\\/]/, ''));
});
$('progress').attr({ value: 0, max: 100 });
$(".pure-menu-link").on('click', showPanel);
$(".button-add-network").on('click', function() {
$("div.more", addNetwork()).toggle();
});
@ -843,6 +956,9 @@ function init() {
websock.send(JSON.stringify({'action': 'ha_send', 'data': $("input[name='haPrefix']").val()}));
});
$(document).on('change', 'input', hasChanged);
$(document).on('change', 'select', hasChanged);
$.ajax({
'method': 'GET',
'url': window.location.href + 'auth'


+ 15
- 15
code/html/index.html View File

@ -126,7 +126,7 @@
</ul>
<div class="main-buttons">
<button class="pure-button button-update">Update</button>
<button class="pure-button button-update">Save</button>
<button class="pure-button button-reconnect">Reconnect</button>
<button class="pure-button button-reset">Reset</button>
</div>
@ -302,7 +302,7 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="hostname">Hostname</label>
<input name="hostname" class="pure-u-1 pure-u-md-3-4" type="text" tabindex="1" />
<input name="hostname" class="pure-u-1 pure-u-md-3-4" type="text" action="reconnect" tabindex="1" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">This name will identify this device in your network (http://&lt;hostname&gt;.local). For this setting to take effect you should restart the wifi interface clicking the "Reconnect" button.</div>
</div>
@ -357,7 +357,7 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="btnDelay">Double click delay</label>
<input name="btnDelay" class="pure-u-1 pure-u-md-3-4" type="number" min="0" step="100" max="1000" tabindex="6" />
<input name="btnDelay" class="pure-u-1 pure-u-md-3-4" type="number" action="reset" min="0" step="100" max="1000" tabindex="6" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">Delay in milliseconds to detect a double click (from 0 to 1000ms).<br />
The lower this number the faster the device will respond to button clicks but the harder it will be to get a double click.
@ -369,7 +369,7 @@
<div class="pure-g module module-color">
<div class="pure-u-1 pure-u-sm-1-4"><label for="useColor">Use colorpicker</label></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="useColor" tabindex="8" /></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="useColor" action="reload" tabindex="8" /></div>
<div class="pure-u-0 pure-u-md-1-2">&nbsp;</div>
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">Use color picker for the first 3 channels as RGB.<br />Will only work if the device has at least 3 dimmable channels.<br />Reload the page to update the web interface.</div>
@ -377,7 +377,7 @@
<div class="pure-g module module-color">
<div class="pure-u-1 pure-u-sm-1-4"><label for="useWhite">Use white channel</label></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="useWhite" tabindex="9" /></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="useWhite" action="reload" tabindex="9" /></div>
<div class="pure-u-0 pure-u-md-1-2">&nbsp;</div>
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">Use forth dimmable channel as white when first 3 have the same RGB value.<br />Will only work if the device has at least 4 dimmable channels.<br />Reload the page to update the web interface.</div>
@ -427,7 +427,7 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="adminPass1">Admin password</label>
<input name="adminPass1" class="pure-u-1 pure-u-md-3-4" type="password" tabindex="11" autocomplete="false" />
<input name="adminPass1" class="pure-u-1 pure-u-md-3-4" type="password" action="reset" tabindex="11" autocomplete="false" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">
The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).<br />
@ -436,12 +436,12 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="adminPass2">Repeat password</label>
<input name="adminPass2" class="pure-u-1 pure-u-md-3-4" type="password" tabindex="12" autocomplete="false" />
<input name="adminPass2" class="pure-u-1 pure-u-md-3-4" type="password" action="reset" tabindex="12" autocomplete="false" />
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="webPort">HTTP port</label>
<input name="webPort" class="pure-u-1 pure-u-md-3-4" type="text" tabindex="13" />
<input name="webPort" class="pure-u-1 pure-u-md-3-4" type="text" action="reset" tabindex="13" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">
This is the port for the web interface and API requests.<br />
@ -680,7 +680,7 @@
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="dczTopicOut">Domoticz OUT Topic</label>
<input class="pure-u-1 pure-u-md-3-4" name="dczTopicOut" type="text" tabindex="32" />
<input class="pure-u-1 pure-u-md-3-4" name="dczTopicOut" type="text" action="reconnect" tabindex="32" />
</div>
<div class="pure-g module module-dht module-ds">
@ -886,7 +886,7 @@
<div class="pure-g">
<label class="pure-u-md-1-6 pure-u-1-4" for="ssid">Network SSID</label>
<div class="pure-u-md-3-4 pure-u-5-8"><input name="ssid" type="text" class="pure-u-23-24" value="" size="8" tabindex="0" placeholder="Network SSID" required autocomplete="false" /></div>
<div class="pure-u-md-3-4 pure-u-5-8"><input name="ssid" type="text" action="reconnect" class="pure-u-23-24" value="" size="8" tabindex="0" placeholder="Network SSID" required autocomplete="false" /></div>
<div class="pure-u-md-1-12 pure-u-1-8"><button type="button" class="pure-button button-more-network pure-u-1">...</button></div>
<div class="more">
@ -894,27 +894,27 @@
<div class="break"></div>
<label class="pure-u-md-1-6 pure-u-1-4" for="pass">Password</label>
<input class="pure-u-md-5-6 pure-u-3-4" name="pass" type="password" value="" tabindex="0" autocomplete="false" />
<input class="pure-u-md-5-6 pure-u-3-4" name="pass" type="password" action="reconnect" value="" tabindex="0" autocomplete="false" />
<div class="break"></div>
<label class="pure-u-md-1-6 pure-u-1-4" for="ip">Static IP</label>
<input class="pure-u-md-5-6 pure-u-3-4" name="ip" type="text" value="" size="15" tabindex="0" autocomplete="false" />
<input class="pure-u-md-5-6 pure-u-3-4" name="ip" type="text" action="reconnect" value="" size="15" tabindex="0" autocomplete="false" />
<div class="pure-u-md-1-6 pure-u-1-4"></div>
<div class="pure-u-md-5-6 pure-u-3-4 hint">Leave empty for DNS negotiation</div>
<label class="pure-u-md-1-6 pure-u-1-4" for="gw">Gateway IP</label>
<input class="pure-u-md-5-6 pure-u-3-4" name="gw" type="text" value="" size="15" tabindex="0" autocomplete="false" />
<input class="pure-u-md-5-6 pure-u-3-4" name="gw" type="text" action="reconnect" value="" size="15" tabindex="0" autocomplete="false" />
<div class="pure-u-md-1-6 pure-u-1-4"></div>
<div class="pure-u-md-5-6 pure-u-3-4 hint">Set when using a static IP</div>
<label class="pure-u-md-1-6 pure-u-1-4" for="mask">Network Mask</label>
<input class="pure-u-md-5-6 pure-u-3-4" name="mask" type="text" value="255.255.255.0" size="15" tabindex="0" autocomplete="false" />
<input class="pure-u-md-5-6 pure-u-3-4" name="mask" type="text" action="reconnect" value="255.255.255.0" size="15" tabindex="0" autocomplete="false" />
<div class="pure-u-md-1-6 pure-u-1-4"></div>
<div class="pure-u-md-5-6 pure-u-3-4 hint">Usually 255.255.255.0 for /24 networks</div>
<label class="pure-u-md-1-6 pure-u-1-4" for="dns">DNS IP</label>
<input class="pure-u-md-5-6 pure-u-3-4" name="dns" type="text" value="8.8.8.8" size="15" tabindex="0" autocomplete="false" />
<input class="pure-u-md-5-6 pure-u-3-4" name="dns" type="text" action="reconnect" value="8.8.8.8" size="15" tabindex="0" autocomplete="false" />
<div class="pure-u-md-1-6 pure-u-1-4"></div>
<div class="pure-u-md-5-6 pure-u-3-4 hint">Set the Domain Name Server IP to use when using a static IP</div>


Loading…
Cancel
Save