Files
AtreidesAssetsFree/client/pages/settings.js
T
2026-10-29 11:57:29 +01:00

209 lines
11 KiB
JavaScript

// Generated, do not edit. sha256:2c8b0cb9cf7b6e6878eecfd14445f87e0333bfc949df19e5e2747b65137b5411
import { getSettings, updateSettings } from '../data/preferences.js';
import { t, setLocale, LOCALES as CATALOGUES } from '../i18n/index.js';
import { CONFIG, ENUMS, PREFERENCES } from '../../shared/config.js';
import { setPageHint } from '../ui/page-hint.js';
import { esc } from '../ui/escape.js';
import { locked } from '../license.js';
const LOCALES = Object.keys(CATALOGUES);
const GLOW_GLASS = Object.freeze({
off: { glow: 'off' },
still: { glow: 'on', motion: 'off' },
step: { glow: 'on', motion: 'step' },
always: { glow: 'on', motion: 'always' },
});
const GLOW_BUSINESS = Object.freeze({ off: { glow: 'off' }, on: { glow: 'on' } });
export function glowChoice(s) {
if (s.glow === 'off') return 'off';
if (s.profile !== 'glass') return 'on';
return s.motion === 'off' ? 'still' : (s.motion || 'step');
}
export function glowPatch(profile, choice) {
const table = profile === 'glass' ? GLOW_GLASS : GLOW_BUSINESS;
return Object.hasOwn(table, choice) ? { ...table[choice] } : null;
}
const media = q => typeof matchMedia === 'function' && matchMedia(q).matches;
export async function render(el) {
const s = await getSettings();
const glass = s.profile === 'glass';
const business = !glass;
const glowVisible = glass || s.depth === 'deep';
const motionBlocked = glass && s.glow !== 'off' && s.motion !== 'off' && media('(prefers-reduced-motion: reduce)');
const transparencyBlocked = glass && media('(prefers-reduced-transparency: reduce)');
const persona = !locked('signals');
const kiosk = !locked('kiosk');
const glowOptions = glass
? [['off', t('settings.glowOff')], ['still', t('settings.glowStill')], ['step', t('settings.glowStep')], ['always', t('settings.glowAlways')]]
: [['off', t('settings.glowOff')], ['on', t('settings.glowOn')]];
el.innerHTML = `
<div class="settings-cols">
<div class="card">
<div class="t-label" style="margin-bottom:1.2rem">${t('settings.appearance')}</div>
${pillRow('theme', t('settings.theme'), [['dark', t('controls.dark')], ['light', t('controls.light')]], s.theme, { maxWidth: 300 })}
${accentRow(s)}
${pillRow('profile', t('settings.profile'), [['business', t('controls.business')], ['glass', t('controls.glass')]], s.profile,
{ maxWidth: 300, notice: transparencyBlocked ? t('settings.transparencyBlocked') : '' })}
${business ? pillRow('depth', t('settings.depth'), [['flat', t('controls.flat')], ['deep', t('controls.deep')]], s.depth, { maxWidth: 300 }) : ''}
${glowVisible ? pillRow('glowmix', t(glass ? 'settings.glowBgMotion' : 'settings.glowBg'), glowOptions, glowChoice(s),
{ maxWidth: glass ? 420 : 300, notice: motionBlocked ? t('settings.motionBlocked') : '' }) : ''}
${glass && s.glow !== 'off' ? pillRow('glowshape', t('settings.glowShape'),
[['blobs', t('settings.glowShapeBlobs')], ['bands', t('settings.glowShapeBands')]], s.glowshape, { maxWidth: 300 }) : ''}
${glass ? pillRow('rowsel', t('settings.rowsel'),
[['light', t('settings.rowselLight')], ['tint', t('settings.rowselTint')], ['pill', t('settings.rowselPill')]], s.rowsel, { maxWidth: 360 }) : ''}
${glass ? pillRow('glass_render', t('settings.glassRender'),
[['mimic', t('settings.glassRenderMimic')], ['real', t('settings.glassRenderReal')]], s.glass_render, { maxWidth: 300 }) : ''}
${glass ? glassRows(s) : ''}
</div>
<div class="card">
<div class="t-label" style="margin-bottom:1.2rem">${t('settings.screen')}</div>
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">${t('settings.assetDisplay')}</label>
<div class="pill-toggle" data-asset-display style="max-width:360px">
<button data-toggle="asset_show_name" class="${s.asset_show_name ? 'on' : ''}">${t('assets.name')}</button>
<button data-toggle="asset_show_symbol" class="${s.asset_show_symbol ? 'on' : ''}">${t('assets.symbol')}</button>
</div>
</div>
${persona ? pillRow('persona', t('settings.persona'),
[['bitcoin', t('setup.persona_bitcoin')], ['broad', t('setup.persona_broad')]], s.persona, { maxWidth: 300 }) : ''}
${pillRow('show_page_title', t('settings.pageTitle'),
[['1', t('settings.pageTitleShow')], ['0', t('settings.pageTitleHide')]], String(s.show_page_title), { maxWidth: 300 })}
${pillRow('show_page_hint', t('settings.pageHint'),
[['1', t('settings.pageTitleShow')], ['0', t('settings.pageTitleHide')]], String(s.show_page_hint), { maxWidth: 300 })}
${pillRow('result_signs', t('settings.resultSigns'),
[['none', t('settings.resultSignsNone')], ['negative', t('settings.resultSignsNegative')], ['both', t('settings.resultSignsBoth')]],
s.result_signs, { maxWidth: 420 })}
${kiosk ? pillRow('kiosk', t('settings.kiosk'),
[['0', t('settings.kioskWindow')], ['1', t('settings.kioskFull')]], String(s.kiosk), { maxWidth: 300 }) : ''}
${pillRow('auto_blur_min', t('settings.autoBlur'),
[...CONFIG.AUTO_BLUR.PRESETS_MIN.map(m => [String(m), `${m} ${t('settings.minutesShort')}`]),
[String(CONFIG.AUTO_BLUR.NEVER_MIN), t('settings.autoBlurManual')]],
String(s.auto_blur_min), { maxWidth: 420 })}
<div class="form-group" style="margin-bottom:0">
<label class="form-label">${t('settings.language')}</label>
<select class="input" id="f-locale" style="max-width:200px">
${LOCALES.map(l => `<option value="${l}"${l === s.locale ? ' selected' : ''}>${t('settings.' + l)}</option>`).join('')}
</select>
</div>
</div>
</div>`;
setPageHint(el, { title: t('settings.hintTitle'), rows: [
{ heading: t('settings.appearance') },
t('settings.hintProfile'),
t('settings.hintTheme'),
t('settings.depthHint'),
t('settings.hintGlow'),
t('settings.hintGlowShape'),
t('settings.hintRowsel'),
t('settings.hintGlassRender'),
t('settings.hintGlass'),
{ heading: t('settings.hintAiHeading') },
t('settings.hintAiIntro'),
t('settings.hintAiTempo'),
{ heading: t('settings.hintScreenHeading') },
`${t('settings.assetDisplay')}: ${t('settings.assetDisplayHint')}`,
...(persona ? [`${t('settings.persona')}: ${t('settings.personaHint')}`] : []),
`${t('settings.pageTitle')}: ${t('settings.pageTitleHint')}`,
`${t('settings.pageHint')}: ${t('settings.pageHintHint')}`,
`${t('settings.resultSigns')}: ${t('settings.resultSignsHint')}`,
...(kiosk ? [`${t('settings.kiosk')}: ${t('settings.kioskHint')}`] : []),
`${t('settings.autoBlur')}: ${t('settings.autoBlurHint')}`,
] });
el.querySelectorAll('[data-field]').forEach(group => {
group.onclick = e => {
const b = e.target.closest('[data-v]'); if (!b) return;
const field = group.dataset.field;
if (field === 'glowmix') {
const patch = glowPatch(s.profile, b.dataset.v);
if (patch) apply(el, patch);
return;
}
apply(el, { [field]: b.dataset.v });
};
});
el.querySelector('#f-locale').onchange = e => apply(el, { locale: e.target.value });
for (const input of el.querySelectorAll('[data-glass]')) {
syncSlider(input);
input.oninput = () => syncSlider(input);
input.onchange = () => apply(el, { [input.dataset.glass]: Number(input.value) });
}
const reset = el.querySelector('[data-glass-reset]');
if (reset) reset.onclick = () => apply(el, Object.fromEntries(['glass_render', ...GLASS.map(([key]) => key)]
.map(key => [key, PREFERENCES.app[key].initial])));
el.querySelector('[data-asset-display]').onclick = e => {
const b = e.target.closest('[data-toggle]'); if (!b) return;
const field = b.dataset.toggle;
const other = field === 'asset_show_name' ? 'asset_show_symbol' : 'asset_show_name';
const next = s[field] ? 0 : 1;
if (next === 0 && !s[other]) return;
apply(el, { [field]: next });
};
}
async function apply(el, patch) {
await updateSettings(patch);
if (patch.locale) setLocale(patch.locale);
document.dispatchEvent(new Event('settings-changed'));
render(el);
}
function pillRow(field, label, options, current, { maxWidth = 500, notice = '' } = {}) {
const buttons = options.map(([v, lbl]) =>
`<button data-v="${v}" class="${v === current ? 'on' : ''}">${lbl}</button>`).join('');
return `
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">${label}</label>
<div class="pill-toggle" data-field="${field}" style="max-width:${maxWidth}px">${buttons}</div>
${notice ? `<div class="form-hint">${notice}</div>` : ''}
</div>`;
}
const GLASS = Object.freeze([
['glass_transparency', 'settings.glassTransparency', '--glass-transparency', '%', '%'],
['glass_blur', 'settings.glassDialog', '--dialog-transparency', '%', ''],
['glass_frost', 'settings.glassFrost', '--glass-frost', '%', '%'],
]);
function glassRows(s) {
const rows = GLASS.map(([key, label, token, unit, tokenUnit]) => {
const { min, max } = PREFERENCES.app[key];
return `<label class="slider-row slider-row--wide" for="f-${key}"><span>${t(label)}</span>
<input class="slider" type="range" id="f-${key}" min="${min}" max="${max}" step="1" value="${Number(s[key])}"
data-glass="${key}" data-token="${token}" data-unit="${unit}" data-token-unit="${tokenUnit}"><output for="f-${key}"></output></label>`;
}).join('');
return `
<div class="form-group" style="margin-bottom:1rem; max-width:420px">
<label class="form-label">${t('settings.glass')}</label>
${rows}
<div><button class="btn btn-ghost btn-xs" type="button" data-glass-reset>${t('settings.glassDefault')}</button></div>
</div>`;
}
function syncSlider(input) {
const min = Number(input.min), max = Number(input.max), v = Number(input.value);
input.style.setProperty('--pct', `${((v - min) / (max - min)) * 100}%`);
const out = input.parentElement.querySelector('output');
if (out) out.textContent = `${v}${input.dataset.unit}`;
document.documentElement.style.setProperty(input.dataset.token, `${v}${input.dataset.tokenUnit}`);
}
function accentRow(s) {
const dots = ENUMS.ACCENT.map(a => {
const label = t(`controls.${a}`);
return `<button type="button" data-v="${a}" data-theme="${esc(s.theme)}" data-accent="${a}" class="${a === s.accent ? 'on' : ''}" title="${esc(label)}" aria-label="${esc(label)}"></button>`;
}).join('');
return `
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label">${t('settings.accent')}</label>
<div class="accent-dots" data-field="accent">${dots}</div>
</div>`;
}