feat: add sensors page

This commit is contained in:
Tomáš Mládek 2025-09-27 13:11:25 +02:00
parent 26cc8a8587
commit 86885d02d6
10 changed files with 786 additions and 10 deletions

View file

@ -137,5 +137,34 @@
"mic_default": "Výchozí",
"mic_on": "Zapnuto",
"mic_off": "Vypnuto",
"mic_mono": "Mono"
"mic_mono": "Mono",
"sensors_title": "Senzory",
"sensors_geolocation": "Geolokace",
"sensors_start": "Start",
"sensors_stop": "Stop",
"sensors_accuracy": "Přesnost (m)",
"sensors_altitude": "Nadmořská výška (m)",
"sensors_heading": "Směr (stupně)",
"sensors_speed": "Rychlost (m/s)",
"sensors_timestamp": "Časové razítko",
"sensors_copy": "Kopírovat JSON",
"sensors_copied": "Zkopírováno do schránky",
"sensors_notSupported": "Není podporováno na tomto zařízení/prohlížeči",
"sensors_deviceMotion": "Pohyb zařízení",
"sensors_deviceOrientation": "Orientace zařízení",
"sensors_accelerometer": "Akcelerometr",
"sensors_gyroscope": "Gyroskop",
"sensors_magnetometer": "Magnetometr",
"sensors_ambientLight": "Okolní světlo",
"sensors_illuminance": "Osvětlení (lux)",
"sensors_barometer": "Barometr",
"sensors_pressure": "Tlak (hPa)",
"sensors_temperature": "Teplota (°C)",
"sensors_permissions": "Oprávnění",
"sensors_enableMotionOrientation": "Povolit pohyb/orientaci",
"sensors_motion": "Pohyb",
"sensors_orientation": "Orientace",
"sensors_status_granted": "Uděleno",
"sensors_status_denied": "Odepřeno",
"sensors_status_unknown": "Neznámý"
}

View file

@ -137,5 +137,34 @@
"mic_default": "Standard",
"mic_on": "Ein",
"mic_off": "Aus",
"mic_mono": "Mono"
"mic_mono": "Mono",
"sensors_title": "Sensoren",
"sensors_geolocation": "Geolokalisierung",
"sensors_start": "Start",
"sensors_stop": "Stopp",
"sensors_accuracy": "Genauigkeit (m)",
"sensors_altitude": "Höhe (m)",
"sensors_heading": "Richtung (Grad)",
"sensors_speed": "Geschwindigkeit (m/s)",
"sensors_timestamp": "Zeitstempel",
"sensors_copy": "JSON kopieren",
"sensors_copied": "In die Zwischenablage kopiert",
"sensors_notSupported": "Auf diesem Gerät/Browser nicht unterstützt",
"sensors_deviceMotion": "Gerätebewegung",
"sensors_deviceOrientation": "Geräteausrichtung",
"sensors_accelerometer": "Beschleunigungsmesser",
"sensors_gyroscope": "Gyroskop",
"sensors_magnetometer": "Magnetometer",
"sensors_ambientLight": "Umgebungslicht",
"sensors_illuminance": "Beleuchtungsstärke (lux)",
"sensors_barometer": "Barometer",
"sensors_pressure": "Druck (hPa)",
"sensors_temperature": "Temperatur (°C)",
"sensors_permissions": "Berechtigungen",
"sensors_enableMotionOrientation": "Bewegung/Ausrichtung aktivieren",
"sensors_motion": "Bewegung",
"sensors_orientation": "Ausrichtung",
"sensors_status_granted": "Gewährt",
"sensors_status_denied": "Verweigert",
"sensors_status_unknown": "Unbekannt"
}

View file

