diff --git a/lib/controllers/food_data.dart b/lib/controllers/food_data.dart index d3abcc5..325b018 100644 --- a/lib/controllers/food_data.dart +++ b/lib/controllers/food_data.dart @@ -25,13 +25,21 @@ class FoodDataController extends GetxController { await _prefs.setString("data_fdc_api_key", value); } + List? getDataCollections() { + return _prefs.getStringList("data_enabled_collections"); + } + + Future setDataCollections(List value) async { + await _prefs.setStringList("data_enabled_collections", value); + } + Future 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', diff --git a/lib/main.dart b/lib/main.dart index 185c498..f338d5c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), + ), ], ), ]); diff --git a/lib/models/food_data.dart b/lib/models/food_data.dart index 0243b2c..371dd91 100644 --- a/lib/models/food_data.dart +++ b/lib/models/food_data.dart @@ -98,7 +98,7 @@ class Nutrients { } class FoodSearchCriteria { - List dataType; + List dataType; String query; String generalSearchInput; int pageNumber; @@ -107,7 +107,7 @@ class FoodSearchCriteria { int numberOfResultsPerPage; int pageSize; bool requireAllWords; - List foodTypes; + List foodTypes; FoodSearchCriteria({ required this.dataType, @@ -124,7 +124,7 @@ class FoodSearchCriteria { factory FoodSearchCriteria.fromJson(Map json) => FoodSearchCriteria( - dataType: List.from( + dataType: List.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.from( + foodTypes: List.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; diff --git a/lib/screens/food/details.dart b/lib/screens/food/details.dart index beeb872..67430be 100644 --- a/lib/screens/food/details.dart +++ b/lib/screens/food/details.dart @@ -28,13 +28,19 @@ class _FoodDetailsScreenState extends State { 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 { 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, + )); } } diff --git a/lib/screens/query.dart b/lib/screens/query.dart index 03584f4..ec4fe9d 100644 --- a/lib/screens/query.dart +++ b/lib/screens/query.dart @@ -75,7 +75,7 @@ class _QueryScreenState extends State { 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(), ), diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 83a2e0f..4d1ab02 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -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 createState() => _SettingsScreenState(); -} - -class _SettingsScreenState extends State { - final TextEditingController _fdcApiKeyController = TextEditingController(); - - List _currentAlerts = List.empty(growable: true); - - void _addAlert() { - setState(() { - _currentAlerts.add(AlertConfiguration( - nutrientId: 0, - maxValue: 0, - minValue: 0, - )); - }); - } - - Future _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 { ), 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'); + }, + ), ], ), ), diff --git a/lib/screens/settings/alert.dart b/lib/screens/settings/alert.dart new file mode 100644 index 0000000..0085b38 --- /dev/null +++ b/lib/screens/settings/alert.dart @@ -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 createState() => _AlertSettingsScreenState(); +} + +class _AlertSettingsScreenState extends State { + List _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 _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), + ), + ); + } +} diff --git a/lib/screens/settings/data_source.dart b/lib/screens/settings/data_source.dart new file mode 100644 index 0000000..369b87a --- /dev/null +++ b/lib/screens/settings/data_source.dart @@ -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 createState() => + _DataSourceSettingsScreenState(); +} + +class _DataSourceSettingsScreenState extends State { + 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 _enabledDataCollections = List.empty(growable: true); + + Future _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( + 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), + ), + ); + } +} diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index b7a074c..266dbd1 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -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. 在此感谢他们慷慨贡献的食品数据并发布在公有领域。', }; diff --git a/lib/widgets/alert_detect_result.dart b/lib/widgets/alert_detect_result.dart index e33d9db..fb514d6 100644 --- a/lib/widgets/alert_detect_result.dart +++ b/lib/widgets/alert_detect_result.dart @@ -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 ?? ''}', ), ); }, diff --git a/pubspec.lock b/pubspec.lock index f1861fd..e1347f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index fc3e524..1bf3153 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: