Release Vulpes OS 2.7-preview.22
This commit is contained in:
parent
200d1fb235
commit
a138f97877
294 changed files with 515786 additions and 187 deletions
|
|
@ -1,5 +1,5 @@
|
|||
export function isGaiaActivity(source) {
|
||||
return source?.name === 'pick' ||
|
||||
return source?.name === 'pick' || source?.name === 'share' ||
|
||||
(source?.name === 'moz_configure_window' && source.data?.target === 'device') ||
|
||||
(source?.name === 'configure' && source.data?.target === 'ringtone') ||
|
||||
(source?.name === 'dial' && source.data?.type === 'webtelephony/number') ||
|
||||
|
|
@ -22,9 +22,9 @@ export class Activities {
|
|||
this.listeners = new Map();
|
||||
this.choices = new Map();
|
||||
}
|
||||
route(source, caller) {
|
||||
route(source, caller, shareProvider) {
|
||||
const types = [].concat(source?.data?.type || []), name=source?.name;
|
||||
let id;
|
||||
let id = name === 'share' && ['sms','email'].includes(shareProvider) ? shareProvider : undefined;
|
||||
if (name === 'pick') {
|
||||
if (types.some(t=>['ringtone','alerttone'].includes(t))) id='ringtones';
|
||||
else if (types.includes('wallpaper')) id='wallpaper';
|
||||
|
|
@ -41,14 +41,16 @@ export class Activities {
|
|||
const definition = candidates.find(d=>{
|
||||
const filter=d.filters?.type;
|
||||
const accepted=[].concat(filter?.value || filter || []);
|
||||
return !accepted.length || types.some(t=>accepted.includes(t));
|
||||
const matches = !accepted.length || types.some(t=>typeof t==='string' && accepted.some(a=>a===t || (typeof a==='string' && a.endsWith('/*') && t.startsWith(a.slice(0,-1)))));
|
||||
const count=source.data?.blobs?.length || source.data?.number || 0;
|
||||
return matches && (!d.filters?.number?.max || count<=d.filters.number.max);
|
||||
});
|
||||
if (!app || !definition?.href) throw Error('ACTIVITY_NOT_SUPPORTED');
|
||||
const url=new URL(definition.href,app.origin);
|
||||
if (url.origin!==new URL(app.origin).origin) throw Error('ACTIVITY_NOT_SUPPORTED');
|
||||
// SMS handles the activity after startup; its old manifest fragment is
|
||||
// not a Navigation view in the packaged Gaia build.
|
||||
if (id==='sms' && name==='new') url.hash='';
|
||||
if (id==='sms' && ['new','share'].includes(name)) url.hash='';
|
||||
const entryPoint=entryPointFor(app,url.href);
|
||||
return {app,definition,url,entryPoint};
|
||||
}
|
||||
|
|
@ -62,18 +64,18 @@ export class Activities {
|
|||
this.launch(settings,settings.manifest.launch_path || '/index.html',{});
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (!app || !['settings','system','homescreen','verticalhome','ringtones','sms','communications','email','calendar'].includes(app.id))
|
||||
if (!app || !['settings','system','homescreen','verticalhome','ringtones','sms','communications','email','calendar','gallery','video','music','camera'].includes(app.id))
|
||||
throw Error('PERMISSION_DENIED');
|
||||
const types = [].concat(source?.data?.type || []);
|
||||
if (source?.name==='pick' && types.length>1 && this.choose) {
|
||||
if (source?.name==='share' || (source?.name==='pick' && types.length>1 && this.choose)) {
|
||||
const routes=[];
|
||||
for (const type of types) {
|
||||
for (const candidate of (source.name==='share' ? ['sms','email'] : types)) {
|
||||
try {
|
||||
const route=this.route({...source,data:{...source.data,type}},app);
|
||||
const route=source.name==='share' ? this.route(source,app,candidate) : this.route({...source,data:{...source.data,type:candidate}},app);
|
||||
if (!routes.some(r=>r.app.id===route.app.id)) routes.push(route);
|
||||
} catch (error) { if (error.message!=='ACTIVITY_NOT_SUPPORTED') throw error; }
|
||||
}
|
||||
if (routes.length>1) {
|
||||
if (routes.length>1 && this.choose) {
|
||||
if (this.choices.size) throw Error('ACTIVITY_BUSY');
|
||||
const id=this.uuid();
|
||||
return new Promise((resolve,reject)=>{
|
||||
|
|
@ -87,6 +89,7 @@ export class Activities {
|
|||
});
|
||||
}
|
||||
if (routes.length===1) return this.open(caller,app,source,routes[0]);
|
||||
if(source.name==='share')throw Error('NO_PROVIDER');
|
||||
}
|
||||
return this.open(caller,app,source,this.route(source,app));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ export class InputEditor {
|
|||
let key=typeof p.key==='object'?p.key.key:p.key;
|
||||
if(key===8 || key==='Backspace') this.document.execCommand('delete');
|
||||
else if(key===13 || key==='Enter') {
|
||||
const options={key:'Enter',code:'Enter',keyCode:13,charCode:13,which:13,bubbles:true,cancelable:true};
|
||||
const accepted=e.dispatchEvent(new win.KeyboardEvent('keydown',options)) && e.dispatchEvent(new win.KeyboardEvent('keypress',options));
|
||||
if(accepted && (e.isContentEditable || e.localName==='textarea')) this.document.execCommand('insertLineBreak');
|
||||
e.dispatchEvent(new win.KeyboardEvent('keyup',options));
|
||||
// Software Enter edits the field or submits its form. Synthetic keyboard
|
||||
// events enter native key-binding IPC on the standalone Wayland host.
|
||||
if(e.isContentEditable || e.localName==='textarea') this.document.execCommand('insertLineBreak');
|
||||
else if(e.localName==='input' && e.form) e.form.requestSubmit();
|
||||
} else {
|
||||
const text=typeof key==='string'?key:String.fromCodePoint(p.charCode || key);
|
||||
if(text.length>16) throw Error('INVALID_VALUE');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
export class Input {
|
||||
constructor(runtime) { this.runtime=runtime; this.focus=null; this.sequence=0; }
|
||||
changed(actor, state) {
|
||||
const app=this.runtime.identity(actor);
|
||||
// Physical keypads edit through their actor; do not cover the small screen with an IME.
|
||||
if (this.runtime.hardwareKeyboard) { if (this.focus) this.hide(); return; }
|
||||
const app=this.runtime.inputIdentity ? this.runtime.inputIdentity(actor) : this.runtime.identity(actor);
|
||||
if (!app || app.id==='keyboard') return;
|
||||
if (!state) { if(this.focus?.actor===actor) this.hide(); return; }
|
||||
this.focus={actor,state:{...state,id:++this.sequence}};
|
||||
|
|
@ -27,6 +29,9 @@ export class Input {
|
|||
}
|
||||
if(!this.focus || p.id!==this.focus.state.id) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
const context=this.focus;
|
||||
if (this.runtime.inputIdentity && !this.runtime.inputIdentity(context.actor)) {
|
||||
this.hide(); throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
}
|
||||
const result=await context.actor.sendQuery('Vulpes:Edit',p);
|
||||
// An application can close or focus another field while the edit is pending.
|
||||
if(this.focus!==context) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
|
|
|
|||
19
services/keypad-navigation.mjs
Normal file
19
services/keypad-navigation.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export function nextKeypadTarget(items, current, direction) {
|
||||
if (!items.length) return -1;
|
||||
if (current < 0 || current >= items.length) return 0;
|
||||
const vectors = {ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1]};
|
||||
const vector = vectors[direction];
|
||||
if (!vector) return -1;
|
||||
const origin = items[current];
|
||||
let best = -1, score = Infinity;
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
if (index === current) continue;
|
||||
const dx = items[index].x - origin.x, dy = items[index].y - origin.y;
|
||||
const forward = dx * vector[0] + dy * vector[1];
|
||||
if (forward <= 1) continue;
|
||||
const sideways = Math.abs(dx * vector[1] - dy * vector[0]);
|
||||
const distance = forward + sideways * 3;
|
||||
if (distance < score) { best = index; score = distance; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
105
services/keypad-text.mjs
Normal file
105
services/keypad-text.mjs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Numeric keypad composition. The caller supplies field identity and selection.
|
||||
const letters = {
|
||||
'0': ' 0', '1': '.,?!@-_:;/1', '2': 'abcàâç2', '3': 'deféèêë3',
|
||||
'4': 'ghiîï4', '5': 'jkl5', '6': 'mnoôö6', '7': 'pqrs7',
|
||||
'8': 'tuvùûü8', '9': 'wxyz9', '*': '*+()[]#',
|
||||
};
|
||||
export class KeypadText {
|
||||
mode = 'abc';
|
||||
pending = null;
|
||||
dictionary = new Map();
|
||||
returnMode = 'abc';
|
||||
setDictionary(lines) {
|
||||
const words = [];
|
||||
for (const line of lines.split(/\r?\n/)) {
|
||||
const [word, frequency] = line.split('\t');
|
||||
const key = t9KeySequence(word || '');
|
||||
if (!key) continue;
|
||||
words.push({word,key,frequency:Number(frequency) || 0});
|
||||
}
|
||||
words.sort((a,b)=>b.frequency-a.frequency || a.word.localeCompare(b.word));
|
||||
const prefixes=new Map();
|
||||
for (const entry of words) for(let length=1;length<=entry.key.length;length++) {
|
||||
const prefix=entry.key.slice(0,length),list=prefixes.get(prefix) || [];
|
||||
if(list.length<32) {list.push(entry);prefixes.set(prefix,list);}
|
||||
}
|
||||
this.dictionary=prefixes;
|
||||
}
|
||||
formatT9(word) { return this.returnMode==='ABC' ? word.toUpperCase() : word; }
|
||||
get suggestion() { return this.pending?.t9 ? this.formatT9(this.pending.candidates[this.pending.index]?.word || '') : null; }
|
||||
setT9(enabled, previousMode=this.mode) {
|
||||
if (enabled) { this.returnMode=previousMode; this.mode='t9'; }
|
||||
else this.mode=previousMode;
|
||||
this.commit();
|
||||
return {mode:this.mode};
|
||||
}
|
||||
toggleT9() {
|
||||
return this.setT9(this.mode!=='t9',this.mode==='t9' ? this.returnMode : this.mode);
|
||||
}
|
||||
commit() { this.pending = null; }
|
||||
press(key, {field, text, start, end, numeric = false, now = Date.now()}) {
|
||||
if (numeric) { this.commit(); return /^[0-9*#+]$/.test(key) ? {text:key, replace:0} : null; }
|
||||
if (key === '#') {
|
||||
this.mode = this.mode==='t9' ? this.returnMode : {abc:'ABC', ABC:'123', '123':'abc'}[this.mode];
|
||||
this.commit(); return {mode:this.mode};
|
||||
}
|
||||
if (this.mode === 't9') return this.pressT9(key,{field,text,start,end,now});
|
||||
if (!letters[key]) { this.commit(); return null; }
|
||||
if (this.mode === '123' && key !== '*') { this.commit(); return {text:key,replace:0}; }
|
||||
const choices = this.mode === 'ABC' ? letters[key].toUpperCase() : letters[key];
|
||||
const p = this.pending;
|
||||
const cycle = p && p.field === field && p.key === key && now - p.time < 900 &&
|
||||
start === end && start === p.end && text.slice(start - p.text.length, start) === p.text;
|
||||
const index = cycle ? (p.index + 1) % choices.length : 0;
|
||||
const value = choices[index];
|
||||
this.pending = {field, key, index, text:value, time:now,
|
||||
end:(cycle ? start - p.text.length : start) + value.length};
|
||||
return {text:value, replace:cycle ? p.text.length : 0};
|
||||
}
|
||||
pressT9(key,{field,text,start,end,now}) {
|
||||
const p=this.pending;
|
||||
const continuing=p?.t9 && p.field===field && start===end && start===p.end &&
|
||||
text.slice(p.start,start)===p.text;
|
||||
if (key==='*' && continuing && p.candidates.length>1) {
|
||||
p.index=(p.index+1)%p.candidates.length;
|
||||
const value=this.formatT9(p.candidates[p.index].word);
|
||||
const replace=p.text.length;
|
||||
p.text=value;p.end=p.start+value.length;p.time=now;
|
||||
return {text:value,replace};
|
||||
}
|
||||
if (key==='0') { this.commit(); return {text:' ',replace:0}; }
|
||||
if (key==='1') { this.commit(); return {text:'.',replace:0}; }
|
||||
if (!/^[2-9]$/.test(key)) { this.commit(); return null; }
|
||||
const sequence=continuing ? p.sequence+key : key;
|
||||
const candidates=this.dictionary.get(sequence) || [];
|
||||
if (!candidates.length) {
|
||||
this.commit();
|
||||
return {text:letters[key][0],replace:0};
|
||||
}
|
||||
const base=continuing ? p.start : start;
|
||||
const value=this.formatT9(candidates[0].word);
|
||||
const replace=continuing ? p.text.length : 0;
|
||||
this.pending={t9:true,field,sequence,candidates,index:0,text:value,
|
||||
start:base,end:base+value.length,time:now};
|
||||
return {text:value,replace};
|
||||
}
|
||||
}
|
||||
|
||||
const latinKeys=['abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'];
|
||||
const cyrillicKeys=['абвг','дежзё','ийкл','мноп','рсту','фхцч','шщъы','ьэюя'];
|
||||
const greekKeys=['αβγ','δεζ','ηθι','κλμ','νξο','πρσ','τυφ','χψω'];
|
||||
export function t9KeySequence(word) {
|
||||
let result='';
|
||||
const normalized=String(word).toLocaleLowerCase()
|
||||
.replace(/[æǽ]/g,'ae').replace(/œ/g,'oe').replace(/ł/g,'l')
|
||||
.replace(/đ/g,'d').replace(/ð/g,'d').replace(/þ/g,'t')
|
||||
.replace(/ß/g,'ss').replace(/ς/g,'σ');
|
||||
for (const original of normalized) {
|
||||
const char=original.normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace('ı','i');
|
||||
const groups=/[a-z]/.test(char) ? latinKeys : /[\u0400-\u04ff]/.test(char) ? cyrillicKeys : greekKeys;
|
||||
const group=groups.findIndex(letters=>letters.includes(char));
|
||||
if (group<0) return '';
|
||||
result+=String(group+2);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
23
services/media-permission.mjs
Normal file
23
services/media-permission.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Use Gaia's permission panel so fullscreen hosts do not open a hidden desktop dialog.
|
||||
export function requestMediaPermission(win, request, clone = value => value) {
|
||||
return new Promise(resolve => {
|
||||
const finish = result => {
|
||||
win.removeEventListener('mozContentEvent', response);
|
||||
win.removeEventListener('pagehide', cancelled);
|
||||
resolve(result);
|
||||
};
|
||||
const response = event => {
|
||||
const detail = event.detail;
|
||||
if (detail?.id !== request.id || !['permission-allow', 'permission-deny'].includes(detail.type)) return;
|
||||
finish({allowed:detail.type === 'permission-allow', remember:detail.remember === true});
|
||||
};
|
||||
const cancelled = () => finish({allowed:false, remember:false});
|
||||
win.addEventListener('mozContentEvent', response);
|
||||
win.addEventListener('pagehide', cancelled, {once:true});
|
||||
win.dispatchEvent(new win.CustomEvent('mozChromeEvent', {detail:clone({
|
||||
type:'permission-prompt', id:request.id, origin:request.origin,
|
||||
manifestURL:request.manifestURL, isApp:true, isGranted:false, remember:false,
|
||||
permissions:{[request.kind === 'microphone' ? 'audio-capture' : 'video-capture']:[]},
|
||||
})}));
|
||||
});
|
||||
}
|
||||
|
|
@ -2,6 +2,10 @@
|
|||
const MAX_ZIP = 16 * 1024 * 1024, MAX_FILES = 2048, MAX_EXPANDED = 48 * 1024 * 1024;
|
||||
export function packageURL(value) {
|
||||
const url = new URL(value);
|
||||
if (url.username || url.password) throw Error('INVALID_PACKAGE_URL');
|
||||
if (url.origin === 'https://ffapps.danielherr.software' && !url.search && !url.hash &&
|
||||
/^\/app\/[^/]+\/(?:manifest\.webapp|[^/]+\.zip)$/.test(decodeURIComponent(url.pathname)) &&
|
||||
!/[\\\x00-\x1f]/.test(decodeURIComponent(url.pathname))) return url.href;
|
||||
if (url.origin !== 'https://vulpes-os.org' || url.pathname !== '/marketplace/index.php' ||
|
||||
!/^\d{1,10}$/.test(url.searchParams.get('download') || '') || url.username || url.password)
|
||||
throw Error('INVALID_PACKAGE_URL');
|
||||
|
|
@ -63,8 +67,17 @@ export class Packages {
|
|||
this.cancel();const controller=this.controller=new AbortController();
|
||||
const timer=this.schedule(()=>controller.abort(),60000);
|
||||
try {
|
||||
const source=packageURL(url),response=await this.fetchPackage(source,{redirect:'error',credentials:'omit',signal:controller.signal});
|
||||
const source=packageURL(url),options={redirect:'error',credentials:'omit',signal:controller.signal};
|
||||
let response=await this.fetchPackage(source,options);
|
||||
if(!response.ok)throw Error('DOWNLOAD_FAILED');
|
||||
if(new URL(source).pathname.endsWith('/manifest.webapp')) {
|
||||
const manifest=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(await bounded(response.body,128*1024)));
|
||||
if(typeof manifest.package_path!=='string')throw Error('MISSING_PACKAGE');
|
||||
const archive=packageURL(new URL(manifest.package_path,source).href);
|
||||
if(new URL(archive).origin!==new URL(source).origin || !new URL(archive).pathname.endsWith('.zip'))throw Error('INVALID_PACKAGE_URL');
|
||||
response=await this.fetchPackage(archive,options);
|
||||
if(!response.ok)throw Error('DOWNLOAD_FAILED');
|
||||
}
|
||||
const item=await unpackPackage(await bounded(response.body,MAX_ZIP),source);
|
||||
if(controller.signal.aborted)throw Error('DOWNLOAD_CANCELLED');
|
||||
this.pending.clear();const token=this.uuid();this.pending.set(token,{item,expires:Date.now()+300000});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export function authorizePlatform(app, operation, params = {}) {
|
|||
settings: ['settings', 'system', 'costcontrol'].includes(app.id),
|
||||
snapshot: ['settings', 'system', 'costcontrol'].includes(app.id),
|
||||
wifi: ['settings', 'system'].includes(app.id),
|
||||
radioPower: ['system', 'settings'].includes(app.id),
|
||||
mobileSettings: app.id === 'settings',
|
||||
deviceSettings: app.id === 'settings',
|
||||
bluetooth: ['settings', 'system', 'bluetooth'].includes(app.id),
|
||||
|
|
@ -34,15 +35,18 @@ export function authorizePlatform(app, operation, params = {}) {
|
|||
if (!Object.hasOwn(allowed, operation)) throw Error('METHOD_NOT_SUPPORTED');
|
||||
if (!allowed[operation]) throw Error('PERMISSION_DENIED');
|
||||
if (operation === 'wifi' && ['hotspotStatus','hotspotSave','hotspotPower'].includes(params.operation) && app.id !== 'settings') throw Error('PERMISSION_DENIED');
|
||||
if (operation === 'radioPower' && (!Number.isInteger(params.slot) || ![0,1].includes(params.slot) || typeof params.enabled !== 'boolean')) throw Error('INVALID_VALUE');
|
||||
if (operation === 'mobileSettings' && !['sim','smsc','calls','messages','deliveryReports','network','dataEnabled','roaming','radioMode','setRadioMode','apns','editApn','setCallWaiting','setCallerId','forwarding','setForwarding'].includes(params.action)) throw Error('INVALID_VALUE');
|
||||
if (operation === 'mobileSettings' && ['deliveryReports','dataEnabled','roaming','setCallWaiting','setForwarding'].includes(params.action) && typeof params.enabled !== 'boolean') throw Error('INVALID_VALUE');
|
||||
if (operation === 'mobileSettings' && JSON.stringify(params).length > 6144) throw Error('INVALID_VALUE');
|
||||
if (operation === 'deviceSettings' && (!['clock','setTime','setTimezone','setAutomatic'].includes(params.action) || JSON.stringify(params).length > 1024)) throw Error('INVALID_VALUE');
|
||||
if (operation === 'cameraPreview' &&
|
||||
(!['start', 'frame', 'stop', 'focus', 'zoom', 'picture'].includes(params.action) ||
|
||||
(!['start', 'frame', 'stop', 'focus', 'zoom', 'picture', 'flash'].includes(params.action) ||
|
||||
(params.action === 'start' && !['back', 'front'].includes(params.camera))))
|
||||
throw Error('INVALID_VALUE');
|
||||
if (operation === 'cameraPreview') {
|
||||
if (params.action === 'flash' && !['off','auto','on','torch'].includes(params.mode))
|
||||
throw Error('INVALID_VALUE');
|
||||
if (params.action === 'zoom' && (!Number.isFinite(params.zoom) || params.zoom<1 || params.zoom>100))
|
||||
throw Error('INVALID_VALUE');
|
||||
if (params.action === 'focus' && params.area != null) {
|
||||
|
|
|
|||
29
services/radio-state.mjs
Normal file
29
services/radio-state.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Public radio state excludes subscriber identifiers, cell location and raw parcels.
|
||||
export function radioState(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.slice(0, 4).map((slot, index) => {
|
||||
if (!slot || slot.slot !== index || slot.available !== true)
|
||||
return {slot:index, available:false, radioState:'unknown'};
|
||||
const state = {
|
||||
slot:index, available:true,
|
||||
radioState:['enabled','disabled','unavailable'].includes(slot.radioState) ? slot.radioState : 'unknown',
|
||||
signal:Number.isFinite(slot.signal) && slot.signal>=0 && slot.signal<=100 ? slot.signal : null,
|
||||
operator:{longName:String(slot.operator?.longName || '').slice(0,100),
|
||||
shortName:String(slot.operator?.shortName || '').slice(0,100)},
|
||||
};
|
||||
if (typeof slot.sim?.present === 'boolean') state.sim = {
|
||||
present:slot.sim.present,
|
||||
cardState:['absent','ready','pinRequired','pukRequired','networkLocked'].includes(slot.sim.cardState)
|
||||
? slot.sim.cardState : 'unknown',
|
||||
};
|
||||
for (const kind of ['voice','data']) {
|
||||
const info=slot[kind] || {};
|
||||
state[kind]={state:['notSearching','registered','searching','denied'].includes(info.state) ? info.state : 'unknown',
|
||||
connected:info.connected === true, roaming:info.roaming === true,
|
||||
emergencyCallsOnly:info.emergencyCallsOnly === true,
|
||||
type:['gprs','edge','umts','is95a','is95b','1xrtt','evdo0','evdoa','hsdpa','hsupa','hspa','evdob','ehrpd','lte','hspa+','gsm'].includes(info.type) ? info.type : null};
|
||||
}
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,25 @@
|
|||
// No Gecko, XPCOM, DOM or filesystem imports in the service contract.
|
||||
export const API_VERSION = 1;
|
||||
|
||||
export function migratePrivacyDefaults(values) {
|
||||
if (values['vulpes.privacy-default.v1']) return false;
|
||||
values['privacy.trackingprotection.enabled'] = true;
|
||||
values['privacy.trackingprotection.shown'] = true;
|
||||
values['privacy.donottrackheader.enabled'] = true;
|
||||
values['privacy.donottrackheader.value'] = 1;
|
||||
values['vulpes.privacy-default.v1'] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function migrateSearchDefaults(values) {
|
||||
if (values['vulpes.search-default.v1']) return false;
|
||||
// A missing/false provider means the old bundled default, not a user choice.
|
||||
if (!values['search.provider']) values['search.provider'] = 'ddg';
|
||||
delete values['search.cache'];
|
||||
values['vulpes.search-default.v1'] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export class ServiceError extends Error {
|
||||
constructor(code) {
|
||||
super(code);
|
||||
|
|
|
|||
9
services/view-visibility.mjs
Normal file
9
services/view-visibility.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// Gaia's transparent edge gesture zones are not modal overlays.
|
||||
export function frameUnobscured(document, frame, rect) {
|
||||
if (document.hidden || rect.width <= 0 || rect.height <= 0) return false;
|
||||
const system = document.location.hostname === 'system.localhost';
|
||||
return [[.5, .5], [.05, .05], [.95, .05], [.05, .95], [.95, .95]].every(([x, y]) => {
|
||||
const hit = document.elementFromPoint(rect.left + rect.width * x, rect.top + rect.height * y);
|
||||
return hit === frame || (system && !!hit?.matches('#left-panel.gesture-panel, #right-panel.gesture-panel'));
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue