VulpesOS/host/gaia-wifi.js
OverSu 1311bed5e9 Release Preview 20 shared desktop, Android and Sargo updates
Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
2026-10-13 00:19:45 +02:00

190 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Tundra's NetworkManager broker, exposed through Gaia's legacy Wi-Fi interface.
(() => {
if (!window.VulpesNativeWifi || !['system','settings'].includes(location.hostname.split('.')[0])) return;
const {call, request} = VulpesCompat;
const manager = Object.assign(new EventTarget(), {
enabled:false, connection:{status:'disconnected',network:null},
connectionInformation:null, macAddress:'',
});
let networks = [], connectionInfo = {};
function emit(type, values = {}) {
const event = Object.assign(new Event(type), values);
manager.dispatchEvent(event); manager['on' + type]?.(event);
}
function power(enabled, acknowledge = false) {
if (manager.enabled === enabled && !acknowledge) return;
manager.enabled = enabled;
emit(enabled ? 'enabled' : 'disabled');
if (!enabled) update(null);
}
function update(network) {
const status = network ? 'connected' : 'disconnected';
const changed = manager.connection.status !== status || manager.connection.network?.ssid !== network?.ssid;
manager.connection = {status, network};
manager.connectionInformation = network ? {...connectionInfo,relSignalStrength:network.relSignalStrength} : null;
if (changed) emit('statuschange', manager.connection);
// Gaia's signal listeners require the associated network. A scan while
// disconnected must not announce connection information for a missing AP.
if (network) emit('connectioninfoupdate', {network, ...manager.connectionInformation});
}
let scanPending;
function scan() {
if (scanPending) return scanPending;
scanPending = scanNetworks().finally(() => {scanPending = null;});
return scanPending;
}
async function scanNetworks() {
const result = await call('platform.wifi',{operation:'scan'});
power(result.enabled !== false);
connectionInfo=result.connectionInformation || {};
networks = result.networks.map(n => ({ssid:n.ssid, relSignalStrength:n.signal,
security:n.security === 'open' ? [] : [['wpa-psk','sae'].includes(n.security) ? 'WPA-PSK' : 'WPA-EAP'],
known:n.known === true, connected:n.connected, vulpesId:n.id, savedId:n.savedId}));
update(networks.find(n => n.connected) || null);
return networks;
}
manager.getNetworks = () => request(scan());
let knownPending;
manager.getKnownNetworks = () => {
if (!knownPending) knownPending = call('platform.wifi',{operation:'known'}).then(r=>r.networks.map(n=>({ssid:n.ssid,security:n.security==='open'?[]:['WPA-PSK'],known:true,savedId:n.id}))).finally(()=>{knownPending=null;});
return request(knownPending);
};
manager.associate = network => request((async () => {
const security=(network.security || []).filter(Boolean);
if (security.length && security[0] !== 'WPA-PSK') throw Error('WIFI_UNSUPPORTED_SECURITY');
await call('platform.wifi',network.hidden ? {operation:'connectHidden',ssid:network.ssid,security:security.length?'wpa-psk':'open',password:network.psk || ''} : {operation:'connect',id:network.vulpesId,password:network.psk || ''});
await scan(); return network;
})());
manager.forget = network => request(call('platform.wifi',{operation:'forget',id:network.savedId}).then(()=>scan()));
const unsupported = () => request(Promise.reject(new DOMException('Not supported by this adapter','NotSupportedError')));
manager.wps = unsupported;
manager.setPowerSavingMode = unsupported;
Object.defineProperty(navigator,'mozWifiManager',{value:manager});
if(location.hostname.startsWith('settings.')) {
const wifi = value => call('platform.wifi',value);
const french = () => (document.documentElement.lang || navigator.language || '').startsWith('fr');
const message = (fr,en) => french() ? fr : en;
const note = panel => {
const p=document.createElement('p');p.className='explanation';p.setAttribute('role','status');
panel.querySelector(':scope > div').prepend(p);return p;
};
function hotspotPanel() {
let panel,toggle,link,status,state,busy=false;
function render() {
toggle.checked=state?.enabled===true;toggle.disabled=busy || !state;
link.setAttribute('aria-disabled',String(busy || !state || state.enabled));
panel.querySelector('#wifi-hotspot-msg').textContent=state?.enabled ? state.ssid : '';
}
async function refresh() {
busy=true;render();
try {state=await wifi({operation:'hotspotStatus'});status.textContent='';
await call('settings.set',{values:{'tethering.wifi.enabled':state.enabled}});
} catch(error) {state=null;status.textContent=message('Impossible de lire le point d’accès Wi-Fi.','Cannot read Wi-Fi hotspot status.');}
finally {busy=false;render();}
}
return {
onInit(element) {
panel=element;toggle=panel.querySelector('#tethering-wifi-enabled');link=panel.querySelector('#hotspot-settings-section a');status=note(panel);
const usb=panel.querySelector('#tethering-usb-enabled');usb.disabled=true;
const explanation=document.createElement('p');explanation.className='explanation';
explanation.textContent=message('Le partage par USB n’est pas encore disponible.','USB tethering is not available yet.');usb.closest('ul').append(explanation);
link.addEventListener('click',event=>{
event.preventDefault();event.stopImmediatePropagation();if(busy || !state || state.enabled)return;
require(['modules/dialog_service'],DialogService=>DialogService.show('hotspot-wifiSettings',{state}).then(refresh));
});
toggle.addEventListener('change',async()=>{
if(busy || !state)return;
if(!state.configured) {toggle.checked=false;status.textContent=message('Configurez d’abord le nom et le mot de passe du point d’accès.','Configure the hotspot name and password first.');return;}
const enabled=toggle.checked;busy=true;render();
status.textContent=message('Modification du partage Wi-Fi…','Changing Wi-Fi sharing…');
try {
if(enabled && (await call('settings.get',{key:'airplaneMode.status'})).value==='enabled')throw Error('AIRPLANE_MODE');
state=await wifi({operation:'hotspotPower',enabled});
await call('settings.set',{values:{'tethering.wifi.enabled':state.enabled}});status.textContent='';
} catch(error) {
try {state=await wifi({operation:'hotspotStatus'});}catch {state=null;}
status.textContent=error.message==='AIRPLANE_MODE' ? message('Désactivez le mode avion avant de partager la connexion.','Turn off airplane mode before sharing the connection.') : message('Le changement a échoué. Vérifiez la connexion réseau.','The change failed. Check the network connection.');
} finally {busy=false;render();}
});
},onBeforeShow:refresh
};
}
function hotspotDialog() {
let panel,ssid,security,password,show,status,busy=false;
function visibility() {
const hidden=security.value==='open';
panel.querySelector('.password').hidden=hidden;panel.querySelector('.password-description').hidden=hidden;
}
return {
onInit(element) {
panel=element;ssid=panel.querySelector('input[type=text]');security=panel.querySelector('select');password=panel.querySelector('input[name=password]');show=panel.querySelector('gaia-checkbox');status=note(panel);
// These credentials belong to NetworkManager, not the global settings database.
for(const control of [ssid,security,password])control.removeAttribute('data-setting');
security.replaceChildren(new Option('WPA2','wpa2-psk'),new Option(message('Ouvert','Open'),'open'));
security.addEventListener('change',visibility);
show.addEventListener('change',()=>{password.type=show.checked?'text':'password';});
},
onBeforeShow(element,options) {
const state=options.state;ssid.value=state.ssid;security.value=state.security;password.value=state.password;
password.type='password';show.checked=false;status.textContent='';visibility();
},
async onSubmit() {
if(busy)throw Error('HOTSPOT_BUSY');
const values={operation:'hotspotSave',ssid:ssid.value,security:security.value,password:password.value};
if(!values.ssid || new TextEncoder().encode(values.ssid).length>32 || (values.security!=='open' && (new TextEncoder().encode(values.password).length<8 || new TextEncoder().encode(values.password).length>63))) {
status.textContent=message('Nom : 1 à 32 octets. Mot de passe WPA2 : 8 à 63 octets.','Name: 1–32 bytes. WPA2 password: 8–63 bytes.');throw Error('HOTSPOT_INVALID');
}
busy=true;const controls=panel.querySelectorAll('input,select,button,gaia-checkbox');controls.forEach(c=>c.disabled=true);
try {await wifi(values);return {};}
catch(error) {status.textContent=message('Impossible d’enregistrer le point d’accès.','Cannot save the hotspot.');throw error;}
finally {busy=false;controls.forEach(c=>c.disabled=false);}
},
onCancel() {if(busy)throw Error('HOTSPOT_BUSY');},
onHide() {password.value='';}
};
}
window.VulpesWifiSettings={panels:{hotspot:hotspotPanel},dialogs:{'hotspot-wifiSettings':hotspotDialog}};
}
addEventListener('DOMContentLoaded', async () => {
try {
const state = await call('platform.wifi',{operation:'status'});
power(state.enabled);
if (location.hostname.startsWith('system.')) {
let queue=Promise.resolve();
navigator.mozSettings.addObserver('wifi.enabled', event => {
const enabled=event.settingValue===true;
queue=queue.then(async () => {
if(manager.enabled===enabled) {power(enabled,true);return;}
try {
const result=await call('platform.wifi',{operation:'setEnabled',enabled});
power(result.enabled,true);
if(!result.enabled)await call('settings.set',{values:{'tethering.wifi.enabled':false}});
} catch(error) {
await call('settings.set',{values:{'wifi.enabled':manager.enabled}});
console.warn('Wi-Fi radio change failed',error);
}
});
});
await call('settings.set',{values:{'wifi.enabled':state.enabled}});
} else {
navigator.mozSettings.addObserver('wifi.enabled', () => {
call('platform.wifi',{operation:'status'}).then(s=>power(s.enabled)).catch(console.warn);
});
}
// Settings may receive the preference before the radio operation completes.
// Refresh while visible so its panel also follows hardware power changes.
if(location.hostname.startsWith('settings.')) {
let pending=false;
const refresh=setInterval(async()=>{
if(document.hidden || pending) return;
pending=true;
try {power((await call('platform.wifi',{operation:'status'})).enabled);}
catch(error) {console.warn('Wi-Fi status refresh failed',error);}
finally {pending=false;}
},2000);
addEventListener('pagehide',()=>clearInterval(refresh),{once:true});
}
if(manager.enabled) await scan();
} catch(error) { console.warn('Wi-Fi initialization failed',error); }
}, {once:true});
})();