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_default": "Výchozí",
|
||||||
"mic_on": "Zapnuto",
|
"mic_on": "Zapnuto",
|
||||||
"mic_off": "Vypnuto",
|
"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_default": "Standard",
|
||||||
"mic_on": "Ein",
|
"mic_on": "Ein",
|
||||||
"mic_off": "Aus",
|
"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_default": "Default",
|
||||||
"mic_on": "On",
|
"mic_on": "On",
|
||||||
"mic_off": "Off",
|
"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_default": "Predeterminado",
|
||||||
"mic_on": "Encendido",
|
"mic_on": "Encendido",
|
||||||
"mic_off": "Apagado",
|
"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_default": "Défaut",
|
||||||
"mic_on": "Activé",
|
"mic_on": "Activé",
|
||||||
"mic_off": "Désactivé",
|
"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_default": "デフォルト",
|
||||||
"mic_on": "オン",
|
"mic_on": "オン",
|
||||||
"mic_off": "オフ",
|
"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_default": "За замовчуванням",
|
||||||
"mic_on": "Увімкнено",
|
"mic_on": "Увімкнено",
|
||||||
"mic_off": "Вимкнено",
|
"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_default": "默认",
|
||||||
"mic_on": "开",
|
"mic_on": "开",
|
||||||
"mic_off": "关",
|
"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',
|
id: 'sensors',
|
||||||
icon: 'ti-cpu-2',
|
icon: 'ti-cpu-2',
|
||||||
categories: ['inputs', 'misc'],
|
categories: ['inputs', 'misc']
|
||||||
disabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'internet',
|
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