mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-22 02:32:10 +10:00
Release v3.1.0
This commit is contained in:
@@ -142,6 +142,7 @@
|
||||
#include <string>
|
||||
// Include utility functions
|
||||
#include "WiFiManagerUtils.h"
|
||||
#include "WiFiManagerPortalUI.h"
|
||||
|
||||
// A station profile is intentionally fixed-size. ESP Wi-Fi accepts one station
|
||||
// configuration at a time, so multi-network behaviour belongs to the
|
||||
@@ -265,10 +266,11 @@ struct PortalHomeCard {
|
||||
};
|
||||
|
||||
struct PortalBrandState {
|
||||
String title = "WiFiManager";
|
||||
String identityTextOverride;
|
||||
String homeIntro;
|
||||
String logoSvg;
|
||||
WiFiManagerPortalText title;
|
||||
WiFiManagerPortalText identityTextOverride;
|
||||
WiFiManagerPortalText homeIntro;
|
||||
WiFiManagerPortalAsset logo;
|
||||
WiFiManagerPortalText logoAltText;
|
||||
};
|
||||
|
||||
struct PortalPageState {
|
||||
@@ -290,12 +292,6 @@ struct PortalLayoutState {
|
||||
bool paramsOnWifiPage = true;
|
||||
};
|
||||
|
||||
struct PortalAssetState {
|
||||
String appendedCss;
|
||||
String overriddenCss;
|
||||
String appendedJs;
|
||||
};
|
||||
|
||||
struct PortalStructuredExtrasState {
|
||||
std::vector<PortalInfoSection> infoSections;
|
||||
std::vector<PortalHomeCard> homeCards;
|
||||
@@ -594,11 +590,10 @@ class WiFiManager
|
||||
// clean connect, always disconnect before connecting
|
||||
void setCleanConnect(bool enable); // default false
|
||||
|
||||
// ---- Portal (all customization entry points use the portal* prefix) ----
|
||||
void portalSetBrandTitle(const String& title);
|
||||
void portalSetContextIdentityText(const String& identityText);
|
||||
void portalSetBrandHomeIntro(const String& text);
|
||||
void portalSetBrandLogoSvg(const String& svgMarkup);
|
||||
// ---- Portal presentation ----
|
||||
// Apply this before a portal is started. The portal response model is
|
||||
// immutable while active so async responses never observe partial UI state.
|
||||
bool setPortalConfig(const WiFiManagerPortalConfig& config);
|
||||
|
||||
void portalSetPageInfoVisible(bool visible);
|
||||
void portalSetPageUpdateVisible(bool visible);
|
||||
@@ -631,10 +626,6 @@ class WiFiManager
|
||||
void portalAddHomeCard(const PortalHomeCard& card);
|
||||
void portalClearHomeCards();
|
||||
|
||||
void portalAppendCss(const String& css);
|
||||
void portalOverrideCss(const String& css);
|
||||
void portalAppendJs(const String& js);
|
||||
|
||||
// get last connection result, including autoconnect and portal credential-save attempts
|
||||
uint8_t getLastConxResult();
|
||||
|
||||
@@ -890,14 +881,19 @@ class WiFiManager
|
||||
boolean _disableConfigPortal = true; // FOR autoconnect - stop config portal if cp wifi save
|
||||
String _hostname = ""; // hostname for esp8266 for dhcp, and or MDNS
|
||||
|
||||
// Grouped portal presentation / customization (see portal* setters; JSON in v2 bootstrap)
|
||||
// Grouped portal presentation / customization (see portal APIs and JSON bootstrap)
|
||||
PortalBrandState _portalBrand;
|
||||
WiFiManagerPortalTheme _portalTheme;
|
||||
String _portalThemeStyle;
|
||||
PortalPageState _portalPages;
|
||||
PortalActionState _portalActions;
|
||||
PortalLayoutState _portalLayout;
|
||||
PortalAssetState _portalAssets;
|
||||
PortalStructuredExtrasState _portalStructured;
|
||||
|
||||
bool canChangePortalPresentation() const;
|
||||
bool isPortalThemeValid(const WiFiManagerPortalTheme& theme) const;
|
||||
void rebuildPortalThemeStyle();
|
||||
|
||||
// internal options
|
||||
|
||||
// wifiscan notes
|
||||
|
||||
@@ -106,7 +106,6 @@ class WiFiManagerHandlers {
|
||||
void appendPortalJsonStaticFields(String& json, bool& first);
|
||||
void appendPortalJsonCustomParams(String& json, bool& first);
|
||||
|
||||
String composePortalStylesheet() const;
|
||||
void appendPortalExtraInfoSectionsJson(String& json, bool& first);
|
||||
void appendPortalExtraHomeCardsJson(String& json, bool& first);
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* WiFiManagerPortalUI.h
|
||||
*
|
||||
* Deliberate, portal-specific presentation configuration. This header does
|
||||
* not depend on DeviceFramework: WiFiManager remains useful on its own.
|
||||
*/
|
||||
|
||||
#ifndef WIFI_MANAGER_PORTAL_UI_H
|
||||
#define WIFI_MANAGER_PORTAL_UI_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
enum class WiFiManagerPortalStorage : uint8_t {
|
||||
Ram,
|
||||
Progmem,
|
||||
};
|
||||
|
||||
/** Non-owning static text. The caller retains the data for the firmware lifetime. */
|
||||
struct WiFiManagerPortalText {
|
||||
const char* data = nullptr;
|
||||
WiFiManagerPortalStorage storage = WiFiManagerPortalStorage::Ram;
|
||||
|
||||
static constexpr WiFiManagerPortalText ram(const char* value) {
|
||||
return {value, WiFiManagerPortalStorage::Ram};
|
||||
}
|
||||
static constexpr WiFiManagerPortalText progmem(const char* value) {
|
||||
return {value, WiFiManagerPortalStorage::Progmem};
|
||||
}
|
||||
|
||||
bool empty() const { return data == nullptr || length() == 0; }
|
||||
size_t length() const {
|
||||
return data == nullptr ? 0 : (storage == WiFiManagerPortalStorage::Progmem ? strlen_P(data) : strlen(data));
|
||||
}
|
||||
char at(size_t index) const {
|
||||
return storage == WiFiManagerPortalStorage::Progmem
|
||||
? static_cast<char>(pgm_read_byte(data + index))
|
||||
: data[index];
|
||||
}
|
||||
};
|
||||
|
||||
/** Optional inline SVG branding asset. It is not a general HTML extension point. */
|
||||
struct WiFiManagerPortalAsset {
|
||||
WiFiManagerPortalText svg;
|
||||
|
||||
static constexpr WiFiManagerPortalAsset svgFromRam(const char* value) {
|
||||
return {WiFiManagerPortalText::ram(value)};
|
||||
}
|
||||
static constexpr WiFiManagerPortalAsset svgFromProgmem(const char* value) {
|
||||
return {WiFiManagerPortalText::progmem(value)};
|
||||
}
|
||||
|
||||
bool empty() const { return svg.empty(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Semantic colour and shape values for WiFiManager's built-in portal.
|
||||
*
|
||||
* Leave a value empty to retain the built-in stylesheet value. Values are
|
||||
* validated before they are emitted into the portal stylesheet; they are not
|
||||
* a general CSS injection mechanism.
|
||||
*/
|
||||
struct WiFiManagerPortalTheme {
|
||||
WiFiManagerPortalText pageBackground;
|
||||
WiFiManagerPortalText surface;
|
||||
WiFiManagerPortalText text;
|
||||
WiFiManagerPortalText mutedText;
|
||||
WiFiManagerPortalText border;
|
||||
WiFiManagerPortalText accent;
|
||||
WiFiManagerPortalText accentHover;
|
||||
WiFiManagerPortalText accentText;
|
||||
WiFiManagerPortalText danger;
|
||||
WiFiManagerPortalText dangerHover;
|
||||
WiFiManagerPortalText success;
|
||||
uint8_t cornerRadiusPx = 0;
|
||||
uint8_t smallCornerRadiusPx = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete setup-time presentation configuration for WiFiManager's portal.
|
||||
*
|
||||
* Text and SVG assets are non-owning static data in RAM or PROGMEM. Apply this
|
||||
* before starting a portal; asynchronous portal responses use immutable state.
|
||||
*/
|
||||
struct WiFiManagerPortalConfig {
|
||||
WiFiManagerPortalText title;
|
||||
WiFiManagerPortalText identityText;
|
||||
WiFiManagerPortalText homeIntro;
|
||||
WiFiManagerPortalAsset logo;
|
||||
WiFiManagerPortalText logoAltText;
|
||||
WiFiManagerPortalTheme theme;
|
||||
};
|
||||
|
||||
#endif // WIFI_MANAGER_PORTAL_UI_H
|
||||
@@ -23,6 +23,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"--wm-border:#e2e8f0;"
|
||||
"--wm-brand:#0ea5e9;"
|
||||
"--wm-brand-hover:#0284c7;"
|
||||
"--wm-brand-text:#ffffff;"
|
||||
"--wm-danger:#dc2626;"
|
||||
"--wm-danger-hover:#b91c1c;"
|
||||
"--wm-success:#16a34a;"
|
||||
@@ -128,16 +129,16 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"a.wm-btn{box-sizing:border-box;text-decoration:none}"
|
||||
".wm-btn .wm-icon-label{justify-content:center}"
|
||||
".wm-btn--block{width:100%}"
|
||||
".wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
||||
".wm-btn--primary{background:var(--wm-brand);color:var(--wm-brand-text)}"
|
||||
".wm-btn--primary:hover{background:var(--wm-brand-hover)}"
|
||||
".wm-btn--secondary{background:var(--wm-bg);color:var(--wm-text);border:1px solid var(--wm-border)}"
|
||||
".wm-btn--secondary:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
|
||||
".wm-btn--danger{background:var(--wm-danger);color:#fff}"
|
||||
".wm-btn--danger{background:var(--wm-danger);color:var(--wm-brand-text)}"
|
||||
".wm-btn--danger:hover{background:var(--wm-danger-hover)}"
|
||||
"input[type=file]{width:100%;padding:10px;border:1px dashed var(--wm-border);border-radius:var(--wm-radius-sm);"
|
||||
"background:var(--wm-bg);font-size:.9rem}"
|
||||
/* Legacy: unclassed submit in forms */
|
||||
"button[type=submit],.wm-form-actions .wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
||||
"button[type=submit],.wm-form-actions .wm-btn--primary{background:var(--wm-brand);color:var(--wm-brand-text)}"
|
||||
"button[type=submit]:hover{background:var(--wm-brand-hover)}"
|
||||
/* Device actions / button stack */
|
||||
".wm-actions-stack{display:flex;flex-direction:column;gap:10px;margin-top:4px}"
|
||||
@@ -198,7 +199,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".wm-dialog-msg{margin:0 0 16px;line-height:1.45;color:var(--wm-text)}"
|
||||
".wm-dialog-actions{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap}"
|
||||
".wm-dialog-actions .wm-btn{min-width:108px}"
|
||||
".wm-dialog-ok,.wm-btn.wm-dialog-ok{background:var(--wm-brand)!important;color:#fff!important}"
|
||||
".wm-dialog-ok,.wm-btn.wm-dialog-ok{background:var(--wm-brand)!important;color:var(--wm-brand-text)!important}"
|
||||
".wm-dialog-ok:hover{background:var(--wm-brand-hover)!important}"
|
||||
"body.invert .wm-toast.ok{background:rgba(22,163,74,.2)}"
|
||||
"body.invert .wm-toast.err{background:rgba(220,38,38,.2)}"
|
||||
|
||||
@@ -66,8 +66,13 @@ function bindPortalChromeEvents(){
|
||||
}
|
||||
|
||||
function bindFormSubmitHandlers(){
|
||||
var wf=$('wm-wifi-form'), pf=$('wm-param-form'), of=$('wm-ota-form');
|
||||
if(wf && window.portalWifiSave) wf.addEventListener('submit', window.portalWifiSave);
|
||||
var wf=document.querySelectorAll('.wm-wifi-data-form'), pf=$('wm-param-form'), of=$('wm-ota-form'), wa=document.querySelectorAll('[data-wm-wifi-action]'), i;
|
||||
if(window.portalWifiSave){
|
||||
for(i=0;i<wf.length;i++)wf[i].addEventListener('submit', window.portalWifiSave);
|
||||
}
|
||||
if(window.portalWifiAction){
|
||||
for(i=0;i<wa.length;i++)wa[i].addEventListener('click', window.portalWifiAction);
|
||||
}
|
||||
if(pf && window.portalParamSave) pf.addEventListener('submit', window.portalParamSave);
|
||||
if(of && window.portalOtaSubmit) of.addEventListener('submit', window.portalOtaSubmit);
|
||||
}
|
||||
@@ -315,7 +320,8 @@ function viewHome(){
|
||||
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
|
||||
var html=navBar('home');
|
||||
html+="<header class='wm-hero'>";
|
||||
if(brand.logoSvg){html+="<div class='wm-brand-logo'>"+brand.logoSvg+"</div>";}
|
||||
var logoAlt=esc(brand.logoAltText||'');
|
||||
if(brand.logoSvg){html+="<div class='wm-brand-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
|
||||
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
|
||||
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
|
||||
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
|
||||
@@ -509,16 +515,16 @@ function renderStationProfileFields(profiles){
|
||||
var hint=primary
|
||||
? 'This network is tried first for a new connection.'
|
||||
: 'Tried when the primary network is unavailable.';
|
||||
html+="<div class='wm-card wm-station-profile'><h2>"+esc(title)+"</h2><p class='wm-help'>"+esc(hint)+"</p>";
|
||||
html+="<form class='wm-card wm-station-profile wm-wifi-data-form' id='wm-wifi-profile-"+i+"'><h2>"+esc(title)+"</h2><p class='wm-help'>"+esc(hint)+"</p>";
|
||||
html+="<label for='wm-s"+i+"'>SSID"+(primary?'':' (leave empty to disable)')+"</label>";
|
||||
html+="<input id='wm-s"+i+"' name='s"+i+"' type='text' maxlength='32' value='"+esc(p.ssid||'')+"'/>";
|
||||
html+="<input id='wm-s"+i+"' name='s"+i+"' type='text' autocomplete='username' maxlength='32' value='"+esc(p.ssid||'')+"'/>";
|
||||
html+="<label for='wm-p"+i+"'>Password</label>";
|
||||
html+="<input id='wm-p"+i+"' class='wm-profile-password' name='p"+i+"' type='password' maxlength='64' placeholder='"+(p.passwordSet?'Configured — enter a new password to replace it':'Leave blank for an open network')+"'/>";
|
||||
html+="<input id='wm-p"+i+"' class='wm-profile-password' name='p"+i+"' type='password' autocomplete='current-password' maxlength='64' placeholder='"+(p.passwordSet?'Configured — enter a new password to replace it':'Leave blank for an open network')+"'/>";
|
||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass"+i+"' class='wm-showpass' data-target='wm-p"+i+"'/> <label for='wm-showpass"+i+"'>Show password</label></div>";
|
||||
if(p.passwordSet){
|
||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-clear"+i+"' name='clear"+i+"' value='1'/> <label for='wm-clear"+i+"'>Clear password / use an open network</label></div>";
|
||||
}
|
||||
html+='</div>';
|
||||
html+='</form>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
@@ -533,10 +539,11 @@ function viewWifi(){
|
||||
html+="<div id='wm-scan-results'></div>";
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
||||
html+="<form id='wm-wifi-form'>";
|
||||
if(Array.isArray(m.profiles) && m.profiles.length){
|
||||
html+=renderStationProfileFields(m.profiles);
|
||||
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
|
||||
}else{
|
||||
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
|
||||
html+=renderFieldList(m.wifiFields||[]);
|
||||
var wf=m.wifiFields||[];
|
||||
var hasPass=false;
|
||||
@@ -548,11 +555,12 @@ function viewWifi(){
|
||||
}
|
||||
html+=renderFieldList(m.staticFields||[]);
|
||||
html+=renderFieldList(m.params||[]);
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block' name='stationAction' value='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
|
||||
html+="</form>";
|
||||
html+="<div class='wm-form-actions'><button type='button' class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
|
||||
if(Array.isArray(m.profiles) && m.profiles.length){
|
||||
html+="<button type='submit' class='wm-btn wm-btn--secondary wm-btn--block' name='stationAction' value='save'>Save for later</button>";
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' data-wm-wifi-action='save'>Save for later</button>";
|
||||
}
|
||||
html+="</div></form>";
|
||||
html+="</div>";
|
||||
html+="<div id='wm-wifi-msg'></div></div>";
|
||||
setView(html);
|
||||
bindWifiViewEvents();
|
||||
@@ -635,12 +643,17 @@ function pollWifiConnectStatus(){
|
||||
poll();
|
||||
}
|
||||
|
||||
window.portalWifiSave=function(ev){
|
||||
ev.preventDefault();
|
||||
var fd=new FormData(document.getElementById('wm-wifi-form'));
|
||||
if(ev.submitter && ev.submitter.name){
|
||||
fd.append(ev.submitter.name,ev.submitter.value||'');
|
||||
function collectWifiFormData(){
|
||||
var fd=new FormData(), forms=document.querySelectorAll('.wm-wifi-data-form'), i;
|
||||
for(i=0;i<forms.length;i++){
|
||||
new FormData(forms[i]).forEach(function(value,key){fd.append(key,value);});
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
function submitWifi(action){
|
||||
var fd=collectWifiFormData();
|
||||
if(action)fd.append('stationAction',action);
|
||||
var msg=$('wm-wifi-msg');
|
||||
if(msg)msg.innerHTML='Submitting...';
|
||||
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
|
||||
@@ -655,6 +668,16 @@ window.portalWifiSave=function(ev){
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
window.portalWifiSave=function(ev){
|
||||
ev.preventDefault();
|
||||
return submitWifi(ev.submitter&&ev.submitter.value?ev.submitter.value:'connect');
|
||||
};
|
||||
|
||||
window.portalWifiAction=function(ev){
|
||||
ev.preventDefault();
|
||||
return submitWifi(ev.currentTarget.getAttribute('data-wm-wifi-action'));
|
||||
};
|
||||
|
||||
// --- Info view ---
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Single-page portal shell: only HTML document served for GET /.
|
||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
||||
*
|
||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%, %PORTAL_APPEND_JS%) are filled
|
||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %PORTAL_THEME%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%) are filled
|
||||
* per request in WiFiManagerHandlers::handleRoot; do not treat placeholders as a customization API.
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%PAGE_TITLE%</title>"
|
||||
"%STYLES%"
|
||||
"%PORTAL_THEME%"
|
||||
"</head>"
|
||||
"<body class='portal'>"
|
||||
"<div id='wm-toast' class='wm-toast' aria-live='polite' role='status' style='display:none'></div>"
|
||||
@@ -41,9 +42,6 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
||||
"<script>"
|
||||
"%PORTAL_APP_JS%"
|
||||
"</script>"
|
||||
"<script>"
|
||||
"%PORTAL_APPEND_JS%"
|
||||
"</script>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
|
||||
@@ -56,6 +56,25 @@ bool normalizeHostname(String& hostname) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const char kDefaultPortalTitle[] PROGMEM = "WiFiManager";
|
||||
|
||||
bool isSafePortalText(const WiFiManagerPortalText& value) {
|
||||
for (size_t i = 0; i < value.length(); ++i) {
|
||||
const char c = value.at(i);
|
||||
if (!(isAlphaNumeric(c) || c == '#' || c == '(' || c == ')' || c == ',' ||
|
||||
c == '.' || c == '%' || c == ' ' || c == '-' || c == '/')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void appendPortalText(String& destination, const WiFiManagerPortalText& value) {
|
||||
for (size_t i = 0; i < value.length(); ++i) {
|
||||
destination += value.at(i);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@@ -149,6 +168,7 @@ WiFiManager::WiFiManager() {
|
||||
}
|
||||
|
||||
void WiFiManager::WiFiManagerInit(){
|
||||
_portalBrand.title = WiFiManagerPortalText::progmem(kDefaultPortalTitle);
|
||||
if (_logEnabled && (uint8_t)_runtimeMaxLevel >= (uint8_t)WiFiManagerLogLevel::Trace) {
|
||||
debugPlatformInfo();
|
||||
}
|
||||
@@ -848,16 +868,6 @@ bool WiFiManager::startAP(){
|
||||
log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("StartAP with SSID: "),_apName);
|
||||
#endif
|
||||
|
||||
#ifdef ESP8266
|
||||
if(!WiFi.enableAP(true)) {
|
||||
#ifndef WM_NO_LOG
|
||||
log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem,F("[ERROR] enableAP failed!"));
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
delay(500); // workaround delay
|
||||
#endif
|
||||
|
||||
// setup optional soft AP static ip config
|
||||
if (_ap_static_ip) {
|
||||
#ifndef WM_NO_LOG
|
||||
@@ -2559,20 +2569,78 @@ void WiFiManager::setWiFiAPHidden(bool hidden){
|
||||
}
|
||||
|
||||
|
||||
void WiFiManager::portalSetBrandTitle(const String& title) {
|
||||
_portalBrand.title = title;
|
||||
bool WiFiManager::canChangePortalPresentation() const {
|
||||
return !configPortalActive && !webPortalActive;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetContextIdentityText(const String& identityText) {
|
||||
_portalBrand.identityTextOverride = identityText;
|
||||
bool WiFiManager::isPortalThemeValid(const WiFiManagerPortalTheme& theme) const {
|
||||
return isSafePortalText(theme.pageBackground) && isSafePortalText(theme.surface) &&
|
||||
isSafePortalText(theme.text) && isSafePortalText(theme.mutedText) &&
|
||||
isSafePortalText(theme.border) && isSafePortalText(theme.accent) &&
|
||||
isSafePortalText(theme.accentHover) && isSafePortalText(theme.accentText) &&
|
||||
isSafePortalText(theme.danger) && isSafePortalText(theme.dangerHover) &&
|
||||
isSafePortalText(theme.success) && theme.cornerRadiusPx <= 64 &&
|
||||
theme.smallCornerRadiusPx <= 64;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBrandHomeIntro(const String& text) {
|
||||
_portalBrand.homeIntro = text;
|
||||
void WiFiManager::rebuildPortalThemeStyle() {
|
||||
_portalThemeStyle = "";
|
||||
const bool hasColours = !_portalTheme.pageBackground.empty() || !_portalTheme.surface.empty() ||
|
||||
!_portalTheme.text.empty() || !_portalTheme.mutedText.empty() || !_portalTheme.border.empty() ||
|
||||
!_portalTheme.accent.empty() || !_portalTheme.accentHover.empty() ||
|
||||
!_portalTheme.accentText.empty() || !_portalTheme.danger.empty() ||
|
||||
!_portalTheme.dangerHover.empty() || !_portalTheme.success.empty();
|
||||
if (!hasColours && _portalTheme.cornerRadiusPx == 0 && _portalTheme.smallCornerRadiusPx == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto appendToken = [this](const char* name, const WiFiManagerPortalText& value) {
|
||||
if (value.empty()) return;
|
||||
_portalThemeStyle += name;
|
||||
_portalThemeStyle += ':';
|
||||
appendPortalText(_portalThemeStyle, value);
|
||||
_portalThemeStyle += ';';
|
||||
};
|
||||
|
||||
_portalThemeStyle = F("<style id='wm-portal-theme'>:root{");
|
||||
appendToken("--wm-bg", _portalTheme.pageBackground);
|
||||
appendToken("--wm-surface", _portalTheme.surface);
|
||||
appendToken("--wm-text", _portalTheme.text);
|
||||
appendToken("--wm-muted", _portalTheme.mutedText);
|
||||
appendToken("--wm-border", _portalTheme.border);
|
||||
appendToken("--wm-brand", _portalTheme.accent);
|
||||
appendToken("--wm-brand-hover", _portalTheme.accentHover);
|
||||
appendToken("--wm-brand-text", _portalTheme.accentText);
|
||||
appendToken("--wm-danger", _portalTheme.danger);
|
||||
appendToken("--wm-danger-hover", _portalTheme.dangerHover);
|
||||
appendToken("--wm-success", _portalTheme.success);
|
||||
if (_portalTheme.cornerRadiusPx > 0) {
|
||||
_portalThemeStyle += F("--wm-radius:");
|
||||
_portalThemeStyle += String(_portalTheme.cornerRadiusPx);
|
||||
_portalThemeStyle += F("px;");
|
||||
}
|
||||
if (_portalTheme.smallCornerRadiusPx > 0) {
|
||||
_portalThemeStyle += F("--wm-radius-sm:");
|
||||
_portalThemeStyle += String(_portalTheme.smallCornerRadiusPx);
|
||||
_portalThemeStyle += F("px;");
|
||||
}
|
||||
_portalThemeStyle += F("}</style>");
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetBrandLogoSvg(const String& svgMarkup) {
|
||||
_portalBrand.logoSvg = svgMarkup;
|
||||
bool WiFiManager::setPortalConfig(const WiFiManagerPortalConfig& config) {
|
||||
if (!canChangePortalPresentation() || !isPortalThemeValid(config.theme)) {
|
||||
return false;
|
||||
}
|
||||
_portalBrand.title = config.title.empty()
|
||||
? WiFiManagerPortalText::progmem(kDefaultPortalTitle)
|
||||
: config.title;
|
||||
_portalBrand.identityTextOverride = config.identityText;
|
||||
_portalBrand.homeIntro = config.homeIntro;
|
||||
_portalBrand.logo = config.logo;
|
||||
_portalBrand.logoAltText = config.logoAltText;
|
||||
_portalTheme = config.theme;
|
||||
rebuildPortalThemeStyle();
|
||||
return true;
|
||||
}
|
||||
|
||||
void WiFiManager::portalSetPageInfoVisible(bool visible) {
|
||||
@@ -2587,6 +2655,7 @@ void WiFiManager::portalSetPageSetupVisible(bool visible) {
|
||||
_portalPages.setupVisible = visible;
|
||||
}
|
||||
|
||||
|
||||
void WiFiManager::portalSetActionEraseVisible(bool visible) {
|
||||
_portalActions.eraseVisible = visible;
|
||||
}
|
||||
@@ -2691,17 +2760,6 @@ void WiFiManager::portalClearHomeCards() {
|
||||
_portalStructured.homeCards.clear();
|
||||
}
|
||||
|
||||
void WiFiManager::portalAppendCss(const String& css) {
|
||||
_portalAssets.appendedCss += css;
|
||||
}
|
||||
|
||||
void WiFiManager::portalOverrideCss(const String& css) {
|
||||
_portalAssets.overriddenCss = css;
|
||||
}
|
||||
|
||||
void WiFiManager::portalAppendJs(const String& js) {
|
||||
_portalAssets.appendedJs += js;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the config portal is running
|
||||
|
||||
@@ -37,8 +37,25 @@ static void jsonAppendEscaped(String& out, const String& s) {
|
||||
}
|
||||
}
|
||||
|
||||
static void jsonAppendEscaped(String& out, const WiFiManagerPortalText& text) {
|
||||
for (size_t i = 0; i < text.length(); ++i) {
|
||||
const char c = text.at(i);
|
||||
if (c == '"') {
|
||||
out += F("\\\"");
|
||||
} else if (c == '\\') {
|
||||
out += F("\\\\");
|
||||
} else if (c == '\n') {
|
||||
out += F("\\n");
|
||||
} else if (c != '\r') {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const char kEmptyPortalPlaceholder[] PROGMEM = "";
|
||||
|
||||
inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) {
|
||||
if (extraBytes == 0) return;
|
||||
const size_t targetLen = page.length() + extraBytes;
|
||||
@@ -60,24 +77,26 @@ void configureDynamicStringDescriptor(DynamicDataDescriptor& descriptor, String&
|
||||
descriptor.userData = &value;
|
||||
}
|
||||
|
||||
String readProgmemString(const char* p) {
|
||||
return String(FPSTR(p));
|
||||
const char* portalTextGetter(void* userData) {
|
||||
const auto* text = static_cast<const WiFiManagerPortalText*>(userData);
|
||||
return text && text->storage == WiFiManagerPortalStorage::Ram && text->data
|
||||
? text->data
|
||||
: "";
|
||||
}
|
||||
|
||||
String stripStyleWrapper(const String& styleBlock) {
|
||||
String css = styleBlock;
|
||||
const String openTag = F("<style>");
|
||||
const String closeTag = F("</style>");
|
||||
|
||||
if (css.startsWith(openTag)) {
|
||||
css.remove(0, openTag.length());
|
||||
}
|
||||
if (css.endsWith(closeTag)) {
|
||||
css.remove(css.length() - closeTag.length());
|
||||
}
|
||||
return css;
|
||||
size_t portalTextLengthGetter(const char* /*data*/, void* userData) {
|
||||
const auto* text = static_cast<const WiFiManagerPortalText*>(userData);
|
||||
return text ? text->length() : 0;
|
||||
}
|
||||
|
||||
void configurePortalTextDescriptor(DynamicDataDescriptor& descriptor,
|
||||
const WiFiManagerPortalText& text) {
|
||||
descriptor.getter = &portalTextGetter;
|
||||
descriptor.getLength = &portalTextLengthGetter;
|
||||
descriptor.userData = const_cast<WiFiManagerPortalText*>(&text);
|
||||
}
|
||||
|
||||
|
||||
const char* portalHomeCardKindJson(PortalHomeCardKind k) {
|
||||
switch (k) {
|
||||
case PortalHomeCardKind::Text:
|
||||
@@ -110,31 +129,27 @@ AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
|
||||
);
|
||||
}
|
||||
|
||||
// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager portal* APIs + bootstrap JSON, not registries):
|
||||
// Shell contract (must match templates/RootShell.h):
|
||||
// %PAGE_TITLE% -> document title (from WiFiManager title state)
|
||||
// %STYLES% -> portal CSS (embedded default + portalAppendCss / portalOverrideCss)
|
||||
// %STYLES% -> immutable built-in portal stylesheet in PROGMEM
|
||||
// %PORTAL_THEME% -> optional validated semantic-token override
|
||||
// %BOOTSTRAP_JSON% -> initial SPA runtime payload
|
||||
// %PORTAL_APP_JS% -> embedded SPA source
|
||||
// %PORTAL_APPEND_JS% -> consumer hook script (portalAppendJs)
|
||||
|
||||
struct PortalShellRenderBundle {
|
||||
String bootstrapJson;
|
||||
String pageTitleStatic;
|
||||
String stylesStatic;
|
||||
String appendJsStatic;
|
||||
WiFiManagerPortalText pageTitle;
|
||||
PlaceholderRegistry registry;
|
||||
TemplateContext context;
|
||||
DynamicDataDescriptor bootstrapDescriptor;
|
||||
DynamicDataDescriptor pageTitleDescriptor;
|
||||
DynamicDataDescriptor stylesDescriptor;
|
||||
DynamicDataDescriptor appendJsDescriptor;
|
||||
DynamicDataDescriptor themeDescriptor;
|
||||
|
||||
PortalShellRenderBundle()
|
||||
: registry(WM_TEMPLATE_REGISTRY_CAPACITY),
|
||||
bootstrapDescriptor{},
|
||||
pageTitleDescriptor{},
|
||||
stylesDescriptor{},
|
||||
appendJsDescriptor{} {}
|
||||
themeDescriptor{} {}
|
||||
};
|
||||
|
||||
// Custom HTML attribute strings (e.g. from DeviceFramework generateCustomHTML) may include
|
||||
@@ -150,24 +165,6 @@ bool portalCustomAttrsIndicatePassword(const String& customAttrs) {
|
||||
|
||||
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
|
||||
|
||||
String WiFiManagerHandlers::composePortalStylesheet() const {
|
||||
String css;
|
||||
if (_wm != nullptr && _wm->_portalAssets.overriddenCss.length() > 0) {
|
||||
css = _wm->_portalAssets.overriddenCss;
|
||||
} else {
|
||||
css = stripStyleWrapper(readProgmemString(CSS_STYLE));
|
||||
}
|
||||
|
||||
if (_wm != nullptr && _wm->_portalAssets.appendedCss.length() > 0) {
|
||||
css += _wm->_portalAssets.appendedCss;
|
||||
}
|
||||
|
||||
String out = F("<style>");
|
||||
out += css;
|
||||
out += F("</style>");
|
||||
return out;
|
||||
}
|
||||
|
||||
void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool& first) {
|
||||
if (_wm == nullptr) {
|
||||
return;
|
||||
@@ -199,7 +196,6 @@ void WiFiManagerHandlers::appendPortalExtraInfoSectionsJson(String& json, bool&
|
||||
json += F("]}");
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManagerHandlers::appendPortalExtraHomeCardsJson(String& json, bool& first) {
|
||||
if (_wm == nullptr) {
|
||||
return;
|
||||
@@ -686,25 +682,24 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
|
||||
// - WiFiManagerHandlers owns request-time shell assembly; WiFiManagerServer owns HTTP lifecycle only.
|
||||
// - Build one request-scoped PortalShellRenderBundle.
|
||||
// - Populate one request-scoped PlaceholderRegistry with shell defaults + request payloads.
|
||||
// - Render WM_ROOT_SHELL_TEMPLATE.
|
||||
// - Render WM_ROOT_SHELL_TEMPLATE with static CSS plus an optional small theme block.
|
||||
auto bundle = std::make_shared<PortalShellRenderBundle>();
|
||||
const bool hasCustomStyles = _wm != nullptr &&
|
||||
(_wm->_portalAssets.overriddenCss.length() > 0 || _wm->_portalAssets.appendedCss.length() > 0);
|
||||
bundle->pageTitleStatic = _wm ? _wm->_portalBrand.title : String();
|
||||
bundle->pageTitle = _wm ? _wm->_portalBrand.title : WiFiManagerPortalText{};
|
||||
bundle->bootstrapJson = buildPortalBootstrapJson();
|
||||
bundle->appendJsStatic = _wm ? _wm->_portalAssets.appendedJs : String();
|
||||
bundle->registry.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||
bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
|
||||
if (hasCustomStyles) {
|
||||
bundle->stylesStatic = composePortalStylesheet();
|
||||
configureDynamicStringDescriptor(bundle->stylesDescriptor, bundle->stylesStatic);
|
||||
bundle->registry.registerDynamicData("%STYLES%", &bundle->stylesDescriptor);
|
||||
if (_wm) {
|
||||
configureDynamicStringDescriptor(bundle->themeDescriptor, _wm->_portalThemeStyle);
|
||||
bundle->registry.registerDynamicData("%PORTAL_THEME%", &bundle->themeDescriptor);
|
||||
} else {
|
||||
bundle->registry.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||
bundle->registry.registerProgmemData("%PORTAL_THEME%", kEmptyPortalPlaceholder);
|
||||
}
|
||||
if (bundle->pageTitle.storage == WiFiManagerPortalStorage::Progmem && bundle->pageTitle.data) {
|
||||
bundle->registry.registerProgmemData("%PAGE_TITLE%", bundle->pageTitle.data);
|
||||
} else {
|
||||
configurePortalTextDescriptor(bundle->pageTitleDescriptor, bundle->pageTitle);
|
||||
bundle->registry.registerDynamicData("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
|
||||
}
|
||||
configureDynamicStringDescriptor(bundle->appendJsDescriptor, bundle->appendJsStatic);
|
||||
bundle->registry.registerDynamicData("%PORTAL_APPEND_JS%", &bundle->appendJsDescriptor);
|
||||
configureDynamicStringDescriptor(bundle->pageTitleDescriptor, bundle->pageTitleStatic);
|
||||
bundle->registry.registerDynamicData("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
|
||||
configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
|
||||
bundle->registry.registerDynamicData("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
|
||||
|
||||
@@ -1076,7 +1071,9 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
|
||||
json += F("\",\"homeIntro\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
|
||||
json += F("\",\"logoSvg\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.logoSvg);
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.logo.svg);
|
||||
json += F("\",\"logoAltText\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.logoAltText);
|
||||
json += F("\"}");
|
||||
json += F(",\"context\":{");
|
||||
json += F("\"portalActive\":");
|
||||
@@ -1094,15 +1091,12 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
|
||||
json += String(timeoutRemainingSeconds);
|
||||
}
|
||||
json += F(",\"identityText\":\"");
|
||||
{
|
||||
String idText;
|
||||
if (_wm->_portalBrand.identityTextOverride.length() > 0) {
|
||||
idText = _wm->_portalBrand.identityTextOverride;
|
||||
} else if (_wm->configPortalActive) {
|
||||
idText = _wm->_apName;
|
||||
} else {
|
||||
idText = _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
|
||||
}
|
||||
if (!_wm->_portalBrand.identityTextOverride.empty()) {
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.identityTextOverride);
|
||||
} else {
|
||||
const String idText = _wm->configPortalActive
|
||||
? _wm->_apName
|
||||
: _wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
|
||||
jsonAppendEscaped(json, idText);
|
||||
}
|
||||
json += F("\",\"statusSummary\":\"");
|
||||
|
||||
Reference in New Issue
Block a user