feat(device-settings): Replace Location TextInput with Picker
- Replace free-text Location input with modal Picker selector - Use ROOM_LOCATIONS constants for predefined room options - Show icon and label for each location option - Highlight currently selected location in picker 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2aff43af34
commit
3bc0d2a8a9
@ -8,13 +8,15 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
Modal,
|
||||||
|
FlatList,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
import * as Device from 'expo-device';
|
import * as Device from 'expo-device';
|
||||||
import { useBLE } from '@/contexts/BLEContext';
|
import { useBLE } from '@/contexts/BLEContext';
|
||||||
import { api } from '@/services/api';
|
import { api, ROOM_LOCATIONS, type RoomLocationId } from '@/services/api';
|
||||||
import type { WiFiStatus } from '@/services/ble';
|
import type { WiFiStatus } from '@/services/ble';
|
||||||
import {
|
import {
|
||||||
AppColors,
|
AppColors,
|
||||||
@ -58,8 +60,9 @@ export default function DeviceSettingsScreen() {
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
// Editable fields
|
// Editable fields
|
||||||
const [location, setLocation] = useState('');
|
const [location, setLocation] = useState<RoomLocationId | ''>('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
|
const [showLocationPicker, setShowLocationPicker] = useState(false);
|
||||||
|
|
||||||
const isConnected = connectedDevices.has(deviceId!);
|
const isConnected = connectedDevices.has(deviceId!);
|
||||||
|
|
||||||
@ -82,13 +85,12 @@ export default function DeviceSettingsScreen() {
|
|||||||
|
|
||||||
if (sensor) {
|
if (sensor) {
|
||||||
setSensorInfo(sensor);
|
setSensorInfo(sensor);
|
||||||
setLocation(sensor.location || '');
|
setLocation((sensor.location as RoomLocationId) || '');
|
||||||
setDescription(sensor.description || '');
|
setDescription(sensor.description || '');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Sensor not found');
|
throw new Error('Sensor not found');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DeviceSettings] Failed to load sensor info:', error);
|
|
||||||
Alert.alert('Error', 'Failed to load sensor information');
|
Alert.alert('Error', 'Failed to load sensor information');
|
||||||
router.back();
|
router.back();
|
||||||
} finally {
|
} finally {
|
||||||
@ -111,7 +113,6 @@ export default function DeviceSettingsScreen() {
|
|||||||
// Load WiFi status after connecting
|
// Load WiFi status after connecting
|
||||||
loadWiFiStatus();
|
loadWiFiStatus();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DeviceSettings] Connection failed:', error);
|
|
||||||
Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.');
|
Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
@ -127,7 +128,6 @@ export default function DeviceSettingsScreen() {
|
|||||||
const wifiStatus = await getCurrentWiFi(deviceId!);
|
const wifiStatus = await getCurrentWiFi(deviceId!);
|
||||||
setCurrentWiFi(wifiStatus);
|
setCurrentWiFi(wifiStatus);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DeviceSettings] Failed to get WiFi status:', error);
|
|
||||||
Alert.alert('Error', 'Failed to get WiFi status');
|
Alert.alert('Error', 'Failed to get WiFi status');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingWiFi(false);
|
setIsLoadingWiFi(false);
|
||||||
@ -173,9 +173,8 @@ export default function DeviceSettingsScreen() {
|
|||||||
Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.');
|
Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.');
|
||||||
router.back();
|
router.back();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DeviceSettings] Reboot failed:', error);
|
|
||||||
Alert.alert('Error', 'Failed to reboot sensor');
|
Alert.alert('Error', 'Failed to reboot sensor');
|
||||||
} finally {
|
} finally{
|
||||||
setIsRebooting(false);
|
setIsRebooting(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -218,7 +217,6 @@ export default function DeviceSettingsScreen() {
|
|||||||
|
|
||||||
Alert.alert('Success', 'Device information updated.');
|
Alert.alert('Success', 'Device information updated.');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[DeviceSettings] Save failed:', error);
|
|
||||||
Alert.alert('Error', error.message || 'Failed to save device information');
|
Alert.alert('Error', error.message || 'Failed to save device information');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@ -353,13 +351,20 @@ export default function DeviceSettingsScreen() {
|
|||||||
<View style={styles.detailsCard}>
|
<View style={styles.detailsCard}>
|
||||||
<View style={styles.editableRow}>
|
<View style={styles.editableRow}>
|
||||||
<Text style={styles.editableLabel}>Location</Text>
|
<Text style={styles.editableLabel}>Location</Text>
|
||||||
<TextInput
|
<TouchableOpacity
|
||||||
style={styles.editableInput}
|
style={styles.pickerButton}
|
||||||
value={location}
|
onPress={() => setShowLocationPicker(true)}
|
||||||
onChangeText={setLocation}
|
>
|
||||||
placeholder="e.g., Living Room, Kitchen..."
|
<Text style={[
|
||||||
placeholderTextColor={AppColors.textMuted}
|
styles.pickerButtonText,
|
||||||
/>
|
!location && styles.pickerButtonPlaceholder
|
||||||
|
]}>
|
||||||
|
{location
|
||||||
|
? `${ROOM_LOCATIONS.find(l => l.id === location)?.icon} ${ROOM_LOCATIONS.find(l => l.id === location)?.label}`
|
||||||
|
: 'Select location...'}
|
||||||
|
</Text>
|
||||||
|
<Ionicons name="chevron-down" size={20} color={AppColors.textMuted} />
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.detailDivider} />
|
<View style={styles.detailDivider} />
|
||||||
<View style={styles.editableRow}>
|
<View style={styles.editableRow}>
|
||||||
@ -514,6 +519,56 @@ export default function DeviceSettingsScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Location Picker Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showLocationPicker}
|
||||||
|
animationType="slide"
|
||||||
|
transparent={true}
|
||||||
|
onRequestClose={() => setShowLocationPicker(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<Text style={styles.modalTitle}>Select Location</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalCloseButton}
|
||||||
|
onPress={() => setShowLocationPicker(false)}
|
||||||
|
>
|
||||||
|
<Ionicons name="close" size={24} color={AppColors.textPrimary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={ROOM_LOCATIONS}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.locationOption,
|
||||||
|
location === item.id && styles.locationOptionSelected
|
||||||
|
]}
|
||||||
|
onPress={() => {
|
||||||
|
setLocation(item.id);
|
||||||
|
setShowLocationPicker(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.locationOptionIcon}>{item.icon}</Text>
|
||||||
|
<Text style={[
|
||||||
|
styles.locationOptionText,
|
||||||
|
location === item.id && styles.locationOptionTextSelected
|
||||||
|
]}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
{location === item.id && (
|
||||||
|
<Ionicons name="checkmark" size={20} color={AppColors.primary} />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
ItemSeparatorComponent={() => <View style={styles.locationSeparator} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -688,6 +743,26 @@ const styles = StyleSheet.create({
|
|||||||
minHeight: 60,
|
minHeight: 60,
|
||||||
textAlignVertical: 'top',
|
textAlignVertical: 'top',
|
||||||
},
|
},
|
||||||
|
// Picker Button
|
||||||
|
pickerButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: AppColors.background,
|
||||||
|
borderRadius: BorderRadius.md,
|
||||||
|
paddingHorizontal: Spacing.md,
|
||||||
|
paddingVertical: Spacing.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: AppColors.border,
|
||||||
|
minHeight: 44,
|
||||||
|
},
|
||||||
|
pickerButtonText: {
|
||||||
|
fontSize: FontSizes.base,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
},
|
||||||
|
pickerButtonPlaceholder: {
|
||||||
|
color: AppColors.textMuted,
|
||||||
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -872,4 +947,63 @@ const styles = StyleSheet.create({
|
|||||||
color: AppColors.info,
|
color: AppColors.info,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
|
// Modal styles
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
backgroundColor: AppColors.surface,
|
||||||
|
borderTopLeftRadius: BorderRadius.xl,
|
||||||
|
borderTopRightRadius: BorderRadius.xl,
|
||||||
|
maxHeight: '70%',
|
||||||
|
paddingBottom: Spacing.xl,
|
||||||
|
},
|
||||||
|
modalHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: Spacing.lg,
|
||||||
|
paddingVertical: Spacing.md,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: AppColors.border,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: FontSizes.lg,
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
},
|
||||||
|
modalCloseButton: {
|
||||||
|
padding: Spacing.xs,
|
||||||
|
},
|
||||||
|
locationOption: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: Spacing.lg,
|
||||||
|
paddingVertical: Spacing.md,
|
||||||
|
gap: Spacing.md,
|
||||||
|
},
|
||||||
|
locationOptionSelected: {
|
||||||
|
backgroundColor: AppColors.primaryLighter,
|
||||||
|
},
|
||||||
|
locationOptionIcon: {
|
||||||
|
fontSize: 24,
|
||||||
|
width: 32,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
locationOptionText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: FontSizes.base,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
},
|
||||||
|
locationOptionTextSelected: {
|
||||||
|
fontWeight: FontWeights.semibold,
|
||||||
|
color: AppColors.primary,
|
||||||
|
},
|
||||||
|
locationSeparator: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: AppColors.border,
|
||||||
|
marginLeft: Spacing.lg + 32 + Spacing.md,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user