mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-15 12:25:15 +03:00
543 lines
17 KiB
JavaScript
543 lines
17 KiB
JavaScript
import { get as getConfig } from './ConfigStorage';
|
|
|
|
window.TABS = {}; // filled by individual tab js file
|
|
|
|
const GUI_MODES = {
|
|
NWJS: "NW.js",
|
|
Cordova: "Cordova",
|
|
Other: "Other",
|
|
};
|
|
|
|
class GuiControl {
|
|
constructor() {
|
|
this.auto_connect = false;
|
|
this.connecting_to = false;
|
|
this.connected_to = false;
|
|
this.connect_lock = false;
|
|
this.active_tab = null;
|
|
this.tab_switch_in_progress = false;
|
|
this.operating_system = null;
|
|
this.interval_array = [];
|
|
this.timeout_array = [];
|
|
this.buttonDisabledClass = "disabled";
|
|
|
|
this.defaultAllowedTabsWhenDisconnected = [
|
|
'landing',
|
|
'changelog',
|
|
'firmware_flasher',
|
|
'privacy_policy',
|
|
'options',
|
|
'help',
|
|
];
|
|
this.defaultAllowedFCTabsWhenConnected = [
|
|
'setup',
|
|
'failsafe',
|
|
'transponder',
|
|
'osd',
|
|
'power',
|
|
'adjustments',
|
|
'auxiliary',
|
|
'presets',
|
|
'cli',
|
|
'configuration',
|
|
'gps',
|
|
'led_strip',
|
|
'logging',
|
|
'onboard_logging',
|
|
'modes',
|
|
'motors',
|
|
'pid_tuning',
|
|
'ports',
|
|
'receiver',
|
|
'sensors',
|
|
'servos',
|
|
'vtx',
|
|
];
|
|
|
|
this.allowedTabs = this.defaultAllowedTabsWhenDisconnected;
|
|
|
|
// check which operating system is user running
|
|
this.operating_system = GUI_checkOperatingSystem();
|
|
|
|
// Check the method of execution
|
|
this.nwGui = null;
|
|
try {
|
|
this.nwGui = require('nw.gui');
|
|
this.Mode = GUI_MODES.NWJS;
|
|
} catch (ex) {
|
|
if (typeof cordovaApp !== 'undefined') {
|
|
this.Mode = GUI_MODES.Cordova;
|
|
} else {
|
|
this.Mode = GUI_MODES.Other;
|
|
}
|
|
}
|
|
}
|
|
// Timer managing methods
|
|
// name = string
|
|
// code = function reference (code to be executed)
|
|
// interval = time interval in miliseconds
|
|
// first = true/false if code should be ran initially before next timer interval hits
|
|
interval_add(name, code, interval, first) {
|
|
const data = { 'name': name, 'timer': null, 'code': code, 'interval': interval, 'fired': 0, 'paused': false };
|
|
|
|
if (first === true) {
|
|
code(); // execute code
|
|
|
|
data.fired++; // increment counter
|
|
}
|
|
|
|
data.timer = setInterval(function () {
|
|
code(); // execute code
|
|
|
|
data.fired++; // increment counter
|
|
}, interval);
|
|
|
|
this.interval_array.push(data); // push to primary interval array
|
|
|
|
return data;
|
|
}
|
|
// name = string
|
|
// code = function reference (code to be executed)
|
|
// interval = time interval in miliseconds
|
|
// first = true/false if code should be ran initially before next timer interval hits
|
|
// condition = function reference with true/false result, a condition to be checked before every interval code execution
|
|
interval_add_condition(name, code, interval, first, condition) {
|
|
this.interval_add(name, () => {
|
|
if (condition()) {
|
|
code();
|
|
} else {
|
|
this.interval_remove(name);
|
|
}
|
|
}, interval, first);
|
|
}
|
|
// name = string
|
|
interval_remove(name) {
|
|
for (let i = 0; i < this.interval_array.length; i++) {
|
|
if (this.interval_array[i].name === name) {
|
|
clearInterval(this.interval_array[i].timer); // stop timer
|
|
|
|
this.interval_array.splice(i, 1); // remove element/object from array
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// name = string
|
|
interval_pause(name) {
|
|
for (let i = 0; i < this.interval_array.length; i++) {
|
|
if (this.interval_array[i].name === name) {
|
|
clearInterval(this.interval_array[i].timer);
|
|
this.interval_array[i].paused = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// name = string
|
|
interval_resume(name) {
|
|
|
|
function executeCode(obj) {
|
|
obj.code(); // execute code
|
|
obj.fired++; // increment counter
|
|
}
|
|
|
|
for (let i = 0; i < this.interval_array.length; i++) {
|
|
if (this.interval_array[i].name === name && this.interval_array[i].paused) {
|
|
const obj = this.interval_array[i];
|
|
|
|
obj.timer = setInterval(executeCode, obj.interval, obj);
|
|
|
|
obj.paused = false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// input = array of timers thats meant to be kept, or nothing
|
|
// return = returns timers killed in last call
|
|
interval_kill_all(keepArray) {
|
|
const self = this;
|
|
let timersKilled = 0;
|
|
|
|
for (let i = (this.interval_array.length - 1); i >= 0; i--) { // reverse iteration
|
|
let keep = false;
|
|
if (keepArray) { // only run through the array if it exists
|
|
keepArray.forEach(function (name) {
|
|
if (self.interval_array[i].name === name) {
|
|
keep = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!keep) {
|
|
clearInterval(this.interval_array[i].timer); // stop timer
|
|
|
|
this.interval_array.splice(i, 1); // remove element/object from array
|
|
|
|
timersKilled++;
|
|
}
|
|
}
|
|
|
|
return timersKilled;
|
|
}
|
|
// name = string
|
|
// code = function reference (code to be executed)
|
|
// timeout = timeout in miliseconds
|
|
timeout_add(name, code, timeout) {
|
|
const self = this;
|
|
const data = {
|
|
'name': name,
|
|
'timer': null,
|
|
'timeout': timeout,
|
|
};
|
|
|
|
// start timer with "cleaning" callback
|
|
data.timer = setTimeout(function () {
|
|
code(); // execute code
|
|
|
|
|
|
// remove object from array
|
|
const index = self.timeout_array.indexOf(data);
|
|
if (index > -1) {
|
|
self.timeout_array.splice(index, 1);
|
|
}
|
|
}, timeout);
|
|
|
|
this.timeout_array.push(data); // push to primary timeout array
|
|
|
|
return data;
|
|
}
|
|
// name = string
|
|
timeout_remove(name) {
|
|
for (let i = 0; i < this.timeout_array.length; i++) {
|
|
if (this.timeout_array[i].name === name) {
|
|
clearTimeout(this.timeout_array[i].timer); // stop timer
|
|
|
|
this.timeout_array.splice(i, 1); // remove element/object from array
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
// no input parameters
|
|
// return = returns timers killed in last call
|
|
timeout_kill_all() {
|
|
let timersKilled = 0;
|
|
|
|
for (let i = 0; i < this.timeout_array.length; i++) {
|
|
clearTimeout(this.timeout_array[i].timer); // stop timer
|
|
|
|
timersKilled++;
|
|
}
|
|
|
|
this.timeout_array = []; // drop objects
|
|
|
|
return timersKilled;
|
|
}
|
|
// message = string
|
|
log(message) {
|
|
const commandLog = $('div#log');
|
|
const d = new Date();
|
|
const year = d.getFullYear();
|
|
const month = (d.getMonth() < 9) ? `0${d.getMonth() + 1}` : (d.getMonth() + 1);
|
|
const date = (d.getDate() < 10) ? `0${d.getDate()}` : d.getDate();
|
|
const hours = (d.getHours() < 10) ? `0${d.getHours()}` : d.getHours();
|
|
const minutes = (d.getMinutes() < 10) ? `0${d.getMinutes()}` : d.getMinutes();
|
|
const seconds = (d.getSeconds() < 10) ? `0${d.getSeconds()}` : d.getSeconds();
|
|
const time = `${hours}:${minutes}:${seconds}`;
|
|
|
|
const formattedDate = `${year}-${month}-${date} @${time}`;
|
|
$('div.wrapper', commandLog).append(`<p>${formattedDate} -- ${message}</p>`);
|
|
commandLog.scrollTop($('div.wrapper', commandLog).height());
|
|
}
|
|
// Method is called every time a valid tab change event is received
|
|
// callback = code to run when cleanup is finished
|
|
// default switch doesn't require callback to be set
|
|
tab_switch_cleanup(callback) {
|
|
MSP.callbacks_cleanup(); // we don't care about any old data that might or might not arrive
|
|
this.interval_kill_all(); // all intervals (mostly data pulling) needs to be removed on tab switch
|
|
|
|
if (this.active_tab && TABS[this.active_tab]) {
|
|
TABS[this.active_tab].cleanup(callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
switchery() {
|
|
|
|
const COLOR_ACCENT = 'var(--accent)';
|
|
const COLOR_SWITCHERY_SECOND = 'var(--switcherysecond)';
|
|
|
|
$('.togglesmall').each(function (index, elem) {
|
|
const switchery = new Switchery(elem, {
|
|
size: 'small',
|
|
color: COLOR_ACCENT,
|
|
secondaryColor: COLOR_SWITCHERY_SECOND,
|
|
});
|
|
$(elem).on("change", function () {
|
|
switchery.setPosition();
|
|
});
|
|
$(elem).removeClass('togglesmall');
|
|
});
|
|
|
|
$('.toggle').each(function (index, elem) {
|
|
const switchery = new Switchery(elem, {
|
|
color: COLOR_ACCENT,
|
|
secondaryColor: COLOR_SWITCHERY_SECOND,
|
|
});
|
|
$(elem).on("change", function () {
|
|
switchery.setPosition();
|
|
});
|
|
$(elem).removeClass('toggle');
|
|
});
|
|
|
|
$('.togglemedium').each(function (index, elem) {
|
|
const switchery = new Switchery(elem, {
|
|
className: 'switcherymid',
|
|
color: COLOR_ACCENT,
|
|
secondaryColor: COLOR_SWITCHERY_SECOND,
|
|
});
|
|
$(elem).on("change", function () {
|
|
switchery.setPosition();
|
|
});
|
|
$(elem).removeClass('togglemedium');
|
|
});
|
|
}
|
|
content_ready(callback) {
|
|
|
|
this.switchery();
|
|
|
|
if (CONFIGURATOR.connectionValid) {
|
|
// Build link to in-use CF version documentation
|
|
const documentationButton = $('div#content #button-documentation');
|
|
documentationButton.html("Wiki");
|
|
documentationButton.attr("href", "https://github.com/betaflight/betaflight/wiki");
|
|
}
|
|
|
|
// loading tooltip
|
|
jQuery(function () {
|
|
|
|
new jBox('Tooltip', {
|
|
attach: '.cf_tip',
|
|
trigger: 'mouseenter',
|
|
closeOnMouseleave: true,
|
|
closeOnClick: 'body',
|
|
delayOpen: 100,
|
|
delayClose: 100,
|
|
position: {
|
|
x: 'right',
|
|
y: 'center',
|
|
},
|
|
outside: 'x',
|
|
});
|
|
|
|
new jBox('Tooltip', {
|
|
theme: 'Widetip',
|
|
attach: '.cf_tip_wide',
|
|
trigger: 'mouseenter',
|
|
closeOnMouseleave: true,
|
|
closeOnClick: 'body',
|
|
delayOpen: 100,
|
|
delayClose: 100,
|
|
position: {
|
|
x: 'right',
|
|
y: 'center',
|
|
},
|
|
outside: 'x',
|
|
});
|
|
});
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
selectDefaultTabWhenConnected() {
|
|
const result = getConfig(['rememberLastTab', 'lastTab']);
|
|
const tab = result.rememberLastTab && result.lastTab ? result.lastTab : 'tab_setup';
|
|
|
|
$(`#tabs ul.mode-connected .${tab} a`).trigger('click');
|
|
}
|
|
isNWJS() {
|
|
return this.Mode === GUI_MODES.NWJS;
|
|
}
|
|
isCordova() {
|
|
return this.Mode === GUI_MODES.Cordova;
|
|
}
|
|
isOther() {
|
|
return this.Mode === GUI_MODES.Other;
|
|
}
|
|
showYesNoDialog(yesNoDialogSettings) {
|
|
// yesNoDialogSettings:
|
|
// title, text, buttonYesText, buttonNoText, buttonYesCallback, buttonNoCallback
|
|
const dialog = $(".dialogYesNo");
|
|
const title = dialog.find(".dialogYesNoTitle");
|
|
const content = dialog.find(".dialogYesNoContent");
|
|
const buttonYes = dialog.find(".dialogYesNo-yesButton");
|
|
const buttonNo = dialog.find(".dialogYesNo-noButton");
|
|
|
|
title.html(yesNoDialogSettings.title);
|
|
content.html(yesNoDialogSettings.text);
|
|
buttonYes.html(yesNoDialogSettings.buttonYesText);
|
|
buttonNo.html(yesNoDialogSettings.buttonNoText);
|
|
|
|
buttonYes.off("click");
|
|
buttonNo.off("click");
|
|
|
|
buttonYes.on("click", () => {
|
|
dialog[0].close();
|
|
yesNoDialogSettings.buttonYesCallback?.();
|
|
});
|
|
|
|
buttonNo.on("click", () => {
|
|
dialog[0].close();
|
|
yesNoDialogSettings.buttonNoCallback?.();
|
|
});
|
|
|
|
dialog[0].showModal();
|
|
}
|
|
showWaitDialog(waitDialogSettings) {
|
|
// waitDialogSettings:
|
|
// title, buttonCancelCallback
|
|
const dialog = $(".dialogWait")[0];
|
|
const title = $(".dialogWaitTitle");
|
|
const buttonCancel = $(".dialogWait-cancelButton");
|
|
|
|
title.html(waitDialogSettings.title);
|
|
buttonCancel.toggle(!!waitDialogSettings.buttonCancelCallback);
|
|
|
|
buttonCancel.off("click");
|
|
|
|
buttonCancel.on("click", () => {
|
|
dialog.close();
|
|
waitDialogSettings.buttonCancelCallback?.();
|
|
});
|
|
|
|
dialog.showModal();
|
|
return dialog;
|
|
}
|
|
showInformationDialog(informationDialogSettings) {
|
|
// informationDialogSettings:
|
|
// title, text, buttonConfirmText
|
|
return new Promise(resolve => {
|
|
const dialog = $(".dialogInformation");
|
|
const title = dialog.find(".dialogInformationTitle");
|
|
const content = dialog.find(".dialogInformationContent");
|
|
const buttonConfirm = dialog.find(".dialogInformation-confirmButton");
|
|
|
|
title.html(informationDialogSettings.title);
|
|
content.html(informationDialogSettings.text);
|
|
buttonConfirm.html(informationDialogSettings.buttonConfirmText);
|
|
|
|
buttonConfirm.off("click");
|
|
|
|
buttonConfirm.on("click", () => {
|
|
dialog[0].close();
|
|
resolve();
|
|
});
|
|
|
|
dialog[0].showModal();
|
|
});
|
|
}
|
|
saveToTextFileDialog(textToSave, suggestedFileName, extension) {
|
|
return new Promise((resolve, reject) => {
|
|
const accepts = [{ description: `${extension.toUpperCase()} files`, extensions: [extension] }];
|
|
|
|
chrome.fileSystem.chooseEntry(
|
|
{
|
|
type: 'saveFile',
|
|
suggestedName: suggestedFileName,
|
|
accepts: accepts,
|
|
},
|
|
entry => this._saveToTextFileDialogFileSelected(entry, textToSave, resolve, reject),
|
|
);
|
|
});
|
|
}
|
|
_saveToTextFileDialogFileSelected(entry, textToSave, resolve, reject) {
|
|
checkChromeRuntimeError();
|
|
|
|
if (!entry) {
|
|
console.log('No file selected for saving');
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
entry.createWriter(writer => {
|
|
writer.onerror = () => {
|
|
reject();
|
|
console.error('Failed to write file');
|
|
};
|
|
|
|
writer.onwriteend = () => {
|
|
if (textToSave.length > 0 && writer.length === 0) {
|
|
writer.write(new Blob([textToSave], { type: 'text/plain' }));
|
|
} else {
|
|
resolve(true);
|
|
console.log('File write complete');
|
|
}
|
|
};
|
|
|
|
writer.truncate(0);
|
|
},
|
|
() => {
|
|
reject();
|
|
console.error('Failed to get file writer');
|
|
});
|
|
}
|
|
readTextFileDialog(extension) {
|
|
const accepts = [{ description: `${extension.toUpperCase()} files`, extensions: [extension] }];
|
|
|
|
return new Promise(resolve => {
|
|
chrome.fileSystem.chooseEntry({ type: 'openFile', accepts: accepts }, function (entry) {
|
|
checkChromeRuntimeError();
|
|
|
|
if (!entry) {
|
|
console.log('No file selected for loading');
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
entry.file((file) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = () => {
|
|
console.error(reader.error);
|
|
reject();
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
addLinksTargetBlank(element) {
|
|
element.find('a').each(function () {
|
|
$(this).attr('target', '_blank');
|
|
});
|
|
}
|
|
}
|
|
|
|
function GUI_checkOperatingSystem() {
|
|
return navigator?.userAgentData?.platform || 'Android';
|
|
}
|
|
|
|
const GUI = new GuiControl();
|
|
|
|
// initialize object into GUI variable
|
|
window.GUI = GUI;
|
|
|
|
export default GUI;
|