diff --git a/.env_template b/.env_template
new file mode 100644
index 00000000..b8d79805
--- /dev/null
+++ b/.env_template
@@ -0,0 +1 @@
+GOOGLE_API_KEY=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b9089e00..75eb2dbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,4 +51,7 @@ __pycache__
.pytest_cache
pyenv
platform_config.yaml
-videos
\ No newline at end of file
+videos
+
+# Env files
+.env
\ No newline at end of file
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 47c459b1..feef2216 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -14,6 +14,13 @@ if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
+// Load .env file
+val envFile = file("../../.env")
+val envProperties = Properties()
+if (envFile.exists()) {
+ envProperties.load(FileInputStream(envFile))
+}
+
android {
flavorDimensions += "store"
@@ -49,18 +56,22 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
+
+ manifestPlaceholders.put("GOOGLE_API_KEY", envProperties.getProperty("GOOGLE_API_KEY") ?: "")
}
signingConfigs {
create("release") {
- keyAlias = keystoreProperties["keyAlias"] as String
- keyPassword = keystoreProperties["keyPassword"] as String
+ keyAlias = keystoreProperties["keyAlias"] as String?
+ keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
- storePassword = keystoreProperties["storePassword"] as String
+ storePassword = keystoreProperties["storePassword"] as String?
+ }
+ getByName("debug") {
+ // Debug signing config uses default values
}
}
-
buildTypes {
release {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2510a452..4ce9bc32 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -20,6 +20,8 @@
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:fullBackupOnly="false">
+
Access to documents is needed to import and export CSV files
UISupportsDocumentBrowser
+ GOOGLE_API_KEY
+ $(GOOGLE_API_KEY)
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index d92f447f..f1d97499 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -15,6 +16,8 @@ import 'services/notifications/notifications_service.dart';
late SharedPreferences _sharedPreferences;
void main() async {
+ await dotenv.load(fileName: ".env");
+
WidgetsFlutterBinding.ensureInitialized();
NotificationService().requestNotificationPermissions();
NotificationService().initializeNotifications();
diff --git a/lib/model/recurring_transaction.dart b/lib/model/recurring_transaction.dart
index a36f253b..8ec97575 100644
--- a/lib/model/recurring_transaction.dart
+++ b/lib/model/recurring_transaction.dart
@@ -17,6 +17,9 @@ class RecurringTransactionFields extends BaseEntityFields {
static String idCategory = 'idCategory';
static String idBankAccount = 'idBankAccount';
static String lastInsertion = 'lastInsertion';
+ static String lat = 'lat';
+ static String lon = 'lon';
+ static String locationName = 'locationName';
static String createdAt = BaseEntityFields.getCreatedAt;
static String updatedAt = BaseEntityFields.getUpdatedAt;
@@ -30,6 +33,9 @@ class RecurringTransactionFields extends BaseEntityFields {
idCategory,
idBankAccount,
lastInsertion,
+ lat,
+ lon,
+ locationName,
BaseEntityFields.createdAt,
BaseEntityFields.updatedAt,
];
@@ -55,72 +61,87 @@ class RecurringTransaction extends BaseEntity {
final int idBankAccount;
final TransactionType type;
final DateTime? lastInsertion;
-
- const RecurringTransaction(
- {super.id,
- required this.fromDate,
- this.toDate,
- required this.amount,
- required this.note,
- required this.recurrency,
- required this.idCategory,
- required this.type,
- required this.idBankAccount,
- this.lastInsertion,
- super.createdAt,
- super.updatedAt});
-
- RecurringTransaction copy(
- {int? id,
- DateTime? fromDate,
- DateTime? toDate,
- num? amount,
- String? note,
- String? recurrency,
- int? idCategory,
- TransactionType? type,
- int? idBankAccount,
- DateTime? lastInsertion,
- DateTime? createdAt,
- DateTime? updatedAt}) =>
+ final num? lat;
+ final num? lon;
+ final String? locationName;
+
+ const RecurringTransaction({
+ super.id,
+ required this.fromDate,
+ this.toDate,
+ required this.amount,
+ required this.note,
+ required this.recurrency,
+ required this.idCategory,
+ required this.type,
+ required this.idBankAccount,
+ this.lat,
+ this.lon,
+ this.locationName,
+ this.lastInsertion,
+ super.createdAt,
+ super.updatedAt,
+ });
+
+ RecurringTransaction copy({
+ int? id,
+ DateTime? fromDate,
+ DateTime? toDate,
+ num? amount,
+ String? note,
+ String? recurrency,
+ int? idCategory,
+ int? idBankAccount,
+ DateTime? lastInsertion,
+ num? lat,
+ num? lon,
+ String? locationName,
+ DateTime? createdAt,
+ DateTime? updatedAt,
+ }) =>
RecurringTransaction(
- id: id ?? this.id,
- fromDate: fromDate ?? this.fromDate,
- toDate: toDate ?? this.toDate,
- amount: amount ?? this.amount,
- note: note ?? this.note,
- recurrency: recurrency ?? this.recurrency,
- idCategory: idCategory ?? this.idCategory,
- type: type ?? this.type,
- idBankAccount: idBankAccount ?? this.idBankAccount,
- lastInsertion: lastInsertion ?? this.lastInsertion,
- createdAt: createdAt ?? this.createdAt,
- updatedAt: updatedAt ?? this.updatedAt);
+ id: id ?? this.id,
+ fromDate: fromDate ?? this.fromDate,
+ toDate: toDate ?? this.toDate,
+ amount: amount ?? this.amount,
+ note: note ?? this.note,
+ recurrency: recurrency ?? this.recurrency,
+ idCategory: idCategory ?? this.idCategory,
+ lat: lat ?? this.lat,
+ lon: lon ?? this.lon,
+ locationName: locationName ?? this.locationName,
+ type: type ?? this.type,
+ idBankAccount: idBankAccount ?? this.idBankAccount,
+ lastInsertion: lastInsertion ?? this.lastInsertion,
+ createdAt: createdAt ?? this.createdAt,
+ updatedAt: updatedAt ?? this.updatedAt,
+ );
static RecurringTransaction fromJson(Map json) =>
RecurringTransaction(
- id: json[BaseEntityFields.id] as int?,
- fromDate: DateTime.parse(
- json[RecurringTransactionFields.fromDate] as String),
- toDate:
- json[RecurringTransactionFields.toDate] != null
- ? DateTime.parse(
- json[RecurringTransactionFields.toDate] as String)
- : null,
- amount: json[RecurringTransactionFields.amount] as num,
- note: json[RecurringTransactionFields.note] as String,
- recurrency: json[RecurringTransactionFields.recurrency] as String,
- idCategory: json[RecurringTransactionFields.idCategory] as int,
- type: TransactionType.fromJson(
+ id: json[BaseEntityFields.id] as int?,
+ fromDate:
+ DateTime.parse(json[RecurringTransactionFields.fromDate] as String),
+ toDate: json[RecurringTransactionFields.toDate] != null
+ ? DateTime.parse(json[RecurringTransactionFields.toDate] as String)
+ : null,
+ amount: json[RecurringTransactionFields.amount] as num,
+ note: json[RecurringTransactionFields.note] as String,
+ recurrency: json[RecurringTransactionFields.recurrency] as String,
+ idCategory: json[RecurringTransactionFields.idCategory] as int,
+ type: TransactionType.fromJson(
json[RecurringTransactionFields.type] as String),
- idBankAccount: json[RecurringTransactionFields.idBankAccount] as int,
- lastInsertion: json[RecurringTransactionFields.lastInsertion] != null
- ? DateTime.parse(
- json[RecurringTransactionFields.lastInsertion] as String)
- : null,
- createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String),
- updatedAt:
- DateTime.parse(json[BaseEntityFields.updatedAt] as String));
+ idBankAccount: json[RecurringTransactionFields.idBankAccount] as int,
+ lastInsertion: json[RecurringTransactionFields.lastInsertion] != null
+ ? DateTime.parse(
+ json[RecurringTransactionFields.lastInsertion] as String)
+ : null,
+ createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String),
+ updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String),
+ lat: json[TransactionFields.lat] as num?,
+ lon: json[TransactionFields.lon] as num?,
+ locationName: json[TransactionFields.locationName] as String?,
+ );
Map toJson() => {
BaseEntityFields.id: id,
@@ -132,6 +153,9 @@ class RecurringTransaction extends BaseEntity {
RecurringTransactionFields.recurrency: recurrency,
RecurringTransactionFields.idCategory: idCategory,
RecurringTransactionFields.idBankAccount: idBankAccount,
+ RecurringTransactionFields.lat: lat,
+ RecurringTransactionFields.lon: lon,
+ RecurringTransactionFields.locationName: locationName,
RecurringTransactionFields.lastInsertion:
lastInsertion?.toIso8601String(),
BaseEntityFields.createdAt: createdAt?.toIso8601String(),
@@ -348,6 +372,9 @@ class RecurringTransactionMethods extends SossoldiDatabase {
idBankAccount: transaction.idBankAccount,
recurring: true,
idRecurringTransaction: transaction.id,
+ locationName: transaction.locationName,
+ lat: transaction.lat,
+ lon: transaction.lon,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
diff --git a/lib/model/transaction.dart b/lib/model/transaction.dart
index 69c418c3..323f464e 100644
--- a/lib/model/transaction.dart
+++ b/lib/model/transaction.dart
@@ -20,6 +20,9 @@ class TransactionFields extends BaseEntityFields {
static String idBankAccountTransfer = 'idBankAccountTransfer';
static String bankAccountTransferName = 'bankAccountTransferName';
static String recurring = 'recurring';
+ static String lat = 'lat';
+ static String lon = 'lon';
+ static String locationName = 'locationName';
static String idRecurringTransaction = 'idRecurringTransaction';
static String createdAt = BaseEntityFields.getCreatedAt;
static String updatedAt = BaseEntityFields.getUpdatedAt;
@@ -34,6 +37,9 @@ class TransactionFields extends BaseEntityFields {
idBankAccount,
idBankAccountTransfer,
recurring,
+ lat,
+ lon,
+ locationName,
idRecurringTransaction,
BaseEntityFields.createdAt,
BaseEntityFields.updatedAt
@@ -128,6 +134,9 @@ class Transaction extends BaseEntity {
final int? idBankAccountTransfer;
final String? bankAccountTransferName;
final bool recurring;
+ final num? lat;
+ final num? lon;
+ final String? locationName;
final int? idRecurringTransaction;
const Transaction(
@@ -145,6 +154,9 @@ class Transaction extends BaseEntity {
this.idBankAccountTransfer,
this.bankAccountTransferName,
required this.recurring,
+ this.lat,
+ this.lon,
+ this.locationName,
this.idRecurringTransaction,
super.createdAt,
super.updatedAt});
@@ -159,6 +171,9 @@ class Transaction extends BaseEntity {
int? idBankAccount,
int? idBankAccountTransfer,
bool? recurring,
+ num? lat,
+ num? lon,
+ String? locationName,
int? idRecurringTransaction,
DateTime? createdAt,
DateTime? updatedAt}) =>
@@ -173,6 +188,9 @@ class Transaction extends BaseEntity {
idBankAccountTransfer:
idBankAccountTransfer ?? this.idBankAccountTransfer,
recurring: recurring ?? this.recurring,
+ lat: lat ?? this.lat,
+ lon: lon ?? this.lon,
+ locationName: locationName ?? this.locationName,
idRecurringTransaction:
idRecurringTransaction ?? this.idRecurringTransaction,
createdAt: createdAt ?? this.createdAt,
@@ -196,6 +214,9 @@ class Transaction extends BaseEntity {
bankAccountTransferName:
json[TransactionFields.bankAccountTransferName] as String?,
recurring: json[TransactionFields.recurring] == 1,
+ lat: json[TransactionFields.lat] as num?,
+ lon: json[TransactionFields.lon] as num?,
+ locationName: json[TransactionFields.locationName] as String?,
idRecurringTransaction:
json[TransactionFields.idRecurringTransaction] as int?,
createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String),
@@ -218,6 +239,9 @@ class Transaction extends BaseEntity {
TransactionFields.idBankAccount: idBankAccount,
TransactionFields.idBankAccountTransfer: idBankAccountTransfer,
TransactionFields.recurring: recurring ? 1 : 0,
+ TransactionFields.lat: lat,
+ TransactionFields.lon: lon,
+ TransactionFields.locationName: locationName,
TransactionFields.idRecurringTransaction: idRecurringTransaction,
BaseEntityFields.createdAt: createdAtDate,
BaseEntityFields.updatedAt: DateTime.now().toIso8601String(),
diff --git a/lib/pages/transactions/create_transaction/create_transaction_page.dart b/lib/pages/transactions/create_transaction/create_transaction_page.dart
index a7bc44fd..477dcd2e 100644
--- a/lib/pages/transactions/create_transaction/create_transaction_page.dart
+++ b/lib/pages/transactions/create_transaction/create_transaction_page.dart
@@ -3,6 +3,7 @@ import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../../../constants/style.dart';
import '../../../model/transaction.dart';
@@ -16,6 +17,7 @@ import "widgets/category_selector.dart";
import 'widgets/details_list_tile.dart';
import 'widgets/duplicate_transaction_dialog.dart';
import 'widgets/label_list_tile.dart';
+import 'widgets/location_selector.dart';
import 'widgets/recurrence_list_tile.dart';
class CreateTransactionPage extends ConsumerStatefulWidget {
@@ -379,7 +381,42 @@ class _CreateTransactionPage extends ConsumerState {
widget.recurrencyEditingPermitted,
selectedTransaction:
ref.read(selectedTransactionUpdateProvider),
- )
+ ),
+ if (selectedType != TransactionType.transfer) ...[
+ const Divider(),
+ DetailsListTile(
+ title: "Location",
+ icon: Icons.location_pin,
+ value: ref.watch(locationTransactionProvider)?.searchText,
+ callback: () {
+ FocusManager.instance.primaryFocus?.unfocus();
+ showModalBottomSheet(
+ context: context,
+ isScrollControlled: true,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ builder: (_) => DraggableScrollableSheet(
+ expand: false,
+ minChildSize: 0.5,
+ initialChildSize: 0.8,
+ maxChildSize: 0.95,
+ builder: (_, scrollController) {
+ return LocationSelector(scrollController: scrollController);
+ },
+ ),
+ );
+ },
+ ),
+ ],
+ if (selectedType == TransactionType.expense) ...[
+ RecurrenceListTile(
+ recurrencyEditingPermitted:
+ widget.recurrencyEditingPermitted,
+ selectedTransaction:
+ ref.read(selectedTransactionUpdateProvider),
+ )
+ ],
],
),
),
diff --git a/lib/pages/transactions/create_transaction/widgets/details_list_tile.dart b/lib/pages/transactions/create_transaction/widgets/details_list_tile.dart
index cf9631b8..4c7c47d7 100644
--- a/lib/pages/transactions/create_transaction/widgets/details_list_tile.dart
+++ b/lib/pages/transactions/create_transaction/widgets/details_list_tile.dart
@@ -41,20 +41,33 @@ class DetailsListTile extends ConsumerWidget {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
- Text(
- value ?? '',
- style: Theme.of(context).textTheme.bodySmall!.copyWith(
- color: isDarkMode
- ? grey3
- : Theme.of(context).colorScheme.secondary),
+ SizedBox(
+ width: 130,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ reverse: true,
+ child: Text(
+ value ?? '',
+ overflow: TextOverflow.visible,
+ softWrap: false,
+ style: Theme.of(context).textTheme.bodySmall!.copyWith(
+ color: isDarkMode
+ ? grey3
+ : Theme.of(context).colorScheme.secondary,
+ ),
+ ),
+ ),
),
const SizedBox(width: Sizes.sm),
Icon(
Icons.chevron_right,
- color: isDarkMode ? grey3 : Theme.of(context).colorScheme.secondary,
+ color: isDarkMode
+ ? grey3
+ : Theme.of(context).colorScheme.secondary,
),
],
),
+
);
}
}
diff --git a/lib/pages/transactions/create_transaction/widgets/location_selector.dart b/lib/pages/transactions/create_transaction/widgets/location_selector.dart
new file mode 100644
index 00000000..dc6bc78d
--- /dev/null
+++ b/lib/pages/transactions/create_transaction/widgets/location_selector.dart
@@ -0,0 +1,379 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:google_maps_flutter/google_maps_flutter.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+import '../../../../constants/style.dart';
+import '../../../../providers/transactions_provider.dart';
+import '../../../../ui/device.dart';
+
+import 'package:flutter_dotenv/flutter_dotenv.dart';
+import 'package:http/http.dart' as http;
+import 'dart:convert';
+
+import '../../../../utils/location_bag.dart';
+import '../../../../ui/snack_bars/snack_bar.dart';
+
+class LocationSelector extends ConsumerStatefulWidget {
+ final ScrollController scrollController;
+ const LocationSelector({required this.scrollController, super.key});
+
+ @override
+ ConsumerState createState() => _LocationSelectorState();
+}
+
+class _LocationSelectorState extends ConsumerState {
+ GoogleMapController? mapController;
+ TextEditingController searchController = TextEditingController();
+
+ LatLng? selectedPosition;
+ Set markers = {};
+
+ LatLng initialPosition = LatLng(0, 0);
+ final String _googleApiKey = dotenv.env['GOOGLE_API_KEY'] ?? '';
+ List