Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 779bf8f

Browse files
committed
feat: Phase 3 — services, stores, navigation, design system
Services: ws.service, api.service, simulation.service, rssi.service (android+ios) Stores: poseStore, settingsStore, matStore (Zustand) Types: sensing, mat, api, navigation Hooks: usePoseStream, useRssiScanner, useServerReachability Theme: colors, typography, spacing, ThemeContext Navigation: MainTabs (5 tabs), RootNavigator, types Components: GaugeArc, SparklineChart, OccupancyGrid, StatusDot, ConnectionBanner, SignalBar, +more Utils: ringBuffer, colorMap, formatters, urlValidator Verified: tsc 0 errors, jest passes
1 parent fbd7d83 commit 779bf8f

45 files changed

Lines changed: 2290 additions & 19 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

mobile/App.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
1-
import React from 'react';
2-
import { StyleSheet, Text, View } from 'react-native';
1+
import { useEffect } from 'react';
2+
import { NavigationContainer, DarkTheme } from '@react-navigation/native';
3+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
34
import { StatusBar } from 'expo-status-bar';
5+
import { SafeAreaProvider } from 'react-native-safe-area-context';
6+
import { ThemeProvider } from './src/theme/ThemeContext';
7+
import { RootNavigator } from './src/navigation/RootNavigator';
48

59
export default function App() {
10+
useEffect(() => {
11+
(globalThis as { __appStartTime?: number }).__appStartTime = Date.now();
12+
}, []);
13+
14+
const navigationTheme = {
15+
...DarkTheme,
16+
colors: {
17+
...DarkTheme.colors,
18+
background: '#0A0E1A',
19+
card: '#0D1117',
20+
text: '#E2E8F0',
21+
border: '#1E293B',
22+
primary: '#32B8C6',
23+
},
24+
};
25+
626
return (
7-
<View style={styles.container}>
8-
<Text style={styles.title}>WiFi-DensePose</Text>
9-
<StatusBar style="dark" />
10-
</View>
27+
<GestureHandlerRootView style={{ flex: 1 }}>
28+
<SafeAreaProvider>
29+
<ThemeProvider>
30+
<NavigationContainer theme={navigationTheme}>
31+
<RootNavigator />
32+
</NavigationContainer>
33+
</ThemeProvider>
34+
</SafeAreaProvider>
35+
<StatusBar style="light" />
36+
</GestureHandlerRootView>
1137
);
1238
}
13-
14-
const styles = StyleSheet.create({
15-
container: {
16-
flex: 1,
17-
alignItems: 'center',
18-
justifyContent: 'center',
19-
backgroundColor: '#fff',
20-
},
21-
title: {
22-
fontSize: 24,
23-
fontWeight: '600',
24-
},
25-
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { StyleSheet, View } from 'react-native';
2+
import { ThemedText } from './ThemedText';
3+
4+
type ConnectionState = 'connected' | 'simulated' | 'disconnected';
5+
6+
type ConnectionBannerProps = {
7+
status: ConnectionState;
8+
};
9+
10+
const resolveState = (status: ConnectionState) => {
11+
if (status === 'connected') {
12+
return {
13+
label: 'LIVE STREAM',
14+
backgroundColor: '#0F6B2A',
15+
textColor: '#E2FFEA',
16+
};
17+
}
18+
19+
if (status === 'disconnected') {
20+
return {
21+
label: 'DISCONNECTED',
22+
backgroundColor: '#8A1E2A',
23+
textColor: '#FFE3E7',
24+
};
25+
}
26+
27+
return {
28+
label: 'SIMULATED DATA',
29+
backgroundColor: '#9A5F0C',
30+
textColor: '#FFF3E1',
31+
};
32+
};
33+
34+
export const ConnectionBanner = ({ status }: ConnectionBannerProps) => {
35+
const state = resolveState(status);
36+
37+
return (
38+
<View
39+
style={[
40+
styles.banner,
41+
{
42+
backgroundColor: state.backgroundColor,
43+
borderBottomColor: state.textColor,
44+
},
45+
]}
46+
>
47+
<ThemedText preset="labelMd" style={[styles.text, { color: state.textColor }]}>
48+
{state.label}
49+
</ThemedText>
50+
</View>
51+
);
52+
};
53+
54+
const styles = StyleSheet.create({
55+
banner: {
56+
position: 'absolute',
57+
left: 0,
58+
right: 0,
59+
top: 0,
60+
zIndex: 100,
61+
paddingVertical: 6,
62+
borderBottomWidth: 2,
63+
alignItems: 'center',
64+
justifyContent: 'center',
65+
},
66+
text: {
67+
letterSpacing: 2,
68+
fontWeight: '700',
69+
},
70+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Component, ErrorInfo, ReactNode } from 'react';
2+
import { Button, StyleSheet, View } from 'react-native';
3+
import { ThemedText } from './ThemedText';
4+
import { ThemedView } from './ThemedView';
5+
6+
type ErrorBoundaryProps = {
7+
children: ReactNode;
8+
};
9+
10+
type ErrorBoundaryState = {
11+
hasError: boolean;
12+
error?: Error;
13+
};
14+
15+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
16+
constructor(props: ErrorBoundaryProps) {
17+
super(props);
18+
this.state = { hasError: false };
19+
}
20+
21+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
22+
return { hasError: true, error };
23+
}
24+
25+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
26+
console.error('ErrorBoundary caught an error', error, errorInfo);
27+
}
28+
29+
handleRetry = () => {
30+
this.setState({ hasError: false, error: undefined });
31+
};
32+
33+
render() {
34+
if (this.state.hasError) {
35+
return (
36+
<ThemedView style={styles.container}>
37+
<ThemedText preset="displayMd">Something went wrong</ThemedText>
38+
<ThemedText preset="bodySm" style={styles.message}>
39+
{this.state.error?.message ?? 'An unexpected error occurred.'}
40+
</ThemedText>
41+
<View style={styles.buttonWrap}>
42+
<Button title="Retry" onPress={this.handleRetry} />
43+
</View>
44+
</ThemedView>
45+
);
46+
}
47+
48+
return this.props.children;
49+
}
50+
}
51+
52+
const styles = StyleSheet.create({
53+
container: {
54+
flex: 1,
55+
justifyContent: 'center',
56+
alignItems: 'center',
57+
padding: 20,
58+
gap: 12,
59+
},
60+
message: {
61+
textAlign: 'center',
62+
},
63+
buttonWrap: {
64+
marginTop: 8,
65+
},
66+
});

