Setup data

This commit is contained in:
2024-08-15 01:26:42 +08:00
parent cb011ddcf9
commit 7797c1b635
14 changed files with 711 additions and 296 deletions

View File

@@ -0,0 +1,46 @@
import 'package:dietary_guard/models/food_data.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class FoodDataController extends GetxController {
RxBool isReady = false.obs;
late final SharedPreferences _prefs;
Future<void> initialize(BuildContext context) async {
if (isReady.value) return;
_prefs = await SharedPreferences.getInstance();
isReady.value = true;
}
String? getApiKey() {
return _prefs.getString("data_fdc_api_key");
}
Future<void> setApiKey(String value) async {
await _prefs.setString("data_fdc_api_key", value);
}
Future<FoodDataQueryResponse> 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',
'pageSize': 25,
'pageNumber': 1,
'sortBy': 'dataType.keyword',
'sortOrder': 'asc',
'api_key': getApiKey(),
},
);
final result = FoodDataQueryResponse.fromJson(resp.data);
return result;
}
}

View File

@@ -1,6 +1,8 @@
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/shells/nav_shell.dart';
import 'package:dietary_guard/translations.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
@@ -42,9 +44,24 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
translations: AppTranslations(),
onInit: () => _initializeProviders(context),
);
}
void _initializeProviders(BuildContext context) async {
Get.put(FoodDataController());
}
}

402
lib/models/food_data.dart Normal file
View File

