Compare commits
5 Commits
bd50859e73
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 58578c734e | |||
| 76f2c0ad58 | |||
| cfa61e1a8a | |||
| 22b863f2bf | |||
| 4569c33430 |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.3 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 521 B |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 791 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -1,15 +1,20 @@
|
||||
import 'package:dietary_guard/models/food_data.dart';
|
||||
|
||||
class AlertConfiguration {
|
||||
String name;
|
||||
int nutrientId;
|
||||
double maxValue;
|
||||
double minValue;
|
||||
|
||||
AlertConfiguration({
|
||||
required this.name,
|
||||
required this.nutrientId,
|
||||
required this.maxValue,
|
||||
required this.minValue,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
'nutrient_id': nutrientId,
|
||||
'min_value': minValue,
|
||||
'max_value': maxValue,
|
||||
@@ -17,6 +22,7 @@ class AlertConfiguration {
|
||||
|
||||
factory AlertConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||
AlertConfiguration(
|
||||
name: json['name'],
|
||||
nutrientId: json['nutrient_id'],
|
||||
minValue: json['min_value'],
|
||||
maxValue: json['max_value'],
|
||||
@@ -25,18 +31,18 @@ class AlertConfiguration {
|
||||
|
||||
class AlertDetectResult {
|
||||
AlertConfiguration config;
|
||||
FoodNutrient? nutrient;
|
||||
String name;
|
||||
String? unitName;
|
||||
double? current;
|
||||
double? difference;
|
||||
bool isOutOfRange;
|
||||
bool isUndetected;
|
||||
|
||||
AlertDetectResult({
|
||||
required this.config,
|
||||
required this.nutrient,
|
||||
required this.name,
|
||||
required this.unitName,
|
||||
required this.current,
|
||||
required this.difference,
|
||||
required this.isOutOfRange,
|
||||
required this.isUndetected,
|
||||
|
||||
@@ -33,14 +33,14 @@ class _FoodDetailsScreenState extends State<FoodDetailsScreen> {
|
||||
double? difference;
|
||||
String name = 'undetected'.tr;
|
||||
String? unitName;
|
||||
double? current;
|
||||
FoodNutrient? current;
|
||||
for (final nutrient in widget.item.foodNutrients) {
|
||||
if (item.nutrientId != nutrient.nutrientId) continue;
|
||||
name = nutrient.nutrientName;
|
||||
unitName = unitNameValues.reverse[nutrient.unitName];
|
||||
if (nutrient.value != null) {
|
||||
current = nutrient;
|
||||
final value = nutrient.value!;
|
||||
current = value;
|
||||
if (value > item.maxValue) {
|
||||
difference = value - item.maxValue;
|
||||
isOutOfRange = true;
|
||||
@@ -54,9 +54,9 @@ class _FoodDetailsScreenState extends State<FoodDetailsScreen> {
|
||||
|
||||
_alertDetectResult.add(AlertDetectResult(
|
||||
config: item,
|
||||
nutrient: current,
|
||||
name: name,
|
||||
unitName: unitName,
|
||||
current: current,
|
||||
difference: difference,
|
||||
isOutOfRange: isOutOfRange,
|
||||
isUndetected: isUndetected,
|
||||
|
||||
@@ -15,7 +15,9 @@ class QueryScreen extends StatefulWidget {
|
||||
|
||||
class _QueryScreenState extends State<QueryScreen> {
|
||||
bool _isLoading = false;
|
||||
bool _hasApiKey = true;
|
||||
|
||||
int? _totalCount;
|
||||
List<FoodData> _foodData = List.empty();
|
||||
|
||||
Future<void> _searchFood(String probe) async {
|
||||
@@ -24,11 +26,15 @@ class _QueryScreenState extends State<QueryScreen> {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final FoodDataController data = Get.find();
|
||||
if (data.getApiKey() == null) return;
|
||||
if (data.getApiKey() == null || data.getApiKey()!.isEmpty) {
|
||||
setState(() => _hasApiKey = false);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await data.searchFood(probe);
|
||||
|
||||
setState(() {
|
||||
_totalCount = result.totalHits;
|
||||
_foodData = result.foods;
|
||||
_isLoading = false;
|
||||
});
|
||||
@@ -38,7 +44,11 @@ class _QueryScreenState extends State<QueryScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
final FoodDataController data = Get.find();
|
||||
data.initialize(context);
|
||||
data.initialize(context).then((_) {
|
||||
setState(() {
|
||||
_hasApiKey = data.getApiKey() != null && data.getApiKey()!.isNotEmpty;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -48,6 +58,14 @@ class _QueryScreenState extends State<QueryScreen> {
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
if (!_hasApiKey)
|
||||
Text('searchNoKeyHint'.tr).paddingSymmetric(vertical: 8)
|
||||
else if (_totalCount != null)
|
||||
Text('searchResultHint'
|
||||
.trParams({'count': _totalCount.toString()}))
|
||||
.paddingSymmetric(vertical: 8)
|
||||
else
|
||||
Text('searchHint'.tr).paddingSymmetric(vertical: 8),
|
||||
SearchBar(
|
||||
padding: const WidgetStatePropertyAll<EdgeInsets>(
|
||||
EdgeInsets.symmetric(horizontal: 16.0),
|
||||
|
||||
@@ -16,6 +16,7 @@ class _AlertSettingsScreenState extends State<AlertSettingsScreen> {
|
||||
void _addAlert() {
|
||||
setState(() {
|
||||
_currentAlerts.add(AlertConfiguration(
|
||||
name: 'Alert #${_currentAlerts.length}',
|
||||
nutrientId: 0,
|
||||
maxValue: 0,
|
||||
minValue: 0,
|
||||
@@ -64,16 +65,16 @@ class _AlertSettingsScreenState extends State<AlertSettingsScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: x.nutrientId.toString(),
|
||||
initialValue: x.name,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
label: Text("alertNutrientId".tr),
|
||||
label: Text("alertName".tr),
|
||||
isDense: true,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (value) {
|
||||
x.nutrientId = int.tryParse(value) ?? 0;
|
||||
x.name = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -87,6 +88,20 @@ class _AlertSettingsScreenState extends State<AlertSettingsScreen> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
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(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
const i18nEnglish = {
|
||||
'appName': 'DietaryGuard',
|
||||
'appDescription': 'Your Healthy Dietary Guard',
|
||||
'appCopyright': 'Copyright © 2024 Solsynth LLC\nOriginal Author @littlesheep',
|
||||
'query': 'Query',
|
||||
'settings': 'Settings',
|
||||
'preparingData': 'Preparing data...',
|
||||
'settingsApplied': 'Settings Applied',
|
||||
'preparingData': 'Preparing data…',
|
||||
'settingsApplied': 'Settings applied',
|
||||
'settingsDataSection': 'Data Source',
|
||||
'settingsAlertSection': 'Alert',
|
||||
'dataSourceSettings': 'Data Source Settings',
|
||||
'alertSettings': 'Alert Configuration',
|
||||
'newAlert': 'New Alert',
|
||||
'apply': 'Apply',
|
||||
'searchHistoryNotIncluded': 'Search History not Included, yet',
|
||||
'searchHistoryNotIncluded': 'Search history not implemented yet',
|
||||
'nutrients': 'Nutrients',
|
||||
'alertNutrientId': 'Nutrient ID',
|
||||
'alertMaxValue': 'Max',
|
||||
'alertMinValue': 'Min',
|
||||
'alertMaxValue': 'Alert Max Value',
|
||||
'alertMinValue': 'Alert Min Value',
|
||||
'alertName': 'Alert Name',
|
||||
'alerts': 'Alerts',
|
||||
'alertOutOfRange':
|
||||
'@count alert rules triggered, click the button on the right for details',
|
||||
'alertUnclear':
|
||||
'@count alert rules lack supporting data, click the button on the right for details',
|
||||
'alertEmpty': 'No alert rules configured, go to settings to add rules',
|
||||
'alertSafe': 'No alert rules triggered, safe to consume',
|
||||
'alertDetectResult': 'Alert Match Details',
|
||||
'undetected': 'Not Detected',
|
||||
'dataCollectionSelection': 'Search Data Collection Range',
|
||||
'dataCollectionFoundation': 'Foundation Data Collection',
|
||||
'dataCollectionFoundationDescription':
|
||||
'Includes food raw materials, with a smaller range but comprehensive data accuracy',
|
||||
'dataCollectionBranded': 'Branded Data Collection',
|
||||
'dataCollectionBrandedDescription':
|
||||
'Includes various branded processed foods',
|
||||
'dataCollectionSurvey': 'Survey Data Collection',
|
||||
'dataCollectionSurveyDescription':
|
||||
'Uses data from the "What We Eat in America" journal',
|
||||
'dataCollectionLegacy': 'Legacy Data Collection',
|
||||
'dataCollectionLegacyDescription':
|
||||
'Historical data from analysis, calculations, and public literature',
|
||||
'fdcApiKey': 'USDA Food Data Central API Key',
|
||||
'fdcApiKeyHint':
|
||||
'DietaryGuard’s data comes from the USDA public API, so you need to configure an API key. But don’t worry, it’s completely free! Check our wiki to learn how to get an API key.',
|
||||
'fdcApiCredit':
|
||||
'DietaryGuard’s food data comes from the U.S. Department of Agriculture, Agricultural Research Service, Beltsville Human Nutrition Research Center. FoodData Central. We appreciate their generous contribution of food data released into the public domain.',
|
||||
'loading': 'Loading',
|
||||
'searchHint': 'Type keywords below to search',
|
||||
'searchNoKeyHint':
|
||||
'No API key, please first set up the API key in "Settings" > "Data Source"',
|
||||
'searchResultHint': '@count records matched (showing 100)',
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ const i18nSimplifiedChinese = {
|
||||
'alertNutrientId': '营养物质编号',
|
||||
'alertMaxValue': '告警上限',
|
||||
'alertMinValue': '告警下限',
|
||||
'alertName': '告警规则名',
|
||||
'alerts': '告警',
|
||||
'alertOutOfRange': '有 @count 项告警规则触发,点击右侧按钮了解详情',
|
||||
'alertUnclear': '有 @count 项告警规则并无数据支持,点击右侧按钮了解详情',
|
||||
@@ -39,4 +40,7 @@ const i18nSimplifiedChinese = {
|
||||
'fdcApiCredit':
|
||||
'DietaryGuard 的食品数据来源于 U.S. Department of Agriculture, Agricultural Research Service, Beltsville Human Nutrition Research Center. FoodData Central. 在此感谢他们慷慨贡献的食品数据并发布在公有领域。',
|
||||
'loading': '加载中',
|
||||
'searchHint': '在下方键入关键词来搜索',
|
||||
'searchNoKeyHint': '无 API 令牌,请先在「设置」>「数据源」设置 API 密钥',
|
||||
'searchResultHint': '共命中 @count 条记录(显示前 100 条)',
|
||||
};
|
||||
|
||||
@@ -60,13 +60,13 @@ class AlertDetectResultDialog extends StatelessWidget {
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(item.name),
|
||||
Text(item.config.name),
|
||||
const SizedBox(width: 6),
|
||||
Badge(label: Text('#${item.config.nutrientId}'))
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${item.current ?? 'undetected'.tr} ${(item.difference ?? 0) > 0 ? '↑' : '↓'}${item.difference?.abs().toPrecision(2) ?? '-'} ${item.unitName ?? ''}',
|
||||
'${item.nutrient?.value ?? 'undetected'.tr} ${(item.difference ?? 0) > 0 ? '↑' : '↓'}${item.difference?.abs().toPrecision(2) ?? '-'} ${item.unitName ?? ''}',
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 590 B After Width: | Height: | Size: 823 B |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 1.0.0+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.0
|
||||
|
||||
BIN
web/favicon.png
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 10 KiB |