Skip to content

Commit cc18d8b

Browse files
feat(Backup Data): User can backup their data
New import and export feature implemented.
1 parent 2683727 commit cc18d8b

13 files changed

+318
-25
lines changed

android/app/src/main/AndroidManifest.xml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
22
<uses-permission android:name="android.permission.INTERNET" />
3+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
4+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
35
<application
46
android:label="Fintracker"
57
android:name="${applicationName}"
6-
android:icon="@mipmap/ic_launcher">
8+
android:icon="@mipmap/ic_launcher"
9+
android:requestLegacyExternalStorage="true">
710
<activity
811
android:name=".MainActivity"
912
android:exported="true"

ios/Runner/Info.plist

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55
<key>CFBundleDevelopmentRegion</key>
66
<string>$(DEVELOPMENT_LANGUAGE)</string>
77
<key>CFBundleDisplayName</key>
8-
<string>Fintracker</string>
8+
<string>Fintracker</string>
99
<key>CFBundleExecutable</key>
1010
<string>$(EXECUTABLE_NAME)</string>
1111
<key>CFBundleIdentifier</key>
1212
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
1313
<key>CFBundleInfoDictionaryVersion</key>
1414
<string>6.0</string>
1515
<key>CFBundleName</key>
16-
<string>Fintracker</string>
16+
<string>Fintracker</string>
1717
<key>CFBundlePackageType</key>
1818
<string>APPL</string>
1919
<key>CFBundleShortVersionString</key>
@@ -47,5 +47,9 @@
4747
<true/>
4848
<key>UIApplicationSupportsIndirectInputEvents</key>
4949
<true/>
50+
<key>LSSupportsOpeningDocumentsInPlace</key>
51+
<true/>
52+
<key>UIFileSharingEnabled</key>
53+
<true/>
5054
</dict>
5155
</plist>

lib/helpers/db.helper.dart

+87
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11

2+
import "dart:convert";
23
import "dart:io";
34
import "package:flutter/material.dart";
45
import "package:path/path.dart";
56
import "package:fintracker/helpers/migrations/migrations.dart";
67
import "package:sqflite_common_ffi/sqflite_ffi.dart";
78