@@ -0,0 +1,402 @@
class FoodDataQueryResponse {
int totalHits;
int currentPage;
int totalPages;
List<int> pageList;
FoodSearchCriteria foodSearchCriteria;
List<FoodData> foods;
Aggregations aggregations;
FoodDataQueryResponse({
required this.totalHits,
required this.currentPage,
required this.totalPages,
required this.pageList,
required this.foodSearchCriteria,
required this.foods,
required this.aggregations,
});
factory FoodDataQueryResponse.fromJson(Map<String, dynamic> json) =>
FoodDataQueryResponse(
totalHits: json["totalHits"],
currentPage: json["currentPage"],
totalPages: json["totalPages"],
pageList: List<int>.from(json["pageList"].map((x) => x)),
foodSearchCriteria:
FoodSearchCriteria.fromJson(json["foodSearchCriteria"]),
foods:
List<FoodData>.from(json["foods"].map((x) => FoodData.fromJson(x))),
aggregations: Aggregations.fromJson(json["aggregations"]),
);
Map<String, dynamic> toJson() => {
"totalHits": totalHits,
"currentPage": currentPage,
"totalPages": totalPages,
"pageList": List<dynamic>.from(pageList.map((x) => x)),
"foodSearchCriteria": foodSearchCriteria.toJson(),
"foods": List<dynamic>.from(foods.map((x) => x.toJson())),
"aggregations": aggregations.toJson(),
};
}
class Aggregations {
DataType dataType;
Nutrients nutrients;
Aggregations({
required this.dataType,
required this.nutrients,
});
factory Aggregations.fromJson(Map<String, dynamic> json) => Aggregations(
dataType: DataType.fromJson(json["dataType"]),
nutrients: Nutrients.fromJson(json["nutrients"]),
);
Map<String, dynamic> toJson() => {
"dataType": dataType.toJson(),
"nutrients": nutrients.toJson(),
};
}
class DataType {
int branded;
int surveyFndds;
int srLegacy;
int foundation;
DataType({
required this.branded,
required this.surveyFndds,
required this.srLegacy,
required this.foundation,
});
factory DataType.fromJson(Map<String, dynamic> json) => DataType(
branded: json["Branded"],
surveyFndds: json["Survey (FNDDS)"],
srLegacy: json["SR Legacy"],
foundation: json["Foundation"],
);
Map<String, dynamic> toJson() => {
"Branded": branded,
"Survey (FNDDS)": surveyFndds,
"SR Legacy": srLegacy,
"Foundation": foundation,
};
}
class Nutrients {
Nutrients();
factory Nutrients.fromJson(Map<String, dynamic> json) => Nutrients();
Map<String, dynamic> toJson() => {};
}
class FoodSearchCriteria {
List<Type> dataType;
String query;
String generalSearchInput;
int pageNumber;
String sortBy;
String sortOrder;
int numberOfResultsPerPage;
int pageSize;
bool requireAllWords;
List<Type> foodTypes;
FoodSearchCriteria({
required this.dataType,
required this.query,
required this.generalSearchInput,
required this.pageNumber,
required this.sortBy,
required this.sortOrder,
required this.numberOfResultsPerPage,
required this.pageSize,
required this.requireAllWords,
required this.foodTypes,
});
factory FoodSearchCriteria.fromJson(Map<String, dynamic> json) =>
FoodSearchCriteria(
dataType:
List<Type>.from(json["dataType"].map((x) => typeValues.map[x]!)),
query: json["query"],
generalSearchInput: json["generalSearchInput"],
pageNumber: json["pageNumber"],
sortBy: json["sortBy"],
sortOrder: json["sortOrder"],
numberOfResultsPerPage: json["numberOfResultsPerPage"],
pageSize: json["pageSize"],
requireAllWords: json["requireAllWords"],
foodTypes:
List<Type>.from(json["foodTypes"].map((x) => typeValues.map[x]!)),
);
Map<String, dynamic> toJson() => {
"dataType":
List<dynamic>.from(dataType.map((x) => typeValues.reverse[x])),
"query": query,
"generalSearchInput": generalSearchInput,
"pageNumber": pageNumber,
"sortBy": sortBy,
"sortOrder": sortOrder,
"numberOfResultsPerPage": numberOfResultsPerPage,
"pageSize": pageSize,
"requireAllWords": requireAllWords,
"foodTypes":
List<dynamic>.from(foodTypes.map((x) => typeValues.reverse[x])),
};
}
enum Type { FOUNDATION, SR_LEGACY }
final typeValues =
EnumValues({"Foundation": Type.FOUNDATION, "SR Legacy": Type.SR_LEGACY});
class FoodData {
int fdcId;
String description;
String commonNames;
String additionalDescriptions;
Type dataType;
int ndbNumber;
DateTime publishedDate;
FoodCategory? foodCategory;
DateTime? mostRecentAcquisitionDate;
String allHighlightFields;
double score;
List<dynamic> microbes;
List<FoodNutrient> foodNutrients;
List<dynamic> finalFoodInputFoods;
List<dynamic> foodMeasures;
List<dynamic> foodAttributes;
List<dynamic> foodAttributeTypes;
List<dynamic> foodVersionIds;
FoodData({
required this.fdcId,
required this.description,
required this.commonNames,
required this.additionalDescriptions,
required this.dataType,
required this.ndbNumber,
required this.publishedDate,
required this.foodCategory,
this.mostRecentAcquisitionDate,
required this.allHighlightFields,
required this.score,
required this.microbes,
required this.foodNutrients,
required this.finalFoodInputFoods,
required this.foodMeasures,
required this.foodAttributes,
required this.foodAttributeTypes,
required this.foodVersionIds,
});
factory FoodData.fromJson(Map<String, dynamic> json) => FoodData(
fdcId: json["fdcId"],
description: json["description"],
commonNames: json["commonNames"],
additionalDescriptions: json["additionalDescriptions"],
dataType: typeValues.map[json["dataType"]]!,
ndbNumber: json["ndbNumber"],
publishedDate: DateTime.parse(json["publishedDate"]),
foodCategory: foodCategoryValues.map[json["foodCategory"]],
mostRecentAcquisitionDate: json["mostRecentAcquisitionDate"] == null
? null
: DateTime.parse(json["mostRecentAcquisitionDate"]),
allHighlightFields: json["allHighlightFields"],
score: json["score"]?.toDouble(),
microbes: List<dynamic>.from(json["microbes"].map((x) => x)),
foodNutrients: List<FoodNutrient>.from(
json["foodNutrients"].map((x) => FoodNutrient.fromJson(x))),
finalFoodInputFoods:
List<dynamic>.from(json["finalFoodInputFoods"].map((x) => x)),
foodMeasures: List<dynamic>.from(json["foodMeasures"].map((x) => x)),
foodAttributes:
List<dynamic>.from(json["foodAttributes"].map((x) => x)),
foodAttributeTypes:
List<dynamic>.from(json["foodAttributeTypes"].map((x) => x)),
foodVersionIds:
List<dynamic>.from(json["foodVersionIds"].map((x) => x)),
);
Map<String, dynamic> toJson() => {
"fdcId": fdcId,
"description": description,
"commonNames": commonNames,
"additionalDescriptions": additionalDescriptions,
"dataType": typeValues.reverse[dataType],
"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],
"mostRecentAcquisitionDate":
"${mostRecentAcquisitionDate!.year.toString().padLeft(4, '0')}-${mostRecentAcquisitionDate!.month.toString().padLeft(2, '0')}-${mostRecentAcquisitionDate!.day.toString().padLeft(2, '0')}",
"allHighlightFields": allHighlightFields,
"score": score,
"microbes": List<dynamic>.from(microbes.map((x) => x)),
"foodNutrients":
List<dynamic>.from(foodNutrients.map((x) => x.toJson())),
"finalFoodInputFoods":
List<dynamic>.from(finalFoodInputFoods.map((x) => x)),
"foodMeasures": List<dynamic>.from(foodMeasures.map((x) => x)),
"foodAttributes": List<dynamic>.from(foodAttributes.map((x) => x)),
"foodAttributeTypes":
List<dynamic>.from(foodAttributeTypes.map((x) => x)),
"foodVersionIds": List<dynamic>.from(foodVersionIds.map((x) => x)),
};
}
enum FoodCategory { DAIRY_AND_EGG_PRODUCTS }
final foodCategoryValues =
EnumValues({"Dairy and Egg Products": FoodCategory.DAIRY_AND_EGG_PRODUCTS});
class FoodNutrient {
int nutrientId;
String nutrientName;
String nutrientNumber;
UnitName unitName;
DerivationCode? derivationCode;
String? derivationDescription;
int? derivationId;
double? value;
int? foodNutrientSourceId;
String? foodNutrientSourceCode;
FoodNutrientSourceDescription? foodNutrientSourceDescription;
int rank;
int indentLevel;
int foodNutrientId;
int? dataPoints;
double? min;
double? max;
double? median;
FoodNutrient({
required this.nutrientId,
required this.nutrientName,
required this.nutrientNumber,
required this.unitName,
this.derivationCode,
this.derivationDescription,
this.derivationId,
this.value,
this.foodNutrientSourceId,
this.foodNutrientSourceCode,
this.foodNutrientSourceDescription,
required this.rank,
required this.indentLevel,
required this.foodNutrientId,
this.dataPoints,
this.min,
this.max,
this.median,
});
factory FoodNutrient.fromJson(Map<String, dynamic> json) => FoodNutrient(
nutrientId: json["nutrientId"],
nutrientName: json["nutrientName"],
nutrientNumber: json["nutrientNumber"],
unitName: unitNameValues.map[json["unitName"]]!,
derivationCode: derivationCodeValues.map[json["derivationCode"]]!,
derivationDescription: json["derivationDescription"],
derivationId: json["derivationId"],
value: json["value"]?.toDouble(),
foodNutrientSourceId: json["foodNutrientSourceId"],
foodNutrientSourceCode: json["foodNutrientSourceCode"],
foodNutrientSourceDescription: foodNutrientSourceDescriptionValues
.map[json["foodNutrientSourceDescription"]]!,
rank: json["rank"],
indentLevel: json["indentLevel"],
foodNutrientId: json["foodNutrientId"],
dataPoints: json["dataPoints"],
min: json["min"]?.toDouble(),
max: json["max"]?.toDouble(),
median: json["median"]?.toDouble(),
);
Map<String, dynamic> toJson() => {
"nutrientId": nutrientId,
"nutrientName": nutrientName,
"nutrientNumber": nutrientNumber,
"unitName": unitNameValues.reverse[unitName],
"derivationCode": derivationCodeValues.reverse[derivationCode],
"derivationDescription": derivationDescription,
"derivationId": derivationId,
"value": value,
"foodNutrientSourceId": foodNutrientSourceId,
"foodNutrientSourceCode": foodNutrientSourceCode,
"foodNutrientSourceDescription": foodNutrientSourceDescriptionValues
.reverse[foodNutrientSourceDescription],
"rank": rank,
"indentLevel": indentLevel,
"foodNutrientId": foodNutrientId,
"dataPoints": dataPoints,
"min": min,
"max": max,
"median": median,
};
}
enum DerivationCode { A, AS, BFFN, BFNN, BFZN, CAZN, LC, NC, NR, T, Z }
final derivationCodeValues = EnumValues({
"A": DerivationCode.A,
"AS": DerivationCode.AS,
"BFFN": DerivationCode.BFFN,
"BFNN": DerivationCode.BFNN,
"BFZN": DerivationCode.BFZN,
"CAZN": DerivationCode.CAZN,
"LC": DerivationCode.LC,
"NC": DerivationCode.NC,
"NR": DerivationCode.NR,
"T": DerivationCode.T,
"Z": DerivationCode.Z
});
enum FoodNutrientSourceDescription {
ANALYTICAL_OR_DERIVED_FROM_ANALYTICAL,
ASSUMED_ZERO,
CALCULATED_FROM_NUTRIENT_LABEL_BY_NDL,
CALCULATED_OR_IMPUTED
}
final foodNutrientSourceDescriptionValues = EnumValues({
"Analytical or derived from analytical":
FoodNutrientSourceDescription.ANALYTICAL_OR_DERIVED_FROM_ANALYTICAL,
"Assumed zero": FoodNutrientSourceDescription.ASSUMED_ZERO,
"Calculated from nutrient label by NDL":
FoodNutrientSourceDescription.CALCULATED_FROM_NUTRIENT_LABEL_BY_NDL,
"Calculated or imputed": FoodNutrientSourceDescription.CALCULATED_OR_IMPUTED
});
enum UnitName { G, IU, KCAL, K_J, MG, UG }
final unitNameValues = EnumValues({
"G": UnitName.G,
"IU": UnitName.IU,
"KCAL": UnitName.KCAL,
"kJ": UnitName.K_J,
"MG": UnitName.MG,
"UG": UnitName.UG
});
class EnumValues<T> {
Map<String, T> map;
late Map<T, String> reverseMap;
EnumValues(this.map);
Map<T, String> get reverse {
reverseMap = map.map((k, v) => MapEntry(v, k));
return reverseMap;
}
}

