feat: add sensors page
This commit is contained in:
parent
26cc8a8587
commit
86885d02d6
10 changed files with 786 additions and 10 deletions
|
@ -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ý"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": "不明"
|
||||
}
|
||||
|
|
|
@ -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": "Невідомо"
|
||||
}
|
||||
|
|
|
@ -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": "未知"
|
||||
}
|
||||
|
|
|
@ -102,8 +102,7 @@
|
|||
{
|
||||
id: 'sensors',
|
||||
icon: 'ti-cpu-2',
|
||||
categories: ['inputs', 'misc'],
|
||||
disabled: true
|
||||
categories: ['inputs', 'misc']
|
||||
},
|
||||
{
|
||||
id: 'internet',
|
||||
|
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue