Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
434
host/gaia-services.js
Normal file
434
host/gaia-services.js
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
// Content-side legacy interfaces; every operation is authorized by the host.
|
||||
(() => {
|
||||
const { call, request, handlers } = window.VulpesCompat;
|
||||
document.documentElement.dataset.vulpesApp = location.hostname.split('.')[0];
|
||||
if(window.VulpesPlatform==='tundra' && location.hostname==='system.localhost') {
|
||||
Object.defineProperty(navigator,'vibrate',{value:pattern=>{
|
||||
pattern=Array.isArray(pattern)?pattern:[pattern];
|
||||
if(pattern.length>32 || pattern.some(n=>!Number.isInteger(n) || n<0 || n>10000) || pattern.reduce((a,b)=>a+b,0)>10000) return false;
|
||||
call('hardware.vibrate',{pattern}).catch(error=>console.warn('Vibration:',error.message));
|
||||
return true;
|
||||
}});
|
||||
} else if (!navigator.vibrate) Object.defineProperty(navigator, 'vibrate', { value: () => false });
|
||||
if (window.Notification && !Notification.get)
|
||||
Notification.get = async () => {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
return (await Promise.all(registrations.map((r) => r.getNotifications()))).flat();
|
||||
};
|
||||
function unavailable(message) {
|
||||
window.VulpesGaiaUI?.notice(message);
|
||||
}
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
if (location.hostname === 'system.localhost' && !window.VulpesPlatform) {
|
||||
const ids = ['wifi', 'bluetooth', 'data', 'airplane-mode'];
|
||||
for (const id of ids) {
|
||||
const e = document.getElementById('quick-settings-' + id);
|
||||
if (!e) continue;
|
||||
e.setAttribute('aria-disabled', 'true');
|
||||
e.title = 'Connexion gérée par le système hôte';
|
||||
e.style.cssText += ';display:block;color:#aaa;opacity:.6';
|
||||
e.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
unavailable(
|
||||
'Sur ce bureau, les connexions réseau sont gérées par Linux. Aucun modem téléphonique n’est connecté.',
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (location.hostname === 'settings.localhost') {
|
||||
document.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
const link = event.target.closest(
|
||||
'a[href="#fxa"],a[href="#fxa-about"],a[href="#fxa-signin"],a[href="#fxa-sync"]',
|
||||
);
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
unavailable(
|
||||
'Les comptes Firefox et la synchronisation distante ne sont pas portés dans cette version bureau.',
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (location.hostname === 'communications.localhost') {
|
||||
document.getElementById('keypad-callbar-call-action')?.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (navigator.mozTelephony || window.VulpesPlatform) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
unavailable(
|
||||
'Appels indisponibles : aucun modem téléphonique n’est connecté à cette version bureau.',
|
||||
);
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(navigator, 'mozAlarms', {
|
||||
value: {
|
||||
getAll: () =>
|
||||
request(
|
||||
call('alarms.request', { operation: 'getAll' }).then((list) =>
|
||||
list.map((r) => ({ ...r, date: new Date(r.date) })),
|
||||
),
|
||||
),
|
||||
add: (date, respectTimezone, data) =>
|
||||
request(
|
||||
call('alarms.request', {
|
||||
operation: 'add',
|
||||
date: +date,
|
||||
respectTimezone,
|
||||
data: data ?? null,
|
||||
local: [
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
date.getHours(),
|
||||
date.getMinutes(),
|
||||
date.getSeconds(),
|
||||
date.getMilliseconds(),
|
||||
],
|
||||
}),
|
||||
),
|
||||
remove: (id) => {
|
||||
call('alarms.request', { operation: 'remove', id }).catch(console.error);
|
||||
},
|
||||
},
|
||||
});
|
||||
addEventListener('vulpes-service-event', (event) => {
|
||||
if (event.detail.type === 'alarm') {
|
||||
const data = event.detail.data;
|
||||
handlers.get('alarm')?.({ ...data, date: new Date(data.date) });
|
||||
}
|
||||
});
|
||||
window.VulpesCompat.openAttention = (url) => {
|
||||
const target = new URL(url, location.href);
|
||||
if (
|
||||
target.origin !== location.origin ||
|
||||
target.pathname !== '/onring.html' ||
|
||||
location.hostname !== 'clock.localhost'
|
||||
)
|
||||
throw new DOMException('Unsupported attention view', 'NotSupportedError');
|
||||
const frame = document.createElement('iframe');
|
||||
frame.dataset.vulpesAttention = 'true';
|
||||
frame.src = target.href;
|
||||
frame.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;border:0;z-index:10000';
|
||||
document.body.append(frame);
|
||||
call('apps.launch', { manifestURL: location.origin + '/manifest.webapp' }).catch(console.error);
|
||||
return frame.contentWindow;
|
||||
};
|
||||
if (
|
||||
location.hostname === 'clock.localhost' &&
|
||||
location.pathname === '/onring.html' &&
|
||||
window.frameElement?.dataset.vulpesAttention === 'true'
|
||||
) {
|
||||
window.opener = parent;
|
||||
window.close = () => {
|
||||
dispatchEvent(new Event('beforeunload'));
|
||||
window.frameElement.remove();
|
||||
};
|
||||
}
|
||||
const ports = new Map();
|
||||
function portFor({ id }) {
|
||||
if (ports.has(id)) return ports.get(id);
|
||||
const port = new EventTarget();
|
||||
let listener = null,
|
||||
started = false,
|
||||
queue = [];
|
||||
const deliver = (data) => port.dispatchEvent(new MessageEvent('message', { data }));
|
||||
port.start = () => {
|
||||
started = true;
|
||||
for (const data of queue) deliver(data);
|
||||
queue = [];
|
||||
};
|
||||
Object.defineProperty(port, 'onmessage', {
|
||||
get: () => listener,
|
||||
set(fn) {
|
||||
if (listener) port.removeEventListener('message', listener);
|
||||
listener = fn;
|
||||
if (fn) port.addEventListener('message', fn);
|
||||
port.start();
|
||||
},
|
||||
});
|
||||
port.postMessage = (data) =>
|
||||
call('iac.send', { id, data }).catch((error) =>
|
||||
port.dispatchEvent(new MessageEvent('messageerror', { data: error.message })),
|
||||
);
|
||||
port.close = () => {
|
||||
ports.delete(id);
|
||||
call('iac.close', { id }).catch(console.error);
|
||||
};
|
||||
port.receive = (data) => (started ? deliver(data) : queue.push(data));
|
||||
ports.set(id, port);
|
||||
return port;
|
||||
}
|
||||
window.VulpesCompat.connect = (keyword) =>
|
||||
call('iac.connect', { keyword }).then((list) => list.map(portFor));
|
||||
addEventListener('vulpes-service-event', (event) => {
|
||||
const { type, data } = event.detail;
|
||||
if (type === 'connection')
|
||||
handlers.get('connection')?.({ keyword: data.keyword, port: portFor(data) });
|
||||
if (type === 'portmessage') portFor(data).receive(data.data);
|
||||
if (type === 'portclose') {
|
||||
const p = ports.get(data.id);
|
||||
ports.delete(data.id);
|
||||
p?.dispatchEvent(new Event('close'));
|
||||
}
|
||||
});
|
||||
function cursor(promise, convert = (value) => value) {
|
||||
let values,
|
||||
index = 0,
|
||||
ready = false;
|
||||
const c = new EventTarget();
|
||||
c.result = null;
|
||||
c.done = false;
|
||||
function next() {
|
||||
if (!ready) return;
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
c.result = index < values.length ? await convert(values[index++]) : null;
|
||||
c.done = c.result === null;
|
||||
c.onsuccess?.({ target: c });
|
||||
c.dispatchEvent(new Event('success'));
|
||||
} catch (error) {
|
||||
c.error = error;
|
||||
c.onerror?.({ target: c });
|
||||
c.dispatchEvent(new Event('error'));
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
c.continue = next;
|
||||
promise.then(
|
||||
(list) => {
|
||||
values = list;
|
||||
ready = true;
|
||||
next();
|
||||
},
|
||||
(error) => {
|
||||
c.error = error;
|
||||
c.onerror?.({ target: c });
|
||||
c.dispatchEvent(new Event('error'));
|
||||
},
|
||||
);
|
||||
return c;
|
||||
}
|
||||
window.VulpesCompat.cursor = cursor;
|
||||
if (location.hostname === 'sms.localhost') {
|
||||
Object.defineProperty(navigator, 'mozMobileConnections', { value: Object.freeze([]) });
|
||||
const messages = new EventTarget();
|
||||
for (const operation of ['getThreads', 'getMessages'])
|
||||
messages[operation] = (...args) =>
|
||||
cursor(call('messages.request', { operation, args }), (value) => ({
|
||||
...value,
|
||||
timestamp: new Date(value.timestamp),
|
||||
}));
|
||||
for (const operation of [
|
||||
'getMessage',
|
||||
'send',
|
||||
'sendMMS',
|
||||
'retrieveMMS',
|
||||
'delete',
|
||||
'markMessageRead',
|
||||
'getSmscAddress',
|
||||
'setSmscAddress',
|
||||
'getSegmentInfoForText',
|
||||
])
|
||||
messages[operation] = (...args) => request(call('messages.request', { operation, args }));
|
||||
const dated = value => value && typeof value==='object' && 'timestamp' in value ?
|
||||
{...value,timestamp:new Date(value.timestamp)} : value;
|
||||
messages.getMessage = id => request(call('messages.request',{operation:'getMessage',args:[id]}).then(dated));
|
||||
messages.send = (recipients,body,options) => {
|
||||
const send = recipient => {
|
||||
let id, settle;
|
||||
const early = new Map();
|
||||
const terminal = new Promise((resolve,reject)=>{settle=message=>{
|
||||
if(message.delivery==='sent') resolve(dated(message));
|
||||
else reject(new DOMException('SMS transmission failed','UnknownError'));
|
||||
};});
|
||||
const listener=({detail})=>{
|
||||
if(!['sms-sent','sms-failed'].includes(detail.type)) return;
|
||||
const message=detail.data;
|
||||
if(id===message.id) settle(message);
|
||||
else if(id===undefined && early.size<100) early.set(message.id,message);
|
||||
};
|
||||
addEventListener('vulpes-service-event',listener);
|
||||
const result=call('messages.request',{operation:'send',args:[recipient,body,options]}).then(message=>{
|
||||
if(message.delivery!=='sending') return dated(message);
|
||||
id=message.id;
|
||||
if(early.has(id)) settle(early.get(id));
|
||||
early.clear();
|
||||
return terminal;
|
||||
}).finally(()=>removeEventListener('vulpes-service-event',listener));
|
||||
return request(result);
|
||||
};
|
||||
return Array.isArray(recipients) ? recipients.map(send) : send(recipients);
|
||||
};
|
||||
addEventListener('vulpes-service-event',({detail})=>{
|
||||
const type = {'sms-received':'received','sms-sending':'sending','sms-sent':'sent','sms-failed':'failed'}[detail.type];
|
||||
if (!type) return;
|
||||
const message=dated(detail.data);
|
||||
const event=Object.assign(new Event(type),{message});
|
||||
messages.dispatchEvent(event);messages['on'+type]?.(event);
|
||||
});
|
||||
Object.defineProperty(navigator, 'mozMobileMessage', { value: messages });
|
||||
}
|
||||
if (
|
||||
['gallery', 'music', 'video', 'camera', 'ringtones'].includes(location.hostname.split('.')[0])
|
||||
) {
|
||||
const stores = new Map();
|
||||
function storage(type) {
|
||||
if (!['pictures', 'music', 'videos'].includes(type)) return null;
|
||||
if (stores.has(type)) return stores.get(type);
|
||||
const s = new EventTarget();
|
||||
s.storageName = 'sdcard';
|
||||
s.default = true;
|
||||
s.canBeMounted = false;
|
||||
const run = (operation, params = {}) => call('media.request', { type, operation, ...params });
|
||||
for (const op of ['available', 'freeSpace', 'usedSpace']) s[op] = () => request(run(op));
|
||||
s.get = (name) =>
|
||||
request(
|
||||
run('get', { name }).then((f) => {
|
||||
const file = new File([f.blob], f.name, { type: f.type, lastModified: f.lastModified });
|
||||
Object.defineProperty(file, 'lastModifiedDate', { value: new Date(f.lastModified) });
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
s.addNamed = (blob, name) => request(run('add', { blob, name }));
|
||||
s.add = (blob) =>
|
||||
s.addNamed(
|
||||
blob,
|
||||
blob.name ||
|
||||
crypto.randomUUID() + { pictures: '.jpg', music: '.mp3', videos: '.mp4' }[type],
|
||||
);
|
||||
s.delete = (name) => request(run('delete', { name }));
|
||||
s.enumerate = (name = '', options = {}) => {
|
||||
if (typeof name === 'object') {
|
||||
options = name;
|
||||
name = '';
|
||||
}
|
||||
return cursor(run('enumerate', { name, since: options.since ? +options.since : 0 }), (f) =>
|
||||
s.get(f.name),
|
||||
);
|
||||
};
|
||||
stores.set(type, s);
|
||||
return s;
|
||||
}
|
||||
navigator.getDeviceStorage = storage;
|
||||
navigator.getDeviceStorages = (type) => {
|
||||
const s = storage(type);
|
||||
return s ? [s] : [];
|
||||
};
|
||||
addEventListener('vulpes-service-event', (event) => {
|
||||
const { type, data } = event.detail;
|
||||
if (type !== 'mediachange') return;
|
||||
const s = stores.get(data.type);
|
||||
if (!s) return;
|
||||
const change = new Event('change');
|
||||
Object.assign(change, { path: data.path, reason: data.reason });
|
||||
s.dispatchEvent(change);
|
||||
s.onchange?.(change);
|
||||
});
|
||||
addEventListener('DOMContentLoaded', () => {
|
||||
const type = { gallery: 'pictures', music: 'music', video: 'videos' }[
|
||||
location.hostname.split('.')[0]
|
||||
];
|
||||
if (!type || !['/', '/index.html'].includes(location.pathname) || window.VulpesPlatform)
|
||||
return;
|
||||
const label = document.createElement('label');
|
||||
label.className = 'vulpes-import';
|
||||
label.textContent = 'Importer';
|
||||
label.title = 'Copier des fichiers dans le profil de cet émulateur';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.accept = { pictures: 'image/*', music: 'audio/*', videos: 'video/*' }[type];
|
||||
input.hidden = true;
|
||||
label.append(input);
|
||||
input.addEventListener('change', async () => {
|
||||
try {
|
||||
for (const file of input.files) await storage(type).addNamed(file, file.name);
|
||||
label.title = 'Import terminé';
|
||||
} catch (error) {
|
||||
unavailable('Import impossible : ' + error.message);
|
||||
} finally {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
document.body.append(label);
|
||||
});
|
||||
}
|
||||
class Contact {
|
||||
constructor(data = {}) {
|
||||
this.init(data);
|
||||
}
|
||||
init(data) {
|
||||
Object.assign(this, data);
|
||||
for (const field of [
|
||||
'name',
|
||||
'givenName',
|
||||
'familyName',
|
||||
'additionalName',
|
||||
'honorificPrefix',
|
||||
'honorificSuffix',
|
||||
'nickname',
|
||||
'email',
|
||||
'tel',
|
||||
'adr',
|
||||
'org',
|
||||
'jobTitle',
|
||||
'note',
|
||||
'url',
|
||||
'category',
|
||||
'photo',
|
||||
])
|
||||
if (!this[field]) this[field] = [];
|
||||
}
|
||||
}
|
||||
window.mozContact = Contact;
|
||||
const contacts = new EventTarget();
|
||||
function convert(value) {
|
||||
const c = new Contact(value);
|
||||
for (const key of ['published', 'updated', 'bday', 'anniversary'])
|
||||
if (c[key]) c[key] = new Date(c[key]);
|
||||
return c;
|
||||
}
|
||||
contacts.find = (options) =>
|
||||
request(
|
||||
call('contacts.request', { operation: 'find', options }).then((list) => list.map(convert)),
|
||||
);
|
||||
contacts.getAll = (options) =>
|
||||
cursor(call('contacts.request', { operation: 'all', options }), convert);
|
||||
contacts.save = (contact) =>
|
||||
request(
|
||||
call('contacts.request', { operation: 'save', contact }).then((saved) => {
|
||||
Object.assign(contact, convert(saved));
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
contacts.remove = (contact) =>
|
||||
request(
|
||||
call('contacts.request', {
|
||||
operation: 'remove',
|
||||
id: typeof contact === 'string' ? contact : contact.id,
|
||||
}),
|
||||
);
|
||||
contacts.clear = () => request(call('contacts.request', { operation: 'clear' }));
|
||||
contacts.getCount = () => request(call('contacts.request', { operation: 'count' }));
|
||||
contacts.getRevision = () => request(call('contacts.request', { operation: 'revision' }));
|
||||
Object.defineProperty(navigator, 'mozContacts', { value: contacts });
|
||||
addEventListener('vulpes-service-event', (event) => {
|
||||
if (event.detail.type !== 'contactchange') return;
|
||||
const change = new Event('contactchange');
|
||||
Object.assign(change, event.detail.data);
|
||||
contacts.dispatchEvent(change);
|
||||
contacts.oncontactchange?.(change);
|
||||
});
|
||||
})();
|
||||
Loading…
Reference in a new issue