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,
|
||||
ActivityIndicator,
|
||||
TextInput,
|
||||
Modal,
|
||||
FlatList,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import * as Device from 'expo-device';
|
||||
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 {
|
||||
AppColors,
|
||||
@ -58,8 +60,9 @@ export default function DeviceSettingsScreen() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Editable fields
|
||||
const [location, setLocation] = useState('');
|
||||
const [location, setLocation] = useState<RoomLocationId | ''>('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [showLocationPicker, setShowLocationPicker] = useState(false);
|
||||
|
||||
const isConnected = connectedDevices.has(deviceId!);
|
||||
|
||||
@ -82,13 +85,12 @@ export default function DeviceSettingsScreen() {
|
||||
|
||||
if (sensor) {
|
||||
setSensorInfo(sensor);
|
||||
setLocation(sensor.location || '');
|
||||
setLocation((sensor.location as RoomLocationId) || '');
|
||||
setDescription(sensor.description || '');
|
||||
} else {
|
||||
throw new Error('Sensor not found');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[DeviceSettings] Failed to load sensor info:', error);
|
||||
Alert.alert('Error', 'Failed to load sensor information');
|
||||
router.back();
|
||||
} finally {
|
||||
@ -111,7 +113,6 @@ export default function DeviceSettingsScreen() {
|
||||
// Load WiFi status after connecting
|
||||
loadWiFiStatus();
|
||||
} catch (error: any) {
|
||||
console.error('[DeviceSettings] Connection failed:', error);
|
||||
Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.');
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
@ -127,7 +128,6 @@ export default function DeviceSettingsScreen() {
|
||||
const wifiStatus = await getCurrentWiFi(deviceId!);
|
||||
setCurrentWiFi(wifiStatus);
|
||||
} catch (error: any) {
|
||||
console.error('[DeviceSettings] Failed to get WiFi status:', error);
|
||||
Alert.alert('Error', 'Failed to get WiFi status');
|
||||
} finally {
|
||||
setIsLoadingWiFi(false);
|
||||
@ -173,7 +173,6 @@ export default function DeviceSettingsScreen() {
|
||||
Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.');
|
||||
router.back();
|
||||
} catch (error: any) {
|
||||
console.error('[DeviceSettings] Reboot failed:', error);
|
||||
Alert.alert('Error', 'Failed to reboot sensor');
|
||||
} finally{
|
||||
setIsRebooting(false);
|
||||
@ -218,7 +217,6 @@ export default function DeviceSettingsScreen() {
|
||||
|
||||
Alert.alert('Success', 'Device information updated.');
|
||||
} catch (error: any) {
|
||||
console.error('[DeviceSettings] Save failed:', error);
|
||||
Alert.alert('Error', error.message || 'Failed to save device information');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
@ -353,13 +351,20 @@ export default function DeviceSettingsScreen() {
|
||||
<View style={styles.detailsCard}>
|
||||
<View style={styles.editableRow}>
|
||||
<Text style={styles.editableLabel}>Location</Text>
|
||||
<TextInput
|
||||
style={styles.editableInput}
|
||||
value={location}
|
||||
onChangeText={setLocation}
|
||||
placeholder="e.g., Living Room, Kitchen..."
|
||||
placeholderTextColor={AppColors.textMuted}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.pickerButton}
|
||||
onPress={() => setShowLocationPicker(true)}
|
||||
>
|
||||
<Text style={[
|
||||
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 style={styles.detailDivider} />
|
||||
<View style={styles.editableRow}>
|
||||
@ -514,6 +519,56 @@ export default function DeviceSettingsScreen() {
|
||||
</Text>
|
||||
</View>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -688,6 +743,26 @@ const styles = StyleSheet.create({
|
||||
minHeight: 60,
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@ -872,4 +947,63 @@ const styles = StyleSheet.create({
|
||||
color: AppColors.info,
|
||||
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