✨ Edit alert configuration
This commit is contained in:
		
							
								
								
									
										38
									
								
								lib/controllers/alert.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/controllers/alert.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:dietary_guard/models/alert_configuration.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
|  | ||||
| class AlertController extends GetxController { | ||||
|   List<AlertConfiguration> configuration = List.empty(); | ||||
|  | ||||
|   late final SharedPreferences _prefs; | ||||
|  | ||||
|   @override | ||||
|   void onInit() async { | ||||
|     _prefs = await SharedPreferences.getInstance(); | ||||
|     loadAlertConfiguration(); | ||||
|     super.onInit(); | ||||
|   } | ||||
|  | ||||
|   void loadAlertConfiguration() { | ||||
|     final raw = _prefs.getString("alert_configuration"); | ||||
|     if (raw == null) return; | ||||
|     configuration = List<AlertConfiguration>.from(jsonDecode(raw).map( | ||||
|       (x) => AlertConfiguration.fromJson(x), | ||||
|     )); | ||||
|   } | ||||
|  | ||||
|   Future<void> setAlertConfiguration(List<AlertConfiguration> value) async { | ||||
|     await _prefs.setString( | ||||
|       "alert_configuration", | ||||
|       jsonEncode(value.map((x) => x.toJson()).toList()), | ||||
|     ); | ||||
|     loadAlertConfiguration(); | ||||
|   } | ||||
|  | ||||
|   void clearAlertConfiguration() { | ||||
|     _prefs.remove("alert_configuration"); | ||||
|   } | ||||
| } | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:dietary_guard/controllers/alert.dart'; | ||||
| import 'package:dietary_guard/controllers/food_data.dart'; | ||||
| import 'package:dietary_guard/screens/query.dart'; | ||||
| import 'package:dietary_guard/screens/settings.dart'; | ||||
| @@ -62,6 +63,7 @@ class MyApp extends StatelessWidget { | ||||
|   } | ||||
|  | ||||
|   void _initializeProviders(BuildContext context) async { | ||||
|     Get.put(FoodDataController()); | ||||
|     Get.lazyPut(() => FoodDataController()); | ||||
|     Get.lazyPut(() => AlertController()); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										24
									
								
								lib/models/alert_configuration.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/models/alert_configuration.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| class AlertConfiguration { | ||||
|   int nutrientId; | ||||
|   double maxValue; | ||||
|   double minValue; | ||||
|  | ||||
|   AlertConfiguration({ | ||||
|     required this.nutrientId, | ||||
|     required this.maxValue, | ||||
|     required this.minValue, | ||||
|   }); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
|         'nutrient_id': nutrientId, | ||||
|         'min_value': minValue, | ||||
|         'max_value': maxValue, | ||||
|       }; | ||||
|  | ||||
|   factory AlertConfiguration.fromJson(Map<String, dynamic> json) => | ||||
|       AlertConfiguration( | ||||
|         nutrientId: json['nutrient_id'], | ||||
|         minValue: json['min_value'], | ||||
|         maxValue: json['max_value'], | ||||
|       ); | ||||
| } | ||||
| @@ -98,7 +98,7 @@ class Nutrients { | ||||
| } | ||||
|  | ||||
| class FoodSearchCriteria { | ||||
|   List<Type> dataType; | ||||
|   List<Type?> dataType; | ||||
|   String query; | ||||
|   String generalSearchInput; | ||||
|   int pageNumber; | ||||
| @@ -107,7 +107,7 @@ class FoodSearchCriteria { | ||||
|   int numberOfResultsPerPage; | ||||
|   int pageSize; | ||||
|   bool requireAllWords; | ||||
|   List<Type> foodTypes; | ||||
|   List<Type?> foodTypes; | ||||
|  | ||||
|   FoodSearchCriteria({ | ||||
|     required this.dataType, | ||||
| @@ -124,8 +124,8 @@ class FoodSearchCriteria { | ||||
|  | ||||
|   factory FoodSearchCriteria.fromJson(Map<String, dynamic> json) => | ||||
|       FoodSearchCriteria( | ||||
|         dataType: | ||||
|             List<Type>.from(json["dataType"].map((x) => typeValues.map[x]!)), | ||||
|         dataType: List<Type>.from( | ||||
|             json["dataType"]?.map((x) => typeValues.map[x]) ?? List.empty()), | ||||
|         query: json["query"], | ||||
|         generalSearchInput: json["generalSearchInput"], | ||||
|         pageNumber: json["pageNumber"], | ||||
| @@ -134,8 +134,8 @@ class FoodSearchCriteria { | ||||
|         numberOfResultsPerPage: json["numberOfResultsPerPage"], | ||||
|         pageSize: json["pageSize"], | ||||
|         requireAllWords: json["requireAllWords"], | ||||
|         foodTypes: | ||||
|             List<Type>.from(json["foodTypes"].map((x) => typeValues.map[x]!)), | ||||
|         foodTypes: List<Type>.from( | ||||
|             json["foodTypes"]?.map((x) => typeValues.map[x]) ?? List.empty()), | ||||
|       ); | ||||
|  | ||||
|   Map<String, dynamic> toJson() => { | ||||
| @@ -162,10 +162,10 @@ final typeValues = | ||||
| class FoodData { | ||||
|   int fdcId; | ||||
|   String description; | ||||
|   String commonNames; | ||||
|   String additionalDescriptions; | ||||
|   Type dataType; | ||||
|   int ndbNumber; | ||||
|   String? commonNames; | ||||
|   String? additionalDescriptions; | ||||
|   Type? dataType; | ||||
|   int? ndbNumber; | ||||
|   DateTime publishedDate; | ||||
|   FoodCategory? foodCategory; | ||||
|   DateTime? mostRecentAcquisitionDate; | ||||
| @@ -205,7 +205,7 @@ class FoodData { | ||||
|         description: json["description"], | ||||
|         commonNames: json["commonNames"], | ||||
|         additionalDescriptions: json["additionalDescriptions"], | ||||
|         dataType: typeValues.map[json["dataType"]]!, | ||||
|         dataType: typeValues.map[json["dataType"]], | ||||
|         ndbNumber: json["ndbNumber"], | ||||
|         publishedDate: DateTime.parse(json["publishedDate"]), | ||||
|         foodCategory: foodCategoryValues.map[json["foodCategory"]], | ||||
| @@ -264,7 +264,7 @@ class FoodNutrient { | ||||
|   int nutrientId; | ||||
|   String nutrientName; | ||||
|   String nutrientNumber; | ||||
|   UnitName unitName; | ||||
|   UnitName? unitName; | ||||
|   DerivationCode? derivationCode; | ||||
|   String? derivationDescription; | ||||
|   int? derivationId; | ||||
| @@ -305,15 +305,15 @@ class FoodNutrient { | ||||
|         nutrientId: json["nutrientId"], | ||||
|         nutrientName: json["nutrientName"], | ||||
|         nutrientNumber: json["nutrientNumber"], | ||||
|         unitName: unitNameValues.map[json["unitName"]]!, | ||||
|         derivationCode: derivationCodeValues.map[json["derivationCode"]]!, | ||||
|         unitName: unitNameValues.map[json["unitName"]], | ||||
|         derivationCode: derivationCodeValues.map[json["derivationCode"]], | ||||
|         derivationDescription: json["derivationDescription"], | ||||
|         derivationId: json["derivationId"], | ||||
|         value: json["value"]?.toDouble(), | ||||
|         foodNutrientSourceId: json["foodNutrientSourceId"], | ||||
|         foodNutrientSourceCode: json["foodNutrientSourceCode"], | ||||
|         foodNutrientSourceDescription: foodNutrientSourceDescriptionValues | ||||
|             .map[json["foodNutrientSourceDescription"]]!, | ||||
|             .map[json["foodNutrientSourceDescription"]], | ||||
|         rank: json["rank"], | ||||
|         indentLevel: json["indentLevel"], | ||||
|         foodNutrientId: json["foodNutrientId"], | ||||
|   | ||||
| @@ -9,37 +9,40 @@ class FoodDetailsScreen extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(item.description), | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: Scaffold( | ||||
|         appBar: AppBar( | ||||
|           title: Text(item.description), | ||||
|         ), | ||||
|         body: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Text('nutrients'.tr).paddingOnly(left: 24, right: 24, bottom: 8), | ||||
|             Expanded( | ||||
|               child: ListView.builder( | ||||
|                 itemCount: item.foodNutrients.length, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   final entry = item.foodNutrients[idx]; | ||||
|                   final unitName = unitNameValues.reverse[entry.unitName]; | ||||
|                   return ListTile( | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                     title: Row( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         Text(entry.nutrientName), | ||||
|                         const SizedBox(width: 6), | ||||
|                         Badge(label: Text('#${entry.nutrientId}')) | ||||
|                       ], | ||||
|                     ), | ||||
|                     subtitle: Text('${entry.nutrientNumber} ${unitName}'), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ) | ||||
|           ], | ||||
|         ).paddingSymmetric(vertical: 24), | ||||
|       ), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text('nutrients'.tr).paddingOnly(left: 24, right: 24, bottom: 8), | ||||
|           Expanded( | ||||
|             child: ListView.builder( | ||||
|               itemCount: item.foodNutrients.length, | ||||
|               itemBuilder: (context, idx) { | ||||
|                 final entry = item.foodNutrients[idx]; | ||||
|                 final unitName = unitNameValues.reverse[entry.unitName]; | ||||
|                 return ListTile( | ||||
|                   contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                   title: Row( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       Text(entry.nutrientName), | ||||
|                       const SizedBox(width: 6), | ||||
|                       Badge(label: Text('#${entry.nutrientId}')) | ||||
|                     ], | ||||
|                   ), | ||||
|                   subtitle: Text('${entry.nutrientNumber} ${unitName}'), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ) | ||||
|         ], | ||||
|       ).paddingSymmetric(vertical: 24), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -45,50 +45,53 @@ class _QueryScreenState extends State<QueryScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SafeArea( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           SearchBar( | ||||
|             padding: const WidgetStatePropertyAll<EdgeInsets>( | ||||
|               EdgeInsets.symmetric(horizontal: 16.0), | ||||
|             ), | ||||
|             onSubmitted: (value) { | ||||
|               _searchFood(value); | ||||
|             }, | ||||
|             leading: const Icon(Icons.search), | ||||
|           ).paddingSymmetric(horizontal: 24), | ||||
|           if (_isLoading) | ||||
|             const SizedBox( | ||||
|               width: 28, | ||||
|               height: 28, | ||||
|               child: CircularProgressIndicator(strokeWidth: 3), | ||||
|             ).paddingSymmetric(vertical: 24) | ||||
|           else | ||||
|             Expanded( | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _foodData.length, | ||||
|                 itemBuilder: (context, index) { | ||||
|                   final item = _foodData[index]; | ||||
|                   return OpenContainer( | ||||
|                     closedBuilder: (_, open) => ListTile( | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       title: Text(item.description), | ||||
|                       subtitle: Text( | ||||
|                         DateFormat("yyyy-MM-dd").format(item.publishedDate), | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: SafeArea( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|             SearchBar( | ||||
|               padding: const WidgetStatePropertyAll<EdgeInsets>( | ||||
|                 EdgeInsets.symmetric(horizontal: 16.0), | ||||
|               ), | ||||
|               onSubmitted: (value) { | ||||
|                 _searchFood(value); | ||||
|               }, | ||||
|               leading: const Icon(Icons.search), | ||||
|             ).paddingSymmetric(horizontal: 24), | ||||
|             if (_isLoading) | ||||
|               const SizedBox( | ||||
|                 width: 28, | ||||
|                 height: 28, | ||||
|                 child: CircularProgressIndicator(strokeWidth: 3), | ||||
|               ).paddingSymmetric(vertical: 24) | ||||
|             else | ||||
|               Expanded( | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: _foodData.length, | ||||
|                   itemBuilder: (context, index) { | ||||
|                     final item = _foodData[index]; | ||||
|                     return OpenContainer( | ||||
|                       closedBuilder: (_, open) => ListTile( | ||||
|                         contentPadding: | ||||
|                             const EdgeInsets.symmetric(horizontal: 24), | ||||
|                         title: Text(item.description), | ||||
|                         subtitle: Text( | ||||
|                           DateFormat("yyyy-MM-dd").format(item.publishedDate), | ||||
|                         ), | ||||
|                         onTap: () => open(), | ||||
|                       ), | ||||
|                       onTap: () => open(), | ||||
|                     ), | ||||
|                     openBuilder: (_, __) => FoodDetailsScreen(item: item), | ||||
|                     openElevation: 0, | ||||
|                     closedElevation: 0, | ||||
|                     closedColor: Colors.transparent, | ||||
|                     openColor: Colors.transparent, | ||||
|                   ); | ||||
|                 }, | ||||
|               ).paddingOnly(top: 8), | ||||
|             ), | ||||
|         ], | ||||
|                       openBuilder: (_, __) => FoodDetailsScreen(item: item), | ||||
|                       openElevation: 0, | ||||
|                       closedElevation: 0, | ||||
|                       closedColor: Theme.of(context).colorScheme.surface, | ||||
|                       openColor: Colors.transparent, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ).paddingOnly(top: 8), | ||||
|               ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| import 'package:dietary_guard/controllers/alert.dart'; | ||||
| import 'package:dietary_guard/controllers/food_data.dart'; | ||||
| import 'package:dietary_guard/models/alert_configuration.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
|  | ||||
| @@ -12,10 +14,25 @@ class SettingsScreen extends StatefulWidget { | ||||
| class _SettingsScreenState extends State<SettingsScreen> { | ||||
|   final TextEditingController _fdcApiKeyController = TextEditingController(); | ||||
|  | ||||
|   List<AlertConfiguration> _currentAlerts = List.empty(growable: true); | ||||
|  | ||||
|   void _addAlert() { | ||||
|     setState(() { | ||||
|       _currentAlerts.add(AlertConfiguration( | ||||
|         nutrientId: 0, | ||||
|         maxValue: 0, | ||||
|         minValue: 0, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _applySettings() async { | ||||
|     final FoodDataController data = Get.find(); | ||||
|     await data.setApiKey(_fdcApiKeyController.text); | ||||
|  | ||||
|     final AlertController alert = Get.find(); | ||||
|     await alert.setAlertConfiguration(_currentAlerts); | ||||
|  | ||||
|     ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||||
|       content: Text('settingsApplied'.tr), | ||||
|     )); | ||||
| @@ -26,6 +43,9 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|     final FoodDataController data = Get.find(); | ||||
|     _fdcApiKeyController.text = data.getApiKey() ?? ''; | ||||
|  | ||||
|     final AlertController alert = Get.find(); | ||||
|     _currentAlerts = List.from(alert.configuration, growable: true); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
| @@ -35,36 +55,116 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   Widget _buildSectionHeader(String title) { | ||||
|     return Container( | ||||
|       color: Theme.of(context).colorScheme.secondaryContainer, | ||||
|       padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 8), | ||||
|       child: Text( | ||||
|         title, | ||||
|         style: Theme.of(context).textTheme.bodyLarge, | ||||
|       ), | ||||
|     ).marginOnly(bottom: 16); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('settings'.tr), | ||||
|       ), | ||||
|       body: ListView( | ||||
|         children: [ | ||||
|           const SizedBox(height: 8), | ||||
|           TextField( | ||||
|             controller: _fdcApiKeyController, | ||||
|             obscureText: true, | ||||
|             decoration: const InputDecoration( | ||||
|               border: OutlineInputBorder(), | ||||
|               label: Text("FDC API Key"), | ||||
|               isDense: true, | ||||
|             ), | ||||
|           ), | ||||
|           Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.end, | ||||
|             children: [ | ||||
|               ElevatedButton.icon( | ||||
|                 icon: const Icon(Icons.save, size: 16), | ||||
|                 label: Text('apply'.tr), | ||||
|                 onPressed: () => _applySettings(), | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: Scaffold( | ||||
|         appBar: AppBar( | ||||
|           title: Text('settings'.tr), | ||||
|         ), | ||||
|         body: ListView( | ||||
|           children: [ | ||||
|             _buildSectionHeader('settingsAlertSection'.tr), | ||||
|             Column( | ||||
|               children: [ | ||||
|                 ...(_currentAlerts.map((x) => Row( | ||||
|                       children: [ | ||||
|                         Expanded( | ||||
|                           child: TextFormField( | ||||
|                             initialValue: x.nutrientId.toString(), | ||||
|                             decoration: InputDecoration( | ||||
|                               border: const OutlineInputBorder(), | ||||
|                               label: Text("alertNutrientId".tr), | ||||
|                               isDense: true, | ||||
|                             ), | ||||
|                             onTapOutside: (_) => | ||||
|                                 FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                             onChanged: (value) { | ||||
|                               x.nutrientId = int.tryParse(value) ?? 0; | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                         const SizedBox(width: 6), | ||||
|                         Expanded( | ||||
|                           child: TextFormField( | ||||
|                             initialValue: x.maxValue.toString(), | ||||
|                             decoration: InputDecoration( | ||||
|                               border: const OutlineInputBorder(), | ||||
|                               label: Text("alertMaxValue".tr), | ||||
|                               isDense: true, | ||||
|                             ), | ||||
|                             onTapOutside: (_) => | ||||
|                                 FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                             onChanged: (value) { | ||||
|                               x.maxValue = double.tryParse(value) ?? 0; | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                         const SizedBox(width: 6), | ||||
|                         Expanded( | ||||
|                           child: TextFormField( | ||||
|                             initialValue: x.minValue.toString(), | ||||
|                             decoration: InputDecoration( | ||||
|                               border: const OutlineInputBorder(), | ||||
|                               label: Text("alertMinValue".tr), | ||||
|                               isDense: true, | ||||
|                             ), | ||||
|                             onTapOutside: (_) => | ||||
|                                 FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                             onChanged: (value) { | ||||
|                               x.minValue = double.tryParse(value) ?? 0; | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ))), | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     TextButton.icon( | ||||
|                       label: Text('newAlert'.tr), | ||||
|                       onPressed: () => _addAlert(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).paddingSymmetric(vertical: 12, horizontal: 24), | ||||
|               ], | ||||
|             ).paddingSymmetric(horizontal: 24), | ||||
|             _buildSectionHeader('settingsDataSection'.tr), | ||||
|             TextField( | ||||
|               controller: _fdcApiKeyController, | ||||
|               obscureText: true, | ||||
|               decoration: const InputDecoration( | ||||
|                 border: UnderlineInputBorder(), | ||||
|                 label: Text("FDC API Key"), | ||||
|                 isDense: false, | ||||
|               ), | ||||
|             ], | ||||
|           ).paddingSymmetric(vertical: 12), | ||||
|         ], | ||||
|       ).paddingSymmetric(horizontal: 24), | ||||
|               onTapOutside: (_) => | ||||
|                   FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ).paddingSymmetric(horizontal: 24), | ||||
|             Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.center, | ||||
|               children: [ | ||||
|                 TextButton.icon( | ||||
|                   label: Text('apply'.tr), | ||||
|                   onPressed: () => _applySettings(), | ||||
|                 ), | ||||
|               ], | ||||
|             ).paddingSymmetric(vertical: 12, horizontal: 24), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,13 @@ const i18nEnglish = { | ||||
|   'settings': 'Settings', | ||||
|   'preparingData': 'Preparing data...', | ||||
|   'settingsApplied': 'Settings Applied', | ||||
|   'settingsDataSection': 'Data Source', | ||||
|   'settingsAlertSection': 'Alert', | ||||
|   'newAlert': 'New Alert', | ||||
|   'apply': 'Apply', | ||||
|   'searchHistoryNotIncluded': 'Search History not Included, yet', | ||||
|   'nutrients': 'Nutrients', | ||||
|   'alertNutrientId': 'Nutrient ID', | ||||
|   'alertMaxValue': 'Max', | ||||
|   'alertMinValue': 'Min', | ||||
| }; | ||||
|   | ||||
| @@ -3,7 +3,13 @@ const i18nSimplifiedChinese = { | ||||
|   'settings': '设置', | ||||
|   'preparingData': '准备数据中…', | ||||
|   'settingsApplied': '设置已应用', | ||||
|   'settingsDataSection': '数据源', | ||||
|   'settingsAlertSection': '危险告警', | ||||
|   'newAlert': '新告警', | ||||
|   'apply': '应用', | ||||
|   'searchHistoryNotIncluded': '搜索记录还没实现', | ||||
|   'nutrients': '营养物质', | ||||
|   'alertNutrientId': '营养物质编号', | ||||
|   'alertMaxValue': '告警上限', | ||||
|   'alertMinValue': '告警下限', | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user