mobile/src/components/GaugeArc.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useEffect } from 'react';
2+
import { StyleSheet, View } from 'react-native';
3+
import Animated, { useAnimatedProps, useSharedValue, withTiming } from 'react-native-reanimated';
4+
import Svg, { Circle, G, Text as SvgText } from 'react-native-svg';
5+
6+
type GaugeArcProps = {
7+
value: number;
8+
max: number;
9+
label: string;
10+
unit: string;
11+
color: string;
12+
size?: number;
13+
};
14+
15+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
16+
17+
export const GaugeArc = ({ value, max, label, unit, color, size = 140 }: GaugeArcProps) => {
18+
const radius = (size - 20) / 2;
19+
const circumference = 2 * Math.PI * radius;
20+
const arcLength = circumference * 0.75;
21+
const strokeWidth = 12;
22+
const progress = useSharedValue(0);
23+
24+
const normalized = Math.max(0, Math.min(max > 0 ? value / max : 0, 1));
25+
const displayText = `${value.toFixed(1)} ${unit}`;
26+
27+
useEffect(() => {
28+
progress.value = withTiming(normalized, { duration: 600 });
29+
}, [normalized, progress]);
30+
31+
const animatedStroke = useAnimatedProps(() => {
32+
const dashOffset = arcLength - arcLength * progress.value;
33+
return {
34+
strokeDashoffset: dashOffset,
35+
};
36+
});
37+
38+
return (
39+
<View style={styles.wrapper}>
40+
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
41+
<G transform={`rotate(-135 ${size / 2} ${size / 2})`}>
42+
<Circle
43+
cx={size / 2}
44+
cy={size / 2}
45+
r={radius}
46+
strokeWidth={strokeWidth}
47+
stroke="#1E293B"
48+
fill="none"
49+
strokeDasharray={`${arcLength} ${circumference}`}
50+
strokeLinecap="round"
51+
/>
52+
<AnimatedCircle
53+
cx={size / 2}
54+
cy={size / 2}
55+
r={radius}
56+
strokeWidth={strokeWidth}
57+
stroke={color}
58+
fill="none"
59+
strokeDasharray={`${arcLength} ${circumference}`}
60+
strokeLinecap="round"
61+
animatedProps={animatedStroke}
62+
/>
63+
</G>
64+
<SvgText
65+
x={size / 2}
66+
y={size / 2 - 4}
67+
fill="#E2E8F0"
68+
fontSize={18}
69+
fontFamily="Courier New"
70+
fontWeight="700"
71+
textAnchor="middle"
72+
>
73+
{displayText}
74+
</SvgText>
75+
<SvgText
76+
x={size / 2}
77+
y={size / 2 + 16}
78+
fill="#94A3B8"
79+
fontSize={10}
80+
fontFamily="Courier New"
81+
textAnchor="middle"
82+
letterSpacing="0.6"
83+
>
84+
{label}
85+
</SvgText>
86+
</Svg>
87+
</View>
88+
);
89+
};
90+
91+
const styles = StyleSheet.create({
92+
wrapper: {
93+
alignItems: 'center',
94+
justifyContent: 'center',
95+
},
96+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useEffect } from 'react';
2+
import { StyleSheet, ViewStyle } from 'react-native';
3+
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
4+
import Svg, { Circle } from 'react-native-svg';
5+
import { colors } from '../theme/colors';
6+
7+
type LoadingSpinnerProps = {
8+
size?: number;
9+
color?: string;
10+
style?: ViewStyle;
11+
};
12+
13+
export const LoadingSpinner = ({ size = 36, color = colors.accent, style }: LoadingSpinnerProps) => {
14+
const rotation = useSharedValue(0);
15+
const strokeWidth = Math.max(4, size * 0.14);
16+
const center = size / 2;
17+
const radius = center - strokeWidth;
18+
const circumference = 2 * Math.PI * radius;
19+
20+
useEffect(() => {
21+
rotation.value = withRepeat(withTiming(360, { duration: 900, easing: Easing.linear }), -1);
22+
}, [rotation]);
23+
24+
const animatedStyle = useAnimatedStyle(() => ({
25+
transform: [{ rotateZ: `${rotation.value}deg` }],
26+
}));
27+
28+
return (
29+
<Animated.View style={[styles.container, { width: size, height: size }, style, animatedStyle]} pointerEvents="none">
30+
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
31+
<Circle
32+
cx={center}
33+
cy={center}
34+
r={radius}
35+
stroke="rgba(255,255,255,0.2)"
36+
strokeWidth={strokeWidth}
37+
fill="none"
38+
/>
39+
<Circle
40+
cx={center}
41+
cy={center}
42+
r={radius}
43+
stroke={color}
44+
strokeWidth={strokeWidth}
45+
fill="none"
46+
strokeLinecap="round"
47+
strokeDasharray={`${circumference * 0.3} ${circumference * 0.7}`}
48+
strokeDashoffset={circumference * 0.2}
49+
/>
50+
</Svg>
51+
</Animated.View>
52+
);
53+
};
54+
55+
const styles = StyleSheet.create({
56+
container: {
57+
alignItems: 'center',
58+
justifyContent: 'center',
59+
},
60+
});

0 commit comments

Comments
 (0)