VulpesOS/android/extension/content.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

131 lines
5.1 KiB
JavaScript

/* Extension world: the browser provides origin/tab/frame identity, never Gaia. */
(() => {
if (location.port !== '18765' || location.hostname.startsWith('user-')) return;
const win = window.wrappedJSObject;
console.info('VULPES Android bridge', location.origin);
let port;
let reconnect;
function connect() {
port = browser.runtime.connect({ name: 'gaia' });
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(onDisconnect);
}
const pending = new Map();
let seq = 0;
function request(data) {
return new window.Promise((resolve, reject) => {
const id = ++seq;
pending.set(id, { resolve, reject });
try {
port.postMessage({ id, request: JSON.parse(JSON.stringify(data)) });
} catch (error) {
pending.delete(id);
reject(new window.Error('VULPES_HOST_CLOSED'));
}
});
}
function onMessage(message) {
if (message.edit) {
try {
const result=editor.edit(message.edit.params);
result.inputMode=originalModes.get(editor.inputElement) ||
(editor.inputElement.classList.contains('recipient')?'tel':'');
port.postMessage({editReply:message.edit.id, result});
}
catch (error) { port.postMessage({editReply:message.edit.id, error:String(error.message)}); }
return;
}
if (message.event) {
if(message.event.type==='input-mode') {
gaiaKeyboard=message.event.data.enabled===true;
if(!gaiaKeyboard) restoreMode();
win.VulpesNativeInput=gaiaKeyboard;
win.dispatchEvent(new window.Event('vulpes-input-configured'));
}
win.dispatchEvent(
new window.CustomEvent('vulpes-service-event', { detail: cloneInto(message.event, win) }),
);
return;
}
const callback = pending.get(message.id);
if (!callback) return;
pending.delete(message.id);
callback.resolve(cloneInto(message.reply, win));
}
function onDisconnect() {
for (const callback of pending.values())
callback.reject(new window.Error('VULPES_HOST_CLOSED'));
pending.clear();
clearTimeout(reconnect);
reconnect = setTimeout(() => {
try {
connect();
} catch (_) {
onDisconnect();
}
}, 1000);
}
connect();
const editor = new InputEditor(window);
let gaiaKeyboard = false;
const originalModes = new WeakMap();
function restoreMode() {
const field = editor.inputElement;
if (!field || !originalModes.has(field)) return;
const value = originalModes.get(field);
if (value === null) field.removeAttribute('inputmode');
else field.setAttribute('inputmode', value);
originalModes.delete(field);
}
request({version:1,id:0,method:'input.config',params:{}}).then(reply => {
gaiaKeyboard = reply.result === true;
win.VulpesNativeInput = gaiaKeyboard;
win.dispatchEvent(new window.Event('vulpes-input-configured'));
}).catch(error => console.error('Gaia input configuration:', error));
for (const type of ['focusin', 'pointerdown', 'click']) addEventListener(type, event => {
if (!gaiaKeyboard || editor.editing || location.hostname === 'keyboard.localhost') return;
let element = event.target;
if (element?.isContentEditable) element=element.closest('[contenteditable=true]') || element;
const editable = element?.isContentEditable ||
(['input','textarea'].includes(element?.localName) &&
!['button','submit','range','checkbox','radio','date','time','datetime-local','file'].includes(element.type));
if (!editable || element.disabled || element.readOnly) {
if (type === 'click' && !element?.closest?.('[data-vulpes-keep-input]')) port.postMessage({inputFocus:null});
return;
}
if (element !== editor.inputElement) restoreMode();
editor.focus(element);
if (!originalModes.has(element)) originalModes.set(element, element.getAttribute('inputmode'));
// Suppress Android's IME only in this editor; external web pages keep it.
element.setAttribute('inputmode', 'none');
const state=editor.state();
state.inputMode=originalModes.get(element) || (element.classList.contains('recipient')?'tel':'');
port.postMessage({inputFocus:state});
}, true);
addEventListener('visibilitychange', () => {
if (document.hidden && gaiaKeyboard) { restoreMode(); port.postMessage({inputFocus:null}); }
});
addEventListener('unload', () => clearTimeout(reconnect));
exportFunction(request, win.navigator, { defineAs: 'vulpesRequest' });
const frames = new Map();
let nextFrame = 0;
exportFunction(
(frame, data) => {
if (frame.ownerDocument !== window.document || frame.localName !== 'iframe')
throw new window.Error('INVALID_VIEW');
let id = [...frames].find(([, f]) => f === frame)?.[0];
if (!id) {
id = ++nextFrame;
frames.set(id, frame);
}
const params = JSON.parse(JSON.stringify(data));
params.contextId = id;
return request({ version: 1, id: 0, method: 'views.request', params });
},
win.navigator,
{ defineAs: 'vulpesFrameRequest', allowCrossOriginArguments: true },
);
exportFunction((id) => frames.get(id) || null, win.navigator, {
defineAs: 'vulpesFrameForContext',
});
})();