design: align visual style with bincio_autarchive
- Add src/theme.ts with centralised color palette - Backgrounds: #111 → #09090b, surfaces #1e1e1e → #18181b - All cards get 1px #27272a borders (matches autarchive cards) - Text: #fff/#888/#555 → #f4f4f5/#a1a1aa/#71717a - Accent: #3b82f6 → #60a5fa (autarchive default palette) - Tab icons: colored emoji → monochromatic Unicode (◉ ☰ ⚙) - Action buttons use muted palette (#16a34a / #d97706 / #dc2626) - Keep-awake toggle uses ◑/◌ symbols, border highlights accent on active - Connect/Scan buttons match autarchive surface+border style
This commit is contained in:
@@ -10,36 +10,34 @@ import { SensorPairingScreen } from '../screens/SensorPairingScreen';
|
|||||||
import { SavedRecordingsScreen } from '../screens/SavedRecordingsScreen';
|
import { SavedRecordingsScreen } from '../screens/SavedRecordingsScreen';
|
||||||
import { SettingsScreen } from '../screens/SettingsScreen';
|
import { SettingsScreen } from '../screens/SettingsScreen';
|
||||||
import { RootStackParamList, TabParamList } from '../types';
|
import { RootStackParamList, TabParamList } from '../types';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
const Tab = createBottomTabNavigator<TabParamList>();
|
const Tab = createBottomTabNavigator<TabParamList>();
|
||||||
|
|
||||||
|
const TAB_ICONS: Record<string, string> = {
|
||||||
|
Recording: '◉',
|
||||||
|
Saved: '☰',
|
||||||
|
Settings: '⚙',
|
||||||
|
};
|
||||||
|
|
||||||
function Tabs() {
|
function Tabs() {
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={{
|
screenOptions={({ route }) => ({
|
||||||
headerStyle: { backgroundColor: '#111' },
|
headerStyle: { backgroundColor: colors.bg },
|
||||||
headerTintColor: '#fff',
|
headerTintColor: colors.text,
|
||||||
tabBarStyle: { backgroundColor: '#111', borderTopColor: '#222' },
|
tabBarStyle: { backgroundColor: colors.surface, borderTopColor: colors.border },
|
||||||
tabBarActiveTintColor: '#3b82f6',
|
tabBarActiveTintColor: colors.accent,
|
||||||
tabBarInactiveTintColor: '#555',
|
tabBarInactiveTintColor: colors.textMuted,
|
||||||
}}
|
tabBarIcon: ({ color }) => (
|
||||||
|
<Text style={{ color, fontSize: 18 }}>{TAB_ICONS[route.name]}</Text>
|
||||||
|
),
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<Tab.Screen
|
<Tab.Screen name="Recording" component={RecordingScreen} />
|
||||||
name="Recording"
|
<Tab.Screen name="Saved" component={SavedRecordingsScreen} />
|
||||||
component={RecordingScreen}
|
<Tab.Screen name="Settings" component={SettingsScreen} />
|
||||||
options={{ tabBarIcon: ({ color }) => <Text style={{ color, fontSize: 20 }}>⏺</Text> }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Saved"
|
|
||||||
component={SavedRecordingsScreen}
|
|
||||||
options={{ tabBarIcon: ({ color }) => <Text style={{ color, fontSize: 20 }}>📋</Text> }}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name="Settings"
|
|
||||||
component={SettingsScreen}
|
|
||||||
options={{ tabBarIcon: ({ color }) => <Text style={{ color, fontSize: 20 }}>⚙️</Text> }}
|
|
||||||
/>
|
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -49,9 +47,9 @@ export function AppNavigator() {
|
|||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<Stack.Navigator
|
<Stack.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerStyle: { backgroundColor: '#111' },
|
headerStyle: { backgroundColor: colors.bg },
|
||||||
headerTintColor: '#fff',
|
headerTintColor: colors.text,
|
||||||
contentStyle: { backgroundColor: '#111' },
|
contentStyle: { backgroundColor: colors.bg },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="Tabs" component={Tabs} options={{ headerShown: false }} />
|
<Stack.Screen name="Tabs" component={Tabs} options={{ headerShown: false }} />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { saveGpx } from '../services/gpx';
|
|||||||
import { insertRecording } from '../services/db';
|
import { insertRecording } from '../services/db';
|
||||||
import { SavedRecording } from '../types';
|
import { SavedRecording } from '../types';
|
||||||
import { randomUUID } from 'expo-crypto';
|
import { randomUUID } from 'expo-crypto';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export function PostRecordingScreen() {
|
export function PostRecordingScreen() {
|
||||||
const nav = useNavigation();
|
const nav = useNavigation();
|
||||||
@@ -65,7 +66,7 @@ export function PostRecordingScreen() {
|
|||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Activity title"
|
placeholder="Activity title"
|
||||||
placeholderTextColor="#555"
|
placeholderTextColor={colors.placeholder}
|
||||||
value={title}
|
value={title}
|
||||||
onChangeText={setTitle}
|
onChangeText={setTitle}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -75,7 +76,7 @@ export function PostRecordingScreen() {
|
|||||||
<Text style={styles.btnText}>{saving ? 'Saving…' : 'Save'}</Text>
|
<Text style={styles.btnText}>{saving ? 'Saving…' : 'Save'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={[styles.btn, styles.btnDiscard]} onPress={handleDiscard}>
|
<TouchableOpacity style={[styles.btn, styles.btnDiscard]} onPress={handleDiscard}>
|
||||||
<Text style={[styles.btnText, { color: '#ef4444' }]}>Discard</Text>
|
<Text style={[styles.btnText, { color: colors.error }]}>Discard</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
@@ -91,16 +92,16 @@ function Stat({ label, value }: { label: string; value: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#111' },
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
content: { padding: 24, gap: 16 },
|
content: { padding: 24, gap: 12 },
|
||||||
heading: { color: '#fff', fontSize: 24, fontWeight: '700', marginBottom: 8 },
|
heading: { color: colors.text, fontSize: 22, fontWeight: '700', marginBottom: 4 },
|
||||||
statsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, marginBottom: 8 },
|
statsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 4 },
|
||||||
stat: { flex: 1, minWidth: '40%', backgroundColor: '#1e1e1e', borderRadius: 12, padding: 16 },
|
stat: { flex: 1, minWidth: '40%', backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 14 },
|
||||||
statLabel: { color: '#888', fontSize: 12, textTransform: 'uppercase' },
|
statLabel: { color: colors.textMuted, fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||||
statValue: { color: '#fff', fontSize: 22, fontWeight: '600', marginTop: 4 },
|
statValue: { color: colors.text, fontSize: 22, fontWeight: '700', marginTop: 4 },
|
||||||
input: { backgroundColor: '#1e1e1e', color: '#fff', borderRadius: 12, padding: 16, fontSize: 18 },
|
input: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, color: colors.text, borderRadius: 10, padding: 14, fontSize: 16 },
|
||||||
btn: { borderRadius: 12, padding: 16, alignItems: 'center' },
|
btn: { borderRadius: 10, padding: 16, alignItems: 'center' },
|
||||||
btnSave: { backgroundColor: '#22c55e' },
|
btnSave: { backgroundColor: colors.btnStart },
|
||||||
btnDiscard: { backgroundColor: '#1e1e1e' },
|
btnDiscard:{ backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border },
|
||||||
btnText: { fontSize: 16, fontWeight: '700', color: '#fff' },
|
btnText: { fontSize: 16, fontWeight: '600', color: colors.text },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { Map, Camera, GeoJSONSource, Layer, UserLocation } from '@maplibre/mapli
|
|||||||
import type { LineLayerStyle } from '@maplibre/maplibre-react-native';
|
import type { LineLayerStyle } from '@maplibre/maplibre-react-native';
|
||||||
import { useRecordingStore } from '../store/recording';
|
import { useRecordingStore } from '../store/recording';
|
||||||
import { startGpsRecording, stopGpsRecording, requestLocationPermissions } from '../services/gps';
|
import { startGpsRecording, stopGpsRecording, requestLocationPermissions } from '../services/gps';
|
||||||
import { RootStackParamList, TrackPoint } from '../types';
|
import { RootStackParamList } from '../types';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
|
||||||
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
|
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
|
||||||
|
|
||||||
const trackLineStyle: LineLayerStyle = {
|
const trackLineStyle: LineLayerStyle = {
|
||||||
lineColor: '#3b82f6',
|
lineColor: colors.accent,
|
||||||
lineWidth: 3,
|
lineWidth: 3,
|
||||||
lineJoin: 'round',
|
lineJoin: 'round',
|
||||||
lineCap: 'round',
|
lineCap: 'round',
|
||||||
@@ -41,10 +42,7 @@ export function RecordingScreen() {
|
|||||||
|
|
||||||
const trackGeoJSON = useMemo<GeoJSON.Feature<GeoJSON.LineString>>(() => ({
|
const trackGeoJSON = useMemo<GeoJSON.Feature<GeoJSON.LineString>>(() => ({
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: {
|
geometry: { type: 'LineString', coordinates: trackPoints.map((p) => [p.lon, p.lat]) },
|
||||||
type: 'LineString',
|
|
||||||
coordinates: trackPoints.map((p) => [p.lon, p.lat]),
|
|
||||||
},
|
|
||||||
properties: {},
|
properties: {},
|
||||||
}), [trackPoints]);
|
}), [trackPoints]);
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ export function RecordingScreen() {
|
|||||||
style={[styles.awakeBtn, keepAwake && styles.awakeBtnOn]}
|
style={[styles.awakeBtn, keepAwake && styles.awakeBtnOn]}
|
||||||
onPress={() => setKeepAwake(!keepAwake)}
|
onPress={() => setKeepAwake(!keepAwake)}
|
||||||
>
|
>
|
||||||
<Text style={styles.awakeBtnText}>{keepAwake ? '☀️ Awake' : '💤 Sleep'}</Text>
|
<Text style={styles.awakeBtnText}>{keepAwake ? '◑ Awake' : '◌ Sleep'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{status === 'idle' && (
|
{status === 'idle' && (
|
||||||
@@ -161,21 +159,21 @@ function StatBox({ label, value }: { label: string; value: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#111' },
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
statsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8 },
|
statsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8, borderBottomWidth: 1, borderBottomColor: colors.border },
|
||||||
statBox: { width: '25%', padding: 8, alignItems: 'center' },
|
statBox: { width: '25%', padding: 8, alignItems: 'center' },
|
||||||
statLabel: { color: '#888', fontSize: 11, textTransform: 'uppercase' },
|
statLabel: { color: colors.textMuted, fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5 },
|
||||||
statValue: { color: '#fff', fontSize: 18, fontWeight: '600', marginTop: 2 },
|
statValue: { color: colors.text, fontSize: 17, fontWeight: '600', marginTop: 2 },
|
||||||
mapArea: { flex: 1, overflow: 'hidden' },
|
mapArea: { flex: 1, overflow: 'hidden' },
|
||||||
sensorBtn: { position: 'absolute', top: 10, right: 10, backgroundColor: 'rgba(17,17,17,0.85)', borderRadius: 8, paddingVertical: 6, paddingHorizontal: 12 },
|
sensorBtn: { position: 'absolute', top: 10, right: 10, backgroundColor: 'rgba(9,9,11,0.8)', borderRadius: 8, borderWidth: 1, borderColor: colors.border, paddingVertical: 6, paddingHorizontal: 12 },
|
||||||
sensorBtnText: { color: '#3b82f6', fontSize: 13, fontWeight: '600' },
|
sensorBtnText: { color: colors.accent, fontSize: 13, fontWeight: '600' },
|
||||||
controls: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, padding: 20 },
|
controls: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, padding: 20, borderTopWidth: 1, borderTopColor: colors.border },
|
||||||
awakeBtn: { backgroundColor: '#1e1e1e', borderRadius: 20, paddingVertical: 8, paddingHorizontal: 14 },
|
awakeBtn: { backgroundColor: colors.surface, borderRadius: 20, borderWidth: 1, borderColor: colors.border, paddingVertical: 8, paddingHorizontal: 14 },
|
||||||
awakeBtnOn: { backgroundColor: '#2a2a1a' },
|
awakeBtnOn: { borderColor: colors.accent },
|
||||||
awakeBtnText: { color: '#aaa', fontSize: 13 },
|
awakeBtnText: { color: colors.textSub, fontSize: 13 },
|
||||||
btn: { paddingVertical: 16, paddingHorizontal: 32, borderRadius: 50 },
|
btn: { paddingVertical: 15, paddingHorizontal: 30, borderRadius: 50 },
|
||||||
btnStart: { backgroundColor: '#22c55e' },
|
btnStart: { backgroundColor: colors.btnStart },
|
||||||
btnPause: { backgroundColor: '#f59e0b' },
|
btnPause: { backgroundColor: colors.btnPause },
|
||||||
btnStop: { backgroundColor: '#ef4444' },
|
btnStop: { backgroundColor: colors.btnStop },
|
||||||
btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
|
btnText: { color: '#fff', fontSize: 17, fontWeight: '700' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { listRecordings, deleteRecording } from '../services/db';
|
|||||||
import { uploadGpx } from '../services/upload';
|
import { uploadGpx } from '../services/upload';
|
||||||
import { SavedRecording } from '../types';
|
import { SavedRecording } from '../types';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export function SavedRecordingsScreen() {
|
export function SavedRecordingsScreen() {
|
||||||
const [recordings, setRecordings] = useState<SavedRecording[]>([]);
|
const [recordings, setRecordings] = useState<SavedRecording[]>([]);
|
||||||
@@ -26,7 +27,7 @@ export function SavedRecordingsScreen() {
|
|||||||
const instanceUrl = await AsyncStorage.getItem('instanceUrl');
|
const instanceUrl = await AsyncStorage.getItem('instanceUrl');
|
||||||
const apiToken = await AsyncStorage.getItem('apiToken');
|
const apiToken = await AsyncStorage.getItem('apiToken');
|
||||||
if (!instanceUrl || !apiToken) {
|
if (!instanceUrl || !apiToken) {
|
||||||
Alert.alert('Not configured', 'Set your bincio instance URL and API token in Settings.');
|
Alert.alert('Not connected', 'Log in to your bincio instance in Settings first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUploading(rec.id);
|
setUploading(rec.id);
|
||||||
@@ -56,7 +57,7 @@ export function SavedRecordingsScreen() {
|
|||||||
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <View style={styles.center}><ActivityIndicator color="#fff" /></View>;
|
if (loading) return <View style={styles.center}><ActivityIndicator color={colors.accent} /></View>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -80,7 +81,7 @@ export function SavedRecordingsScreen() {
|
|||||||
<Text style={styles.action}>{uploading === item.id ? '…' : 'Upload'}</Text>
|
<Text style={styles.action}>{uploading === item.id ? '…' : 'Upload'}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => handleDelete(item)}>
|
<TouchableOpacity onPress={() => handleDelete(item)}>
|
||||||
<Text style={[styles.action, { color: '#ef4444' }]}>Delete</Text>
|
<Text style={[styles.action, { color: colors.error }]}>Delete</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -92,14 +93,14 @@ export function SavedRecordingsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#111' },
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
center: { flex: 1, backgroundColor: '#111', alignItems: 'center', justifyContent: 'center' },
|
center: { flex: 1, backgroundColor: colors.bg, alignItems: 'center', justifyContent: 'center' },
|
||||||
list: { padding: 16, gap: 12 },
|
list: { padding: 16, gap: 10 },
|
||||||
card: { backgroundColor: '#1e1e1e', borderRadius: 12, padding: 16 },
|
card: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 12, padding: 16 },
|
||||||
cardMeta: { marginBottom: 12 },
|
cardMeta: { marginBottom: 12 },
|
||||||
cardTitle: { color: '#fff', fontSize: 17, fontWeight: '600' },
|
cardTitle: { color: colors.text, fontSize: 15, fontWeight: '600' },
|
||||||
cardSub: { color: '#888', fontSize: 13, marginTop: 4 },
|
cardSub: { color: colors.textMuted, fontSize: 12, marginTop: 4 },
|
||||||
cardActions: { flexDirection: 'row', gap: 20 },
|
cardActions:{ flexDirection: 'row', gap: 20 },
|
||||||
action: { color: '#3b82f6', fontWeight: '600', fontSize: 15 },
|
action: { color: colors.accent, fontWeight: '600', fontSize: 14 },
|
||||||
empty: { color: '#555', textAlign: 'center', marginTop: 60 },
|
empty: { color: colors.textMuted, textAlign: 'center', marginTop: 60 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
subscribeForDevice, savePairedDevice, loadPairedDevices, removePairedDevice,
|
subscribeForDevice, savePairedDevice, loadPairedDevices, removePairedDevice,
|
||||||
} from '../services/ble';
|
} from '../services/ble';
|
||||||
import { BleDevice } from '../types';
|
import { BleDevice } from '../types';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
|
||||||
const TYPE_LABEL: Record<BleDevice['type'], string> = {
|
const TYPE_LABEL: Record<BleDevice['type'], string> = {
|
||||||
hr: 'Heart Rate',
|
hr: 'Heart Rate',
|
||||||
@@ -29,7 +30,6 @@ export function SensorPairingScreen() {
|
|||||||
const [connecting, setConnecting] = useState<Record<string, boolean>>({});
|
const [connecting, setConnecting] = useState<Record<string, boolean>>({});
|
||||||
const stopScanRef = useRef<(() => void) | null>(null);
|
const stopScanRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
// Load saved devices and attempt reconnect on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const devices = await loadPairedDevices();
|
const devices = await loadPairedDevices();
|
||||||
@@ -89,14 +89,14 @@ export function SensorPairingScreen() {
|
|||||||
setSaved((prev) => prev.filter((e) => e.device.id !== device.id));
|
setSaved((prev) => prev.filter((e) => e.device.id !== device.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const savedIds = new Set(saved.map((e) => e.device.id));
|
||||||
|
const newFound = found.filter((d) => !savedIds.has(d.id));
|
||||||
|
|
||||||
type ListItem =
|
type ListItem =
|
||||||
| { kind: 'header'; label: string }
|
| { kind: 'header'; label: string }
|
||||||
| { kind: 'saved'; entry: SavedEntry }
|
| { kind: 'saved'; entry: SavedEntry }
|
||||||
| { kind: 'found'; device: BleDevice };
|
| { kind: 'found'; device: BleDevice };
|
||||||
|
|
||||||
const savedIds = new Set(saved.map((e) => e.device.id));
|
|
||||||
const newFound = found.filter((d) => !savedIds.has(d.id));
|
|
||||||
|
|
||||||
const listData: ListItem[] = [
|
const listData: ListItem[] = [
|
||||||
...(saved.length > 0 ? [{ kind: 'header' as const, label: 'Saved sensors' }, ...saved.map((e) => ({ kind: 'saved' as const, entry: e }))] : []),
|
...(saved.length > 0 ? [{ kind: 'header' as const, label: 'Saved sensors' }, ...saved.map((e) => ({ kind: 'saved' as const, entry: e }))] : []),
|
||||||
...(newFound.length > 0 ? [{ kind: 'header' as const, label: 'Found nearby' }, ...newFound.map((d) => ({ kind: 'found' as const, device: d }))] : []),
|
...(newFound.length > 0 ? [{ kind: 'header' as const, label: 'Found nearby' }, ...newFound.map((d) => ({ kind: 'found' as const, device: d }))] : []),
|
||||||
@@ -104,11 +104,10 @@ export function SensorPairingScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Text style={styles.heading}>Sensors</Text>
|
|
||||||
<Text style={styles.sub}>Pair your HR monitor, power meter, or cadence sensor.</Text>
|
<Text style={styles.sub}>Pair your HR monitor, power meter, or cadence sensor.</Text>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.scanBtn} onPress={handleScan} disabled={scanning}>
|
<TouchableOpacity style={styles.scanBtn} onPress={handleScan} disabled={scanning}>
|
||||||
{scanning ? <ActivityIndicator color="#fff" /> : <Text style={styles.scanBtnText}>Scan for sensors</Text>}
|
{scanning ? <ActivityIndicator color={colors.text} /> : <Text style={styles.scanBtnText}>Scan for sensors</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -128,7 +127,7 @@ export function SensorPairingScreen() {
|
|||||||
<Text style={styles.deviceType}>{TYPE_LABEL[entry.device.type]}</Text>
|
<Text style={styles.deviceType}>{TYPE_LABEL[entry.device.type]}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.deviceActions}>
|
<View style={styles.deviceActions}>
|
||||||
{entry.status === 'connecting' && <ActivityIndicator color="#3b82f6" />}
|
{entry.status === 'connecting' && <ActivityIndicator color={colors.accent} />}
|
||||||
{entry.status === 'connected' && <Text style={styles.connectedLabel}>Connected</Text>}
|
{entry.status === 'connected' && <Text style={styles.connectedLabel}>Connected</Text>}
|
||||||
{(entry.status === 'saved' || entry.status === 'error') && (
|
{(entry.status === 'saved' || entry.status === 'error') && (
|
||||||
<TouchableOpacity style={styles.reconnectBtn} onPress={() => reconnect(entry.device, true)}>
|
<TouchableOpacity style={styles.reconnectBtn} onPress={() => reconnect(entry.device, true)}>
|
||||||
@@ -155,7 +154,9 @@ export function SensorPairingScreen() {
|
|||||||
onPress={() => handleConnect(device)}
|
onPress={() => handleConnect(device)}
|
||||||
disabled={isConnecting}
|
disabled={isConnecting}
|
||||||
>
|
>
|
||||||
{isConnecting ? <ActivityIndicator color="#fff" /> : <Text style={styles.connectBtnText}>Connect</Text>}
|
{isConnecting
|
||||||
|
? <ActivityIndicator color={colors.text} />
|
||||||
|
: <Text style={styles.connectBtnText}>Connect</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -169,23 +170,22 @@ export function SensorPairingScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#111', padding: 24 },
|
container: { flex: 1, backgroundColor: colors.bg, padding: 16 },
|
||||||
heading: { color: '#fff', fontSize: 24, fontWeight: '700' },
|
sub: { color: colors.textMuted, fontSize: 13, marginBottom: 16 },
|
||||||
sub: { color: '#888', marginTop: 4, marginBottom: 20 },
|
scanBtn: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 14, alignItems: 'center', marginBottom: 4 },
|
||||||
scanBtn: { backgroundColor: '#3b82f6', borderRadius: 12, padding: 14, alignItems: 'center', marginBottom: 8 },
|
scanBtnText: { color: colors.text, fontSize: 15, fontWeight: '600' },
|
||||||
scanBtnText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
list: { flex: 1, marginTop: 4 },
|
||||||
list: { flex: 1, marginTop: 8 },
|
sectionHeader: { color: colors.textMuted, fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.8, marginTop: 20, marginBottom: 8 },
|
||||||
sectionHeader: { color: '#555', fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.8, marginTop: 16, marginBottom: 8 },
|
deviceRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 14, marginBottom: 8 },
|
||||||
deviceRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#1e1e1e', borderRadius: 12, padding: 16, marginBottom: 8 },
|
|
||||||
deviceInfo: { flex: 1, marginRight: 12 },
|
deviceInfo: { flex: 1, marginRight: 12 },
|
||||||
deviceName: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
deviceName: { color: colors.text, fontSize: 15, fontWeight: '600' },
|
||||||
deviceType: { color: '#888', fontSize: 13, marginTop: 2 },
|
deviceType: { color: colors.textMuted, fontSize: 12, marginTop: 2 },
|
||||||
deviceActions: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
deviceActions: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||||||
connectedLabel: { color: '#22c55e', fontWeight: '600', fontSize: 14 },
|
connectedLabel: { color: colors.success, fontWeight: '600', fontSize: 13 },
|
||||||
reconnectBtn: { backgroundColor: '#1e3a5f', borderRadius: 8, paddingVertical: 7, paddingHorizontal: 12 },
|
reconnectBtn: { backgroundColor: colors.accentDim, borderRadius: 8, paddingVertical: 6, paddingHorizontal: 12 },
|
||||||
reconnectBtnText: { color: '#3b82f6', fontWeight: '600', fontSize: 14 },
|
reconnectBtnText:{ color: colors.accent, fontWeight: '600', fontSize: 13 },
|
||||||
forgetLabel: { color: '#ef4444', fontSize: 14 },
|
forgetLabel: { color: colors.error, fontSize: 13 },
|
||||||
connectBtn: { backgroundColor: '#3b82f6', borderRadius: 8, paddingVertical: 8, paddingHorizontal: 16, minWidth: 80, alignItems: 'center' },
|
connectBtn: { backgroundColor: colors.accentDim, borderRadius: 8, paddingVertical: 7, paddingHorizontal: 14, minWidth: 80, alignItems: 'center' },
|
||||||
connectBtnText: { color: '#fff', fontWeight: '600' },
|
connectBtnText: { color: colors.accent, fontWeight: '600' },
|
||||||
empty: { color: '#555', textAlign: 'center', marginTop: 40 },
|
empty: { color: colors.textMuted, textAlign: 'center', marginTop: 40 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { login, logout, loadAuthState } from '../services/auth';
|
import { login, logout, loadAuthState } from '../services/auth';
|
||||||
|
import { colors } from '../theme';
|
||||||
|
|
||||||
export function SettingsScreen() {
|
export function SettingsScreen() {
|
||||||
const [instanceUrl, setInstanceUrl] = useState('');
|
const [instanceUrl, setInstanceUrl] = useState('');
|
||||||
@@ -77,7 +78,7 @@ export function SettingsScreen() {
|
|||||||
value={instanceUrl}
|
value={instanceUrl}
|
||||||
onChangeText={setInstanceUrl}
|
onChangeText={setInstanceUrl}
|
||||||
placeholder="https://bincio.example.com"
|
placeholder="https://bincio.example.com"
|
||||||
placeholderTextColor="#555"
|
placeholderTextColor={colors.placeholder}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
keyboardType="url"
|
keyboardType="url"
|
||||||
editable={!connectedAs}
|
editable={!connectedAs}
|
||||||
@@ -101,7 +102,7 @@ export function SettingsScreen() {
|
|||||||
value={handle}
|
value={handle}
|
||||||
onChangeText={setHandle}
|
onChangeText={setHandle}
|
||||||
placeholder="your-handle"
|
placeholder="your-handle"
|
||||||
placeholderTextColor="#555"
|
placeholderTextColor={colors.placeholder}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
@@ -112,7 +113,7 @@ export function SettingsScreen() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
placeholderTextColor="#555"
|
placeholderTextColor={colors.placeholder}
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ export function SettingsScreen() {
|
|||||||
disabled={connecting}
|
disabled={connecting}
|
||||||
>
|
>
|
||||||
{connecting
|
{connecting
|
||||||
? <ActivityIndicator color="#fff" />
|
? <ActivityIndicator color={colors.text} />
|
||||||
: <Text style={styles.connectBtnText}>Connect</Text>}
|
: <Text style={styles.connectBtnText}>Connect</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
@@ -136,27 +137,32 @@ export function SettingsScreen() {
|
|||||||
|
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Text style={styles.rowLabel}>Kilometre alerts</Text>
|
<Text style={styles.rowLabel}>Kilometre alerts</Text>
|
||||||
<Switch value={kmNotifications} onValueChange={handleKmToggle} trackColor={{ true: '#3b82f6' }} />
|
<Switch
|
||||||
|
value={kmNotifications}
|
||||||
|
onValueChange={handleKmToggle}
|
||||||
|
trackColor={{ true: colors.accent }}
|
||||||
|
thumbColor={colors.text}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#111' },
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
content: { padding: 24, gap: 12 },
|
content: { padding: 16, gap: 10 },
|
||||||
sectionTitle: { color: '#888', fontSize: 13, textTransform: 'uppercase', letterSpacing: 0.8, marginTop: 16 },
|
sectionTitle: { color: colors.textMuted, fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.8, marginTop: 16 },
|
||||||
label: { color: '#aaa', fontSize: 14, marginBottom: 4 },
|
label: { color: colors.textSub, fontSize: 13, marginBottom: 2 },
|
||||||
input: { backgroundColor: '#1e1e1e', color: '#fff', borderRadius: 10, padding: 14, fontSize: 16 },
|
input: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, color: colors.text, borderRadius: 8, padding: 12, fontSize: 15 },
|
||||||
hint: { color: '#555', fontSize: 13, lineHeight: 18 },
|
hint: { color: colors.textMuted, fontSize: 12, lineHeight: 18 },
|
||||||
connectedBox: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#1e1e1e', borderRadius: 10, padding: 14 },
|
connectedBox: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 14 },
|
||||||
connectedLabel: { color: '#22c55e', fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.6 },
|
connectedLabel: { color: colors.success, fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.6 },
|
||||||
connectedName: { color: '#fff', fontSize: 16, fontWeight: '600', marginTop: 2 },
|
connectedName: { color: colors.text, fontSize: 15, fontWeight: '600', marginTop: 2 },
|
||||||
disconnectBtn: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, borderColor: '#ef4444' },
|
disconnectBtn: { paddingVertical: 6, paddingHorizontal: 12, borderRadius: 8, borderWidth: 1, borderColor: colors.errorBg },
|
||||||
disconnectBtnText: { color: '#ef4444', fontWeight: '600' },
|
disconnectBtnText: { color: colors.error, fontWeight: '600', fontSize: 13 },
|
||||||
connectBtn: { backgroundColor: '#3b82f6', borderRadius: 12, padding: 16, alignItems: 'center', marginTop: 4 },
|
connectBtn: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 14, alignItems: 'center', marginTop: 4 },
|
||||||
connectBtnDisabled: { opacity: 0.6 },
|
connectBtnDisabled: { opacity: 0.5 },
|
||||||
connectBtnText: { color: '#fff', fontSize: 16, fontWeight: '700' },
|
connectBtnText: { color: colors.text, fontSize: 15, fontWeight: '600' },
|
||||||
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: '#1e1e1e', borderRadius: 10, padding: 14 },
|
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 10, padding: 14 },
|
||||||
rowLabel: { color: '#fff', fontSize: 16 },
|
rowLabel: { color: colors.text, fontSize: 15 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export const colors = {
|
||||||
|
bg: '#09090b',
|
||||||
|
surface: '#18181b',
|
||||||
|
border: '#27272a',
|
||||||
|
borderStrong: '#3f3f46',
|
||||||
|
|
||||||
|
text: '#f4f4f5',
|
||||||
|
textSub: '#a1a1aa',
|
||||||
|
textMuted: '#71717a',
|
||||||
|
placeholder: '#52525b',
|
||||||
|
|
||||||
|
accent: '#60a5fa',
|
||||||
|
accentDim: 'rgba(96,165,250,0.15)',
|
||||||
|
|
||||||
|
success: '#86efac',
|
||||||
|
successBg: '#14532d',
|
||||||
|
error: '#fca5a5',
|
||||||
|
errorBg: '#7f1d1d',
|
||||||
|
|
||||||
|
// recording action buttons
|
||||||
|
btnStart: '#16a34a',
|
||||||
|
btnPause: '#d97706',
|
||||||
|
btnStop: '#dc2626',
|
||||||
|
} as const;
|
||||||
Reference in New Issue
Block a user