1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-25 17:25:16 +03:00
betaflight-configurator/src/js/main.js
David Anson f6cc33756f Replace all 4 instances of jBox-style modal dialog boxes with HTML dialog element
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
2025-06-23 21:29:20 +00:00

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;