@ -137,5 +137,34 @@
"mic_default": "Default",
"mic_on": "On",
"mic_off": "Off",
"mic_mono": "Mono"
"mic_mono": "Mono",
"sensors_title": "Sensors",
"sensors_geolocation": "Geolocation",
"sensors_start": "Start",
"sensors_stop": "Stop",
"sensors_accuracy": "Accuracy (m)",
"sensors_altitude": "Altitude (m)",
"sensors_heading": "Heading (deg)",
"sensors_speed": "Speed (m/s)",
"sensors_timestamp": "Timestamp",
"sensors_copy": "Copy JSON",
"sensors_copied": "Copied to clipboard",
"sensors_notSupported": "Not supported on this device/browser",
"sensors_deviceMotion": "Device Motion",
"sensors_deviceOrientation": "Device Orientation",
"sensors_accelerometer": "Accelerometer",
"sensors_gyroscope": "Gyroscope",
"sensors_magnetometer": "Magnetometer",
"sensors_ambientLight": "Ambient Light",
"sensors_illuminance": "Illuminance (lux)",
"sensors_barometer": "Barometer",
"sensors_pressure": "Pressure (hPa)",
"sensors_temperature": "Temperature (°C)",
"sensors_permissions": "Permissions",
"sensors_enableMotionOrientation": "Enable Motion/Orientation",
"sensors_motion": "Motion",
"sensors_orientation": "Orientation",
"sensors_status_granted": "Granted",
"sensors_status_denied": "Denied",
"sensors_status_unknown": "Unknown"
}

View file

@ -137,5 +137,34 @@
"mic_default": "Predeterminado",
"mic_on": "Encendido",
"mic_off": "Apagado",
"mic_mono": "Mono"
"mic_mono": "Mono",
"sensors_title": "Sensores",
"sensors_geolocation": "Geolocalización",
"sensors_start": "Iniciar",
"sensors_stop": "Detener",
"sensors_accuracy": "Precisión (m)",
"sensors_altitude": "Altitud (m)",
"sensors_heading": "Rumbo (grados)",
"sensors_speed": "Velocidad (m/s)",
"sensors_timestamp": "Marca de tiempo",
"sensors_copy": "Copiar JSON",
"sensors_copied": "Copiado al portapapeles",
"sensors_notSupported": "No compatible con este dispositivo/navegador",
"sensors_deviceMotion": "Movimiento del dispositivo",
"sensors_deviceOrientation": "Orientación del dispositivo",
"sensors_accelerometer": "Acelerómetro",
"sensors_gyroscope": "Giroscopio",
"sensors_magnetometer": "Magnetómetro",
"sensors_ambientLight": "Luz ambiental",
"sensors_illuminance": "Iluminancia (lux)",
"sensors_barometer": "Barómetro",
"sensors_pressure": "Presión (hPa)",
"sensors_temperature": "Temperatura (°C)",
"sensors_permissions": "Permisos",
"sensors_enableMotionOrientation": "Activar Movimiento/Orientación",
"sensors_motion": "Movimiento",
"sensors_orientation": "Orientación",
"sensors_status_granted": "Concedido",
"sensors_status_denied": "Denegado",
"sensors_status_unknown": "Desconocido"
}

View file

@ -137,5 +137,34 @@
"mic_default": "Défaut",
"mic_on": "Activé",
"mic_off": "Désactivé",
"mic_mono": "Mono"
"mic_mono": "Mono",
"sensors_title": "Capteurs",
"sensors_geolocation": "Géolocalisation",
"sensors_start": "Démarrer",
"sensors_stop": "Arrêter",
"sensors_accuracy": "Précision (m)",
"sensors_altitude": "Altitude (m)",
"sensors_heading": "Cap (deg)",
"sensors_speed": "Vitesse (m/s)",
"sensors_timestamp": "Horodatage",
"sensors_copy": "Copier JSON",
"sensors_copied": "Copié dans le presse-papiers",
"sensors_notSupported": "Non pris en charge sur cet appareil/navigateur",
"sensors_deviceMotion": "Mouvement de l'appareil",
"sensors_deviceOrientation": "Orientation de l'appareil",
"sensors_accelerometer": "Accéléromètre",
"sensors_gyroscope": "Gyroscope",
"sensors_magnetometer": "Magnétomètre",
"sensors_ambientLight": "Lumière ambiante",
"sensors_illuminance": "Éclairement (lux)",
"sensors_barometer": "Baromètre",
"sensors_pressure": "Pression (hPa)",
"sensors_temperature": "Température (°C)",
"sensors_permissions": "Autorisations",
"sensors_enableMotionOrientation": "Activer Mouvement/Orientation",
"sensors_motion": "Mouvement",
"sensors_orientation": "Orientation",
"sensors_status_granted": "Accordé",
"sensors_status_denied": "Refusé",
"sensors_status_unknown": "Inconnu"
}

