268 lines
13 KiB
JavaScript
268 lines
13 KiB
JavaScript
// Shared appearance preference; external browser pages are not injected.
|
|
(() => {
|
|
const key = 'vulpes.theme';
|
|
const system = matchMedia('(prefers-color-scheme: dark)');
|
|
let mode = 'light', select;
|
|
function apply(value) {
|
|
mode = ['light', 'dark', 'system'].includes(value) ? value : 'light';
|
|
document.documentElement.dataset.vulpesTheme =
|
|
mode === 'system' ? (system.matches ? 'dark' : 'light') : mode;
|
|
if (select) select.value = mode;
|
|
}
|
|
system.addEventListener('change', () => apply(mode));
|
|
addEventListener('vulpes-service-event', ({detail}) => {
|
|
if (detail.type === 'settingchange' && detail.data.key === key) apply(detail.data.value);
|
|
});
|
|
VulpesCompat.call('appearance.current', {}).then(apply).catch(console.error);
|
|
|
|
if (location.hostname !== 'settings.localhost') return;
|
|
function label() {
|
|
const lang = document.documentElement.lang || navigator.language;
|
|
const labels = lang.startsWith('fr') ? ['Apparence', 'Clair', 'Sombre', 'Selon le système'] :
|
|
lang.startsWith('el') ? ['Εμφάνιση', 'Φωτεινό', 'Σκοτεινό', 'Σύμφωνα με το σύστημα'] :
|
|
['Appearance', 'Light', 'Dark', 'Follow system'];
|
|
const title = document.getElementById('vulpes-theme-label');
|
|
if (title && title.textContent !== labels[0]) title.textContent = labels[0];
|
|
if (select) [...select.options].forEach((option, i) => {
|
|
if (option.textContent !== labels[i + 1]) option.textContent = labels[i + 1];
|
|
});
|
|
}
|
|
function insert() {
|
|
const list = document.querySelector('#display > div > ul');
|
|
if (!list || select?.isConnected) return;
|
|
const row = document.createElement('li'), title = document.createElement('p');
|
|
row.id = 'vulpes-theme-row';
|
|
title.id = 'vulpes-theme-label';
|
|
const wrapper = document.createElement('span');
|
|
wrapper.className = 'button icon icon-dialog';
|
|
select = document.createElement('select');
|
|
select.id = 'vulpes-theme';
|
|
select.setAttribute('aria-labelledby', title.id);
|
|
for (const value of ['light', 'dark', 'system']) {
|
|
const option = document.createElement('option');
|
|
option.value = value; select.append(option);
|
|
}
|
|
select.value = mode;
|
|
select.onchange = async () => {
|
|
select.disabled = true;
|
|
try { await navigator.mozSettings.createLock().set({[key]:select.value}); }
|
|
catch (error) { select.value = mode; console.error(error); }
|
|
finally { select.disabled = false; }
|
|
};
|
|
wrapper.append(select); row.append(title, wrapper); list.append(row); label();
|
|
}
|
|
addEventListener('localized', label);
|
|
new MutationObserver(() => { insert(); label(); }).observe(document.documentElement,
|
|
{childList:true, subtree:true, attributes:true, attributeFilter:['lang']});
|
|
insert();
|
|
})();
|
|
|
|
|
|
// Compact is a user preference, with a keypad-device default supplied by the host.
|
|
(() => {
|
|
const key = 'vulpes.layout';
|
|
let mode = 'auto', compactDefault = false, select;
|
|
function apply(value) {
|
|
mode = ['auto', 'compact', 'standard'].includes(value) ? value : 'auto';
|
|
const compact = mode === 'compact' || (mode === 'auto' && compactDefault);
|
|
document.documentElement.dataset.vulpesLayout = compact ? 'compact' : 'standard';
|
|
if (select) select.value = mode;
|
|
// Gaia caches grid coordinates; switching layout also invalidates that cache.
|
|
requestAnimationFrame(() => {
|
|
const apps = window.appWindow?.apps;
|
|
if (!apps?.icons) return;
|
|
if (compact) {
|
|
window.appWindow.panels.scrollLeft = 0;
|
|
window.appWindow.updatePanelIndicator();
|
|
}
|
|
apps._iconSize = 0;
|
|
apps.icons.synchronise();
|
|
apps.refreshGridSize();
|
|
});
|
|
}
|
|
addEventListener('vulpes-service-event', ({detail}) => {
|
|
if (detail.type === 'settingchange' && detail.data.key === key) apply(detail.data.value);
|
|
});
|
|
VulpesCompat.call('layout.current', {}).then(value => {
|
|
compactDefault = value.compactDefault;
|
|
document.documentElement.dataset.vulpesMotion = value.reducedMotion ? 'reduced' : 'standard';
|
|
if (value.reducedMotion) {
|
|
// Preserve animationend callbacks used by Gaia, and leave busy indicators running.
|
|
const shorten = animations => {
|
|
for (const animation of animations) {
|
|
const timing = animation.effect?.getTiming();
|
|
if (timing && Number.isFinite(timing.iterations) && animation.playState !== 'finished')
|
|
animation.effect.updateTiming({duration:1, delay:0, endDelay:0});
|
|
}
|
|
};
|
|
document.addEventListener('animationstart', event => shorten(event.target.getAnimations()), true);
|
|
shorten(document.getAnimations());
|
|
}
|
|
apply(value.mode);
|
|
}).catch(console.error);
|
|
|
|
if (location.hostname !== 'settings.localhost') return;
|
|
function label() {
|
|
const fr = (document.documentElement.lang || navigator.language).startsWith('fr');
|
|
const labels = fr ? ['Mode compact', 'Automatique', 'Activé', 'Désactivé'] :
|
|
['Compact mode', 'Automatic', 'On', 'Off'];
|
|
const title = document.getElementById('vulpes-layout-label');
|
|
if (title && title.textContent !== labels[0]) title.textContent = labels[0];
|
|
if (select) [...select.options].forEach((option, i) => {
|
|
if (option.textContent !== labels[i + 1]) option.textContent = labels[i + 1];
|
|
});
|
|
}
|
|
function insert() {
|
|
const list = document.querySelector('#display > div > ul');
|
|
if (!list || select?.isConnected) return;
|
|
const row = document.createElement('li'), title = document.createElement('p');
|
|
row.id = 'vulpes-layout-row'; title.id = 'vulpes-layout-label';
|
|
const wrapper = document.createElement('span');
|
|
wrapper.className = 'button icon icon-dialog';
|
|
select = document.createElement('select'); select.id = 'vulpes-layout';
|
|
select.setAttribute('aria-labelledby', title.id);
|
|
for (const value of ['auto', 'compact', 'standard']) {
|
|
const option = document.createElement('option'); option.value = value; select.append(option);
|
|
}
|
|
select.value = mode;
|
|
select.onchange = async () => {
|
|
select.disabled = true;
|
|
try { await navigator.mozSettings.createLock().set({[key]:select.value}); }
|
|
catch (error) { select.value = mode; console.error(error); }
|
|
finally { select.disabled = false; }
|
|
};
|
|
wrapper.append(select); row.append(title, wrapper); list.append(row); label();
|
|
}
|
|
addEventListener('localized', label);
|
|
new MutationObserver(() => { insert(); label(); }).observe(document.documentElement,
|
|
{childList:true, subtree:true, attributes:true, attributeFilter:['lang']});
|
|
insert();
|
|
})();
|
|
|
|
|
|
// One readable application name above the compact grid, including keypad focus.
|
|
(() => {
|
|
if (location.hostname !== 'homescreen.localhost') return;
|
|
let heading, selected, grid;
|
|
const nameObserver = new MutationObserver(updateName);
|
|
function updateName() {
|
|
if (!heading || !selected) return;
|
|
const name = selected._subtitle?.textContent || selected.getAttribute('aria-label') || '';
|
|
if (heading.textContent !== name) heading.textContent = name;
|
|
}
|
|
function choose(icon) {
|
|
if (!icon || !grid?.contains(icon)) return;
|
|
if (selected !== icon) {
|
|
selected?.removeAttribute('data-vulpes-compact-selected');
|
|
selected?.removeAttribute('data-vulpes-keypad-focus');
|
|
selected = icon;
|
|
selected.setAttribute('data-vulpes-compact-selected', '');
|
|
nameObserver.disconnect();
|
|
if (selected.shadowRoot) nameObserver.observe(selected.shadowRoot,
|
|
{childList:true, subtree:true, characterData:true});
|
|
}
|
|
updateName();
|
|
}
|
|
function refresh() {
|
|
const focus = grid?.querySelector('gaia-app-icon[data-vulpes-keypad-focus]');
|
|
choose(focus || (selected?.isConnected && selected) || grid?.querySelector('gaia-app-icon'));
|
|
}
|
|
function init() {
|
|
grid = document.getElementById('apps');
|
|
if (!grid) return;
|
|
heading = document.createElement('div');
|
|
heading.id = 'vulpes-compact-app-name';
|
|
heading.setAttribute('aria-live', 'polite');
|
|
heading.setAttribute('aria-atomic', 'true');
|
|
document.body.insertBefore(heading, document.getElementById('panels'));
|
|
new MutationObserver(refresh).observe(grid,
|
|
{childList:true, subtree:true, attributes:true, attributeFilter:['data-vulpes-keypad-focus']});
|
|
for (const type of ['focus', 'pointerdown', 'pointerover']) grid.addEventListener(type, event => {
|
|
choose(event.target.closest('gaia-app-icon'));
|
|
}, true);
|
|
addEventListener('localized', updateName);
|
|
refresh();
|
|
}
|
|
if (document.readyState === 'loading') addEventListener('DOMContentLoaded', init, {once:true});
|
|
else init();
|
|
})();
|
|
|
|
// Cover-screen preferences live in the shared settings store, like appearance.
|
|
(() => {
|
|
if (location.hostname !== 'settings.localhost') return;
|
|
VulpesCompat.call('layout.current', {}).then(async capabilities => {
|
|
if (!capabilities.coverScreen) return;
|
|
const defaults = {'vulpes.cover.title':'VULPES', 'vulpes.cover.titleColor':'#33a6ff',
|
|
'vulpes.cover.batteryColor':'#33a6ff'};
|
|
const values = {...defaults}, controls = {};
|
|
const colors = ['#33a6ff', '#ffffff', '#4ade80', '#fbbf24', '#f472b6',
|
|
'#ef4444', '#f97316', '#a855f7', '#22d3ee', '#14b8a6', '#9ca3af'];
|
|
const result = await navigator.mozSettings.createLock().get('*');
|
|
for (const key of Object.keys(defaults)) if (typeof result[key] === 'string') values[key] = result[key];
|
|
let row, title, previewTitle, previewBattery;
|
|
function labels() {
|
|
const fr = (document.documentElement.lang || navigator.language).startsWith('fr');
|
|
if (!row) return;
|
|
title.textContent = fr ? 'Écran extérieur' : 'Cover screen';
|
|
const texts = fr ? ['Texte', 'Couleur du texte', 'Couleur de la batterie'] :
|
|
['Text', 'Text colour', 'Battery colour'];
|
|
const names = fr ? ['Bleu', 'Blanc', 'Vert', 'Jaune', 'Rose', 'Rouge', 'Orange', 'Violet', 'Cyan', 'Turquoise', 'Gris'] :
|
|
['Blue', 'White', 'Green', 'Yellow', 'Pink', 'Red', 'Orange', 'Purple', 'Cyan', 'Turquoise', 'Grey'];
|
|
Object.keys(defaults).forEach((key, index) => {
|
|
const control = controls[key];
|
|
control.previousElementSibling.textContent = texts[index];
|
|
if (control.options) [...control.options].forEach((option, i) => option.textContent = names[i]);
|
|
});
|
|
previewBattery.textContent = fr ? 'Batterie · aperçu' : 'Battery · preview';
|
|
}
|
|
function preview() {
|
|
previewTitle.textContent = controls['vulpes.cover.title'].value.trim() || 'VULPES';
|
|
previewTitle.style.color = controls['vulpes.cover.titleColor'].value;
|
|
previewBattery.style.color = controls['vulpes.cover.batteryColor'].value;
|
|
}
|
|
function insert() {
|
|
const list = document.querySelector('#display > div > ul');
|
|
if (!list || row?.isConnected) return;
|
|
row = document.createElement('li'); row.id = 'vulpes-cover-settings';
|
|
title = document.createElement('h2'); row.append(title);
|
|
for (const key of Object.keys(defaults)) {
|
|
const label = document.createElement('label'), caption = document.createElement('span');
|
|
const control = document.createElement(key.endsWith('.title') ? 'input' : 'select');
|
|
control.id = key.replaceAll('.', '-'); label.htmlFor = control.id;
|
|
if (control.localName === 'input') {control.type = 'text'; control.maxLength = 24;}
|
|
else for (const color of colors) {
|
|
const option = document.createElement('option'); option.value = color; control.append(option);
|
|
}
|
|
control.value = values[key];
|
|
if (control.localName === 'select' && !colors.includes(values[key])) control.value = defaults[key];
|
|
controls[key] = control;
|
|
let pending, writes = Promise.resolve();
|
|
const save = () => {
|
|
clearTimeout(pending);
|
|
preview();
|
|
const value = control.value.trim() || defaults[key];
|
|
writes = writes.then(async () => {
|
|
await navigator.mozSettings.createLock().set({[key]:value});
|
|
values[key] = value;
|
|
}).catch(console.error);
|
|
return writes;
|
|
};
|
|
control.oninput = () => {
|
|
preview();
|
|
// Keypad edits emit input; closing the lid need not blur the field.
|
|
if (control.localName === 'input') {
|
|
clearTimeout(pending); pending = setTimeout(save, 250);
|
|
}
|
|
};
|
|
control.onchange = save;
|
|
control.onblur = save;
|
|
label.append(caption, control); row.append(label);
|
|
}
|
|
const sample = document.createElement('div'); sample.className = 'vulpes-cover-preview';
|
|
previewTitle = document.createElement('div'); previewBattery = document.createElement('div');
|
|
sample.append(previewTitle, previewBattery); row.append(sample); list.append(row); labels(); preview();
|
|
}
|
|
new MutationObserver(insert).observe(document.documentElement, {childList:true, subtree:true});
|
|
addEventListener('localized', labels); insert();
|
|
}).catch(console.error);
|
|
})();
|