View File

@@ -1,10 +1,84 @@
import 'package:dietary_guard/controllers/food_data.dart';
import 'package:dietary_guard/models/food_data.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
class QueryScreen extends StatelessWidget {
class QueryScreen extends StatefulWidget {
const QueryScreen({super.key});
@override
State<QueryScreen> createState() => _QueryScreenState();
}
class _QueryScreenState extends State<QueryScreen> {
bool _isLoading = false;
int _totalCount = 0;
List<FoodData> _foodData = List.empty();
Future<void> _searchFood(String probe) async {
if (_isLoading) return;
setState(() => _isLoading = true);
final FoodDataController data = Get.find();
if (data.getApiKey() == null) return;
final result = await data.searchFood(probe);
setState(() {
_totalCount = result.totalHits;
_foodData = result.foods;
_isLoading = false;
});
}
@override
void initState() {
super.initState();
final FoodDataController data = Get.find();
data.initialize(context);
}
@override
Widget build(BuildContext context) {
return const SizedBox();
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: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 3),
).paddingSymmetric(vertical: 16)
else
Expanded(
child: ListView.builder(
itemCount: _foodData.length,
itemBuilder: (context, index) {
final item = _foodData[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(item.description),
subtitle: Text(
DateFormat("yyyy-MM-dd").format(item.publishedDate),
),
);
},
).paddingOnly(top: 8),
),
],
),
);
}
}

