✨ Edit alert configuration
This commit is contained in:
		| @@ -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), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user