✨ Editable search data collections
This commit is contained in:
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user