✨ Editable search data collections
This commit is contained in:
parent
09e904f52f
commit
403639088f
@ -25,13 +25,21 @@ class FoodDataController extends GetxController {
|
|||||||
await _prefs.setString("data_fdc_api_key", value);
|
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 {
|
Future<FoodDataQueryResponse> searchFood(String probe) async {
|
||||||
final client = Dio();
|
final client = Dio();
|
||||||
final resp = await client.get(
|
final resp = await client.get(
|
||||||
'https://api.nal.usda.gov/fdc/v1/foods/search',
|
'https://api.nal.usda.gov/fdc/v1/foods/search',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'query': probe,
|
'query': probe,
|
||||||
'dataType': 'Foundation',
|
'dataType': getDataCollections()?.join(','),
|
||||||
'pageSize': 25,
|
'pageSize': 25,
|
||||||
'pageNumber': 1,
|
'pageNumber': 1,
|
||||||
'sortBy': 'dataType.keyword',
|
'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/controllers/food_data.dart';
|
||||||
import 'package:dietary_guard/screens/query.dart';
|
import 'package:dietary_guard/screens/query.dart';
|
||||||
import 'package:dietary_guard/screens/settings.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/shells/nav_shell.dart';
|
||||||
import 'package:dietary_guard/translations.dart';
|
import 'package:dietary_guard/translations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -26,6 +28,16 @@ final router = GoRouter(routes: [
|
|||||||
name: "settings",
|
name: "settings",
|
||||||
builder: (context, state) => const SettingsScreen(),
|
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 {
|
class FoodSearchCriteria {
|
||||||
List<Type?> dataType;
|
List<FoodType?> dataType;
|
||||||
String query;
|
String query;
|
||||||
String generalSearchInput;
|
String generalSearchInput;
|
||||||
int pageNumber;
|
int pageNumber;
|
||||||
@ -107,7 +107,7 @@ class FoodSearchCriteria {
|
|||||||
int numberOfResultsPerPage;
|
int numberOfResultsPerPage;
|
||||||
int pageSize;
|
int pageSize;
|
||||||
bool requireAllWords;
|
bool requireAllWords;
|
||||||
List<Type?> foodTypes;
|
List<FoodType?> foodTypes;
|
||||||
|
|
||||||
FoodSearchCriteria({
|
FoodSearchCriteria({
|
||||||
required this.dataType,
|
required this.dataType,
|
||||||
@ -124,7 +124,7 @@ class FoodSearchCriteria {
|
|||||||
|
|
||||||
factory FoodSearchCriteria.fromJson(Map<String, dynamic> json) =>
|
factory FoodSearchCriteria.fromJson(Map<String, dynamic> json) =>
|
||||||
FoodSearchCriteria(
|
FoodSearchCriteria(
|
||||||
dataType: List<Type>.from(
|
dataType: List<FoodType?>.from(
|
||||||
json["dataType"]?.map((x) => typeValues.map[x]) ?? List.empty()),
|
json["dataType"]?.map((x) => typeValues.map[x]) ?? List.empty()),
|
||||||
query: json["query"],
|
query: json["query"],
|
||||||
generalSearchInput: json["generalSearchInput"],
|
generalSearchInput: json["generalSearchInput"],
|
||||||
@ -134,7 +134,7 @@ class FoodSearchCriteria {
|
|||||||
numberOfResultsPerPage: json["numberOfResultsPerPage"],
|
numberOfResultsPerPage: json["numberOfResultsPerPage"],
|
||||||
pageSize: json["pageSize"],
|
pageSize: json["pageSize"],
|
||||||
requireAllWords: json["requireAllWords"],
|
requireAllWords: json["requireAllWords"],
|
||||||
foodTypes: List<Type>.from(
|
foodTypes: List<FoodType>.from(
|
||||||
json["foodTypes"]?.map((x) => typeValues.map[x]) ?? List.empty()),
|
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 =
|
final typeValues = EnumValues(
|
||||||
EnumValues({"Foundation": Type.FOUNDATION, "SR Legacy": Type.SR_LEGACY});
|
{"Foundation": FoodType.FOUNDATION, "SR Legacy": FoodType.SR_LEGACY});
|
||||||
|
|
||||||
class FoodData {
|
class FoodData {
|
||||||
int fdcId;
|
int fdcId;
|
||||||
String description;
|
String description;
|
||||||
String? commonNames;
|
String? commonNames;
|
||||||
String? additionalDescriptions;
|
String? additionalDescriptions;
|
||||||
Type? dataType;
|
FoodType? dataType;
|
||||||
int? ndbNumber;
|
int? ndbNumber;
|
||||||
DateTime publishedDate;
|
DateTime publishedDate;
|
||||||
FoodCategory? foodCategory;
|
String? foodCategory;
|
||||||
DateTime? mostRecentAcquisitionDate;
|
DateTime? mostRecentAcquisitionDate;
|
||||||
String allHighlightFields;
|
String allHighlightFields;
|
||||||
double score;
|
double score;
|
||||||
@ -208,7 +208,7 @@ class FoodData {
|
|||||||
dataType: typeValues.map[json["dataType"]],
|
dataType: typeValues.map[json["dataType"]],
|
||||||
ndbNumber: json["ndbNumber"],
|
ndbNumber: json["ndbNumber"],
|
||||||
publishedDate: DateTime.parse(json["publishedDate"]),
|
publishedDate: DateTime.parse(json["publishedDate"]),
|
||||||
foodCategory: foodCategoryValues.map[json["foodCategory"]],
|
foodCategory: json["foodCategory"],
|
||||||
mostRecentAcquisitionDate: json["mostRecentAcquisitionDate"] == null
|
mostRecentAcquisitionDate: json["mostRecentAcquisitionDate"] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json["mostRecentAcquisitionDate"]),
|
: DateTime.parse(json["mostRecentAcquisitionDate"]),
|
||||||
@ -237,7 +237,7 @@ class FoodData {
|
|||||||
"ndbNumber": ndbNumber,
|
"ndbNumber": ndbNumber,
|
||||||
"publishedDate":
|
"publishedDate":
|
||||||
"${publishedDate.year.toString().padLeft(4, '0')}-${publishedDate.month.toString().padLeft(2, '0')}-${publishedDate.day.toString().padLeft(2, '0')}",
|
"${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":
|
||||||
"${mostRecentAcquisitionDate!.year.toString().padLeft(4, '0')}-${mostRecentAcquisitionDate!.month.toString().padLeft(2, '0')}-${mostRecentAcquisitionDate!.day.toString().padLeft(2, '0')}",
|
"${mostRecentAcquisitionDate!.year.toString().padLeft(4, '0')}-${mostRecentAcquisitionDate!.month.toString().padLeft(2, '0')}-${mostRecentAcquisitionDate!.day.toString().padLeft(2, '0')}",
|
||||||
"allHighlightFields": allHighlightFields,
|
"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 {
|
class FoodNutrient {
|
||||||
int nutrientId;
|
int nutrientId;
|
||||||
String nutrientName;
|
String nutrientName;
|
||||||
|
@ -28,13 +28,19 @@ class _FoodDetailsScreenState extends State<FoodDetailsScreen> {
|
|||||||
if (alert.configuration.isEmpty) return;
|
if (alert.configuration.isEmpty) return;
|
||||||
|
|
||||||
for (final item in alert.configuration) {
|
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) {
|
for (final nutrient in widget.item.foodNutrients) {
|
||||||
if (item.nutrientId != nutrient.nutrientId) continue;
|
if (item.nutrientId != nutrient.nutrientId) continue;
|
||||||
bool isOutOfRange = false;
|
name = nutrient.nutrientName;
|
||||||
double? difference;
|
unitName = unitNameValues.reverse[nutrient.unitName];
|
||||||
bool isUndetected = false;
|
|
||||||
if (nutrient.value != null) {
|
if (nutrient.value != null) {
|
||||||
final value = nutrient.value ?? 0;
|
final value = nutrient.value!;
|
||||||
|
current = value;
|
||||||
if (value > item.maxValue) {
|
if (value > item.maxValue) {
|
||||||
difference = value - item.maxValue;
|
difference = value - item.maxValue;
|
||||||
isOutOfRange = true;
|
isOutOfRange = true;
|
||||||
@ -42,20 +48,19 @@ class _FoodDetailsScreenState extends State<FoodDetailsScreen> {
|
|||||||
difference = value - item.minValue;
|
difference = value - item.minValue;
|
||||||
isOutOfRange = true;
|
isOutOfRange = true;
|
||||||
}
|
}
|
||||||
} else {
|
isUndetected = false;
|
||||||
isUndetected = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_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),
|
const EdgeInsets.symmetric(horizontal: 24),
|
||||||
title: Text(item.description),
|
title: Text(item.description),
|
||||||
subtitle: Text(
|
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(),
|
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:flutter/material.dart';
|
||||||
import 'package:get/get.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});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
@ -76,92 +15,22 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildSectionHeader('settingsAlertSection'.tr),
|
ListTile(
|
||||||
Column(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
children: [
|
trailing: const Icon(Icons.chevron_right),
|
||||||
...(_currentAlerts.map((x) => Row(
|
title: Text('settingsAlertSection'.tr),
|
||||||
children: [
|
onTap: () {
|
||||||
Expanded(
|
GoRouter.of(context).pushNamed('settingsAlert');
|
||||||
child: TextFormField(
|
},
|
||||||
initialValue: x.nutrientId.toString(),
|
),
|
||||||
decoration: InputDecoration(
|
ListTile(
|
||||||
border: const OutlineInputBorder(),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
label: Text("alertNutrientId".tr),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
isDense: true,
|
title: Text('settingsDataSection'.tr),
|
||||||
),
|
onTap: () {
|
||||||
onTapOutside: (_) =>
|
GoRouter.of(context).pushNamed('settingsDataSource');
|
||||||
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),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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': '设置已应用',
|
'settingsApplied': '设置已应用',
|
||||||
'settingsDataSection': '数据源',
|
'settingsDataSection': '数据源',
|
||||||
'settingsAlertSection': '危险告警',
|
'settingsAlertSection': '危险告警',
|
||||||
|
'dataSourceSettings': '数据源设置',
|
||||||
|
'alertSettings': '危险告警配置',
|
||||||
'newAlert': '新告警',
|
'newAlert': '新告警',
|
||||||
'apply': '应用',
|
'apply': '应用',
|
||||||
'searchHistoryNotIncluded': '搜索记录还没实现',
|
'searchHistoryNotIncluded': '搜索记录还没实现',
|
||||||
@ -18,4 +20,19 @@ const i18nSimplifiedChinese = {
|
|||||||
'alertEmpty': '无告警规则配置,前往设置添加规则',
|
'alertEmpty': '无告警规则配置,前往设置添加规则',
|
||||||
'alertSafe': '无告警规则触发,可安心食用',
|
'alertSafe': '无告警规则触发,可安心食用',
|
||||||
'alertDetectResult': '告警匹配详情',
|
'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(
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -42,6 +42,7 @@ dependencies:
|
|||||||
dio: ^5.6.0
|
dio: ^5.6.0
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
animations: ^2.0.11
|
animations: ^2.0.11
|
||||||
|
dropdown_button2: ^2.3.9
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user