367 lines
13 KiB
Dart
367 lines
13 KiB
Dart
|
import 'dart:convert';
|
||
|
import 'dart:io';
|
||
|
import 'package:diameter/localization_keys.dart';
|
||
|
import 'package:diameter/models/accuracy.dart';
|
||
|
import 'package:diameter/models/basal.dart';
|
||
|
import 'package:diameter/models/basal_profile.dart';
|
||
|
import 'package:diameter/models/bolus.dart';
|
||
|
import 'package:diameter/models/bolus_profile.dart';
|
||
|
import 'package:diameter/models/glucose_target.dart';
|
||
|
import 'package:diameter/models/log_bolus.dart';
|
||
|
import 'package:diameter/models/log_entry.dart';
|
||
|
import 'package:diameter/models/log_event.dart';
|
||
|
import 'package:diameter/models/log_event_type.dart';
|
||
|
import 'package:diameter/models/log_meal.dart';
|
||
|
import 'package:diameter/models/meal.dart';
|
||
|
import 'package:diameter/models/meal_category.dart';
|
||
|
import 'package:diameter/models/meal_portion_type.dart';
|
||
|
import 'package:diameter/models/meal_source.dart';
|
||
|
import 'package:diameter/models/settings.dart';
|
||
|
import 'package:flutter_translate/flutter_translate.dart';
|
||
|
// import 'package:flutter/material.dart';
|
||
|
import 'package:google_sign_in/google_sign_in.dart';
|
||
|
import 'package:intl/intl.dart';
|
||
|
import 'package:path_provider/path_provider.dart';
|
||
|
import 'package:http/http.dart' as http;
|
||
|
import 'package:googleapis/drive/v3.dart' as drive;
|
||
|
|
||
|
class GoogleAuthClient extends http.BaseClient {
|
||
|
final Map<String, String> _headers;
|
||
|
|
||
|
final http.Client _client = http.Client();
|
||
|
|
||
|
GoogleAuthClient(this._headers);
|
||
|
|
||
|
@override
|
||
|
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||
|
return _client.send(request..headers.addAll(_headers));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DataExport {
|
||
|
static Map<String, dynamic> appDataToJson(DateTime timestamp) {
|
||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||
|
|
||
|
data['exportDate'] = timestamp.toString();
|
||
|
data['accuracies'] =
|
||
|
Accuracy.box.getAll().map((accuracy) => accuracy.toJson()).toList();
|
||
|
data['basalProfiles'] = BasalProfile.box
|
||
|
.getAll()
|
||
|
.map((basalProfile) => basalProfile.toJson())
|
||
|
.toList();
|
||
|
data['basalRates'] =
|
||
|
Basal.box.getAll().map((basal) => basal.toJson()).toList();
|
||
|
data['bolusProfiles'] = BolusProfile.box
|
||
|
.getAll()
|
||
|
.map((bolusProfile) => bolusProfile.toJson())
|
||
|
.toList();
|
||
|
data['bolusRates'] =
|
||
|
Bolus.box.getAll().map((bolusRates) => bolusRates.toJson()).toList();
|
||
|
data['glucoseTargets'] = GlucoseTarget.box
|
||
|
.getAll()
|
||
|
.map((glucoseTarget) => glucoseTarget.toJson())
|
||
|
.toList();
|
||
|
data['logBoli'] =
|
||
|
LogBolus.box.getAll().map((logBolus) => logBolus.toJson()).toList();
|
||
|
data['logEntries'] =
|
||
|
LogEntry.box.getAll().map((logEntry) => logEntry.toJson()).toList();
|
||
|
data['logEventTypes'] = LogEventType.box
|
||
|
.getAll()
|
||
|
.map((logEventType) => logEventType.toJson())
|
||
|
.toList();
|
||
|
data['logEvents'] =
|
||
|
LogEvent.box.getAll().map((logEvent) => logEvent.toJson()).toList();
|
||
|
data['logMeals'] =
|
||
|
LogMeal.box.getAll().map((logMeal) => logMeal.toJson()).toList();
|
||
|
data['mealCategories'] = MealCategory.box
|
||
|
.getAll()
|
||
|
.map((mealCategory) => mealCategory.toJson())
|
||
|
.toList();
|
||
|
data['mealPortionTypes'] = MealPortionType.box
|
||
|
.getAll()
|
||
|
.map((mealPortionType) => mealPortionType.toJson())
|
||
|
.toList();
|
||
|
data['mealSources'] = MealSource.box
|
||
|
.getAll()
|
||
|
.map((mealSource) => mealSource.toJson())
|
||
|
.toList();
|
||
|
data['meals'] = Meal.box.getAll().map((meal) => meal.toJson()).toList();
|
||
|
data['settings'] = Settings.toJson();
|
||
|
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
List<String> appDataFromJson(File file,
|
||
|
{String source = '', bool overrideExisting = true, bool skipDeleted = false}) {
|
||
|
final Map<String, dynamic> data = json.decode(file.openRead().toString());
|
||
|
final List<String> errors = [];
|
||
|
|
||
|
final exportDate = DateTime.tryParse(data['exportDate']);
|
||
|
if (exportDate != null && Settings.lastExportTimeStamp != null && exportDate.isAfter(Settings.lastExportTimeStamp!)) {
|
||
|
if (data.keys.contains('accuracies')) {
|
||
|
for (var entry in data['accuracies']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = Accuracy.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('basalProfiles')) {
|
||
|
for (var entry in data['basalProfiles']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = BasalProfile.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('basalRates')) {
|
||
|
for (var entry in data['basalRates']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = Basal.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('bolusRates')) {
|
||
|
for (var entry in data['bolusRates']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = Bolus.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('glucoseTargets')) {
|
||
|
for (var entry in data['glucoseTargets']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = GlucoseTarget.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('logBoli')) {
|
||
|
for (var entry in data['logBoli']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = LogBolus.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('logEntries')) {
|
||
|
for (var entry in data['logEntries']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = LogEntry.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('logEvents')) {
|
||
|
for (var entry in data['logEvents']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = LogEvent.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('logMeals')) {
|
||
|
for (var entry in data['logMeals']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = LogMeal.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('mealCategories')) {
|
||
|
for (var entry in data['mealCategories']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = MealCategory.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('mealPortionTypes')) {
|
||
|
for (var entry in data['mealPortionTypes']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = MealPortionType.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('mealSources')) {
|
||
|
for (var entry in data['mealSources']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = MealSource.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data.keys.contains('meals')) {
|
||
|
for (var entry in data['meals']) {
|
||
|
if (!skipDeleted || entry['deleted'] == false) {
|
||
|
final error = Meal.putFromJson(entry, overrideExisting, source);
|
||
|
if (error != null) {
|
||
|
errors.add(translate(LocalizationKeys.export_error, args: {"data": entry}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// show confirmation dialog because export is newer than last saved timestamp
|
||
|
}
|
||
|
|
||
|
return errors;
|
||
|
}
|
||
|
|
||
|
static Future<File> exportDataToFile(DateTime timestamp) async {
|
||
|
final appDocDir = await getApplicationDocumentsDirectory();
|
||
|
final appDocPath = appDocDir.path;
|
||
|
|
||
|
final DateFormat formatter = DateFormat(DateFormat.YEAR_MONTH_DAY);
|
||
|
final date = DateTime.now();
|
||
|
final file = File('$appDocPath/diameter_${formatter.format(date)}.json');
|
||
|
|
||
|
file.writeAsStringSync(json.encode(appDataToJson(timestamp)));
|
||
|
return file;
|
||
|
}
|
||
|
|
||
|
static Future<void> exportToGoogleDrive() async {
|
||
|
final login = GoogleSignIn.standard(scopes: [drive.DriveApi.driveScope]);
|
||
|
final GoogleSignInAccount? account = await login.signIn();
|
||
|
|
||
|
if (account != null) {
|
||
|
final authenticateClient = GoogleAuthClient(await account.authHeaders);
|
||
|
final driveApi = drive.DriveApi(authenticateClient);
|
||
|
|
||
|
final timestamp = DateTime.now();
|
||
|
var localFile = await DataExport.exportDataToFile(timestamp);
|
||
|
var media = drive.Media(localFile.openRead(), localFile.lengthSync());
|
||
|
|
||
|
final settings = Settings.get();
|
||
|
settings.lastExportTimestamp = timestamp;
|
||
|
Settings.put(settings);
|
||
|
|
||
|
drive.File driveFile = drive.File()..name = 'diameter.json';
|
||
|
await driveApi.files.create(driveFile, uploadMedia: media);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static Future<void> importFromGoogleDrive() async {
|
||
|
final login = GoogleSignIn.standard(scopes: [drive.DriveApi.driveScope]);
|
||
|
final GoogleSignInAccount? account = await login.signIn();
|
||
|
|
||
|
if (account != null) {
|
||
|
final authenticateClient = GoogleAuthClient(await account.authHeaders);
|
||
|
final driveApi = drive.DriveApi(authenticateClient);
|
||
|
|
||
|
final timestamp = DateTime.now();
|
||
|
var localFile = await DataExport.exportDataToFile(timestamp);
|
||
|
var media = drive.Media(localFile.openRead(), localFile.lengthSync());
|
||
|
|
||
|
final settings = Settings.get();
|
||
|
settings.lastExportTimestamp = timestamp;
|
||
|
Settings.put(settings);
|
||
|
|
||
|
drive.File driveFile = drive.File()..name = 'diameter.json';
|
||
|
await driveApi.files.create(driveFile, uploadMedia: media);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// class DataExportDialog extends StatefulWidget {
|
||
|
// static const String routeName = '/data-export';
|
||
|
// const DataExportDialog({Key? key}) : super(key: key);
|
||
|
|
||
|
// @override
|
||
|
// _DataExportDialogState createState() => _DataExportDialogState();
|
||
|
// }
|
||
|
|
||
|
// class _DataExportDialogState extends State<DataExportDialog> {
|
||
|
// final ScrollController _scrollController = ScrollController();
|
||
|
|
||
|
// bool _isSaving = false;
|
||
|
|
||
|
// @override
|
||
|
// void dispose() {
|
||
|
// _scrollController.dispose();
|
||
|
// super.dispose();
|
||
|
// }
|
||
|
|
||
|
// @override
|
||
|
// void initState() {
|
||
|
// super.initState();
|
||
|
// }
|
||
|
|
||
|
// void onExport(BuildContext context) async {
|
||
|
// setState(() {
|
||
|
// _isSaving = true;
|
||
|
// });
|
||
|
|
||
|
// Navigator.pop(context);
|
||
|
// setState(() {
|
||
|
// _isSaving = false;
|
||
|
// });
|
||
|
// }
|
||
|
|
||
|
// @override
|
||
|
// Widget build(BuildContext context) {
|
||
|
// return AlertDialog(
|
||
|
// content: Column(
|
||
|
// mainAxisSize: MainAxisSize.min,
|
||
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||
|
// children: const [
|
||
|
// // CheckboxListTile(
|
||
|
// // value: showChart,
|
||
|
// // onChanged: (_) => setState(() => showChart = !showChart),
|
||
|
// // title: const Text('show Chart'),
|
||
|
// // controlAffinity: ListTileControlAffinity.leading,
|
||
|
// // ),
|
||
|
// ],
|
||
|
// ),
|
||
|
// actions: <Widget>[
|
||
|
// TextButton(
|
||
|
// onPressed: () => Navigator.pop(context),
|
||
|
// child: const Text('CANCEL'),
|
||
|
// ),
|
||
|
// ElevatedButton(
|
||
|
// onPressed: !_isSaving ? () => onExport(context) : null,
|
||
|
// child: const Text('EXPORT'),
|
||
|
// ),
|
||
|
// ]);
|
||
|
// }
|
||
|
// }
|