mirror of
https://github.com/Laxilef/OTGateway.git
synced 2026-11-02 23:18:44 +05:00
* feat: new portal & network manager
* refactor: migrate from PubSubClient to ArduinoMqttClient * refactor: migrate from EEManager to FileData * chore: bump ESP Telnet to 2.2 * chore: bump TinyLogger to 1.1.0
This commit is contained in:
1 parent
b36e4dca42
commit
ab1e9c761f
34 files changed
+4683
-1125
No files matched your search
@@ -0,0 +1,64 @@
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
header,
|
||||
main,
|
||||
footer {
|
||||
padding-top: 1.5em !important;
|
||||
padding-bottom: 1.5em !important;
|
||||
}
|
||||
|
||||
article {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.success {
|
||||
background-color: var(--pico-form-element-valid-border-color);
|
||||
border-color: var(--pico-form-element-valid-border-color);
|
||||
}
|
||||
|
||||
button.failed {
|
||||
background-color: var(--pico-form-element-invalid-border-color);
|
||||
border-color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
tr.network:hover {
|
||||
--pico-background-color: var(--pico-primary-focus);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.greatSignal {
|
||||
background-color: var(--pico-form-element-valid-border-color);
|
||||
}
|
||||
|
||||
.normalSignal {
|
||||
background-color: #e48500;
|
||||
}
|
||||
|
||||
.badSignal {
|
||||
background-color: var(--pico-form-element-invalid-border-color);
|
||||
}
|
||||
|
||||
.primary {
|
||||
border: 0.25em solid var(--pico-form-element-invalid-border-color);
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
function setupForm(formSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
button.onmouseout = function (event) {
|
||||
if (button.hasAttribute('aria-busy')) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
};
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
button.textContent = 'Please wait...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = function (response) {
|
||||
if (button) {
|
||||
button.textContent = 'Saved';
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.add('success');
|
||||
button.removeAttribute('aria-busy');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const onFailed = function (response) {
|
||||
if (button) {
|
||||
button.textContent = 'Error';
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let fd = new FormData(form);
|
||||
let checkboxes = form.querySelectorAll('input[type="checkbox"]');
|
||||
for (let checkbox of checkboxes) {
|
||||
fd.append(checkbox.getAttribute('name'), checkbox.checked);
|
||||
}
|
||||
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: form2json(fd)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupNetworkScanForm(formSelector, tableSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
console.error("form not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.innerHTML;
|
||||
}
|
||||
|
||||
const onSubmitFn = async function (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = 'Please wait...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
let table = document.querySelector(tableSelector);
|
||||
if (!table) {
|
||||
console.error("table not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const onSuccess = async function (response) {
|
||||
let result = await response.json();
|
||||
console.log('networks: ', result);
|
||||
|
||||
let tbody = table.querySelector('tbody');
|
||||
if (!tbody) {
|
||||
tbody = table.createTBody();
|
||||
}
|
||||
|
||||
while (tbody.rows.length > 0) {
|
||||
tbody.rows[0].remove();
|
||||
}
|
||||
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
let row = tbody.insertRow(-1);
|
||||
row.classList.add("network");
|
||||
row.setAttribute('data-ssid', result[i].hidden ? '' : result[i].ssid);
|
||||
row.onclick = function () {
|
||||
const input = document.querySelector('input.sta-ssid');
|
||||
const ssid = this.getAttribute('data-ssid');
|
||||
if (!input || !ssid) {
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = ssid;
|
||||
input.focus();
|
||||
};
|
||||
|
||||
row.insertCell().textContent = "#" + (i + 1);
|
||||
row.insertCell().innerHTML = result[i].hidden ? '<i>Hidden</i>' : result[i].ssid;
|
||||
|
||||
const signalCell = row.insertCell();
|
||||
const signalElement = document.createElement("kbd");
|
||||
signalElement.textContent = result[i].signalQuality + "%";
|
||||
if (result[i].signalQuality > 60) {
|
||||
signalElement.classList.add('greatSignal');
|
||||
} else if (result[i].signalQuality > 40) {
|
||||
signalElement.classList.add('normalSignal');
|
||||
} else {
|
||||
signalElement.classList.add('badSignal');
|
||||
}
|
||||
signalCell.appendChild(signalElement);
|
||||
}
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
const onFailed = async function (response) {
|
||||
table.classList.remove('hidden');
|
||||
|
||||
if (button) {
|
||||
button.innerHTML = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
let attempts = 5;
|
||||
let timer = setInterval(async function () {
|
||||
attempts--;
|
||||
|
||||
try {
|
||||
let response = await fetch(url, { cache: 'no-cache' });
|
||||
|
||||
if (response.status == 200) {
|
||||
clearInterval(timer);
|
||||
await onSuccess(response);
|
||||
|
||||
} else if (attempts <= 0) {
|
||||
await onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
clearInterval(timer);
|
||||
onFailed(err);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
form.addEventListener('submit', onSubmitFn);
|
||||
onSubmitFn();
|
||||
}
|
||||
|
||||
function setupRestoreBackupForm(formSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
button.onmouseout = function (event) {
|
||||
if (button.hasAttribute('aria-busy')) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
};
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
button.textContent = 'Please wait...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
const onSuccess = function (response) {
|
||||
if (button) {
|
||||
button.textContent = 'Restored';
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.add('success');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
const onFailed = function (response) {
|
||||
if (button) {
|
||||
button.textContent = 'Error';
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
const files = form.querySelector('#restore-file').files;
|
||||
if (files.length <= 0) {
|
||||
onFailed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(files[0]);
|
||||
reader.onload = async function() {
|
||||
try {
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: reader.result
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
};
|
||||
reader.onerror = function() {
|
||||
console.log(reader.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setupUpgradeForm(formSelector) {
|
||||
const form = document.querySelector(formSelector);
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = form.action;
|
||||
let button = form.querySelector('button[type="submit"]');
|
||||
let defaultText;
|
||||
|
||||
if (button) {
|
||||
defaultText = button.textContent;
|
||||
button.onmouseout = function (event) {
|
||||
if (button.hasAttribute('aria-busy')) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.classList.remove('success', 'failed');
|
||||
button.textContent = defaultText;
|
||||
};
|
||||
}
|
||||
|
||||
const statusToText = function (status) {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "None";
|
||||
case 1:
|
||||
return "No file";
|
||||
case 2:
|
||||
return "Success";
|
||||
case 3:
|
||||
return "Prohibited";
|
||||
case 4:
|
||||
return "Aborted";
|
||||
case 5:
|
||||
return "Error on start";
|
||||
case 6:
|
||||
return "Error on write";
|
||||
case 7:
|
||||
return "Error on finish";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const onResult = async function (response) {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
let resItem = form.querySelector('.upgrade-firmware-result');
|
||||
if (resItem && result.firmware.status > 1) {
|
||||
resItem.textContent = statusToText(result.firmware.status);
|
||||
resItem.classList.remove('hidden');
|
||||
|
||||
if (result.firmware.status == 2) {
|
||||
resItem.classList.remove('failed');
|
||||
resItem.classList.add('success');
|
||||
} else {
|
||||
resItem.classList.remove('success');
|
||||
resItem.classList.add('failed');
|
||||
|
||||
if (result.firmware.error != "") {
|
||||
resItem.textContent += ": " + result.firmware.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resItem = form.querySelector('.upgrade-filesystem-result');
|
||||
if (resItem && result.filesystem.status > 1) {
|
||||
resItem.textContent = statusToText(result.filesystem.status);
|
||||
resItem.classList.remove('hidden');
|
||||
|
||||
if (result.filesystem.status == 2) {
|
||||
resItem.classList.remove('failed');
|
||||
resItem.classList.add('success');
|
||||
} else {
|
||||
resItem.classList.remove('success');
|
||||
resItem.classList.add('failed');
|
||||
|
||||
if (result.filesystem.error != "") {
|
||||
resItem.textContent += ": " + result.filesystem.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onSuccess = function (response) {
|
||||
onResult(response);
|
||||
|
||||
if (button) {
|
||||
button.textContent = defaultText;
|
||||
button.removeAttribute('disabled');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
const onFailed = function (response) {
|
||||
if (button) {
|
||||
button.textContent = 'Error';
|
||||
button.removeAttribute('disabled');
|
||||
button.classList.add('failed');
|
||||
button.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (button) {
|
||||
button.textContent = 'Uploading...';
|
||||
button.setAttribute('disabled', true);
|
||||
button.setAttribute('aria-busy', true);
|
||||
}
|
||||
|
||||
try {
|
||||
let fd = new FormData(form);
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
cache: 'no-cache',
|
||||
body: fd
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 500) {
|
||||
onSuccess(response);
|
||||
|
||||
} else {
|
||||
onFailed(response);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
onFailed(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadNetworkStatus() {
|
||||
let response = await fetch('/api/network/status', { cache: 'no-cache' });
|
||||
let result = await response.json();
|
||||
|
||||
setValue('.network-hostname', result.hostname);
|
||||
setValue('.network-mac', result.mac);
|
||||
setState('.network-connected', result.isConnected);
|
||||
setValue('.network-ssid', result.ssid);
|
||||
setValue('.network-signal', result.signalQuality);
|
||||
setValue('.network-ip', result.ip);
|
||||
setValue('.network-subnet', result.subnet);
|
||||
setValue('.network-gateway', result.gateway);
|
||||
setValue('.network-dns', result.dns);
|
||||
|
||||
setBusy('.main-busy', '.main-table', false);
|
||||
}
|
||||
|
||||
async function loadNetworkSettings() {
|
||||
let response = await fetch('/api/network/settings', { cache: 'no-cache' });
|
||||
let result = await response.json();
|
||||
|
||||
setInputValue('.network-hostname', result.hostname);
|
||||
setCheckboxValue('.network-use-dhcp', result.useDhcp);
|
||||
setInputValue('.network-static-ip', result.staticConfig.ip);
|
||||
setInputValue('.network-static-gateway', result.staticConfig.gateway);
|
||||
setInputValue('.network-static-subnet', result.staticConfig.subnet);
|
||||
setInputValue('.network-static-dns', result.staticConfig.dns);
|
||||
setBusy('#network-settings-busy', '#network-settings', false);
|
||||
|
||||
setInputValue('.sta-ssid', result.sta.ssid);
|
||||
setInputValue('.sta-password', result.sta.password);
|
||||
setInputValue('.sta-channel', result.sta.channel);
|
||||
setBusy('#sta-settings-busy', '#sta-settings', false);
|
||||
|
||||
setInputValue('.ap-ssid', result.ap.ssid);
|
||||
setInputValue('.ap-password', result.ap.password);
|
||||
setInputValue('.ap-channel', result.ap.channel);
|
||||
setBusy('#ap-settings-busy', '#ap-settings', false);
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
let response = await fetch('/api/settings', { cache: 'no-cache' });
|
||||
let result = await response.json();
|
||||
|
||||
setCheckboxValue('.portal-use-auth', result.portal.useAuth);
|
||||
setInputValue('.portal-login', result.portal.login);
|
||||
setInputValue('.portal-password', result.portal.password);
|
||||
setBusy('#portal-settings-busy', '#portal-settings', false);
|
||||
|
||||
setInputValue('.opentherm-in-pin', result.opentherm.inPin);
|
||||
setInputValue('.opentherm-out-pin', result.opentherm.outPin);
|
||||
setInputValue('.opentherm-member-id-code', result.opentherm.memberIdCode);
|
||||
setCheckboxValue('.opentherm-dhw-present', result.opentherm.dhwPresent);
|
||||
setCheckboxValue('.opentherm-sw-mode', result.opentherm.summerWinterMode);
|
||||
setCheckboxValue('.opentherm-heating-ch2-enabled', result.opentherm.heatingCh2Enabled);
|
||||
setCheckboxValue('.opentherm-heating-ch1-to-ch2', result.opentherm.heatingCh1ToCh2);
|
||||
setCheckboxValue('.opentherm-dhw-to-ch2', result.opentherm.dhwToCh2);
|
||||
setCheckboxValue('.opentherm-dhw-blocking', result.opentherm.dhwBlocking);
|
||||
setCheckboxValue('.opentherm-sync-modulation-with-heating', result.opentherm.modulationSyncWithHeating);
|
||||
setBusy('#opentherm-settings-busy', '#opentherm-settings', false);
|
||||
|
||||
setInputValue('.mqtt-server', result.mqtt.server);
|
||||
setInputValue('.mqtt-port', result.mqtt.port);
|
||||
setInputValue('.mqtt-user', result.mqtt.user);
|
||||
setInputValue('.mqtt-password', result.mqtt.password);
|
||||
setInputValue('.mqtt-prefix', result.mqtt.prefix);
|
||||
setInputValue('.mqtt-interval', result.mqtt.interval);
|
||||
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
|
||||
|
||||
setInputValue('.sensors-outdoor-pin', result.sensors.outdoor.pin);
|
||||
setInputValue('.sensors-outdoor-offset', result.sensors.outdoor.offset);
|
||||
setInputValue('.sensors-indoor-pin', result.sensors.indoor.pin);
|
||||
setInputValue('.sensors-indoor-offset', result.sensors.indoor.offset);
|
||||
setInputValue('.sensors-indoor-ble-addresss', result.sensors.indoor.bleAddresss);
|
||||
setBusy('#sensors-settings-busy', '#sensors-settings', false);
|
||||
|
||||
setCheckboxValue('.extpump-use', result.externalPump.use);
|
||||
setInputValue('.extpump-pin', result.externalPump.pin);
|
||||
setInputValue('.extpump-pc-time', result.externalPump.postCirculationTime);
|
||||
setInputValue('.extpump-as-interval', result.externalPump.antiStuckInterval);
|
||||
setInputValue('.extpump-as-time', result.externalPump.antiStuckTime);
|
||||
setBusy('#extpump-settings-busy', '#extpump-settings', false);
|
||||
}
|
||||
|
||||
async function loadVars() {
|
||||
let response = await fetch('/api/vars');
|
||||
let result = await response.json();
|
||||
|
||||
setState('.ot-connected', result.states.otStatus);
|
||||
setState('.ot-emergency', result.states.emergency);
|
||||
setState('.ot-heating', result.states.heating);
|
||||
setState('.ot-dhw', result.states.dhw);
|
||||
setState('.ot-flame', result.states.flame);
|
||||
setState('.ot-fault', result.states.fault);
|
||||
setState('.ot-diagnostic', result.states.diagnostic);
|
||||
setState('.ot-external-pump', result.states.externalPump);
|
||||
|
||||
setValue('.ot-modulation', result.sensors.modulation);
|
||||
setValue('.ot-pressure', result.sensors.pressure);
|
||||
setValue('.ot-dhw-flow-rate', result.sensors.dhwFlowRate);
|
||||
setValue('.ot-fault-code', result.sensors.faultCode ? ("E" + result.sensors.faultCode) : "-");
|
||||
|
||||
setValue('.indoor-temp', result.temperatures.indoor);
|
||||
setValue('.outdoor-temp', result.temperatures.outdoor);
|
||||
setValue('.heating-temp', result.temperatures.heating);
|
||||
setValue('.heating-setpoint-temp', result.parameters.heatingSetpoint);
|
||||
setValue('.dhw-temp', result.temperatures.dhw);
|
||||
|
||||
setBusy('.ot-busy', '.ot-table', false);
|
||||
|
||||
setValue('.version', result.system.version);
|
||||
setValue('.build-date', result.system.buildDate);
|
||||
setValue('.uptime', result.system.uptime);
|
||||
setValue('.free-heap', result.system.freeHeap);
|
||||
setValue('.total-heap', result.system.totalHeap);
|
||||
setValue('.max-free-block-heap', result.system.maxFreeBlockHeap);
|
||||
setValue('.reset-reason', result.system.resetReason);
|
||||
setState('.mqtt-connected', result.system.mqttConnected);
|
||||
|
||||
setBusy('.system-busy', '.system-table', false);
|
||||
}
|
||||
|
||||
function setBusy(busySelector, contentSelector, value) {
|
||||
let busy = document.querySelector(busySelector);
|
||||
let content = document.querySelector(contentSelector);
|
||||
if (!busy || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
busy.classList.add('hidden');
|
||||
content.classList.remove('hidden');
|
||||
|
||||
} else {
|
||||
busy.classList.remove('hidden');
|
||||
content.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function setState(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.setAttribute('aria-invalid', !value);
|
||||
}
|
||||
|
||||
function setValue(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.innerHTML = value;
|
||||
}
|
||||
|
||||
function setCheckboxValue(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.checked = value;
|
||||
}
|
||||
|
||||
function setInputValue(selector, value) {
|
||||
let item = document.querySelector(selector);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.value = value;
|
||||
}
|
||||
|
||||
|
||||
function form2json(data) {
|
||||
let method = function (object, pair) {
|
||||
let keys = pair[0].replace(/\]/g, '').split('[');
|
||||
let key = keys[0];
|
||||
let value = pair[1];
|
||||
if (value === 'true' || value === 'false') {
|
||||
value = value === 'true';
|
||||
} else if (typeof(value) === 'string' && value.trim() !== '' && !isNaN(value)) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
if (keys.length > 1) {
|
||||
let i, x, segment;
|
||||
let last = value;
|
||||
let type = isNaN(keys[1]) ? {} : [];
|
||||
value = segment = object[key] || type;
|
||||
|
||||
for (i = 1; i < keys.length; i++) {
|
||||
x = keys[i];
|
||||
if (i == keys.length - 1) {
|
||||
if (Array.isArray(segment)) {
|
||||
segment.push(last);
|
||||
} else {
|
||||
segment[x] = last;
|
||||
}
|
||||
} else if (segment[x] == undefined) {
|
||||
segment[x] = isNaN(keys[i + 1]) ? {} : [];
|
||||
}
|
||||
segment = segment[x];
|
||||
}
|
||||
}
|
||||
|
||||
object[key] = value;
|
||||
return object;
|
||||
}
|
||||
|
||||
let object = Array.from(data).reduce(method, {});
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
@charset "UTF-8";/*!
|
||||
* Pico CSS ✨ v2.0.0-rc2 (https://picocss.com)
|
||||
* Copyright 2019-2024 - Licensed under MIT
|
||||
*/:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:0.1rem;--pico-border-radius:0.25rem;--pico-border-width:0.0625rem;--pico-outline-width:0.125rem;--pico-transition:0.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:calc(var(--pico-spacing) * 2);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:0.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 rgba(0, 0, 0, 0);--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 0.0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(0.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:0.5rem;--pico-nav-link-spacing-vertical:0.5rem;--pico-nav-link-spacing-horizontal:0.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--pico-icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--pico-icon-loading:url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E")}@media (min-width:576px){:root{--pico-font-size:106.25%}}@media (min-width:768px){:root{--pico-font-size:112.5%}}@media (min-width:1024px){:root{--pico-font-size:118.75%}}@media (min-width:1280px){:root{--pico-font-size:125%}}@media (min-width:1536px){:root{--pico-fontLine truncated
|
||||
Reference in new issue
Block a user