mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
feat: Implement RSSI service for iOS and Web platforms
- Added IosRssiService to handle synthetic RSSI data for iOS. - Created WebRssiService to simulate RSSI scanning on the web. - Defined shared types for WifiNetwork and RssiService in rssi.service.ts. - Introduced simulation service to generate synthetic sensing data. - Implemented WebSocket service for real-time data handling with reconnection logic. - Established Zustand stores for managing application state related to MAT and pose data. - Developed theme context and utility functions for consistent styling and formatting. - Added type definitions for various application entities including API responses and sensing data. - Created utility functions for color mapping and URL validation. - Configured TypeScript settings for the mobile application.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Platform, Switch, View } from 'react-native';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
|
||||
type RssiToggleProps = {
|
||||
enabled: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const RssiToggle = ({ enabled, onChange }: RssiToggleProps) => {
|
||||
return (
|
||||
<View>
|
||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<ThemedText preset="bodyMd">RSSI Scan</ThemedText>
|
||||
<ThemedText preset="bodySm" style={{ color: colors.textSecondary }}>
|
||||
Scan for nearby Wi-Fi signals from Android devices
|
||||
</ThemedText>
|
||||
</View>
|
||||
<Switch
|
||||
value={enabled}
|
||||
onValueChange={onChange}
|
||||
trackColor={{ true: colors.accent, false: colors.surfaceAlt }}
|
||||
thumbColor={colors.textPrimary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{Platform.OS === 'ios' && (
|
||||
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginTop: spacing.xs }}>
|
||||
iOS: RSSI scan is currently limited — using stub data.
|
||||
</ThemedText>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState } from 'react';
|
||||
import { Pressable, TextInput, View } from 'react-native';
|
||||
import { validateServerUrl } from '@/utils/urlValidator';
|
||||
import { apiService } from '@/services/api.service';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
|
||||
type ServerUrlInputProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSave: () => void;
|
||||
};
|
||||
|
||||
export const ServerUrlInput = ({ value, onChange, onSave }: ServerUrlInputProps) => {
|
||||
const [testResult, setTestResult] = useState('');
|
||||
|
||||
const validation = validateServerUrl(value);
|
||||
|
||||
const handleTest = async () => {
|
||||
if (!validation.valid) {
|
||||
setTestResult('✗ Invalid URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
await apiService.getStatus();
|
||||
setTestResult(`✓ ${Date.now() - start}ms`);
|
||||
} catch {
|
||||
setTestResult('✗ Failed');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ThemedText preset="labelMd" style={{ marginBottom: spacing.sm }}>
|
||||
Server URL
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="http://192.168.1.100:8080"
|
||||
keyboardType="url"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: validation.valid ? colors.border : colors.danger,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.textPrimary,
|
||||
padding: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
}}
|
||||
/>
|
||||
{!validation.valid && (
|
||||
<ThemedText preset="bodySm" style={{ color: colors.danger, marginBottom: spacing.sm }}>
|
||||
{validation.error}
|
||||
</ThemedText>
|
||||
)}
|
||||
|
||||
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginBottom: spacing.sm }}>
|
||||
{testResult || 'Ready to test connection'}
|
||||
</ThemedText>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: spacing.sm }}>
|
||||
<Pressable
|
||||
onPress={handleTest}
|
||||
disabled={!validation.valid}
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
backgroundColor: validation.valid ? colors.accentDim : colors.surfaceAlt,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ThemedText preset="labelMd" style={{ color: colors.textPrimary }}>
|
||||
Test Connection
|
||||
</ThemedText>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={onSave}
|
||||
disabled={!validation.valid}
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
backgroundColor: validation.valid ? colors.success : colors.surfaceAlt,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ThemedText preset="labelMd" style={{ color: colors.textPrimary }}>
|
||||
Save
|
||||
</ThemedText>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Pressable, View } from 'react-native';
|
||||
import { ThemeMode } from '@/theme/ThemeContext';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
|
||||
type ThemePickerProps = {
|
||||
value: ThemeMode;
|
||||
onChange: (value: ThemeMode) => void;
|
||||
};
|
||||
|
||||
const OPTIONS: ThemeMode[] = ['light', 'dark', 'system'];
|
||||
|
||||
export const ThemePicker = ({ value, onChange }: ThemePickerProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginTop: spacing.sm,
|
||||
}}
|
||||
>
|
||||
{OPTIONS.map((option) => {
|
||||
const isActive = option === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={option}
|
||||
onPress={() => onChange(option)}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: isActive ? colors.accent : colors.border,
|
||||
backgroundColor: isActive ? `${colors.accent}22` : '#0D1117',
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ThemedText preset="labelMd" style={{ color: isActive ? colors.accent : colors.textSecondary }}>
|
||||
{option.toUpperCase()}
|
||||
</ThemedText>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Linking, ScrollView, View } from 'react-native';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { spacing } from '@/theme/spacing';
|
||||
import { WS_PATH } from '@/constants/websocket';
|
||||
import { apiService } from '@/services/api.service';
|
||||
import { wsService } from '@/services/ws.service';
|
||||
import { useSettingsStore } from '@/stores/settingsStore';
|
||||
import { Alert, Pressable, Platform } from 'react-native';
|
||||
import { ThemePicker } from './ThemePicker';
|
||||
import { RssiToggle } from './RssiToggle';
|
||||
import { ServerUrlInput } from './ServerUrlInput';
|
||||
|
||||
type GlowCardProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const GlowCard = ({ title, children }: GlowCardProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: '#0F141E',
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: `${colors.accent}35`,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.md,
|
||||
}}
|
||||
>
|
||||
<ThemedText preset="labelMd" style={{ marginBottom: spacing.sm, color: colors.textPrimary }}>
|
||||
{title}
|
||||
</ThemedText>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ScanIntervalPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}) => {
|
||||
const options = [1, 2, 5];
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', gap: spacing.sm, marginTop: spacing.sm }}>
|
||||
{options.map((interval) => {
|
||||
const isActive = interval === value;
|
||||
return (
|
||||
<Pressable
|
||||
key={interval}
|
||||
onPress={() => onChange(interval)}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: isActive ? colors.accent : colors.border,
|
||||
borderRadius: 8,
|
||||
backgroundColor: isActive ? `${colors.accent}20` : colors.surface,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ThemedText
|
||||
preset="bodySm"
|
||||
style={{
|
||||
color: isActive ? colors.accent : colors.textSecondary,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
{interval}s
|
||||
</ThemedText>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsScreen = () => {
|
||||
const serverUrl = useSettingsStore((state) => state.serverUrl);
|
||||
const rssiScanEnabled = useSettingsStore((state) => state.rssiScanEnabled);
|
||||
const theme = useSettingsStore((state) => state.theme);
|
||||
const setServerUrl = useSettingsStore((state) => state.setServerUrl);
|
||||
const setRssiScanEnabled = useSettingsStore((state) => state.setRssiScanEnabled);
|
||||
const setTheme = useSettingsStore((state) => state.setTheme);
|
||||
|
||||
const [draftUrl, setDraftUrl] = useState(serverUrl);
|
||||
const [scanInterval, setScanInterval] = useState(2);
|
||||
|
||||
useEffect(() => {
|
||||
setDraftUrl(serverUrl);
|
||||
}, [serverUrl]);
|
||||
|
||||
const intervalSummary = useMemo(() => `${scanInterval}s`, [scanInterval]);
|
||||
|
||||
const handleSaveUrl = () => {
|
||||
const newUrl = draftUrl.trim();
|
||||
setServerUrl(newUrl);
|
||||
wsService.disconnect();
|
||||
wsService.connect(newUrl);
|
||||
apiService.setBaseUrl(newUrl);
|
||||
};
|
||||
|
||||
const handleOpenGitHub = async () => {
|
||||
const handled = await Linking.canOpenURL('https://github.com');
|
||||
if (!handled) {
|
||||
Alert.alert('Unable to open link', 'Please open https://github.com manually in your browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
await Linking.openURL('https://github.com');
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={{ flex: 1, backgroundColor: colors.bg, padding: spacing.md }}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingBottom: spacing.xl,
|
||||
}}
|
||||
>
|
||||
<GlowCard title="SERVER">
|
||||
<ServerUrlInput value={draftUrl} onChange={setDraftUrl} onSave={handleSaveUrl} />
|
||||
</GlowCard>
|
||||
|
||||
<GlowCard title="SENSING">
|
||||
<RssiToggle enabled={rssiScanEnabled} onChange={setRssiScanEnabled} />
|
||||
<ThemedText preset="bodyMd" style={{ marginTop: spacing.md }}>
|
||||
Scan interval
|
||||
</ThemedText>
|
||||
<ScanIntervalPicker value={scanInterval} onChange={setScanInterval} />
|
||||
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginTop: spacing.sm }}>
|
||||
Active interval: {intervalSummary}
|
||||
</ThemedText>
|
||||
{Platform.OS === 'ios' && (
|
||||
<ThemedText preset="bodySm" style={{ color: colors.textSecondary, marginTop: spacing.sm }}>
|
||||
iOS: RSSI scanning uses stubbed telemetry in this build.
|
||||
</ThemedText>
|
||||
)}
|
||||
</GlowCard>
|
||||
|
||||
<GlowCard title="APPEARANCE">
|
||||
<ThemePicker value={theme} onChange={setTheme} />
|
||||
</GlowCard>
|
||||
|
||||
<GlowCard title="ABOUT">
|
||||
<ThemedText preset="bodyMd" style={{ marginBottom: spacing.xs }}>
|
||||
WiFi-DensePose Mobile v1.0.0
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
preset="bodySm"
|
||||
style={{ color: colors.accent, marginBottom: spacing.sm }}
|
||||
onPress={handleOpenGitHub}
|
||||
>
|
||||
View on GitHub
|
||||
</ThemedText>
|
||||
<ThemedText preset="bodySm">WebSocket: {WS_PATH}</ThemedText>
|
||||
<ThemedText preset="bodySm" style={{ color: colors.textSecondary }}>
|
||||
Triage priority mapping: Immediate/Delayed/Minor/Deceased/Unknown
|
||||
</ThemedText>
|
||||
</GlowCard>
|
||||
</ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsScreen;
|
||||
Reference in New Issue
Block a user