View File

@@ -1,10 +1,70 @@
import 'package:dietary_guard/controllers/food_data.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class SettingsScreen extends StatelessWidget {
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
final TextEditingController _fdcApiKeyController = TextEditingController();
Future<void> _applySettings() async {
final FoodDataController data = Get.find();
await data.setApiKey(_fdcApiKeyController.text);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('settingsApplied'.tr),
));
}
@override
void initState() {
final FoodDataController data = Get.find();
_fdcApiKeyController.text = data.getApiKey() ?? '';
super.initState();
}
@override
void dispose() {
_fdcApiKeyController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Placeholder();
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(),
),
],
).paddingSymmetric(vertical: 12),
],
).paddingSymmetric(horizontal: 24),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
class Destination {
@@ -21,8 +22,8 @@ class _NavShellState extends State<NavShell> {
int _focusDestination = 0;
final List<Destination> _allDestinations = <Destination>[
const Destination('Query', 'query', Icons.search),
const Destination('Settings', 'settings', Icons.settings)
Destination('query'.tr, 'query', Icons.search),
Destination('settings'.tr, 'settings', Icons.settings)
];
@override

11
lib/translations.dart Normal file
View File

@@ -0,0 +1,11 @@
import 'package:dietary_guard/translations/en_us.dart';
import 'package:dietary_guard/translations/zh_cn.dart';
import 'package:get/get.dart';
class AppTranslations extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': i18nEnglish,
'zh_CN': i18nSimplifiedChinese,
};
}

View File

@@ -0,0 +1,8 @@
const i18nEnglish = {
'query': 'Query',
'settings': 'Settings',
'preparingData': 'Preparing data...',
'settingsApplied': 'Settings Applied',
'apply': 'Apply',
'searchHistoryNotIncluded': 'Search History not Included, yet',
};

View File

@@ -0,0 +1,8 @@
const i18nSimplifiedChinese = {
'query': '查询',
'settings': '设置',
'preparingData': '准备数据中…',
'settingsApplied': '设置已应用',
'apply': '应用',
'searchHistoryNotIncluded': '搜索记录还没实现',
};