mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-25 17:25:16 +03:00
Notes: - The Power & Battery (2), OSD, and CLI tabs have been updated accordingly - HTML default control behavior was used whereever possible - HTML indent for the dialog tag is deliberately off to minimize diff noise in GitHub - Layout is not 100% identical, but spiratually the same - in most cases no changes were needed - OSD’s Font Manager dialog didn’t transition cleanly, so includes some slight restyling - OSD Font Manager previously had a green background/color for the graphic - that “just works” now - Specific cleanup (i.e., calling destroy()) does not seem necessary and was removed - Removal of jBox from setup.js and global scope seems not to affect jBox tooltips - Replacement of single quotes with double in main.js was done automatically by Prettier
476 lines
19 KiB
JavaScript
476 lines
19 KiB
JavaScript
import "./jqueryPlugins";
|
|
import $ from "jquery";
|
|
import "../components/init.js";
|
|
import { gui_log } from "./gui_log.js";
|
|
// same, msp seems to be everywhere used from global scope
|
|
import "./msp/MSPHelper.js";
|
|
import { i18n } from "./localization.js";
|
|
import GUI, { TABS } from "./gui.js";
|
|
import { get as getConfig, set as setConfig } from "./ConfigStorage.js";
|
|
import { checkSetupAnalytics } from "./Analytics.js";
|
|
import { initializeSerialBackend } from "./serial_backend.js";
|
|
import FC from "./fc.js";
|
|
import CONFIGURATOR from "./data_storage.js";
|
|
import CliAutoComplete from "./CliAutoComplete.js";
|
|
import DarkTheme, { setDarkTheme } from "./DarkTheme.js";
|
|
import { isExpertModeEnabled } from "./utils/isExportModeEnabled.js";
|
|
import { updateTabList } from "./utils/updateTabList.js";
|
|
import * as THREE from "three";
|
|
import NotificationManager from "./utils/notifications.js";
|
|
|
|
if (typeof String.prototype.replaceAll === "undefined") {
|
|
String.prototype.replaceAll = function (match, replace) {
|
|
return this.replace(new RegExp(match, "g"), () => replace);
|
|
};
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
appReady();
|
|
});
|
|
|
|
function readConfiguratorVersionMetadata() {
|
|
// These are injected by vite. Check for undefined is needed to prevent race conditions
|
|
CONFIGURATOR.productName =
|
|
typeof __APP_PRODUCTNAME__ !== "undefined" ? __APP_PRODUCTNAME__ : "Betaflight Configurator";
|
|
CONFIGURATOR.version = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0";
|
|
CONFIGURATOR.gitRevision = typeof __APP_REVISION__ !== "undefined" ? __APP_REVISION__ : "unknown";
|
|
}
|
|
|
|
function cleanupLocalStorage() {
|
|
// storage quota is 5MB, we need to clean up some stuff (more info see PR #2937)
|
|
const cleanupLocalStorageList = [
|
|
"cache",
|
|
"firmware",
|
|
"https",
|
|
"selected_board",
|
|
"unifiedConfigLast",
|
|
"unifiedSourceCache",
|
|
];
|
|
|
|
for (const key in localStorage) {
|
|
for (const item of cleanupLocalStorageList) {
|
|
if (key.includes(item)) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
setConfig({ erase_chip: true }); // force erase chip on first run
|
|
}
|
|
|
|
function appReady() {
|
|
readConfiguratorVersionMetadata();
|
|
|
|
cleanupLocalStorage();
|
|
|
|
i18n.init(function () {
|
|
// pass the configurator version as a custom header for every AJAX request.
|
|
$.ajaxSetup({
|
|
headers: {
|
|
"X-CFG-VER": `${CONFIGURATOR.version}`,
|
|
},
|
|
});
|
|
|
|
startProcess();
|
|
|
|
checkSetupAnalytics(function (analyticsService) {
|
|
analyticsService.sendEvent(analyticsService.EVENT_CATEGORIES.APPLICATION, "AppStart", {
|
|
sessionControl: "start",
|
|
configuratorVersion: CONFIGURATOR.getDisplayVersion(),
|
|
gitRevision: CONFIGURATOR.gitRevision,
|
|
productName: CONFIGURATOR.productName,
|
|
operatingSystem: GUI.operating_system,
|
|
language: i18n.selectedLanguage,
|
|
});
|
|
});
|
|
|
|
$("a.connection_button__link").removeClass("disabled");
|
|
$("a.firmware_flasher_button__link").removeClass("disabled");
|
|
|
|
initializeSerialBackend();
|
|
});
|
|
|
|
const showNotifications = getConfig("showNotifications", false).showNotifications;
|
|
if (showNotifications && NotificationManager.checkPermission() === "default") {
|
|
NotificationManager.requestPermission();
|
|
}
|
|
}
|
|
|
|
//Process to execute to real start the app
|
|
function startProcess() {
|
|
// translate to user-selected language
|
|
i18n.localizePage();
|
|
|
|
gui_log(i18n.getMessage("infoVersionOs", { operatingSystem: GUI.operating_system }));
|
|
gui_log(i18n.getMessage("infoVersionConfigurator", { configuratorVersion: CONFIGURATOR.getDisplayVersion() }));
|
|
|
|
$("a.connection_button__link").removeClass("disabled");
|
|
// with Vue reactive system we don't need to call these,
|
|
// our view is reactive to model changes
|
|
// updateTopBarVersion();
|
|
|
|
// log webgl capability
|
|
// it would seem the webgl "enabling" through advanced settings will be ignored in the future
|
|
// and webgl will be supported if gpu supports it by default (canary 40.0.2175.0), keep an eye on this one
|
|
document.createElement("canvas");
|
|
|
|
// log library versions in console to make version tracking easier
|
|
console.log(`Libraries: jQuery - ${$.fn.jquery}, three.js - ${THREE.REVISION}`);
|
|
|
|
// Check if this is the first visit
|
|
if (getConfig("firstRun").firstRun === undefined) {
|
|
setConfig({ firstRun: true });
|
|
import("./tabs/static_tab.js").then(({ staticTab }) => {
|
|
staticTab.initialize("options", () => {
|
|
setTimeout(() => {
|
|
// Open the options tab after a delay
|
|
$("#tabs .tab_options a").click();
|
|
}, 100);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Tabs
|
|
$("#tabs ul.mode-connected li").click(function () {
|
|
// store the first class of the current tab (omit things like ".active")
|
|
const tabName = $(this).attr("class").split(" ")[0];
|
|
|
|
const tabNameWithoutPrefix = tabName.substring(4);
|
|
if (tabNameWithoutPrefix !== "cli") {
|
|
// Don't store 'cli' otherwise you can never connect to another tab.
|
|
setConfig({ lastTab: tabName });
|
|
}
|
|
});
|
|
|
|
$("a.firmware_flasher_button__link").on("click", function () {
|
|
if (
|
|
$("a.firmware_flasher_button__label").hasClass("active") &&
|
|
$("a.firmware_flasher_button__link").hasClass("active")
|
|
) {
|
|
$("a.firmware_flasher_button__label").removeClass("active");
|
|
$("a.firmware_flasher_button__link").removeClass("active");
|
|
$("#tabs ul.mode-disconnected .tab_landing a").click();
|
|
} else {
|
|
$("#tabs ul.mode-disconnected .tab_firmware_flasher a").click();
|
|
$("a.firmware_flasher_button__label").addClass("active");
|
|
$("a.firmware_flasher_button__link").addClass("active");
|
|
}
|
|
});
|
|
|
|
const ui_tabs = $("#tabs > ul");
|
|
$("a", "#tabs > ul").click(function () {
|
|
if ($(this).parent().hasClass("active") === false && !GUI.tab_switch_in_progress) {
|
|
// only initialize when the tab isn't already active
|
|
const self = this;
|
|
const tabClass = $(self).parent().prop("class");
|
|
|
|
const tabRequiresConnection = $(self).parent().hasClass("mode-connected");
|
|
|
|
const tab = tabClass.substring(4);
|
|
const tabName = $(self).text();
|
|
|
|
if (tabRequiresConnection && !CONFIGURATOR.connectionValid) {
|
|
gui_log(i18n.getMessage("tabSwitchConnectionRequired"));
|
|
return;
|
|
}
|
|
|
|
if (GUI.connect_lock) {
|
|
// tab switching disabled while operation is in progress
|
|
gui_log(i18n.getMessage("tabSwitchWaitForOperation"));
|
|
return;
|
|
}
|
|
|
|
if (GUI.allowedTabs.indexOf(tab) < 0 && tab === "firmware_flasher") {
|
|
if (GUI.connected_to || GUI.connecting_to) {
|
|
$("a.connection_button__link").click();
|
|
}
|
|
// this line is required but it triggers opening the firmware flasher tab again
|
|
$("a.firmware_flasher_button__link").click();
|
|
} else if (GUI.allowedTabs.indexOf(tab) < 0) {
|
|
gui_log(i18n.getMessage("tabSwitchUpgradeRequired", [tabName]));
|
|
return;
|
|
}
|
|
|
|
GUI.tab_switch_in_progress = true;
|
|
|
|
GUI.tab_switch_cleanup(function () {
|
|
// disable active firmware flasher if it was active
|
|
if (
|
|
$("a.firmware_flasher_button__label").hasClass("active") &&
|
|
$("a.firmware_flasher_button__link").hasClass("active")
|
|
) {
|
|
$("a.firmware_flasher_button__label").removeClass("active");
|
|
$("a.firmware_flasher_button__link").removeClass("active");
|
|
}
|
|
// disable previously active tab highlight
|
|
$("li", ui_tabs).removeClass("active");
|
|
|
|
// Highlight selected tab
|
|
$(self).parent().addClass("active");
|
|
|
|
// detach listeners and remove element data
|
|
const content = $("#content");
|
|
content.empty();
|
|
|
|
// display loading screen
|
|
$("#cache .data-loading").clone().appendTo(content);
|
|
|
|
function content_ready() {
|
|
GUI.tab_switch_in_progress = false;
|
|
}
|
|
|
|
checkSetupAnalytics(function (analyticsService) {
|
|
analyticsService.sendAppView(tab);
|
|
});
|
|
|
|
switch (tab) {
|
|
case "landing":
|
|
import("./tabs/landing").then(({ landing }) => landing.initialize(content_ready));
|
|
break;
|
|
case "changelog":
|
|
import("./tabs/static_tab").then(({ staticTab }) =>
|
|
staticTab.initialize("changelog", content_ready),
|
|
);
|
|
break;
|
|
case "privacy_policy":
|
|
import("./tabs/static_tab").then(({ staticTab }) =>
|
|
staticTab.initialize("privacy_policy", content_ready),
|
|
);
|
|
break;
|
|
case "options":
|
|
import("./tabs/options").then(({ options }) => options.initialize(content_ready));
|
|
break;
|
|
case "firmware_flasher":
|
|
import("./tabs/firmware_flasher").then(({ firmware_flasher }) =>
|
|
firmware_flasher.initialize(content_ready),
|
|
);
|
|
break;
|
|
case "help":
|
|
import("./tabs/help").then(({ help }) => help.initialize(content_ready));
|
|
break;
|
|
case "auxiliary":
|
|
import("./tabs/auxiliary").then(({ auxiliary }) => auxiliary.initialize(content_ready));
|
|
break;
|
|
case "adjustments":
|
|
import("./tabs/adjustments").then(({ adjustments }) => adjustments.initialize(content_ready));
|
|
break;
|
|
case "ports":
|
|
import("./tabs/ports").then(({ ports }) => ports.initialize(content_ready));
|
|
break;
|
|
case "led_strip":
|
|
import("./tabs/led_strip").then(({ led_strip }) => led_strip.initialize(content_ready));
|
|
break;
|
|
case "failsafe":
|
|
import("./tabs/failsafe").then(({ failsafe }) => failsafe.initialize(content_ready));
|
|
break;
|
|
case "transponder":
|
|
import("./tabs/transponder").then(({ transponder }) => transponder.initialize(content_ready));
|
|
break;
|
|
case "osd":
|
|
import("./tabs/osd").then(({ osd }) => osd.initialize(content_ready));
|
|
break;
|
|
case "vtx":
|
|
import("./tabs/vtx").then(({ vtx }) => vtx.initialize(content_ready));
|
|
break;
|
|
case "power":
|
|
import("./tabs/power").then(({ power }) => power.initialize(content_ready));
|
|
break;
|
|
case "setup":
|
|
import("./tabs/setup").then(({ setup }) => setup.initialize(content_ready));
|
|
break;
|
|
case "setup_osd":
|
|
import("./tabs/setup_osd").then(({ setup_osd }) => setup_osd.initialize(content_ready));
|
|
break;
|
|
case "configuration":
|
|
import("./tabs/configuration").then(({ configuration }) =>
|
|
configuration.initialize(content_ready),
|
|
);
|
|
break;
|
|
case "pid_tuning":
|
|
import("./tabs/pid_tuning").then(({ pid_tuning }) => pid_tuning.initialize(content_ready));
|
|
break;
|
|
case "receiver":
|
|
import("./tabs/receiver").then(({ receiver }) => receiver.initialize(content_ready));
|
|
break;
|
|
case "servos":
|
|
import("./tabs/servos").then(({ servos }) => servos.initialize(content_ready));
|
|
break;
|
|
case "gps":
|
|
import("./tabs/gps").then(({ gps }) => gps.initialize(content_ready));
|
|
break;
|
|
case "motors":
|
|
import("./tabs/motors").then(({ motors }) => motors.initialize(content_ready));
|
|
break;
|
|
case "sensors":
|
|
import("./tabs/sensors").then(({ sensors }) => sensors.initialize(content_ready));
|
|
break;
|
|
case "logging":
|
|
import("./tabs/logging").then(({ logging }) => logging.initialize(content_ready));
|
|
break;
|
|
case "onboard_logging":
|
|
import("./tabs/onboard_logging").then(({ onboard_logging }) =>
|
|
onboard_logging.initialize(content_ready),
|
|
);
|
|
break;
|
|
case "cli":
|
|
import("./tabs/cli").then(({ cli }) => cli.initialize(content_ready));
|
|
break;
|
|
case "presets":
|
|
import("../tabs/presets/presets").then(({ presets }) => presets.initialize(content_ready));
|
|
break;
|
|
|
|
default:
|
|
console.log(`Tab not found: ${tab}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
$(".tab_container").removeClass("reveal");
|
|
$("#background").hide();
|
|
});
|
|
|
|
$("#tabs ul.mode-disconnected li a:first").click();
|
|
|
|
$("#menu_btn").on("click", function () {
|
|
$(".tab_container").toggleClass("reveal");
|
|
$("#background").toggle();
|
|
});
|
|
|
|
$("#background").on("click", function () {
|
|
$(".tab_container").removeClass("reveal");
|
|
$("#background").hide();
|
|
});
|
|
|
|
$(window).on("resize", function () {
|
|
// 575px is the mobile breakpoint defined in CSS
|
|
if (window.innerWidth > 575) {
|
|
$(".tab_container").removeClass("reveal");
|
|
$("#background").hide();
|
|
}
|
|
});
|
|
|
|
// listen to all input change events and adjust the value within limits if necessary
|
|
$("#content").on("focus", 'input[type="number"]', function () {
|
|
const element = $(this);
|
|
const val = element.val();
|
|
|
|
if (!isNaN(val)) {
|
|
element.data("previousValue", parseFloat(val));
|
|
}
|
|
});
|
|
|
|
$("#content").on("change", 'input[type="number"]', function () {
|
|
const element = $(this);
|
|
const min = parseFloat(element.prop("min"));
|
|
const max = parseFloat(element.prop("max"));
|
|
const step = parseFloat(element.prop("step"));
|
|
|
|
let val = parseFloat(element.val());
|
|
|
|
// only adjust minimal end if bound is set
|
|
if (element.prop("min") && val < min) {
|
|
element.val(min);
|
|
val = min;
|
|
}
|
|
|
|
// only adjust maximal end if bound is set
|
|
if (element.prop("max") && val > max) {
|
|
element.val(max);
|
|
val = max;
|
|
}
|
|
|
|
// if entered value is illegal use previous value instead
|
|
if (isNaN(val)) {
|
|
element.val(element.data("previousValue"));
|
|
val = element.data("previousValue");
|
|
}
|
|
|
|
// if step is not set or step is int and value is float use previous value instead
|
|
if ((isNaN(step) || step % 1 === 0) && val % 1 !== 0) {
|
|
element.val(element.data("previousValue"));
|
|
val = element.data("previousValue");
|
|
}
|
|
|
|
// if step is set and is float and value is int, convert to float, keep decimal places in float according to step *experimental*
|
|
if (!isNaN(step) && step % 1 !== 0) {
|
|
const decimal_places = String(step).split(".")[1].length;
|
|
|
|
if (val % 1 === 0 || String(val).split(".")[1].length !== decimal_places) {
|
|
element.val(val.toFixed(decimal_places));
|
|
}
|
|
}
|
|
});
|
|
|
|
$("#showlog").on("click", function () {
|
|
let state = $(this).data("state");
|
|
if (state) {
|
|
setTimeout(function () {
|
|
const command_log = $("div#log");
|
|
command_log.scrollTop($("div.wrapper", command_log).height());
|
|
}, 200);
|
|
$("#log").removeClass("active");
|
|
$("#tab-content-container").removeClass("logopen");
|
|
$("#scrollicon").removeClass("active");
|
|
setConfig({ logopen: false });
|
|
|
|
state = false;
|
|
} else {
|
|
$("#log").addClass("active");
|
|
$("#tab-content-container").addClass("logopen");
|
|
$("#scrollicon").addClass("active");
|
|
setConfig({ logopen: true });
|
|
|
|
state = true;
|
|
}
|
|
$(this).text(state ? i18n.getMessage("logActionHide") : i18n.getMessage("logActionShow"));
|
|
$(this).data("state", state);
|
|
});
|
|
|
|
let result = getConfig("logopen");
|
|
if (result.logopen) {
|
|
$("#showlog").trigger("click");
|
|
}
|
|
|
|
result = getConfig("expertMode").expertMode ?? false;
|
|
|
|
const expertModeCheckbox = $('input[name="expertModeCheckbox"]');
|
|
expertModeCheckbox.prop("checked", result).trigger("change");
|
|
|
|
expertModeCheckbox.on("change", () => {
|
|
const checked = expertModeCheckbox.is(":checked");
|
|
|
|
checkSetupAnalytics(function (analyticsService) {
|
|
analyticsService.sendEvent(analyticsService.EVENT_CATEGORIES.APPLICATION, "ExpertMode", {
|
|
status: checked ? "On" : "Off",
|
|
});
|
|
});
|
|
|
|
if (FC.FEATURE_CONFIG && FC.FEATURE_CONFIG.features !== 0) {
|
|
updateTabList(FC.FEATURE_CONFIG.features);
|
|
}
|
|
|
|
if (GUI.active_tab) {
|
|
TABS[GUI.active_tab]?.expertModeChanged?.(checked);
|
|
}
|
|
|
|
setConfig({ expertMode: checked });
|
|
});
|
|
|
|
result = getConfig("cliAutoComplete");
|
|
CliAutoComplete.setEnabled(typeof result.cliAutoComplete === "undefined" || result.cliAutoComplete); // On by default
|
|
|
|
result = getConfig("darkTheme");
|
|
if (result.darkTheme === undefined || typeof result.darkTheme !== "number") {
|
|
// sets dark theme to auto if not manually changed
|
|
setDarkTheme(2);
|
|
} else {
|
|
setDarkTheme(result.darkTheme);
|
|
}
|
|
|
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function () {
|
|
DarkTheme.autoSet();
|
|
});
|
|
}
|
|
|
|
window.isExpertModeEnabled = isExpertModeEnabled;
|
|
window.appReady = appReady;
|