View file

@ -137,5 +137,34 @@
"mic_default": "デフォルト",
"mic_on": "オン",
"mic_off": "オフ",
"mic_mono": "モノラル"
"mic_mono": "モノラル",
"sensors_title": "センサー",
"sensors_geolocation": "地理位置情報",
"sensors_start": "開始",
"sensors_stop": "停止",
"sensors_accuracy": "精度 (m)",
"sensors_altitude": "高度 (m)",
"sensors_heading": "方角 (度)",
"sensors_speed": "速度 (m/s)",
"sensors_timestamp": "タイムスタンプ",
"sensors_copy": "JSONをコピー",
"sensors_copied": "クリップボードにコピーしました",
"sensors_notSupported": "このデバイス/ブラウザではサポートされていません",
"sensors_deviceMotion": "デバイスの動き",
"sensors_deviceOrientation": "デバイスの向き",
"sensors_accelerometer": "加速度計",
"sensors_gyroscope": "ジャイロスコープ",
"sensors_magnetometer": "磁力計",
"sensors_ambientLight": "環境光",
"sensors_illuminance": "照度 (lux)",
"sensors_barometer": "気圧計",
"sensors_pressure": "圧力 (hPa)",
"sensors_temperature": "温度 (°C)",
"sensors_permissions": "権限",
"sensors_enableMotionOrientation": "モーション/オリエンテーションを有効にする",
"sensors_motion": "モーション",
"sensors_orientation": "オリエンテーション",
"sensors_status_granted": "許可",
"sensors_status_denied": "拒否",
"sensors_status_unknown": "不明"
}

View file

@ -137,5 +137,34 @@
"mic_default": "За замовчуванням",
"mic_on": "Увімкнено",
"mic_off": "Вимкнено",
"mic_mono": "Моно"
"mic_mono": "Моно",
"sensors_title": "Датчики",
"sensors_geolocation": "Геолокація",
"sensors_start": "Старт",
"sensors_stop": "Стоп",
"sensors_accuracy": "Точність (м)",
"sensors_altitude": "Висота (м)",
"sensors_heading": "Напрямок (град)",
"sensors_speed": "Швидкість (м/с)",
"sensors_timestamp": "Часова мітка",
"sensors_copy": "Копіювати JSON",
"sensors_copied": "Скопійовано в буфер обміну",
"sensors_notSupported": "Не підтримується на цьому пристрої/браузері",
"sensors_deviceMotion": "Рух пристрою",
"sensors_deviceOrientation": "Орієнтація пристрою",
"sensors_accelerometer": "Акселерометр",
"sensors_gyroscope": "Гіроскоп",
"sensors_magnetometer": "Магнітометр",
"sensors_ambientLight": "Освітленість",
"sensors_illuminance": "Освітленість (люкс)",
"sensors_barometer": "Барометр",
"sensors_pressure": "Тиск (гПа)",
"sensors_temperature": "Температура (°C)",
"sensors_permissions": "Дозволи",
"sensors_enableMotionOrientation": "Увімкнути рух/орієнтацію",
"sensors_motion": "Рух",
"sensors_orientation": "Орієнтація",
"sensors_status_granted": "Надано",
"sensors_status_denied": "Відмовлено",
"sensors_status_unknown": "Невідомо"
}

View file

