✨ Editable search data collections
This commit is contained in:
		| @@ -25,13 +25,21 @@ class FoodDataController extends GetxController { | ||||
|     await _prefs.setString("data_fdc_api_key", value); | ||||
|   } | ||||
|  | ||||
|   List<String>? getDataCollections() { | ||||
|     return _prefs.getStringList("data_enabled_collections"); | ||||
|   } | ||||
|  | ||||
|   Future<void> setDataCollections(List<String> value) async { | ||||
|     await _prefs.setStringList("data_enabled_collections", value); | ||||
|   } | ||||
|  | ||||
|   Future<FoodDataQueryResponse> searchFood(String probe) async { | ||||
|     final client = Dio(); | ||||
|     final resp = await client.get( | ||||
|       'https://api.nal.usda.gov/fdc/v1/foods/search', | ||||
|       queryParameters: { | ||||
|         'query': probe, | ||||
|         'dataType': 'Foundation', | ||||
|         'dataType': getDataCollections()?.join(','), | ||||
|         'pageSize': 25, | ||||
|         'pageNumber': 1, | ||||
|         'sortBy': 'dataType.keyword', | ||||
|   | ||||
| @@ -2,6 +2,8 @@ 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'; | ||||
| import 'package:dietary_guard/screens/settings/alert.dart'; | ||||
| import 'package:dietary_guard/screens/settings/data_source.dart'; | ||||
| import 'package:dietary_guard/shells/nav_shell.dart'; | ||||
| import 'package:dietary_guard/translations.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| @@ -26,6 +28,16 @@ final router = GoRouter(routes: [ | ||||
|         name: "settings", | ||||
|         builder: (context, state) => const SettingsScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: "/settings/alerts", | ||||
|         name: "settingsAlert", | ||||
|         builder: (context, state) => const AlertSettingsScreen(), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: "/settings/data-source", | ||||
|         name: "settingsDataSource", | ||||
|         builder: (context, state) => const DataSourceSettingsScreen(), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
| ]); | ||||
|   | ||||
| @@ -98,7 +98,7 @@ class Nutrients { | ||||
| } | ||||
|  | ||||
| class FoodSearchCriteria { | ||||
|   List<Type?> dataType; | ||||
|   List<FoodType?> dataType; | ||||
|   String query; | ||||
|   String generalSearchInput; | ||||
|   int pageNumber; | ||||
| @@ -107,7 +107,7 @@ class FoodSearchCriteria { | ||||
|   int numberOfResultsPerPage; | ||||
|   int pageSize; | ||||
|   bool requireAllWords; | ||||
|   List<Type?> foodTypes; | ||||
|   List<FoodType?> foodTypes; | ||||
|  | ||||
|   FoodSearchCriteria({ | ||||
|     required this.dataType, | ||||
| @@ -124,7 +124,7 @@ class FoodSearchCriteria { | ||||
|  | ||||
|   factory FoodSearchCriteria.fromJson(Map<String, dynamic> json) => | ||||
|       FoodSearchCriteria( | ||||
|         dataType: List<Type>.from( | ||||
|         dataType: List<FoodType?>.from( | ||||
|             json["dataType"]?.map((x) => typeValues.map[x]) ?? List.empty()), | ||||
|         query: json["query"], | ||||
|         generalSearchInput: json["generalSearchInput"], | ||||
| @@ -134,7 +134,7 @@ class FoodSearchCriteria { | ||||
|         numberOfResultsPerPage: json["numberOfResultsPerPage"], | ||||
|         pageSize: json["pageSize"], | ||||
|         requireAllWords: json["requireAllWords"], | ||||
|         foodTypes: List<Type>.from( | ||||
|         foodTypes: List<FoodType>.from( | ||||
|             json["foodTypes"]?.map((x) => typeValues.map[x]) ?? List.empty()), | ||||
|       ); | ||||
|  | ||||
| @@ -154,20 +154,20 @@ class FoodSearchCriteria { | ||||
|       }; | ||||
| } | ||||
|  | ||||
| enum Type { FOUNDATION, SR_LEGACY } | ||||
| enum FoodType { FOUNDATION, SR_LEGACY } | ||||
|  | ||||
| final typeValues = | ||||
|     EnumValues({"Foundation": Type.FOUNDATION, "SR Legacy": Type.SR_LEGACY}); | ||||
| final typeValues = EnumValues( | ||||
|     {"Foundation": FoodType.FOUNDATION, "SR Legacy": FoodType.SR_LEGACY}); | ||||
|  | ||||
| class FoodData { | ||||
|   int fdcId; | ||||
|   String description; | ||||
|   String? commonNames; | ||||
|   String? additionalDescriptions; | ||||
|   Type? dataType; | ||||
|   FoodType? dataType; | ||||
|   int? ndbNumber; | ||||
|   DateTime publishedDate; | ||||
|   FoodCategory? foodCategory; | ||||
|   String? foodCategory; | ||||
|   DateTime? mostRecentAcquisitionDate; | ||||
|   String allHighlightFields; | ||||
|   double score; | ||||
| @@ -208,7 +208,7 @@ class FoodData { | ||||
|         dataType: typeValues.map[json["dataType"]], | ||||
|         ndbNumber: json["ndbNumber"], | ||||
|         publishedDate: DateTime.parse(json["publishedDate"]), | ||||
|         foodCategory: foodCategoryValues.map[json["foodCategory"]], | ||||
|         foodCategory: json["foodCategory"], | ||||
|         mostRecentAcquisitionDate: json["mostRecentAcquisitionDate"] == null | ||||
|             ? null | ||||
|             : DateTime.parse(json["mostRecentAcquisitionDate"]), | ||||
| @@ -237,7 +237,7 @@ class FoodData { | ||||
|         "ndbNumber": ndbNumber, | ||||
|         "publishedDate": | ||||
|             "${publishedDate.year.toString().padLeft(4, '0')}-${publishedDate.month.toString().padLeft(2, '0')}-${publishedDate.day.toString().padLeft(2, '0')}", | ||||
|         "foodCategory": foodCategoryValues.reverse[foodCategory], | ||||
|         "foodCategory": foodCategory, | ||||
|         "mostRecentAcquisitionDate": | ||||
|             "${mostRecentAcquisitionDate!.year.toString().padLeft(4, '0')}-${mostRecentAcquisitionDate!.month.toString().padLeft(2, '0')}-${mostRecentAcquisitionDate!.day.toString().padLeft(2, '0')}", | ||||
|         "allHighlightFields": allHighlightFields, | ||||
| @@ -255,11 +255,6 @@ class FoodData { | ||||
|       }; | ||||
| } | ||||
|  | ||||
| enum FoodCategory { DAIRY_AND_EGG_PRODUCTS } | ||||
|  | ||||
| final foodCategoryValues = | ||||
|     EnumValues({"Dairy and Egg Products": FoodCategory.DAIRY_AND_EGG_PRODUCTS}); | ||||
|  | ||||
| class FoodNutrient { | ||||
|   int nutrientId; | ||||
|   String nutrientName; | ||||
|   | ||||
| @@ -28,13 +28,19 @@ class _FoodDetailsScreenState extends State<FoodDetailsScreen> { | ||||
|     if (alert.configuration.isEmpty) return; | ||||
|  | ||||
|     for (final item in alert.configuration) { | ||||
|       bool isUndetected = true; | ||||
|       bool isOutOfRange = false; | ||||
|       double? difference; | ||||
|       String name = 'undetected'.tr; | ||||
|       String? unitName; | ||||
|       double? current; | ||||
|       for (final nutrient in widget.item.foodNutrients) { | ||||
|         if (item.nutrientId != nutrient.nutrientId) continue; | ||||
|         bool isOutOfRange = false; | ||||
|         double? difference; | ||||
|         bool isUndetected = false; | ||||
|         name = nutrient.nutrientName; | ||||
|         unitName = unitNameValues.reverse[nutrient.unitName]; | ||||
|         if (nutrient.value != null) { | ||||
|           final value = nutrient.value ?? 0; | ||||
|           final value = nutrient.value!; | ||||
|           current = value; | ||||
|           if (value > item.maxValue) { | ||||
|             difference = value - item.maxValue; | ||||
|             isOutOfRange = true; | ||||
| @@ -42,20 +48,19 @@ class _FoodDetailsScreenState extends State<FoodDetailsScreen> { | ||||
|             difference = value - item.minValue; | ||||
|             isOutOfRange = true; | ||||
|           } | ||||
|         } else { | ||||
|           isUndetected = true; | ||||
|           isUndetected = false; | ||||
|         } | ||||
|  | ||||
|         _alertDetectResult.add(AlertDetectResult( | ||||
|           config: item, | ||||
|           name: nutrient.nutrientName, | ||||
|           unitName: unitNameValues.reverse[nutrient.unitName], | ||||
|           current: nutrient.value, | ||||
|           difference: difference, | ||||
|           isOutOfRange: isOutOfRange, | ||||
|           isUndetected: isUndetected, | ||||
|         )); | ||||
|       } | ||||
|  | ||||
|       _alertDetectResult.add(AlertDetectResult( | ||||
|         config: item, | ||||
|         name: name, | ||||
|         unitName: unitName, | ||||
|         current: current, | ||||
|         difference: difference, | ||||
|         isOutOfRange: isOutOfRange, | ||||
|         isUndetected: isUndetected, | ||||
|       )); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -75,7 +75,7 @@ class _QueryScreenState extends State<QueryScreen> { | ||||
|                             const EdgeInsets.symmetric(horizontal: 24), | ||||
|                         title: Text(item.description), | ||||
|                         subtitle: Text( | ||||
|                           '${DateFormat("yyyy-MM-dd").format(item.mostRecentAcquisitionDate ?? item.publishedDate)} ${foodCategoryValues.reverse[item.foodCategory] ?? ''}', | ||||
|                           '${DateFormat("yyyy-MM-dd").format(item.mostRecentAcquisitionDate ?? item.publishedDate)} ${item.foodCategory ?? ''}', | ||||
|                         ), | ||||
|                         onTap: () => open(), | ||||
|                       ), | ||||
|   | ||||
| @@ -1,71 +1,10 @@ | ||||
| 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'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
|  | ||||
| class SettingsScreen extends StatefulWidget { | ||||
| class SettingsScreen extends StatelessWidget { | ||||
|   const SettingsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<SettingsScreen> createState() => _SettingsScreenState(); | ||||
| } | ||||
|  | ||||
| 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), | ||||
|     )); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     final FoodDataController data = Get.find(); | ||||
|     _fdcApiKeyController.text = data.getApiKey() ?? ''; | ||||
|  | ||||
|     final AlertController alert = Get.find(); | ||||
|     _currentAlerts = List.from(alert.configuration, growable: true); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _fdcApiKeyController.dispose(); | ||||
|     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 Material( | ||||
| @@ -76,92 +15,22 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|         ), | ||||
|         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, | ||||
|               ), | ||||
|               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), | ||||
|             ListTile( | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|               trailing: const Icon(Icons.chevron_right), | ||||
|               title: Text('settingsAlertSection'.tr), | ||||
|               onTap: () { | ||||
|                 GoRouter.of(context).pushNamed('settingsAlert'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|               trailing: const Icon(Icons.chevron_right), | ||||
|               title: Text('settingsDataSection'.tr), | ||||
|               onTap: () { | ||||
|                 GoRouter.of(context).pushNamed('settingsDataSource'); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
							
								
								
									
										152
									
								
								lib/screens/settings/alert.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								lib/screens/settings/alert.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| import 'package:dietary_guard/controllers/alert.dart'; | ||||
| import 'package:dietary_guard/models/alert_configuration.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
|  | ||||
| class AlertSettingsScreen extends StatefulWidget { | ||||
|   const AlertSettingsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<AlertSettingsScreen> createState() => _AlertSettingsScreenState(); | ||||
| } | ||||
|  | ||||
| class _AlertSettingsScreenState extends State<AlertSettingsScreen> { | ||||
|   List<AlertConfiguration> _currentAlerts = List.empty(growable: true); | ||||
|  | ||||
|   void _addAlert() { | ||||
|     setState(() { | ||||
|       _currentAlerts.add(AlertConfiguration( | ||||
|         nutrientId: 0, | ||||
|         maxValue: 0, | ||||
|         minValue: 0, | ||||
|       )); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void _removeAlert(AlertConfiguration item) { | ||||
|     setState(() { | ||||
|       _currentAlerts.remove(item); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _applySettings() async { | ||||
|     final AlertController alert = Get.find(); | ||||
|     await alert.setAlertConfiguration(_currentAlerts); | ||||
|  | ||||
|     ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||||
|       content: Text('settingsApplied'.tr), | ||||
|     )); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     final AlertController alert = Get.find(); | ||||
|     _currentAlerts = List.from(alert.configuration, growable: true); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: Scaffold( | ||||
|         appBar: AppBar( | ||||
|           title: Text('alertSettings'.tr), | ||||
|         ), | ||||
|         body: ListView( | ||||
|           children: [ | ||||
|             ..._currentAlerts.map( | ||||
|               (x) => Card( | ||||
|                 child: Column( | ||||
|                   children: [ | ||||
|                     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), | ||||
|                         IconButton( | ||||
|                           icon: const Icon(Icons.close), | ||||
|                           onPressed: () { | ||||
|                             _removeAlert(x); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                     const SizedBox(height: 12), | ||||
|                     Row( | ||||
|                       children: [ | ||||
|                         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; | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).paddingAll(16), | ||||
|               ), | ||||
|             ), | ||||
|             Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               children: [ | ||||
|                 TextButton.icon( | ||||
|                   icon: const Icon(Icons.save, size: 16), | ||||
|                   label: Text('apply'.tr), | ||||
|                   onPressed: () => _applySettings(), | ||||
|                 ), | ||||
|                 ElevatedButton.icon( | ||||
|                   style: const ButtonStyle( | ||||
|                     foregroundColor: WidgetStatePropertyAll(Colors.teal), | ||||
|                   ), | ||||
|                   icon: const Icon(Icons.add, size: 16), | ||||
|                   label: Text('newAlert'.tr), | ||||
|                   onPressed: () => _addAlert(), | ||||
|                 ), | ||||
|               ], | ||||
|             ).paddingSymmetric(vertical: 4), | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: 12), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										188
									
								
								lib/screens/settings/data_source.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								lib/screens/settings/data_source.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| import 'package:dietary_guard/controllers/food_data.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:get/get.dart'; | ||||
|  | ||||
| class DataSourceSettingsScreen extends StatefulWidget { | ||||
|   const DataSourceSettingsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<DataSourceSettingsScreen> createState() => | ||||
|       _DataSourceSettingsScreenState(); | ||||
| } | ||||
|  | ||||
| class _DataSourceSettingsScreenState extends State<DataSourceSettingsScreen> { | ||||
|   final TextEditingController _fdcApiKeyController = TextEditingController(); | ||||
|  | ||||
|   final List<(String, String, String)> _dataCollectionList = [ | ||||
|     ( | ||||
|       "Foundation", | ||||
|       "dataCollectionFoundation".tr, | ||||
|       "dataCollectionFoundationDescription".tr, | ||||
|     ), | ||||
|     ( | ||||
|       "Branded", | ||||
|       "dataCollectionBranded".tr, | ||||
|       "dataCollectionBrandedDescription".tr, | ||||
|     ), | ||||
|     ( | ||||
|       "Survey (FNDDS)", | ||||
|       "dataCollectionSurvey".tr, | ||||
|       "dataCollectionSurveyDescription".tr, | ||||
|     ), | ||||
|     ( | ||||
|       "SR Legacy", | ||||
|       "dataCollectionLegacy".tr, | ||||
|       "dataCollectionLegacyDescription".tr, | ||||
|     ), | ||||
|   ]; | ||||
|  | ||||
|   List<String> _enabledDataCollections = List.empty(growable: true); | ||||
|  | ||||
|   Future<void> _applySettings() async { | ||||
|     final FoodDataController data = Get.find(); | ||||
|     await data.setApiKey(_fdcApiKeyController.text); | ||||
|     await data.setDataCollections(_enabledDataCollections); | ||||
|  | ||||
|     ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||||
|       content: Text('settingsApplied'.tr), | ||||
|     )); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     final FoodDataController data = Get.find(); | ||||
|     _fdcApiKeyController.text = data.getApiKey() ?? ''; | ||||
|     _enabledDataCollections = List.from( | ||||
|       data.getDataCollections() ?? List.empty(), | ||||
|       growable: true, | ||||
|     ); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   Color get _unFocusColor => | ||||
|       Theme.of(context).colorScheme.onSurface.withOpacity(0.75); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: Scaffold( | ||||
|         appBar: AppBar( | ||||
|           title: Text('dataSourceSettings'.tr), | ||||
|         ), | ||||
|         body: ListView( | ||||
|           children: [ | ||||
|             const SizedBox(height: 8), | ||||
|             DropdownButtonFormField2<String>( | ||||
|               isExpanded: true, | ||||
|               decoration: const InputDecoration( | ||||
|                 border: OutlineInputBorder( | ||||
|                   borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                 ), | ||||
|               ), | ||||
|               hint: Text( | ||||
|                 'dataCollectionSelection'.tr, | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 14, | ||||
|                   color: Theme.of(context).hintColor, | ||||
|                 ), | ||||
|               ), | ||||
|               items: _dataCollectionList.map((item) { | ||||
|                 return DropdownMenuItem( | ||||
|                   value: item.$1, | ||||
|                   enabled: false, | ||||
|                   child: StatefulBuilder( | ||||
|                     builder: (context, menuSetState) { | ||||
|                       final isSelected = | ||||
|                           _enabledDataCollections.contains(item.$1); | ||||
|                       return InkWell( | ||||
|                         onTap: () { | ||||
|                           isSelected | ||||
|                               ? _enabledDataCollections.remove(item.$1) | ||||
|                               : _enabledDataCollections.add(item.$1); | ||||
|                           setState(() {}); | ||||
|                           menuSetState(() {}); | ||||
|                         }, | ||||
|                         child: ListTile( | ||||
|                           contentPadding: | ||||
|                               const EdgeInsets.symmetric(horizontal: 16.0), | ||||
|                           leading: isSelected | ||||
|                               ? const Icon(Icons.check_box_outlined) | ||||
|                               : const Icon(Icons.check_box_outline_blank), | ||||
|                           title: Text(item.$2), | ||||
|                           subtitle: Text(item.$3), | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|               }).toList(), | ||||
|               value: _enabledDataCollections.isEmpty | ||||
|                   ? null | ||||
|                   : _enabledDataCollections.last, | ||||
|               onChanged: (value) {}, | ||||
|               selectedItemBuilder: (context) { | ||||
|                 return _dataCollectionList.map( | ||||
|                   (item) { | ||||
|                     return Text( | ||||
|                       _enabledDataCollections.join(', '), | ||||
|                       style: const TextStyle( | ||||
|                         fontSize: 14, | ||||
|                         overflow: TextOverflow.ellipsis, | ||||
|                       ), | ||||
|                       maxLines: 1, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ).toList(); | ||||
|               }, | ||||
|               buttonStyleData: const ButtonStyleData(height: 20), | ||||
|               menuItemStyleData: const MenuItemStyleData( | ||||
|                 height: 80, | ||||
|                 padding: EdgeInsets.zero, | ||||
|               ), | ||||
|             ), | ||||
|             const SizedBox(height: 16), | ||||
|             TextField( | ||||
|               controller: _fdcApiKeyController, | ||||
|               obscureText: true, | ||||
|               decoration: InputDecoration( | ||||
|                 border: const OutlineInputBorder(), | ||||
|                 label: Text("fdcApiKey".tr), | ||||
|                 isDense: true, | ||||
|               ), | ||||
|               onTapOutside: (_) => | ||||
|                   FocusManager.instance.primaryFocus?.unfocus(), | ||||
|             ), | ||||
|             Text( | ||||
|               'fdcApiKeyHint'.tr, | ||||
|               style: TextStyle(color: _unFocusColor), | ||||
|             ).paddingOnly( | ||||
|               left: 8, | ||||
|               right: 8, | ||||
|               top: 8, | ||||
|             ), | ||||
|             const Divider(height: 1, thickness: 0.3).paddingSymmetric( | ||||
|               vertical: 8, | ||||
|             ), | ||||
|             Text( | ||||
|               'fdcApiCredit'.tr, | ||||
|               style: TextStyle(color: _unFocusColor), | ||||
|             ).paddingSymmetric(horizontal: 8), | ||||
|             Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               children: [ | ||||
|                 ElevatedButton.icon( | ||||
|                   icon: const Icon(Icons.save, size: 16), | ||||
|                   label: Text('apply'.tr), | ||||
|                   onPressed: () => _applySettings(), | ||||
|                 ), | ||||
|               ], | ||||
|             ).paddingSymmetric(vertical: 12), | ||||
|           ], | ||||
|         ).paddingSymmetric(horizontal: 24), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -5,6 +5,8 @@ const i18nSimplifiedChinese = { | ||||
|   'settingsApplied': '设置已应用', | ||||
|   'settingsDataSection': '数据源', | ||||
|   'settingsAlertSection': '危险告警', | ||||
|   'dataSourceSettings': '数据源设置', | ||||
|   'alertSettings': '危险告警配置', | ||||
|   'newAlert': '新告警', | ||||
|   'apply': '应用', | ||||
|   'searchHistoryNotIncluded': '搜索记录还没实现', | ||||
| @@ -18,4 +20,19 @@ const i18nSimplifiedChinese = { | ||||
|   'alertEmpty': '无告警规则配置,前往设置添加规则', | ||||
|   'alertSafe': '无告警规则触发,可安心食用', | ||||
|   'alertDetectResult': '告警匹配详情', | ||||
|   'undetected': '未检出', | ||||
|   'dataCollectionSelection': '搜索数据集范围', | ||||
|   'dataCollectionFoundation': '基础数据集', | ||||
|   'dataCollectionFoundationDescription': '包含食品原材料等,范围较小,但数据准确检测方面全', | ||||
|   'dataCollectionBranded': '品牌数据集', | ||||
|   'dataCollectionBrandedDescription': '包含各种品牌精加工食品', | ||||
|   'dataCollectionSurvey': '调查数据集', | ||||
|   'dataCollectionSurveyDescription': '使用在 We Eat in America 期刊上的数据', | ||||
|   'dataCollectionLegacy': '归档数据集', | ||||
|   'dataCollectionLegacyDescription': '来自分析、计算和公开文献的历史数据', | ||||
|   'fdcApiKey': 'USDA 食品数据中心 API 令牌', | ||||
|   'fdcApiKeyHint': | ||||
|       'DietaryGuard 的数据来自于美国农业部公开 API,因此你需要配置一个 API 令牌,但是别担心,这是完全免费的,查看我们的维基了解如何获取一个 API 令牌。', | ||||
|   'fdcApiCredit': | ||||
|       'DietaryGuard 的食品数据来源于 U.S. Department of Agriculture, Agricultural Research Service, Beltsville Human Nutrition Research Center. FoodData Central. 在此感谢他们慷慨贡献的食品数据并发布在公有领域。', | ||||
| }; | ||||
|   | ||||
| @@ -66,7 +66,7 @@ class AlertDetectResultDialog extends StatelessWidget { | ||||
|                     ], | ||||
|                   ), | ||||
|                   subtitle: Text( | ||||
|                     '${item.current} ${(item.difference ?? 0) > 0 ? '↑' : '↓'}${item.difference?.abs()} ${item.unitName}', | ||||
|                     '${item.current ?? 'undetected'.tr} ${(item.difference ?? 0) > 0 ? '↑' : '↓'}${item.difference?.abs().toPrecision(2) ?? '-'} ${item.unitName ?? ''}', | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|   | ||||
| @@ -73,6 +73,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|   dropdown_button2: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dropdown_button2 | ||||
|       sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.9" | ||||
|   fake_async: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -42,6 +42,7 @@ dependencies: | ||||
|   dio: ^5.6.0 | ||||
|   intl: ^0.19.0 | ||||
|   animations: ^2.0.11 | ||||
|   dropdown_button2: ^2.3.9 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user