This commit is contained in:
2026-03-20 20:32:25 +03:00
parent a2931618b2
commit 50a34bf277
58 changed files with 110 additions and 172 deletions
-1
View File
@@ -11,7 +11,6 @@ class App extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeProvider);
// Trigger exchange rate fetch on app start
ref.watch(ratesInitProvider);
return MaterialApp.router(
-1
View File
@@ -211,5 +211,4 @@ class AppTheme {
}
}
// Keep for backward compatibility
ThemeData buildAppTheme() => AppTheme.darkTheme;
-2
View File
@@ -12,7 +12,6 @@ class AppColors {
static const divider = Color(0xFF2A2A38);
static const warning = Color(0xFFFFB74D);
// Light theme colors
static const lightBackground = Color(0xFFF5F5F7);
static const lightSurface = Color(0xFFFFFFFF);
static const lightTextPrimary = Color(0xFF1A1A24);
@@ -96,7 +95,6 @@ extension AmountFormatExt on AmountFormat {
String format(double amount) {
switch (this) {
case AmountFormat.commasDot:
// groups of 3 with commas, dot decimal
final parts = amount.toStringAsFixed(2).split('.');
final intPart = parts[0].replaceAllMapped(
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 setType(TransactionType v) {
// Reset category to first item of new type
final newCategory = AppCategories.forType(v).first;
state = state.copyWith(type: v, category: newCategory);
}
@@ -102,7 +101,6 @@ final addTransactionProvider = StateNotifierProvider.autoDispose
(ref, initial) => AddTransactionNotifier(initial),
);
// Reactive categories based on selected type
final availableCategoriesProvider =
Provider.autoDispose.family<List<String>, Transaction?>((ref, initial) {
final type = ref.watch(addTransactionProvider(initial).select((s) => s.type));
-7
View File
@@ -40,7 +40,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
_amountController.text = widget.initial!.amount.toString();
_noteController.text = widget.initial!.note ?? '';
} else {
// Set default currency from global provider after first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
final curr = ref.read(currencyProvider);
ref.read(addTransactionProvider(null).notifier).setCurrency(curr.symbol, curr.code);
@@ -161,7 +160,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
child: ListView(
padding: const EdgeInsets.all(20),
children: [
// Type toggle
_TypeToggle(
selected: state.type,
onChanged: (t) =>
@@ -169,7 +167,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
),
const SizedBox(height: 24),
// Amount
_SectionLabel('Amount'),
const SizedBox(height: 8),
Container(
@@ -231,7 +228,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
),
const SizedBox(height: 20),
// Currency
Text(
'Currency',
style: TextStyle(
@@ -247,7 +243,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
),
const SizedBox(height: 20),
// Category
_SectionLabel('Category'),
const SizedBox(height: 8),
_CategoryPicker(
@@ -258,7 +253,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
),
const SizedBox(height: 20),
// Date
_SectionLabel('Date'),
const SizedBox(height: 8),
InkWell(
@@ -288,7 +282,6 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
),
const SizedBox(height: 20),
// Note
_SectionLabel('Note (optional)'),
const SizedBox(height: 8),
TextFormField(
-1
View File
@@ -13,7 +13,6 @@ final categoryExpenseProvider = Provider<Map<String, double>>((ref) {
return map;
});
// Monthly breakdown for last 6 months
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
final txs = ref.watch(transactionsProvider)
.where((t) => t.type == TransactionType.expense);
-1
View File
@@ -29,7 +29,6 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
final total = data.values.fold(0.0, (a, b) => a + b);
final currencyInfo = ref.watch(currencyProvider);
// Sort categories by amount descending
final sortedEntries = data.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
-6
View File
@@ -45,12 +45,10 @@ class TransactionsNotifier extends StateNotifier<List<Transaction>> {
void clearAll() {
state = [];
// also clear from SharedPreferences:
SharedPreferences.getInstance().then((prefs) => prefs.remove('transactions'));
}
}
// Search and filter state
final searchQueryProvider = StateProvider<String>((ref) => '');
enum TransactionFilter { all, income, expense }
@@ -58,7 +56,6 @@ enum TransactionFilter { all, income, expense }
final transactionFilterProvider =
StateProvider<TransactionFilter>((ref) => TransactionFilter.all);
// Converted balance providers (convert all transactions to selected currency)
final totalBalanceProvider = Provider<double>((ref) {
final txs = ref.watch(transactionsProvider);
final exchangeService = ref.watch(exchangeRateServiceProvider);
@@ -111,14 +108,12 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
var filtered = txs;
// Apply type filter
if (filter == TransactionFilter.income) {
filtered = filtered.where((t) => t.type == TransactionType.income).toList();
} else if (filter == TransactionFilter.expense) {
filtered = filtered.where((t) => t.type == TransactionType.expense).toList();
}
// Apply search query
if (query.isNotEmpty) {
filtered = filtered.where((t) {
final matchesCategory = t.category.toLowerCase().contains(query);
@@ -127,7 +122,6 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
}).toList();
}
// Sort by date descending
filtered.sort((a, b) => b.date.compareTo(a.date));
return filtered;
});
-2
View File
@@ -12,7 +12,6 @@ import '../../shared/providers/amount_format_provider.dart';
import '../settings/provider.dart';
import 'provider.dart';
// Helper for balance card only - hides .00 decimals
String _smartBalance(double amount, AmountFormat fmt, String symbol) {
const spaceAfter = {'Br'};
final sep = spaceAfter.contains(symbol) ? ' ' : '';
@@ -20,7 +19,6 @@ String _smartBalance(double amount, AmountFormat fmt, String symbol) {
String formatted;
if (isWhole) {
// format the integer, then manually remove .00
formatted = fmt.format(amount);
if (formatted.endsWith('.00')) {
formatted = formatted.substring(0, formatted.length - 3);
-5
View File
@@ -32,7 +32,6 @@ class BudgetNotifier extends StateNotifier<double?> {
}
}
// Currency info: symbol and code
class CurrencyInfo {
final String symbol;
final String code;
@@ -95,7 +94,6 @@ final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(),
);
// Exchange rate service
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
final prefs = ref.watch(sharedPreferencesProvider);
return ExchangeRateService(prefs);
@@ -119,11 +117,9 @@ class ExportService {
final currency = _ref.read(currencyProvider);
final fmt = _ref.read(amountFormatProvider);
// CSV header
final buffer = StringBuffer();
buffer.writeln('Date,Type,Category,Amount,Currency,Note');
// CSV rows
for (final tx in transactions) {
final date = DateFormat('yyyy-MM-dd').format(tx.date);
final type = tx.type.name;
@@ -133,7 +129,6 @@ class ExportService {
buffer.writeln('$date,$type,$category,$amount,${tx.currencyCode},$note');
}
// Save to Downloads
final directory = await getApplicationDocumentsDirectory();
final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
final file = File('${directory.path}/transactions_$timestamp.csv');
-8
View File
@@ -52,7 +52,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
}
void _confirmClearData(BuildContext context, WidgetRef ref) {
// First confirmation
showDialog(
context: context,
builder: (ctx) => AlertDialog(
@@ -66,7 +65,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
TextButton(
onPressed: () {
Navigator.pop(ctx);
// Second confirmation
showDialog(
context: context,
builder: (ctx2) => AlertDialog(
@@ -109,7 +107,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final fmt = ref.watch(amountFormatProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
// Update currency format when it changes
_currencyFmt = NumberFormat.currency(symbol: currencyInfo.symbol, decimalDigits: 2);
return Scaffold(
@@ -139,7 +136,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
children: [
// Theme Toggle
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -194,7 +190,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
const SizedBox(height: 16),
// Budget Setting
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -310,7 +305,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
const SizedBox(height: 16),
// Amount Format Selector
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -393,7 +387,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
const SizedBox(height: 16),
// Currency Selector
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -486,7 +479,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
const SizedBox(height: 24),
// Danger Zone
Text(
'Danger Zone',
style: TextStyle(
@@ -40,7 +40,6 @@ class ExchangeRateService {
Future<void> fetchRates() async {
try {
// Try primary URL
final response = await http
.get(Uri.parse(_primaryUrl))
.timeout(const Duration(seconds: 10));
@@ -60,11 +59,9 @@ class ExchangeRateService {
}
}
} catch (e) {
// Primary failed, try fallback
}
try {
// Try fallback URL
final response = await http
.get(Uri.parse(_fallbackUrl))
.timeout(const Duration(seconds: 10));
@@ -84,10 +81,8 @@ class ExchangeRateService {
}
}
} catch (e) {
// Both failed, use cached or fallback
}
// If both failed and no cache, use fallback
if (_rates.isEmpty) {
_rates = Map.from(_fallbackRates);
}
@@ -104,7 +99,6 @@ class ExchangeRateService {
final fromRate = currentRates[from] ?? 1.0;
final toRate = currentRates[to] ?? 1.0;
// Convert to USD first, then to target currency
final amountInUsd = amount / fromRate;
return amountInUsd * toRate;
}
+1 -4
View File
@@ -70,14 +70,13 @@ class StorageService {
}
bool loadThemeMode() {
return _prefs.getBool(_themeKey) ?? true; // default dark
return _prefs.getBool(_themeKey) ?? true;
}
Future<void> saveThemeMode(bool isDark) async {
await _prefs.setBool(_themeKey, isDark);
}
// Process recurring transactions
Future<void> processRecurringTransactions() async {
final transactions = loadTransactions();
final now = DateTime.now();
@@ -115,7 +114,6 @@ class StorageService {
}
if (shouldCreate) {
// Create new occurrence
final newTx = Transaction(
id: _uuid.v4(),
amount: tx.amount,
@@ -128,7 +126,6 @@ class StorageService {
);
transactions.add(newTx);
// Update original transaction's lastOccurrence
final index = transactions.indexWhere((t) => t.id == tx.id);
if (index != -1) {
transactions[index] = tx.copyWith(lastOccurrence: today);
-1
View File
@@ -1,7 +1,6 @@
import '../../core/constants.dart';
String formatAmount(String symbol, double amount, AmountFormat fmt) {
// Symbols that need a space after them (prefix symbols like Br, ₽ etc.)
const spaceAfter = {'Br'};
final formatted = fmt.format(amount);
final sep = spaceAfter.contains(symbol) ? ' ' : '';