@ -137,5 +137,34 @@
"mic_default": "默认",
"mic_on": "开",
"mic_off": "关",
"mic_mono": "单声道"
"mic_mono": "单声道",
"sensors_title": "传感器",
"sensors_geolocation": "地理位置",
"sensors_start": "开始",
"sensors_stop": "停止",
"sensors_accuracy": "精度 (m)",
"sensors_altitude": "高度 (m)",
"sensors_heading": "方向 (度)",
"sensors_speed": "速度 (m/s)",
"sensors_timestamp": "时间戳",
"sensors_copy": "复制JSON",
"sensors_copied": "已复制到剪贴板",
"sensors_notSupported": "此设备/浏览器不支持",
"sensors_deviceMotion": "设备运动",
"sensors_deviceOrientation": "设备方向",
"sensors_accelerometer": "加速度计",
"sensors_gyroscope": "陀螺仪",
"sensors_magnetometer": "磁力计",
"sensors_ambientLight": "环境光",
"sensors_illuminance": "照度 (lux)",
"sensors_barometer": "气压计",
"sensors_pressure": "压力 (hPa)",
"sensors_temperature": "温度 (°C)",
"sensors_permissions": "权限",
"sensors_enableMotionOrientation": "启用运动/方向",
"sensors_motion": "运动",
"sensors_orientation": "方向",
"sensors_status_granted": "已授予",
"sensors_status_denied": "已拒绝",
"sensors_status_unknown": "未知"
}

View file