9+
import 'package:path_provider/path_provider.dart';
10+
import 'package:permission_handler/permission_handler.dart';
11+
812
Database? database;
913
Future<Database> getDBInstance() async {
1014
if(database == null) {
@@ -86,3 +90,86 @@ Future<void> resetDatabase() async {
8690
}
8791
}
8892

93+
94+
Future<String> getExternalDocumentPath() async {
95+
// To check whether permission is given for this app or not.
96+
var status = await Permission.storage.status;
97+
if (!status.isGranted) {
98+
// If not we will ask for permission first
99+
await Permission.storage.request();
100+
}
101+
Directory directory = Directory("");
102+
if (Platform.isAndroid) {
103+
// Redirects it to download folder in android
104+
directory = Directory("/storage/emulated/0/Download");
105+
} else {
106+
directory = await getApplicationDocumentsDirectory();
107+
}
108+
109+
final exPath = directory.path;
110+
await Directory(exPath).create(recursive: true);
111+
return exPath;
112+
}
113+
Future<dynamic> export() async {
114+
List<dynamic> accounts = await database!.query("accounts",);
115+
List<dynamic> categories = await database!.query("categories",);
116+
List<dynamic> payments = await database!.query("payments",);
117+
Map<String, dynamic> data = {};
118+
data["accounts"] = accounts;
119+
data["categories"] = categories;
120+
data["payments"] = payments;
121+
122+
final path = await getExternalDocumentPath();
123+
String name = "fintracker-backup-${DateTime.now().millisecondsSinceEpoch}.json";
124+
File file= File('$path/$name');
125+
await file.writeAsString(jsonEncode(data));
126+
return file.path;
127+
}
128+
129+
130+
Future<void> import(String path) async {
131+
File file = File(path);
132+
Map<int, int> accountsMap = {};
133+
Map<int, int> categoriesMap = {};
134+
135+
try{
136+
Map<String, dynamic> data = await jsonDecode(file.readAsStringSync());
137+
await database!.transaction((transaction) async{
138+
await transaction.delete("categories", where: "id!=0");
139+
await transaction.delete("accounts", where: "id!=0");
140+
await transaction.delete("payments", where: "id!=0");
141+
142+
143+
List<dynamic> categories = data["categories"];
144+
List<dynamic> accounts = data["accounts"];
145+
List<dynamic> payments = data["payments"];
146+
147+
148+
for(Map<String, dynamic> category in categories){
149+
int id0 = category["id"];
150+
category.remove("id");
151+
int id = await transaction.insert("categories", category);
152+
categoriesMap[id0] = id;
153+
}
154+
155+
156+
for(Map<String, dynamic> account in accounts){
157+
int id0 = account["id"];
158+
account.remove("id");
159+
int id = await transaction.insert("accounts", account);
160+
accountsMap[id0] = id;
161+
}
162+
163+
for(Map<String, dynamic> payment in payments){
164+
payment.remove("id");
165+
payment["account"] = accountsMap[payment["account"]];
166+
payment["category"] = categoriesMap[payment["category"]];
167+
await transaction.insert("payments", payment);
168+
}
169+
return transaction;
170+
});
171+
} catch(err){
172+
rethrow;
173+
}
174+
}
175+

lib/screens/settings/settings.screen.dart

+82-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'package:currency_picker/currency_picker.dart';
2+
import 'package:file_picker/file_picker.dart';
23
import 'package:fintracker/bloc/cubit/app_cubit.dart';
34
import 'package:fintracker/helpers/color.helper.dart';
45
import 'package:fintracker/helpers/db.helper.dart';
5-
import 'package:fintracker/theme/colors.dart';
66
import 'package:fintracker/widgets/buttons/button.dart';
77
import 'package:fintracker/widgets/dialog/confirm.modal.dart';
8+
import 'package:fintracker/widgets/dialog/loading_dialog.dart';
89
import 'package:flutter/material.dart';
910
import 'package:flutter_bloc/flutter_bloc.dart';
1011
import 'package:material_symbols_icons/material_symbols_icons.dart';
@@ -107,28 +108,98 @@ class _SettingsScreenState extends State<SettingsScreen> {
107108
),
108109
ListTile(
109110
dense: true,
110-
onTap: () async {
111+
onTap:() async {
111112
ConfirmModal.showConfirmDialog(
112113
context, title: "Are you sure?",
113-
content: const Text("After deleting data can't be recovered"),
114+
content: const Text("want to export all the data to a file"),
114115
onConfirm: ()async{
115116
Navigator.of(context).pop();
116-
Navigator.of(context).pop();
117-
await context.read<AppCubit>().reset();
118-
await resetDatabase();
117+
LoadingModal.showLoadingDialog(context, content: const Text("Exporting data please wait"));
118+
await export().then((value){
119+
ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("File has been saved in $value")));
120+
}).catchError((err){
121+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Something went wrong while exporting data")));
122+
}).whenComplete((){
123+
Navigator.of(context).pop();
124+
});
119125
},
120126
onCancel: (){
121127
Navigator.of(context).pop();
122128
}
123129
);
124130
},
125-
leading: CircleAvatar(
126-
backgroundColor: ThemeColors.error.withAlpha(90),
127-
child: const Icon(Icons.delete,)
131+
leading: const CircleAvatar(
132+
child: Icon(Symbols.download,)
133+
),
134+
title: Text('Export', style: Theme.of(context).textTheme.bodyMedium?.merge(const TextStyle(fontWeight: FontWeight.w500, fontSize: 15))),
135+
subtitle: Text("Export to file",style: Theme.of(context).textTheme.bodySmall?.apply(color: Colors.grey, overflow: TextOverflow.ellipsis)),
136+
),
137+
ListTile(
138+
dense: true,
139+
onTap:() async {
140+
await FilePicker.platform.pickFiles(
141+
dialogTitle: "Pick file",
142+
allowMultiple: false,
143+
allowCompression: false,
144+
type:FileType.custom,
145+
allowedExtensions: ["json"]
146+
).then((pick){
147+
if(pick == null || pick.files.isEmpty) {
148+
return ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Please select file")));
149+
}
150+
PlatformFile file = pick.files.first;
151+
ConfirmModal.showConfirmDialog(
152+
context, title: "Are you sure?",
153+
content: const Text("All payment data, categories, and accounts will be erased and replaced with the information imported from the backup."),
154+
onConfirm: ()async{
155+
Navigator.of(context).pop();
156+
LoadingModal.showLoadingDialog(context, content: const Text("Exporting data please wait"));
157+
await import(file.path!).then((value){
158+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Successfully imported.")));
159+
Navigator.of(context).pop();
160+
}).catchError((err){
161+
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Something went wrong while importing data")));
162+
});
163+
},
164+
onCancel: (){
165+
Navigator.of(context).pop();
166+
}
167+
);
168+
}).catchError((err){
169+
return ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Something went wrong while importing data")));
170+
});
171+
},
172+
leading: const CircleAvatar(
173+
child: Icon(Symbols.upload,)
128174
),
129-
title: Text('Reset', style: Theme.of(context).textTheme.bodyMedium?.merge(const TextStyle(fontWeight: FontWeight.w500, fontSize: 15))),
130-
subtitle: Text("Delete all the data",style: Theme.of(context).textTheme.bodySmall?.apply(color: Colors.grey, overflow: TextOverflow.ellipsis)),
175+
title: Text('Import', style: Theme.of(context).textTheme.bodyMedium?.merge(const TextStyle(fontWeight: FontWeight.w500, fontSize: 15))),
176+
subtitle: Text("Import from backup file",style: Theme.of(context).textTheme.bodySmall?.apply(color: Colors.grey, overflow: TextOverflow.ellipsis)),
177+
131178
),
179+
// ListTile(
180+
// dense: true,
181+
// onTap: () async {
182+
// ConfirmModal.showConfirmDialog(
183+
// context, title: "Are you sure?",
184+
// content: const Text("After deleting data can't be recovered"),
185+
// onConfirm: ()async{
186+
// Navigator.of(context).pop();
187+
// Navigator.of(context).pop();
188+
// await context.read<AppCubit>().reset();
189+
// await resetDatabase();
190+
// },
191+
// onCancel: (){
192+
// Navigator.of(context).pop();
193+
// }
194+
// );
195+
// },
196+
// leading: CircleAvatar(
197+
// backgroundColor: ThemeColors.error.withAlpha(90),
198+
// child: const Icon(Symbols.device_reset,)
199+
// ),
200+
// title: Text('Reset', style: Theme.of(context).textTheme.bodyMedium?.merge(const TextStyle(fontWeight: FontWeight.w500, fontSize: 15))),
201+
// subtitle: Text("Delete all the data",style: Theme.of(context).textTheme.bodySmall?.apply(color: Colors.grey, overflow: TextOverflow.ellipsis)),
202+
// ),
132203
],
133204
)
134205
);
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import 'package:flutter/material.dart';
2+
3+
class LoadingModal extends StatelessWidget{
4+
final Widget content;
5+
const LoadingModal({
6+
super.key,
7+
required this.content,
8+
});
9+
@override
10+
Widget build(BuildContext context) {
11+
return AlertDialog(
12+
insetPadding: const EdgeInsets.all(20),
13+
content: Row(
14+
crossAxisAlignment: CrossAxisAlignment.center,
15+
children: [
16+
const CircularProgressIndicator(),
17+
const SizedBox(width: 20,),
18+
Expanded(child: content)
19+
],
20+
)
21+
);
22+
}
23+
24+
static showLoadingDialog(BuildContext context, {
25+
required Widget content,
26+
}){
27+
showDialog(context: context,
28+
builder: (BuildContext context){
29+
return LoadingModal(content: content);
30+
}
31+
);
32+
}
33+
34+
}

linux/flutter/generated_plugin_registrant.cc

-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66

77
#include "generated_plugin_registrant.h"
88

9-
#include <dynamic_color/dynamic_color_plugin.h>
109

1110
void fl_register_plugins(FlPluginRegistry* registry) {
12-
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
13-
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
14-
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
1511
}

linux/flutter/generated_plugins.cmake

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44

55
list(APPEND FLUTTER_PLUGIN_LIST
6-
dynamic_color
76
)
87

98
list(APPEND FLUTTER_FFI_PLUGIN_LIST

macos/Flutter/GeneratedPluginRegistrant.swift

-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
import FlutterMacOS
66
import Foundation
77

8-
import dynamic_color
98
import path_provider_foundation
109
import shared_preferences_foundation
1110
import sqflite
1211

1312
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
14-
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
1513
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
1614
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
1715
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

macos/Runner/DebugProfile.entitlements

+2
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@
1010
<true/>
1111
<key>com.apple.security.network.client</key>
1212
<true/>
13+
<key>com.apple.security.files.downloads.read-write</key>
14+
<true/>
1315
</dict>
1416
</plist>

0 commit comments

Comments
 (0)