Files
digital-pilates/app/workout/notification-settings.tsx
richarjiang 8cbf6be50a feat: add nutrition and mood reminder settings
- Implemented nutrition and mood reminder toggles in notification settings screen.
- Added corresponding utility functions for managing nutrition and mood reminder preferences.
- Updated user preferences interface to include nutrition and mood reminder states.
- Enhanced localization for new reminder settings and alerts.
- Incremented iOS app version to 1.0.30.
2025-11-23 22:47:54 +08:00

352 lines
9.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HeaderBar } from '@/components/ui/HeaderBar';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
import {
getWorkoutNotificationPreferences,
resetWorkoutNotificationPreferences,
saveWorkoutNotificationPreferences,
WorkoutNotificationPreferences
} from '@/utils/workoutPreferences';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Alert,
ScrollView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View,
} from 'react-native';
const WORKOUT_TYPES = [
{ key: 'running', label: '跑步' },
{ key: 'cycling', label: '骑行' },
{ key: 'swimming', label: '游泳' },
{ key: 'yoga', label: '瑜伽' },
{ key: 'functionalstrengthtraining', label: '功能性力量训练' },
{ key: 'traditionalstrengthtraining', label: '传统力量训练' },
{ key: 'highintensityintervaltraining', label: '高强度间歇训练' },
{ key: 'walking', label: '步行' },
{ key: 'other', label: '其他运动' },
];
const WORKOUT_TYPE_KEYS = WORKOUT_TYPES.map(type => type.key);
export default function WorkoutNotificationSettingsScreen() {
const safeAreaTop = useSafeAreaTop()
const router = useRouter();
const [preferences, setPreferences] = useState<WorkoutNotificationPreferences>({
enabled: true,
startTimeHour: 8,
endTimeHour: 22,
enabledWorkoutTypes: [],
});
const [loading, setLoading] = useState(true);
useEffect(() => {
loadPreferences();
}, []);
const loadPreferences = async () => {
try {
const prefs = await getWorkoutNotificationPreferences();
setPreferences(prefs);
} catch (error) {
console.error('加载偏好设置失败:', error);
Alert.alert('错误', '加载设置失败,请重试');
} finally {
setLoading(false);
}
};
const savePreferences = async (newPreferences: Partial<WorkoutNotificationPreferences>) => {
try {
await saveWorkoutNotificationPreferences(newPreferences);
setPreferences(prev => ({ ...prev, ...newPreferences }));
} catch (error) {
console.error('保存偏好设置失败:', error);
Alert.alert('错误', '保存设置失败,请重试');
}
};
const handleEnabledToggle = (enabled: boolean) => {
savePreferences({ enabled });
};
const handleTimeRangeChange = (type: 'start' | 'end', hour: number) => {
if (type === 'start') {
savePreferences({ startTimeHour: hour });
} else {
savePreferences({ endTimeHour: hour });
}
};
const handleWorkoutTypeToggle = (workoutType: string) => {
const currentTypes = preferences.enabledWorkoutTypes.length === 0
? [...WORKOUT_TYPE_KEYS] // 空数组表示全部启用,先展开成完整列表,避免影响其他开关的当前状态
: [...preferences.enabledWorkoutTypes];
const nextTypes = currentTypes.includes(workoutType)
? currentTypes.filter(type => type !== workoutType)
: [...currentTypes, workoutType];
// 如果全部类型都开启,回退为空数组表示“全部启用”,以保持原有存储约定
const normalizedTypes = nextTypes.length === WORKOUT_TYPE_KEYS.length ? [] : nextTypes;
savePreferences({ enabledWorkoutTypes: normalizedTypes });
};
const handleReset = () => {
Alert.alert(
'重置设置',
'确定要重置所有锻炼通知设置为默认值吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '重置',
style: 'destructive',
onPress: async () => {
try {
await resetWorkoutNotificationPreferences();
await loadPreferences();
Alert.alert('成功', '设置已重置为默认值');
} catch (error) {
Alert.alert('错误', '重置设置失败');
}
},
},
]
);
};
const formatHour = (hour: number) => {
return `${hour.toString().padStart(2, '0')}:00`;
};
const TimeSelector = ({
label,
value,
onValueChange
}: {
label: string;
value: number;
onValueChange: (hour: number) => void;
}) => (
<View style={styles.timeSelector}>
<Text style={styles.timeLabel}>{label}</Text>
<View style={styles.timeButtons}>
<TouchableOpacity
style={[styles.timeButton, value === 0 && styles.timeButtonDisabled]}
onPress={() => onValueChange(Math.max(0, value - 1))}
disabled={value === 0}
>
<Text style={styles.timeButtonText}>-</Text>
</TouchableOpacity>
<Text style={styles.timeValue}>{formatHour(value)}</Text>
<TouchableOpacity
style={[styles.timeButton, value === 23 && styles.timeButtonDisabled]}
onPress={() => onValueChange(Math.min(23, value + 1))}
disabled={value === 23}
>
<Text style={styles.timeButtonText}>+</Text>
</TouchableOpacity>
</View>
</View>
);
if (loading) {
return (
<View style={styles.container}>
<HeaderBar title="锻炼通知设置" onBack={() => router.back()} />
<View style={{
paddingTop: safeAreaTop
}}></View>
<View style={styles.loadingContainer}>
<Text>...</Text>
</View>
</View>
);
}
return (
<View style={styles.container}>
<HeaderBar title="锻炼通知设置" onBack={() => router.back()} />
<ScrollView style={styles.content} showsVerticalScrollIndicator={false} contentContainerStyle={
{
paddingTop: safeAreaTop
}
}>
{/* 主开关 */}
<View style={styles.section}>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}></Text>
<Switch
value={preferences.enabled}
onValueChange={handleEnabledToggle}
trackColor={{ false: '#E5E5E5', true: '#4CAF50' }}
thumbColor={preferences.enabled ? '#FFFFFF' : '#FFFFFF'}
/>
</View>
<Text style={styles.settingDescription}>
</Text>
</View>
{preferences.enabled && (
<>
{/* 时间范围设置 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.sectionDescription}>
</Text>
<TimeSelector
label="开始时间"
value={preferences.startTimeHour}
onValueChange={(hour) => handleTimeRangeChange('start', hour)}
/>
<TimeSelector
label="结束时间"
value={preferences.endTimeHour}
onValueChange={(hour) => handleTimeRangeChange('end', hour)}
/>
</View>
{/* 锻炼类型设置 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.sectionDescription}>
</Text>
{WORKOUT_TYPES.map((type) => (
<View key={type.key} style={styles.settingItem}>
<Text style={styles.settingLabel}>{type.label}</Text>
<Switch
value={
preferences.enabledWorkoutTypes.length === 0 ||
preferences.enabledWorkoutTypes.includes(type.key)
}
onValueChange={() => handleWorkoutTypeToggle(type.key)}
trackColor={{ false: '#E5E5E5', true: '#4CAF50' }}
thumbColor="#FFFFFF"
/>
</View>
))}
</View>
</>
)}
{/* 重置按钮 */}
<View style={styles.section}>
<TouchableOpacity style={styles.resetButton} onPress={handleReset}>
<Text style={styles.resetButtonText}></Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8F9FA',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
content: {
flex: 1,
padding: 16,
},
section: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
marginBottom: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: '#1A1A1A',
marginBottom: 8,
},
sectionDescription: {
fontSize: 14,
color: '#666666',
marginBottom: 16,
lineHeight: 20,
},
settingItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
},
settingLabel: {
fontSize: 16,
color: '#1A1A1A',
},
settingDescription: {
fontSize: 14,
color: '#666666',
marginTop: -8,
marginBottom: 8,
lineHeight: 20,
},
timeSelector: {
marginBottom: 16,
},
timeLabel: {
fontSize: 16,
color: '#1A1A1A',
marginBottom: 8,
},
timeButtons: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
timeButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#4CAF50',
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 16,
},
timeButtonDisabled: {
backgroundColor: '#E5E5E5',
},
timeButtonText: {
fontSize: 18,
fontWeight: '600',
color: '#FFFFFF',
},
timeValue: {
fontSize: 16,
fontWeight: '600',
color: '#1A1A1A',
minWidth: 60,
textAlign: 'center',
},
resetButton: {
backgroundColor: '#F44336',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
resetButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});