@ -102,8 +102,7 @@
{
id: 'sensors',
icon: 'ti-cpu-2',
categories: ['inputs', 'misc'],
disabled: true
categories: ['inputs', 'misc']
},
{
id: 'internet',

View file

@ -0,0 +1,545 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import { m } from '$lib/paraglide/messages';
// Geolocation
let geoWatchId: number | null = $state(null);
let geolocation: GeolocationPosition | null = $state(null);
let geoError: string | null = $state(null);
let geoSupported = $state<boolean>(false);
// DeviceMotion / DeviceOrientation (useful fallbacks on iOS/Safari)
type Motion = { ax?: number; ay?: number; az?: number; gx?: number; gy?: number; gz?: number };
let deviceMotion: Motion | null = $state(null);
let deviceOrientation: { alpha?: number; beta?: number; gamma?: number } | null = $state(null);
let motionSupported = $state(false);
let orientationSupported = $state(false);
// iOS Safari permission flow for motion/orientation
let motionPermissionAvailable = $state(false);
let orientationPermissionAvailable = $state(false);
let motionPermission: 'unknown' | 'granted' | 'denied' = $state('unknown');
let orientationPermission: 'unknown' | 'granted' | 'denied' = $state('unknown');
// Generic Sensor API (subject to browser/flag support)
type SensorHandle = {
instance?: any;
supported: boolean;
active: boolean;
error?: string | null;
data: Record<string, number | string | undefined>;
};
let accelerometer: SensorHandle = $state({ supported: false, active: false, data: {} });
let gyroscope: SensorHandle = $state({ supported: false, active: false, data: {} });
let magnetometer: SensorHandle = $state({ supported: false, active: false, data: {} });
let ambientLight: SensorHandle = $state({ supported: false, active: false, data: {} });
let barometer: SensorHandle = $state({ supported: false, active: false, data: {} });
const w = browser ? (window as any) : undefined;
function detectSupport() {
geoSupported = browser && 'geolocation' in navigator;
motionSupported = browser && 'DeviceMotionEvent' in (window as any);
orientationSupported = browser && 'DeviceOrientationEvent' in (window as any);
accelerometer.supported = Boolean(w?.Accelerometer);
gyroscope.supported = Boolean(w?.Gyroscope);
magnetometer.supported = Boolean(w?.Magnetometer);
ambientLight.supported = Boolean(w?.AmbientLightSensor);
barometer.supported = Boolean(w?.Barometer);
}
onMount(() => {
detectSupport();
// Check for iOS-style permission request APIs
motionPermissionAvailable =
browser &&
typeof (window as any).DeviceMotionEvent !== 'undefined' &&
typeof (DeviceMotionEvent as any).requestPermission === 'function';
orientationPermissionAvailable =
browser &&
typeof (window as any).DeviceOrientationEvent !== 'undefined' &&
typeof (DeviceOrientationEvent as any).requestPermission === 'function';
if (orientationSupported) {
const handler = (e: DeviceOrientationEvent) => {
deviceOrientation = {
alpha: e.alpha ?? undefined,
beta: e.beta ?? undefined,
gamma: e.gamma ?? undefined
};
};
window.addEventListener('deviceorientation', handler);
onDestroy(() => window.removeEventListener('deviceorientation', handler));
}
if (motionSupported) {
const handler = (e: DeviceMotionEvent) => {
deviceMotion = {
ax: e.acceleration?.x ?? undefined,
ay: e.acceleration?.y ?? undefined,
az: e.acceleration?.z ?? undefined,
gx: e.rotationRate?.alpha ?? undefined,
gy: e.rotationRate?.beta ?? undefined,
gz: e.rotationRate?.gamma ?? undefined
};
};
window.addEventListener('devicemotion', handler);
onDestroy(() => window.removeEventListener('devicemotion', handler));
}
});
onDestroy(() => {
stopGeolocation();
stopSensor(accelerometer);
stopSensor(gyroscope);
stopSensor(magnetometer);
stopSensor(ambientLight);
stopSensor(barometer);
});
// (Permissions are requested implicitly when starting sensors where applicable)
// Geolocation controls
async function startGeolocation() {
if (!geoSupported) return;
try {
geoError = null;
geolocation = await new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000
});
});
geoWatchId = navigator.geolocation.watchPosition(
(pos) => (geolocation = pos),
(err) => (geoError = err?.message || String(err)),
{ enableHighAccuracy: true }
);
} catch (e: any) {
geoError = e?.message ?? String(e);
}
}
function stopGeolocation() {
if (geoWatchId != null) {
navigator.geolocation.clearWatch(geoWatchId);
geoWatchId = null;
}
}
// Generic Sensor helpers
function startSensor(handle: SensorHandle, ctorName: string, options: any = { frequency: 60 }) {
try {
handle.error = null;
if (!w?.[ctorName]) {
handle.supported = false;
return;
}
handle.instance = new w[ctorName](options);
handle.instance.addEventListener('reading', () => {
// Populate based on sensor type
if (ctorName === 'Accelerometer') {
handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
} else if (ctorName === 'Gyroscope') {
handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
} else if (ctorName === 'Magnetometer') {
handle.data = { x: handle.instance.x, y: handle.instance.y, z: handle.instance.z };
} else if (ctorName === 'AmbientLightSensor') {
handle.data = { illuminance: handle.instance.illuminance };
} else if (ctorName === 'Barometer') {
handle.data = {
pressure: handle.instance.pressure,
temperature: handle.instance?.temperature
};
}
});
handle.instance.addEventListener('error', (event: any) => {
handle.error = event?.error?.message || String(event);
});
handle.instance.start();
handle.active = true;
} catch (e: any) {
handle.error = e?.message ?? String(e);
handle.active = false;
}
}
function stopSensor(handle: SensorHandle) {
try {
handle.instance?.stop?.();
} catch {}
handle.active = false;
}
// UI helpers
function toFixed(n: number | undefined, digits = 2) {
return typeof n === 'number' && Number.isFinite(n) ? n.toFixed(digits) : '—';
}
async function copyJSON(data: unknown) {
try {
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
alert(m.sensors_copied());
} catch {}
}
async function requestMotionOrientation() {
try {
if (motionPermissionAvailable) {
const res = await (DeviceMotionEvent as any).requestPermission();
motionPermission = res === 'granted' ? 'granted' : 'denied';
}
if (orientationPermissionAvailable) {
const res2 = await (DeviceOrientationEvent as any).requestPermission();
orientationPermission = res2 === 'granted' ? 'granted' : 'denied';
}
} catch (_) {
if (motionPermissionAvailable && motionPermission === 'unknown') motionPermission = 'denied';
if (orientationPermissionAvailable && orientationPermission === 'unknown')
orientationPermission = 'denied';
}
}
// Kick off light permission checks lazily in UI; starting sensors will request permissions where needed.
</script>
<h2><i class="ti ti-cpu-2"></i> {m.sensors_title()}</h2>
<div class="sections">
{#if motionPermissionAvailable || orientationPermissionAvailable}
<section>
<h3>{m.sensors_permissions()}</h3>
<div class="row">
<button onclick={requestMotionOrientation}
><i class="ti ti-key"></i> {m.sensors_enableMotionOrientation()}</button
>
</div>
<ul class="kv">
{#if motionPermissionAvailable}
<li>
<span class="key">{m.sensors_motion()}</span>
<span>
{#if motionPermission === 'granted'}{m.sensors_status_granted()}
{:else if motionPermission === 'denied'}{m.sensors_status_denied()}
{:else}{m.sensors_status_unknown()}{/if}
</span>
</li>
{/if}
{#if orientationPermissionAvailable}
<li>
<span class="key">{m.sensors_orientation()}</span>
<span>
{#if orientationPermission === 'granted'}{m.sensors_status_granted()}
{:else if orientationPermission === 'denied'}{m.sensors_status_denied()}
{:else}{m.sensors_status_unknown()}{/if}
</span>
</li>
{/if}
</ul>
</section>
{/if}
<section>
<h3>{m.sensors_geolocation()}</h3>
{#if geoSupported}
<div class="row">
<button onclick={startGeolocation} disabled={geoWatchId !== null}>
<i class="ti ti-player-play"></i>
{m.sensors_start()}
</button>
<button onclick={stopGeolocation} disabled={geoWatchId === null}>
<i class="ti ti-player-stop"></i>
{m.sensors_stop()}
</button>
</div>
{#if geoError}
<div class="error">{geoError}</div>
{/if}
{#if geolocation}
<ul class="kv">
<li><span class="key">lat</span><span>{geolocation.coords.latitude}</span></li>
<li><span class="key">lon</span><span>{geolocation.coords.longitude}</span></li>
<li>
<span class="key">{m.sensors_accuracy()}</span><span
>{toFixed(geolocation.coords.accuracy)}</span
>
</li>
<li>
<span class="key">{m.sensors_altitude()}</span><span
>{geolocation.coords.altitude ?? '—'}</span
>
</li>
<li>
<span class="key">{m.sensors_heading()}</span><span
>{toFixed(geolocation.coords.heading ?? undefined)}</span
>
</li>
<li>
<span class="key">{m.sensors_speed()}</span><span
>{toFixed(geolocation.coords.speed ?? undefined)}</span
>
</li>
<li>
<span class="key">{m.sensors_timestamp()}</span><span
>{new Date(geolocation.timestamp).toLocaleString()}</span
>
</li>
</ul>
<div class="row">
<button onclick={() => copyJSON(geolocation)}
><i class="ti ti-copy"></i> {m.sensors_copy()}</button
>
</div>
{/if}
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_deviceMotion()}</h3>
{#if motionSupported}
<ul class="kv">
<li><span class="key">ax</span><span>{toFixed(deviceMotion?.ax)}</span></li>
<li><span class="key">ay</span><span>{toFixed(deviceMotion?.ay)}</span></li>
<li><span class="key">az</span><span>{toFixed(deviceMotion?.az)}</span></li>
<li><span class="key">α</span><span>{toFixed(deviceMotion?.gx)}</span></li>
<li><span class="key">β</span><span>{toFixed(deviceMotion?.gy)}</span></li>
<li><span class="key">γ</span><span>{toFixed(deviceMotion?.gz)}</span></li>
</ul>
<div class="row">
<button onclick={() => copyJSON(deviceMotion)}
><i class="ti ti-copy"></i> {m.sensors_copy()}</button
>
</div>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_deviceOrientation()}</h3>
{#if orientationSupported}
<ul class="kv">
<li><span class="key">alpha</span><span>{toFixed(deviceOrientation?.alpha)}</span></li>
<li><span class="key">beta</span><span>{toFixed(deviceOrientation?.beta)}</span></li>
<li><span class="key">gamma</span><span>{toFixed(deviceOrientation?.gamma)}</span></li>
</ul>
<div class="row">
<button onclick={() => copyJSON(deviceOrientation)}
><i class="ti ti-copy"></i> {m.sensors_copy()}</button
>
</div>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_accelerometer()}</h3>
{#if accelerometer.supported}
<div class="row">
{#if !accelerometer.active}
<button onclick={() => startSensor(accelerometer, 'Accelerometer')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(accelerometer)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if accelerometer.error}
<div class="error">{accelerometer.error}</div>
{/if}
<ul class="kv">
<li><span class="key">x</span><span>{toFixed(accelerometer.data.x as number)}</span></li>
<li><span class="key">y</span><span>{toFixed(accelerometer.data.y as number)}</span></li>
<li><span class="key">z</span><span>{toFixed(accelerometer.data.z as number)}</span></li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_gyroscope()}</h3>
{#if gyroscope.supported}
<div class="row">
{#if !gyroscope.active}
<button onclick={() => startSensor(gyroscope, 'Gyroscope')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(gyroscope)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if gyroscope.error}
<div class="error">{gyroscope.error}</div>
{/if}
<ul class="kv">
<li><span class="key">x</span><span>{toFixed(gyroscope.data.x as number)}</span></li>
<li><span class="key">y</span><span>{toFixed(gyroscope.data.y as number)}</span></li>
<li><span class="key">z</span><span>{toFixed(gyroscope.data.z as number)}</span></li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_magnetometer()}</h3>
{#if magnetometer.supported}
<div class="row">
{#if !magnetometer.active}
<button onclick={() => startSensor(magnetometer, 'Magnetometer', { frequency: 10 })}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(magnetometer)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if magnetometer.error}
<div class="error">{magnetometer.error}</div>
{/if}
<ul class="kv">
<li><span class="key">x</span><span>{toFixed(magnetometer.data.x as number)}</span></li>
<li><span class="key">y</span><span>{toFixed(magnetometer.data.y as number)}</span></li>
<li><span class="key">z</span><span>{toFixed(magnetometer.data.z as number)}</span></li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_ambientLight()}</h3>
{#if ambientLight.supported}
<div class="row">
{#if !ambientLight.active}
<button onclick={() => startSensor(ambientLight, 'AmbientLightSensor')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(ambientLight)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if ambientLight.error}
<div class="error">{ambientLight.error}</div>
{/if}
<ul class="kv">
<li>
<span class="key">{m.sensors_illuminance()}</span><span
>{toFixed(ambientLight.data.illuminance as number)}</span
>
</li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
<section>
<h3>{m.sensors_barometer()}</h3>
{#if barometer.supported}
<div class="row">
{#if !barometer.active}
<button onclick={() => startSensor(barometer, 'Barometer')}
><i class="ti ti-player-play"></i> {m.sensors_start()}</button
>
{:else}
<button onclick={() => stopSensor(barometer)}
><i class="ti ti-player-stop"></i> {m.sensors_stop()}</button
>
{/if}
</div>
{#if barometer.error}
<div class="error">{barometer.error}</div>
{/if}
<ul class="kv">
<li>
<span class="key">{m.sensors_pressure()}</span><span
>{toFixed(barometer.data.pressure as number)}</span
>
</li>
<li>
<span class="key">{m.sensors_temperature()}</span><span
>{barometer.data.temperature ?? '—'}</span
>
</li>
</ul>
{:else}
<div class="subdued">{m.sensors_notSupported()}</div>
{/if}
</section>
</div>
<style>
.sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
section {
border: 1px solid currentColor;
border-radius: 0.5rem;
padding: 0.75rem;
}
h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
.row {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
}
.kv {
list-style: none;
padding: 0;
margin: 0.5rem 0 0 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 0.75rem;
align-items: baseline;
}
.kv .key {
opacity: 0.8;
margin-right: 0.25em;
}
.error {
color: #ff6b6b;
margin: 0.25rem 0;
}
.subdued {
opacity: 0.7;
}
button {
background: none;
color: inherit;
border: 1px solid currentColor;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
</style>