update
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground>
|
||||||
|
<inset
|
||||||
|
android:drawable="@drawable/ic_launcher_foreground"
|
||||||
|
android:inset="16%" />
|
||||||
|
</foreground>
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#0F0F14</color>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 60 KiB |
@@ -431,7 +431,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
@@ -488,7 +488,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
|||||||
@@ -1,122 +1 @@
|
|||||||
{
|
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-20x20@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-29x29@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-40x40@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "Icon-App-60x60@3x.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-20x20@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-29x29@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-40x40@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "76x76",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-76x76@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "83.5x83.5",
|
|
||||||
"idiom" : "ipad",
|
|
||||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "Icon-App-1024x1024@1x.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 20 KiB |
@@ -11,7 +11,6 @@ class App extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final themeMode = ref.watch(themeProvider);
|
final themeMode = ref.watch(themeProvider);
|
||||||
|
|
||||||
// Trigger exchange rate fetch on app start
|
|
||||||
ref.watch(ratesInitProvider);
|
ref.watch(ratesInitProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
|
|||||||
@@ -211,5 +211,4 @@ class AppTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep for backward compatibility
|
|
||||||
ThemeData buildAppTheme() => AppTheme.darkTheme;
|
ThemeData buildAppTheme() => AppTheme.darkTheme;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class AppColors {
|
|||||||
static const divider = Color(0xFF2A2A38);
|
static const divider = Color(0xFF2A2A38);
|
||||||
static const warning = Color(0xFFFFB74D);
|
static const warning = Color(0xFFFFB74D);
|
||||||
|
|
||||||
// Light theme colors
|
|
||||||
static const lightBackground = Color(0xFFF5F5F7);
|
static const lightBackground = Color(0xFFF5F5F7);
|
||||||
static const lightSurface = Color(0xFFFFFFFF);
|
static const lightSurface = Color(0xFFFFFFFF);
|
||||||
static const lightTextPrimary = Color(0xFF1A1A24);
|
static const lightTextPrimary = Color(0xFF1A1A24);
|
||||||
@@ -96,7 +95,6 @@ extension AmountFormatExt on AmountFormat {
|
|||||||
String format(double amount) {
|
String format(double amount) {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case AmountFormat.commasDot:
|
case AmountFormat.commasDot:
|
||||||
// groups of 3 with commas, dot decimal
|
|
||||||
final parts = amount.toStringAsFixed(2).split('.');
|
final parts = amount.toStringAsFixed(2).split('.');
|
||||||
final intPart = parts[0].replaceAllMapped(
|
final intPart = parts[0].replaceAllMapped(
|
||||||
RegExp(r'(\d)(?=(\d{3})+$)'), (m) => '${m[1]},');
|
RegExp(r'(\d)(?=(\d{3})+$)'), (m) => '${m[1]},');
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ class AddTransactionNotifier extends StateNotifier<AddTransactionState> {
|
|||||||
void setCategory(String v) => state = state.copyWith(category: v);
|
void setCategory(String v) => state = state.copyWith(category: v);
|
||||||
|
|
||||||
void setType(TransactionType v) {
|
void setType(TransactionType v) {
|
||||||
// Reset category to first item of new type
|
|
||||||
final newCategory = AppCategories.forType(v).first;
|
final newCategory = AppCategories.forType(v).first;
|
||||||
state = state.copyWith(type: v, category: newCategory);
|
state = state.copyWith(type: v, category: newCategory);
|
||||||
}
|
}
|
||||||
@@ -102,7 +101,6 @@ final addTransactionProvider = StateNotifierProvider.autoDispose
|
|||||||
(ref, initial) => AddTransactionNotifier(initial),
|
(ref, initial) => AddTransactionNotifier(initial),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reactive categories based on selected type
|
|
||||||
final availableCategoriesProvider =
|
final availableCategoriesProvider =
|
||||||
Provider.autoDispose.family<List<String>, Transaction?>((ref, initial) {
|
Provider.autoDispose.family<List<String>, Transaction?>((ref, initial) {
|
||||||
final type = ref.watch(addTransactionProvider(initial).select((s) => s.type));
|
final type = ref.watch(addTransactionProvider(initial).select((s) => s.type));
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
_amountController.text = widget.initial!.amount.toString();
|
_amountController.text = widget.initial!.amount.toString();
|
||||||
_noteController.text = widget.initial!.note ?? '';
|
_noteController.text = widget.initial!.note ?? '';
|
||||||
} else {
|
} else {
|
||||||
// Set default currency from global provider after first frame
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final curr = ref.read(currencyProvider);
|
final curr = ref.read(currencyProvider);
|
||||||
ref.read(addTransactionProvider(null).notifier).setCurrency(curr.symbol, curr.code);
|
ref.read(addTransactionProvider(null).notifier).setCurrency(curr.symbol, curr.code);
|
||||||
@@ -161,7 +160,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
children: [
|
children: [
|
||||||
// Type toggle
|
|
||||||
_TypeToggle(
|
_TypeToggle(
|
||||||
selected: state.type,
|
selected: state.type,
|
||||||
onChanged: (t) =>
|
onChanged: (t) =>
|
||||||
@@ -169,7 +167,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Amount
|
|
||||||
_SectionLabel('Amount'),
|
_SectionLabel('Amount'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
@@ -231,7 +228,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Currency
|
|
||||||
Text(
|
Text(
|
||||||
'Currency',
|
'Currency',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -247,7 +243,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Category
|
|
||||||
_SectionLabel('Category'),
|
_SectionLabel('Category'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_CategoryPicker(
|
_CategoryPicker(
|
||||||
@@ -258,7 +253,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Date
|
|
||||||
_SectionLabel('Date'),
|
_SectionLabel('Date'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
InkWell(
|
InkWell(
|
||||||
@@ -288,7 +282,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Note
|
|
||||||
_SectionLabel('Note (optional)'),
|
_SectionLabel('Note (optional)'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ final categoryExpenseProvider = Provider<Map<String, double>>((ref) {
|
|||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Monthly breakdown for last 6 months
|
|
||||||
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
|
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
|
||||||
final txs = ref.watch(transactionsProvider)
|
final txs = ref.watch(transactionsProvider)
|
||||||
.where((t) => t.type == TransactionType.expense);
|
.where((t) => t.type == TransactionType.expense);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
final total = data.values.fold(0.0, (a, b) => a + b);
|
final total = data.values.fold(0.0, (a, b) => a + b);
|
||||||
final currencyInfo = ref.watch(currencyProvider);
|
final currencyInfo = ref.watch(currencyProvider);
|
||||||
|
|
||||||
// Sort categories by amount descending
|
|
||||||
final sortedEntries = data.entries.toList()
|
final sortedEntries = data.entries.toList()
|
||||||
..sort((a, b) => b.value.compareTo(a.value));
|
..sort((a, b) => b.value.compareTo(a.value));
|
||||||
|
|
||||||
|
|||||||
@@ -45,12 +45,10 @@ class TransactionsNotifier extends StateNotifier<List<Transaction>> {
|
|||||||
|
|
||||||
void clearAll() {
|
void clearAll() {
|
||||||
state = [];
|
state = [];
|
||||||
// also clear from SharedPreferences:
|
|
||||||
SharedPreferences.getInstance().then((prefs) => prefs.remove('transactions'));
|
SharedPreferences.getInstance().then((prefs) => prefs.remove('transactions'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search and filter state
|
|
||||||
final searchQueryProvider = StateProvider<String>((ref) => '');
|
final searchQueryProvider = StateProvider<String>((ref) => '');
|
||||||
|
|
||||||
enum TransactionFilter { all, income, expense }
|
enum TransactionFilter { all, income, expense }
|
||||||
@@ -58,7 +56,6 @@ enum TransactionFilter { all, income, expense }
|
|||||||
final transactionFilterProvider =
|
final transactionFilterProvider =
|
||||||
StateProvider<TransactionFilter>((ref) => TransactionFilter.all);
|
StateProvider<TransactionFilter>((ref) => TransactionFilter.all);
|
||||||
|
|
||||||
// Converted balance providers (convert all transactions to selected currency)
|
|
||||||
final totalBalanceProvider = Provider<double>((ref) {
|
final totalBalanceProvider = Provider<double>((ref) {
|
||||||
final txs = ref.watch(transactionsProvider);
|
final txs = ref.watch(transactionsProvider);
|
||||||
final exchangeService = ref.watch(exchangeRateServiceProvider);
|
final exchangeService = ref.watch(exchangeRateServiceProvider);
|
||||||
@@ -111,14 +108,12 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
|
|||||||
|
|
||||||
var filtered = txs;
|
var filtered = txs;
|
||||||
|
|
||||||
// Apply type filter
|
|
||||||
if (filter == TransactionFilter.income) {
|
if (filter == TransactionFilter.income) {
|
||||||
filtered = filtered.where((t) => t.type == TransactionType.income).toList();
|
filtered = filtered.where((t) => t.type == TransactionType.income).toList();
|
||||||
} else if (filter == TransactionFilter.expense) {
|
} else if (filter == TransactionFilter.expense) {
|
||||||
filtered = filtered.where((t) => t.type == TransactionType.expense).toList();
|
filtered = filtered.where((t) => t.type == TransactionType.expense).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search query
|
|
||||||
if (query.isNotEmpty) {
|
if (query.isNotEmpty) {
|
||||||
filtered = filtered.where((t) {
|
filtered = filtered.where((t) {
|
||||||
final matchesCategory = t.category.toLowerCase().contains(query);
|
final matchesCategory = t.category.toLowerCase().contains(query);
|
||||||
@@ -127,7 +122,6 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date descending
|
|
||||||
filtered.sort((a, b) => b.date.compareTo(a.date));
|
filtered.sort((a, b) => b.date.compareTo(a.date));
|
||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import '../../shared/providers/amount_format_provider.dart';
|
|||||||
import '../settings/provider.dart';
|
import '../settings/provider.dart';
|
||||||
import 'provider.dart';
|
import 'provider.dart';
|
||||||
|
|
||||||
// Helper for balance card only - hides .00 decimals
|
|
||||||
String _smartBalance(double amount, AmountFormat fmt, String symbol) {
|
String _smartBalance(double amount, AmountFormat fmt, String symbol) {
|
||||||
const spaceAfter = {'Br'};
|
const spaceAfter = {'Br'};
|
||||||
final sep = spaceAfter.contains(symbol) ? ' ' : '';
|
final sep = spaceAfter.contains(symbol) ? ' ' : '';
|
||||||
@@ -20,7 +19,6 @@ String _smartBalance(double amount, AmountFormat fmt, String symbol) {
|
|||||||
|
|
||||||
String formatted;
|
String formatted;
|
||||||
if (isWhole) {
|
if (isWhole) {
|
||||||
// format the integer, then manually remove .00
|
|
||||||
formatted = fmt.format(amount);
|
formatted = fmt.format(amount);
|
||||||
if (formatted.endsWith('.00')) {
|
if (formatted.endsWith('.00')) {
|
||||||
formatted = formatted.substring(0, formatted.length - 3);
|
formatted = formatted.substring(0, formatted.length - 3);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class BudgetNotifier extends StateNotifier<double?> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currency info: symbol and code
|
|
||||||
class CurrencyInfo {
|
class CurrencyInfo {
|
||||||
final String symbol;
|
final String symbol;
|
||||||
final String code;
|
final String code;
|
||||||
@@ -95,7 +94,6 @@ final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
|||||||
(ref) => ThemeModeNotifier(),
|
(ref) => ThemeModeNotifier(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Exchange rate service
|
|
||||||
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
|
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
|
||||||
final prefs = ref.watch(sharedPreferencesProvider);
|
final prefs = ref.watch(sharedPreferencesProvider);
|
||||||
return ExchangeRateService(prefs);
|
return ExchangeRateService(prefs);
|
||||||
@@ -119,11 +117,9 @@ class ExportService {
|
|||||||
final currency = _ref.read(currencyProvider);
|
final currency = _ref.read(currencyProvider);
|
||||||
final fmt = _ref.read(amountFormatProvider);
|
final fmt = _ref.read(amountFormatProvider);
|
||||||
|
|
||||||
// CSV header
|
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.writeln('Date,Type,Category,Amount,Currency,Note');
|
buffer.writeln('Date,Type,Category,Amount,Currency,Note');
|
||||||
|
|
||||||
// CSV rows
|
|
||||||
for (final tx in transactions) {
|
for (final tx in transactions) {
|
||||||
final date = DateFormat('yyyy-MM-dd').format(tx.date);
|
final date = DateFormat('yyyy-MM-dd').format(tx.date);
|
||||||
final type = tx.type.name;
|
final type = tx.type.name;
|
||||||
@@ -133,7 +129,6 @@ class ExportService {
|
|||||||
buffer.writeln('$date,$type,$category,$amount,${tx.currencyCode},$note');
|
buffer.writeln('$date,$type,$category,$amount,${tx.currencyCode},$note');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to Downloads
|
|
||||||
final directory = await getApplicationDocumentsDirectory();
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
||||||
final file = File('${directory.path}/transactions_$timestamp.csv');
|
final file = File('${directory.path}/transactions_$timestamp.csv');
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _confirmClearData(BuildContext context, WidgetRef ref) {
|
void _confirmClearData(BuildContext context, WidgetRef ref) {
|
||||||
// First confirmation
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
@@ -66,7 +65,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
// Second confirmation
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx2) => AlertDialog(
|
builder: (ctx2) => AlertDialog(
|
||||||
@@ -109,7 +107,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
// Update currency format when it changes
|
|
||||||
_currencyFmt = NumberFormat.currency(symbol: currencyInfo.symbol, decimalDigits: 2);
|
_currencyFmt = NumberFormat.currency(symbol: currencyInfo.symbol, decimalDigits: 2);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -139,7 +136,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
// Theme Toggle
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -194,7 +190,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Budget Setting
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -310,7 +305,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Amount Format Selector
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -393,7 +387,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Currency Selector
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -486,7 +479,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Danger Zone
|
|
||||||
Text(
|
Text(
|
||||||
'Danger Zone',
|
'Danger Zone',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class ExchangeRateService {
|
|||||||
|
|
||||||
Future<void> fetchRates() async {
|
Future<void> fetchRates() async {
|
||||||
try {
|
try {
|
||||||
// Try primary URL
|
|
||||||
final response = await http
|
final response = await http
|
||||||
.get(Uri.parse(_primaryUrl))
|
.get(Uri.parse(_primaryUrl))
|
||||||
.timeout(const Duration(seconds: 10));
|
.timeout(const Duration(seconds: 10));
|
||||||
@@ -60,11 +59,9 @@ class ExchangeRateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Primary failed, try fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try fallback URL
|
|
||||||
final response = await http
|
final response = await http
|
||||||
.get(Uri.parse(_fallbackUrl))
|
.get(Uri.parse(_fallbackUrl))
|
||||||
.timeout(const Duration(seconds: 10));
|
.timeout(const Duration(seconds: 10));
|
||||||
@@ -84,10 +81,8 @@ class ExchangeRateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Both failed, use cached or fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both failed and no cache, use fallback
|
|
||||||
if (_rates.isEmpty) {
|
if (_rates.isEmpty) {
|
||||||
_rates = Map.from(_fallbackRates);
|
_rates = Map.from(_fallbackRates);
|
||||||
}
|
}
|
||||||
@@ -104,7 +99,6 @@ class ExchangeRateService {
|
|||||||
final fromRate = currentRates[from] ?? 1.0;
|
final fromRate = currentRates[from] ?? 1.0;
|
||||||
final toRate = currentRates[to] ?? 1.0;
|
final toRate = currentRates[to] ?? 1.0;
|
||||||
|
|
||||||
// Convert to USD first, then to target currency
|
|
||||||
final amountInUsd = amount / fromRate;
|
final amountInUsd = amount / fromRate;
|
||||||
return amountInUsd * toRate;
|
return amountInUsd * toRate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,14 +70,13 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool loadThemeMode() {
|
bool loadThemeMode() {
|
||||||
return _prefs.getBool(_themeKey) ?? true; // default dark
|
return _prefs.getBool(_themeKey) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveThemeMode(bool isDark) async {
|
Future<void> saveThemeMode(bool isDark) async {
|
||||||
await _prefs.setBool(_themeKey, isDark);
|
await _prefs.setBool(_themeKey, isDark);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process recurring transactions
|
|
||||||
Future<void> processRecurringTransactions() async {
|
Future<void> processRecurringTransactions() async {
|
||||||
final transactions = loadTransactions();
|
final transactions = loadTransactions();
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
@@ -115,7 +114,6 @@ class StorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
// Create new occurrence
|
|
||||||
final newTx = Transaction(
|
final newTx = Transaction(
|
||||||
id: _uuid.v4(),
|
id: _uuid.v4(),
|
||||||
amount: tx.amount,
|
amount: tx.amount,
|
||||||
@@ -128,7 +126,6 @@ class StorageService {
|
|||||||
);
|
);
|
||||||
transactions.add(newTx);
|
transactions.add(newTx);
|
||||||
|
|
||||||
// Update original transaction's lastOccurrence
|
|
||||||
final index = transactions.indexWhere((t) => t.id == tx.id);
|
final index = transactions.indexWhere((t) => t.id == tx.id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
transactions[index] = tx.copyWith(lastOccurrence: today);
|
transactions[index] = tx.copyWith(lastOccurrence: today);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import '../../core/constants.dart';
|
import '../../core/constants.dart';
|
||||||
|
|
||||||
String formatAmount(String symbol, double amount, AmountFormat fmt) {
|
String formatAmount(String symbol, double amount, AmountFormat fmt) {
|
||||||
// Symbols that need a space after them (prefix symbols like Br, ₽ etc.)
|
|
||||||
const spaceAfter = {'Br'};
|
const spaceAfter = {'Br'};
|
||||||
final formatted = fmt.format(amount);
|
final formatted = fmt.format(amount);
|
||||||
final sep = spaceAfter.contains(symbol) ? ' ' : '';
|
final sep = spaceAfter.contains(symbol) ? ' ' : '';
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.9"
|
||||||
|
args:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: args
|
||||||
|
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -25,6 +41,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
checked_yaml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: checked_yaml
|
||||||
|
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.4"
|
||||||
|
cli_util:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cli_util
|
||||||
|
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.2"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -118,6 +150,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_launcher_icons:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_launcher_icons
|
||||||
|
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.14.4"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -192,6 +232,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.8.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -200,6 +248,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "0.19.0"
|
||||||
|
json_annotation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: json_annotation
|
||||||
|
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.11.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -336,6 +392,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -352,6 +416,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -549,6 +621,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -21,10 +21,22 @@ dependencies:
|
|||||||
http: ^1.2.0
|
http: ^1.2.0
|
||||||
sensors_plus: ^6.1.0
|
sensors_plus: ^6.1.0
|
||||||
|
|
||||||
|
flutter_launcher_icons:
|
||||||
|
android: true
|
||||||
|
ios: true
|
||||||
|
image_path: "assets/icon/icon.png"
|
||||||
|
min_sdk_android: 21
|
||||||
|
web:
|
||||||
|
generate: true
|
||||||
|
image_path: "assets/icon/icon.png"
|
||||||
|
adaptive_icon_background: "#0F0F14"
|
||||||
|
adaptive_icon_foreground: "assets/icon/icon.png"
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
|
flutter_launcher_icons: ^0.14.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 68 KiB |