mirror of
https://github.com/koloideal/Casha.git
synced 2026-08-08 17:51:14 +03:00
step
This commit is contained in:
@@ -22,6 +22,10 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.kolo.casha"
|
applicationId = "com.kolo.casha"
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = flutter.minSdkVersion
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
@@ -115,9 +115,11 @@ class AppStrings {
|
|||||||
String get dangerZone => _ru ? 'Опасная зона' : 'Danger Zone';
|
String get dangerZone => _ru ? 'Опасная зона' : 'Danger Zone';
|
||||||
|
|
||||||
String get navDashboard => _ru ? 'Главная' : 'Dashboard';
|
String get navDashboard => _ru ? 'Главная' : 'Dashboard';
|
||||||
String get navCategories => _ru ? 'Категории' : 'Categories';
|
String get navCategories => _ru ? 'Статистика' : 'Statistics';
|
||||||
String get navSettings => _ru ? 'Настройки' : 'Settings';
|
String get navSettings => _ru ? 'Настройки' : 'Settings';
|
||||||
|
|
||||||
|
String get statistics => _ru ? 'Статистика' : 'Statistics';
|
||||||
|
String get allAccounts => _ru ? 'Все счета' : 'All accounts';
|
||||||
String get categories => _ru ? 'Категории' : 'Categories';
|
String get categories => _ru ? 'Категории' : 'Categories';
|
||||||
String get rankedByAmount => _ru ? 'По сумме' : 'Ranked by Amount';
|
String get rankedByAmount => _ru ? 'По сумме' : 'Ranked by Amount';
|
||||||
String get addCategory => _ru ? 'Добавить категорию' : 'Add Category';
|
String get addCategory => _ru ? 'Добавить категорию' : 'Add Category';
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const kBalanceCardHeight = 200.0;
|
||||||
|
const kBalanceCardCarouselHeight = 210.0;
|
||||||
|
const kAddAccountCardHeight = 200.0;
|
||||||
|
|
||||||
|
class CardOverlayLayout {
|
||||||
|
final bool compact;
|
||||||
|
final double cardHeight;
|
||||||
|
final double cardTop;
|
||||||
|
final double cardPreviewGap;
|
||||||
|
final double editorPanelHeight;
|
||||||
|
final double sectionGap;
|
||||||
|
final double panelPaddingTop;
|
||||||
|
final double panelPaddingBottom;
|
||||||
|
final double reservedBelowControls;
|
||||||
|
final double hueSliderHeight;
|
||||||
|
final double hexRowHeight;
|
||||||
|
final double tabSpacing;
|
||||||
|
final double controlSpacing;
|
||||||
|
final double buttonVerticalPadding;
|
||||||
|
|
||||||
|
const CardOverlayLayout._({
|
||||||
|
required this.compact,
|
||||||
|
required this.cardHeight,
|
||||||
|
required this.cardTop,
|
||||||
|
required this.cardPreviewGap,
|
||||||
|
required this.editorPanelHeight,
|
||||||
|
required this.sectionGap,
|
||||||
|
required this.panelPaddingTop,
|
||||||
|
required this.panelPaddingBottom,
|
||||||
|
required this.reservedBelowControls,
|
||||||
|
required this.hueSliderHeight,
|
||||||
|
required this.hexRowHeight,
|
||||||
|
required this.tabSpacing,
|
||||||
|
required this.controlSpacing,
|
||||||
|
required this.buttonVerticalPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CardOverlayLayout.fromMediaQuery(MediaQueryData mq) {
|
||||||
|
final compact = mq.size.height < 780;
|
||||||
|
return CardOverlayLayout._(
|
||||||
|
compact: compact,
|
||||||
|
cardHeight: kBalanceCardHeight,
|
||||||
|
cardTop: mq.padding.top + kToolbarHeight + (compact ? 8 : 16),
|
||||||
|
cardPreviewGap: compact ? 12 : 32,
|
||||||
|
editorPanelHeight: compact ? 88 : 96,
|
||||||
|
sectionGap: compact ? 8 : 12,
|
||||||
|
panelPaddingTop: compact ? 10 : 14,
|
||||||
|
panelPaddingBottom: compact ? 14 : 22,
|
||||||
|
reservedBelowControls: compact ? 62 : 78,
|
||||||
|
hueSliderHeight: compact ? 28 : 36,
|
||||||
|
hexRowHeight: compact ? 22 : 26,
|
||||||
|
tabSpacing: compact ? 6 : 10,
|
||||||
|
controlSpacing: compact ? 5 : 8,
|
||||||
|
buttonVerticalPadding: compact ? 8 : 10,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double colorPanelHeight(MediaQueryData mq, double panelTop) {
|
||||||
|
final available = mq.size.height - panelTop - mq.padding.bottom - 8;
|
||||||
|
return available.clamp(compact ? 250 : 320, 410);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +1,132 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../shared/models/transaction.dart';
|
import '../../shared/models/transaction.dart';
|
||||||
import '../dashboard/provider.dart';
|
import '../dashboard/provider.dart';
|
||||||
|
import '../settings/provider.dart';
|
||||||
|
|
||||||
|
String _statsTargetCurrency(Ref ref) {
|
||||||
|
final index = ref.watch(activeAccountIndexProvider);
|
||||||
|
final accountsAsync = ref.watch(accountsProvider);
|
||||||
|
final globalCurrency = ref.watch(currencyProvider).code;
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
final accounts = accountsAsync.value ?? [];
|
||||||
|
if (index <= accounts.length) {
|
||||||
|
return accounts[index - 1].currency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalCurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrencyInfo _statsCurrencyInfo(Ref ref) {
|
||||||
|
final code = _statsTargetCurrency(ref);
|
||||||
|
return CurrencyInfo(currencyMap[code]?.symbol ?? '\$', code);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Transaction> _statsScopedTransactions(Ref ref) {
|
||||||
|
final txs = ref.watch(accountFilteredTransactionsProvider);
|
||||||
|
final timeFilter = ref.watch(timeFilterProvider);
|
||||||
|
var filtered = txs.where((t) => t.category != 'Transfer');
|
||||||
|
|
||||||
|
if (timeFilter == TimeFilter.lastMonth) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
filtered = filtered.where(
|
||||||
|
(t) => t.date.year == now.year && t.date.month == now.month,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _convertAmount(Ref ref, Transaction t) {
|
||||||
|
final exchange = ref.watch(exchangeRateServiceProvider);
|
||||||
|
return exchange.convert(
|
||||||
|
t.amount,
|
||||||
|
t.currencyCode,
|
||||||
|
_statsTargetCurrency(ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final statsCurrencyProvider = Provider<CurrencyInfo>((ref) {
|
||||||
|
return _statsCurrencyInfo(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
final statsIncomeTotalProvider = Provider<double>((ref) {
|
||||||
|
return _statsScopedTransactions(ref)
|
||||||
|
.where((t) => t.type == TransactionType.income)
|
||||||
|
.fold(0.0, (sum, t) => sum + _convertAmount(ref, t));
|
||||||
|
});
|
||||||
|
|
||||||
|
final statsExpenseTotalProvider = Provider<double>((ref) {
|
||||||
|
return _statsScopedTransactions(ref)
|
||||||
|
.where((t) => t.type == TransactionType.expense)
|
||||||
|
.fold(0.0, (sum, t) => sum + _convertAmount(ref, t));
|
||||||
|
});
|
||||||
|
|
||||||
final categoryExpenseProvider = Provider<Map<String, double>>((ref) {
|
final categoryExpenseProvider = Provider<Map<String, double>>((ref) {
|
||||||
final txsAsync = ref.watch(transactionsProvider);
|
|
||||||
final txs = txsAsync.value ?? [];
|
|
||||||
final filtered = txs.where((t) => t.type == TransactionType.expense);
|
|
||||||
|
|
||||||
final map = <String, double>{};
|
final map = <String, double>{};
|
||||||
for (final t in filtered) {
|
for (final t in _statsScopedTransactions(ref)) {
|
||||||
map[t.category] = (map[t.category] ?? 0) + t.amount;
|
if (t.type != TransactionType.expense) continue;
|
||||||
|
map[t.category] = (map[t.category] ?? 0) + _convertAmount(ref, t);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
final categoryIncomeProvider = Provider<Map<String, double>>((ref) {
|
final categoryIncomeProvider = Provider<Map<String, double>>((ref) {
|
||||||
final txsAsync = ref.watch(transactionsProvider);
|
|
||||||
final txs = txsAsync.value ?? [];
|
|
||||||
final filtered = txs.where((t) => t.type == TransactionType.income);
|
|
||||||
|
|
||||||
final map = <String, double>{};
|
final map = <String, double>{};
|
||||||
for (final t in filtered) {
|
for (final t in _statsScopedTransactions(ref)) {
|
||||||
map[t.category] = (map[t.category] ?? 0) + t.amount;
|
if (t.type != TransactionType.income) continue;
|
||||||
|
map[t.category] = (map[t.category] ?? 0) + _convertAmount(ref, t);
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
|
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
|
||||||
final txsAsync = ref.watch(transactionsProvider);
|
final txs = ref.watch(accountFilteredTransactionsProvider);
|
||||||
final txs = txsAsync.value ?? [];
|
final exchange = ref.watch(exchangeRateServiceProvider);
|
||||||
final filtered = txs.where((t) => t.type == TransactionType.expense);
|
final target = _statsTargetCurrency(ref);
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final months = <MonthlyData>[];
|
final months = <MonthlyData>[];
|
||||||
|
|
||||||
for (var i = 5; i >= 0; i--) {
|
for (var i = 5; i >= 0; i--) {
|
||||||
final month = DateTime(now.year, now.month - i, 1);
|
final month = DateTime(now.year, now.month - i, 1);
|
||||||
final total = filtered
|
final total = txs
|
||||||
.where((t) => t.date.year == month.year && t.date.month == month.month)
|
.where(
|
||||||
.fold(0.0, (sum, t) => sum + t.amount);
|
(t) =>
|
||||||
|
t.type == TransactionType.expense &&
|
||||||
|
t.category != 'Transfer' &&
|
||||||
|
t.date.year == month.year &&
|
||||||
|
t.date.month == month.month,
|
||||||
|
)
|
||||||
|
.fold(0.0, (sum, t) {
|
||||||
|
return sum + exchange.convert(t.amount, t.currencyCode, target);
|
||||||
|
});
|
||||||
|
months.add(MonthlyData(month: month, amount: total));
|
||||||
|
}
|
||||||
|
|
||||||
|
return months;
|
||||||
|
});
|
||||||
|
|
||||||
|
final monthlyIncomeBreakdownProvider = Provider<List<MonthlyData>>((ref) {
|
||||||
|
final txs = ref.watch(accountFilteredTransactionsProvider);
|
||||||
|
final exchange = ref.watch(exchangeRateServiceProvider);
|
||||||
|
final target = _statsTargetCurrency(ref);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final months = <MonthlyData>[];
|
||||||
|
|
||||||
|
for (var i = 5; i >= 0; i--) {
|
||||||
|
final month = DateTime(now.year, now.month - i, 1);
|
||||||
|
final total = txs
|
||||||
|
.where(
|
||||||
|
(t) =>
|
||||||
|
t.type == TransactionType.income &&
|
||||||
|
t.category != 'Transfer' &&
|
||||||
|
t.date.year == month.year &&
|
||||||
|
t.date.month == month.month,
|
||||||
|
)
|
||||||
|
.fold(0.0, (sum, t) {
|
||||||
|
return sum + exchange.convert(t.amount, t.currencyCode, target);
|
||||||
|
});
|
||||||
months.add(MonthlyData(month: month, amount: total));
|
months.add(MonthlyData(month: month, amount: total));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../core/constants.dart';
|
import '../../core/constants.dart';
|
||||||
|
import '../../core/l10n/app_strings.dart';
|
||||||
import '../../core/l10n/locale_provider.dart';
|
import '../../core/l10n/locale_provider.dart';
|
||||||
import '../../shared/utils/currency_utils.dart';
|
import '../../core/services/haptic_service.dart';
|
||||||
import '../../shared/providers/amount_format_provider.dart';
|
import '../../shared/providers/amount_format_provider.dart';
|
||||||
|
import '../../shared/utils/currency_utils.dart';
|
||||||
import '../../shared/widgets/byn_sign.dart';
|
import '../../shared/widgets/byn_sign.dart';
|
||||||
|
import '../dashboard/provider.dart';
|
||||||
|
import '../dashboard/widgets/summary_row.dart';
|
||||||
import '../settings/provider.dart';
|
import '../settings/provider.dart';
|
||||||
import 'provider.dart';
|
import 'provider.dart';
|
||||||
|
import 'widgets/account_scope_chips.dart';
|
||||||
|
import 'widgets/stats_hero_card.dart';
|
||||||
|
|
||||||
enum ChartType { pie, bar }
|
enum ChartType { pie, bar }
|
||||||
|
|
||||||
@@ -24,15 +30,27 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
ChartType _chartType = ChartType.pie;
|
ChartType _chartType = ChartType.pie;
|
||||||
bool _showIncome = false;
|
bool _showIncome = false;
|
||||||
|
|
||||||
|
String _scopeLabel(AppStrings s) {
|
||||||
|
final activeAccount = ref.watch(activeAccountProvider);
|
||||||
|
if (activeAccount == null) return s.allAccounts;
|
||||||
|
return activeAccount.name;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
final data = _showIncome
|
final data = _showIncome
|
||||||
? ref.watch(categoryIncomeProvider)
|
? ref.watch(categoryIncomeProvider)
|
||||||
: ref.watch(categoryExpenseProvider);
|
: ref.watch(categoryExpenseProvider);
|
||||||
final monthlyData = ref.watch(monthlyBreakdownProvider);
|
final monthlyData = _showIncome
|
||||||
|
? ref.watch(monthlyIncomeBreakdownProvider)
|
||||||
|
: ref.watch(monthlyBreakdownProvider);
|
||||||
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(statsCurrencyProvider);
|
||||||
|
final income = ref.watch(statsIncomeTotalProvider);
|
||||||
|
final expense = ref.watch(statsExpenseTotalProvider);
|
||||||
|
final timeFilter = ref.watch(timeFilterProvider);
|
||||||
|
final scopeLabel = _scopeLabel(s);
|
||||||
|
|
||||||
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));
|
||||||
@@ -41,7 +59,7 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
s.categories,
|
s.statistics,
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
@@ -57,87 +75,59 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const AccountScopeChips(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
StatsHeroCard(
|
||||||
|
amount: total,
|
||||||
|
label: _showIncome ? s.income : s.expenses,
|
||||||
|
accentColor: _showIncome ? AppColors.income : AppColors.expense,
|
||||||
|
scopeLabel: scopeLabel,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SummaryRow(
|
||||||
|
income: income,
|
||||||
|
expense: expense,
|
||||||
|
currencyInfo: currencyInfo,
|
||||||
|
strings: s,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_StatsTimeFilter(
|
||||||
|
strings: s,
|
||||||
|
timeFilter: timeFilter,
|
||||||
|
onAllTime: () =>
|
||||||
|
ref.read(timeFilterProvider.notifier).set(TimeFilter.allTime),
|
||||||
|
onMonth: () => ref
|
||||||
|
.read(timeFilterProvider.notifier)
|
||||||
|
.set(TimeFilter.lastMonth),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: _TypeToggle(
|
||||||
|
label: s.expenses,
|
||||||
|
isSelected: !_showIncome,
|
||||||
|
color: AppColors.expense,
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() {
|
||||||
_showIncome = false;
|
_showIncome = false;
|
||||||
_touchedIndex = -1;
|
_touchedIndex = -1;
|
||||||
}),
|
}),
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: !_showIncome
|
|
||||||
? AppColors.accent
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: !_showIncome
|
|
||||||
? AppColors.accent
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withOpacity(0.2),
|
|
||||||
width: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
s.expenses,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: !_showIncome
|
|
||||||
? Colors.white
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: _TypeToggle(
|
||||||
|
label: s.income,
|
||||||
|
isSelected: _showIncome,
|
||||||
|
color: AppColors.income,
|
||||||
onTap: () => setState(() {
|
onTap: () => setState(() {
|
||||||
_showIncome = true;
|
_showIncome = true;
|
||||||
_touchedIndex = -1;
|
_touchedIndex = -1;
|
||||||
}),
|
}),
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _showIncome
|
|
||||||
? AppColors.accent
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(
|
|
||||||
color: _showIncome
|
|
||||||
? AppColors.accent
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withOpacity(0.2),
|
|
||||||
width: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
s.income,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: _showIncome
|
|
||||||
? Colors.white
|
|
||||||
: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -155,12 +145,14 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
total: total,
|
total: total,
|
||||||
touchedIndex: _touchedIndex,
|
touchedIndex: _touchedIndex,
|
||||||
onTouch: (i) => setState(() => _touchedIndex = i),
|
onTouch: (i) => setState(() => _touchedIndex = i),
|
||||||
currency: currencyInfo.symbol,
|
currencyInfo: currencyInfo,
|
||||||
|
isIncome: _showIncome,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
_BarChartCard(
|
_BarChartCard(
|
||||||
monthlyData: monthlyData,
|
monthlyData: monthlyData,
|
||||||
currency: currencyInfo.symbol,
|
currencyInfo: currencyInfo,
|
||||||
|
isIncome: _showIncome,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
@@ -183,7 +175,7 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
category: cat,
|
category: cat,
|
||||||
amount: amount,
|
amount: amount,
|
||||||
total: total,
|
total: total,
|
||||||
currency: currencyInfo.symbol,
|
currencyInfo: currencyInfo,
|
||||||
isIncome: _showIncome,
|
isIncome: _showIncome,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -200,6 +192,140 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _StatsTimeFilter extends StatelessWidget {
|
||||||
|
final AppStrings strings;
|
||||||
|
final TimeFilter timeFilter;
|
||||||
|
final VoidCallback onAllTime;
|
||||||
|
final VoidCallback onMonth;
|
||||||
|
|
||||||
|
const _StatsTimeFilter({
|
||||||
|
required this.strings,
|
||||||
|
required this.timeFilter,
|
||||||
|
required this.onAllTime,
|
||||||
|
required this.onMonth,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
_TimeChip(
|
||||||
|
label: strings.filterAllTime,
|
||||||
|
isSelected: timeFilter == TimeFilter.allTime,
|
||||||
|
isDark: isDark,
|
||||||
|
onTap: () {
|
||||||
|
HapticService.selection();
|
||||||
|
onAllTime();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
_TimeChip(
|
||||||
|
label: strings.filterMonth,
|
||||||
|
isSelected: timeFilter == TimeFilter.lastMonth,
|
||||||
|
isDark: isDark,
|
||||||
|
onTap: () {
|
||||||
|
HapticService.selection();
|
||||||
|
onMonth();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeChip extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool isDark;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _TimeChip({
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.isDark,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? AppColors.accent.withOpacity(0.2)
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: AppColors.accent, width: 1.5)
|
||||||
|
: isDark
|
||||||
|
? null
|
||||||
|
: Border.all(color: const Color(0xFFDDDDEE), width: 1),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
color: isSelected
|
||||||
|
? AppColors.accent
|
||||||
|
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TypeToggle extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _TypeToggle({
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.color,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? color : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? color
|
||||||
|
: Theme.of(context).colorScheme.onSurface.withOpacity(0.2),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ChartToggle extends StatelessWidget {
|
class _ChartToggle extends StatelessWidget {
|
||||||
final ChartType selected;
|
final ChartType selected;
|
||||||
final ValueChanged<ChartType> onChanged;
|
final ValueChanged<ChartType> onChanged;
|
||||||
@@ -207,10 +333,15 @@ class _ChartToggle extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: isDark
|
||||||
|
? null
|
||||||
|
: Border.all(color: const Color(0xFFDDDDEE), width: 1),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -270,14 +401,16 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
final double total;
|
final double total;
|
||||||
final int touchedIndex;
|
final int touchedIndex;
|
||||||
final ValueChanged<int> onTouch;
|
final ValueChanged<int> onTouch;
|
||||||
final String currency;
|
final CurrencyInfo currencyInfo;
|
||||||
|
final bool isIncome;
|
||||||
|
|
||||||
const _PieChartCard({
|
const _PieChartCard({
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.total,
|
required this.total,
|
||||||
required this.touchedIndex,
|
required this.touchedIndex,
|
||||||
required this.onTouch,
|
required this.onTouch,
|
||||||
required this.currency,
|
required this.currencyInfo,
|
||||||
|
required this.isIncome,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -285,12 +418,20 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
final entries = data.entries.toList();
|
final entries = data.entries.toList();
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final accent = isIncome ? AppColors.income : AppColors.expense;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white.withOpacity(0.08)
|
||||||
|
: const Color(0xFFDDDDEE),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -327,10 +468,10 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
? '${(val / total * 100).toStringAsFixed(1)}%'
|
? '${(val / total * 100).toStringAsFixed(1)}%'
|
||||||
: '',
|
: '',
|
||||||
radius: isTouched ? 60 : 50,
|
radius: isTouched ? 60 : 50,
|
||||||
titleStyle: TextStyle(
|
titleStyle: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -348,9 +489,9 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
formatAmount(currency, total, fmt),
|
formatAmount(currencyInfo.symbol, total, fmt),
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: accent,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -367,16 +508,24 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
|
|
||||||
class _BarChartCard extends ConsumerWidget {
|
class _BarChartCard extends ConsumerWidget {
|
||||||
final List<MonthlyData> monthlyData;
|
final List<MonthlyData> monthlyData;
|
||||||
final String currency;
|
final CurrencyInfo currencyInfo;
|
||||||
const _BarChartCard({required this.monthlyData, required this.currency});
|
final bool isIncome;
|
||||||
|
|
||||||
|
const _BarChartCard({
|
||||||
|
required this.monthlyData,
|
||||||
|
required this.currencyInfo,
|
||||||
|
required this.isIncome,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
final maxY = monthlyData
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
.map((e) => e.amount)
|
final barColor = isIncome ? AppColors.income : AppColors.expense;
|
||||||
.reduce((a, b) => a > b ? a : b);
|
final maxY = monthlyData.isEmpty
|
||||||
|
? 100.0
|
||||||
|
: monthlyData.map((e) => e.amount).reduce((a, b) => a > b ? a : b);
|
||||||
final adjustedMaxY = maxY * 1.2;
|
final adjustedMaxY = maxY * 1.2;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -384,6 +533,12 @@ class _BarChartCard extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white.withOpacity(0.08)
|
||||||
|
: const Color(0xFFDDDDEE),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -406,9 +561,9 @@ class _BarChartCard extends ConsumerWidget {
|
|||||||
touchTooltipData: BarTouchTooltipData(
|
touchTooltipData: BarTouchTooltipData(
|
||||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||||
return BarTooltipItem(
|
return BarTooltipItem(
|
||||||
formatAmount(currency, rod.toY, fmt),
|
formatAmount(currencyInfo.symbol, rod.toY, fmt),
|
||||||
TextStyle(
|
const TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
@@ -469,7 +624,7 @@ class _BarChartCard extends ConsumerWidget {
|
|||||||
barRods: [
|
barRods: [
|
||||||
BarChartRodData(
|
BarChartRodData(
|
||||||
toY: monthlyData[i].amount,
|
toY: monthlyData[i].amount,
|
||||||
color: AppColors.accent,
|
color: barColor,
|
||||||
width: 24,
|
width: 24,
|
||||||
borderRadius: const BorderRadius.vertical(
|
borderRadius: const BorderRadius.vertical(
|
||||||
top: Radius.circular(6),
|
top: Radius.circular(6),
|
||||||
@@ -492,14 +647,15 @@ class _CategoryRow extends ConsumerWidget {
|
|||||||
final String category;
|
final String category;
|
||||||
final double amount;
|
final double amount;
|
||||||
final double total;
|
final double total;
|
||||||
final String currency;
|
final CurrencyInfo currencyInfo;
|
||||||
final bool isIncome;
|
final bool isIncome;
|
||||||
|
|
||||||
const _CategoryRow({
|
const _CategoryRow({
|
||||||
required this.rank,
|
required this.rank,
|
||||||
required this.category,
|
required this.category,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
required this.total,
|
required this.total,
|
||||||
required this.currency,
|
required this.currencyInfo,
|
||||||
required this.isIncome,
|
required this.isIncome,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -507,6 +663,7 @@ class _CategoryRow extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
final color = AppCategories.colors[category] ?? AppColors.accent;
|
final color = AppCategories.colors[category] ?? AppColors.accent;
|
||||||
final icon = AppCategories.icons[category] ?? Icons.category_rounded;
|
final icon = AppCategories.icons[category] ?? Icons.category_rounded;
|
||||||
final pct = total > 0 ? amount / total : 0.0;
|
final pct = total > 0 ? amount / total : 0.0;
|
||||||
@@ -516,6 +673,12 @@ class _CategoryRow extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white.withOpacity(0.08)
|
||||||
|
: const Color(0xFFDDDDEE),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -565,10 +728,36 @@ class _CategoryRow extends ConsumerWidget {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
currencyInfo.code == 'BYN'
|
||||||
|
? Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
BynSign(
|
||||||
|
fontSize: 14,
|
||||||
|
color: isIncome
|
||||||
|
? AppColors.income
|
||||||
|
: AppColors.expense,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
Text(
|
Text(
|
||||||
formatAmount(currency, amount, fmt),
|
formatAmount('', amount, fmt),
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
color: isIncome ? AppColors.income : AppColors.expense,
|
?.copyWith(
|
||||||
|
color: isIncome
|
||||||
|
? AppColors.income
|
||||||
|
: AppColors.expense,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
formatAmount(currencyInfo.symbol, amount, fmt),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
|
?.copyWith(
|
||||||
|
color: isIncome
|
||||||
|
? AppColors.income
|
||||||
|
: AppColors.expense,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -607,6 +796,8 @@ class _EmptyState extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -616,6 +807,12 @@ class _EmptyState extends ConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white.withOpacity(0.08)
|
||||||
|
: const Color(0xFFDDDDEE),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.pie_chart_outline_rounded,
|
Icons.pie_chart_outline_rounded,
|
||||||
@@ -634,6 +831,7 @@ class _EmptyState extends ConsumerWidget {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
isIncome ? s.addIncomeToSeeBreakdown : s.addExpensesToSeeBreakdown,
|
isIncome ? s.addIncomeToSeeBreakdown : s.addExpensesToSeeBreakdown,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../core/constants.dart';
|
||||||
|
import '../../../core/l10n/locale_provider.dart';
|
||||||
|
import '../../../core/services/haptic_service.dart';
|
||||||
|
import '../../dashboard/provider.dart';
|
||||||
|
|
||||||
|
class AccountScopeChips extends ConsumerWidget {
|
||||||
|
const AccountScopeChips({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final s = ref.watch(stringsProvider);
|
||||||
|
final accountsAsync = ref.watch(accountsProvider);
|
||||||
|
final activeIndex = ref.watch(activeAccountIndexProvider);
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
|
return accountsAsync.when(
|
||||||
|
data: (accounts) {
|
||||||
|
if (accounts.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_ScopeChip(
|
||||||
|
label: s.allAccounts,
|
||||||
|
isSelected: activeIndex == 0,
|
||||||
|
isDark: isDark,
|
||||||
|
onTap: () {
|
||||||
|
ref.read(activeAccountIndexProvider.notifier).set(0);
|
||||||
|
HapticService.selection();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
...accounts.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key + 1;
|
||||||
|
final account = entry.value;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 6),
|
||||||
|
child: _ScopeChip(
|
||||||
|
label: account.name,
|
||||||
|
isSelected: activeIndex == index,
|
||||||
|
isDark: isDark,
|
||||||
|
onTap: () {
|
||||||
|
ref.read(activeAccountIndexProvider.notifier).set(index);
|
||||||
|
HapticService.selection();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
error: (_, __) => const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ScopeChip extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final bool isDark;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ScopeChip({
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.isDark,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? AppColors.accent.withOpacity(0.2)
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: AppColors.accent, width: 1.5)
|
||||||
|
: isDark
|
||||||
|
? null
|
||||||
|
: Border.all(color: const Color(0xFFDDDDEE), width: 1),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
color: isSelected
|
||||||
|
? AppColors.accent
|
||||||
|
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../../../core/constants.dart';
|
||||||
|
import '../../../core/services/card_color_service.dart';
|
||||||
|
import '../../../shared/providers/amount_format_provider.dart';
|
||||||
|
import '../../../shared/utils/card_gradient.dart';
|
||||||
|
import '../../../shared/utils/currency_utils.dart';
|
||||||
|
import '../../../shared/widgets/byn_sign.dart';
|
||||||
|
import '../../dashboard/provider.dart';
|
||||||
|
import '../../settings/provider.dart';
|
||||||
|
import '../provider.dart';
|
||||||
|
|
||||||
|
class StatsHeroCard extends ConsumerWidget {
|
||||||
|
final double amount;
|
||||||
|
final String label;
|
||||||
|
final Color accentColor;
|
||||||
|
final String scopeLabel;
|
||||||
|
|
||||||
|
const StatsHeroCard({
|
||||||
|
super.key,
|
||||||
|
required this.amount,
|
||||||
|
required this.label,
|
||||||
|
required this.accentColor,
|
||||||
|
required this.scopeLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
|
final currencyInfo = ref.watch(statsCurrencyProvider);
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
|
final activeAccount = ref.watch(activeAccountProvider);
|
||||||
|
final globalColors = ref.watch(cardColorsProvider);
|
||||||
|
|
||||||
|
final CardColors colors;
|
||||||
|
if (activeAccount != null) {
|
||||||
|
colors = ref.watch(accountCardColorsProvider(activeAccount.id));
|
||||||
|
} else {
|
||||||
|
colors = globalColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
final primary = Color.lerp(colors.primary, accentColor, 0.35)!;
|
||||||
|
final secondary = Color.lerp(colors.secondary, accentColor, 0.2)!;
|
||||||
|
final gradientType = colors.gradientTypeForBrightness(brightness);
|
||||||
|
final onCard = primary.computeLuminance() > 0.35
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: 128,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
gradient: buildCardGradient(primary, secondary, gradientType),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.35),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: onCard.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
scopeLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: onCard.withOpacity(0.85),
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
color: onCard.withOpacity(0.65),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: currencyInfo.code == 'BYN'
|
||||||
|
? Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
BynSign(fontSize: 28, color: onCard),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
formatAmount('', amount, fmt),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: onCard,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
formatAmount(currencyInfo.symbol, amount, fmt),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: onCard,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -362,12 +362,9 @@ final recentTransactionsProvider = Provider<List<Transaction>>((ref) {
|
|||||||
return ref.watch(filteredTransactionsProvider).take(20).toList();
|
return ref.watch(filteredTransactionsProvider).take(20).toList();
|
||||||
});
|
});
|
||||||
|
|
||||||
final accountsProvider = StreamProvider<List<Account>>((ref) async* {
|
final accountsProvider = StreamProvider<List<Account>>((ref) {
|
||||||
final repository = ref.watch(accountRepositoryProvider);
|
final repository = ref.watch(accountRepositoryProvider);
|
||||||
while (true) {
|
return repository.watchAll();
|
||||||
yield await repository.getAll();
|
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final activeAccountIndexProvider = NotifierProvider<_ActiveAccountIndexNotifier, int>(
|
final activeAccountIndexProvider = NotifierProvider<_ActiveAccountIndexNotifier, int>(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:ui';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../../core/constants.dart';
|
import '../../../../core/constants.dart';
|
||||||
|
import '../../../../core/utils/card_layout.dart';
|
||||||
import '../../../../shared/models/account.dart';
|
import '../../../../shared/models/account.dart';
|
||||||
import '../../../../shared/models/transaction.dart';
|
import '../../../../shared/models/transaction.dart';
|
||||||
import '../../../../shared/widgets/byn_sign.dart';
|
import '../../../../shared/widgets/byn_sign.dart';
|
||||||
@@ -84,12 +85,13 @@ class _AccountEditorOverlayState extends State<AccountEditorOverlay> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mq = MediaQuery.of(widget.context);
|
final mq = MediaQuery.of(widget.context);
|
||||||
final cardTop = mq.padding.top + kToolbarHeight + 16;
|
final layout = CardOverlayLayout.fromMediaQuery(mq);
|
||||||
const cardHeight = 190.0;
|
final cardTop = layout.cardTop;
|
||||||
const editorPanelHeight = 102.0;
|
final cardHeight = layout.cardHeight;
|
||||||
final editorPanelTop = cardTop + cardHeight + 20;
|
final editorPanelHeight = layout.editorPanelHeight;
|
||||||
final colorPanelTop = editorPanelTop + editorPanelHeight + 12;
|
final editorPanelTop = cardTop + cardHeight + layout.sectionGap;
|
||||||
const colorPanelHeight = 410.0;
|
final colorPanelTop = editorPanelTop + editorPanelHeight + layout.sectionGap;
|
||||||
|
final colorPanelHeight = layout.colorPanelHeight(mq, colorPanelTop);
|
||||||
|
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
@@ -263,6 +265,7 @@ class _AccountEditorOverlayState extends State<AccountEditorOverlay> {
|
|||||||
dashboardState: dash,
|
dashboardState: dash,
|
||||||
dashboardContext: widget.context,
|
dashboardContext: widget.context,
|
||||||
panelHeight: colorPanelHeight,
|
panelHeight: colorPanelHeight,
|
||||||
|
layout: layout,
|
||||||
isDuplicateName: _isDuplicateName,
|
isDuplicateName: _isDuplicateName,
|
||||||
onDuplicateError: () {
|
onDuplicateError: () {
|
||||||
setState(() => _showDuplicateError = true);
|
setState(() => _showDuplicateError = true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../../../../core/l10n/app_strings.dart';
|
|||||||
import '../../../../core/l10n/locale_provider.dart';
|
import '../../../../core/l10n/locale_provider.dart';
|
||||||
import '../../../../core/services/card_color_service.dart';
|
import '../../../../core/services/card_color_service.dart';
|
||||||
import '../../../../core/services/haptic_service.dart';
|
import '../../../../core/services/haptic_service.dart';
|
||||||
|
import '../../../../core/utils/card_layout.dart';
|
||||||
import '../../../../shared/models/account.dart';
|
import '../../../../shared/models/account.dart';
|
||||||
import '../../provider.dart';
|
import '../../provider.dart';
|
||||||
import './panel_tab.dart';
|
import './panel_tab.dart';
|
||||||
@@ -14,6 +15,7 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
final dynamic dashboardState;
|
final dynamic dashboardState;
|
||||||
final BuildContext dashboardContext;
|
final BuildContext dashboardContext;
|
||||||
final double panelHeight;
|
final double panelHeight;
|
||||||
|
final CardOverlayLayout layout;
|
||||||
final bool Function(List<Account>, String) isDuplicateName;
|
final bool Function(List<Account>, String) isDuplicateName;
|
||||||
final VoidCallback onDuplicateError;
|
final VoidCallback onDuplicateError;
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
required this.dashboardState,
|
required this.dashboardState,
|
||||||
required this.dashboardContext,
|
required this.dashboardContext,
|
||||||
required this.panelHeight,
|
required this.panelHeight,
|
||||||
|
required this.layout,
|
||||||
required this.isDuplicateName,
|
required this.isDuplicateName,
|
||||||
required this.onDuplicateError,
|
required this.onDuplicateError,
|
||||||
});
|
});
|
||||||
@@ -77,7 +80,12 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
: dashboardState.tempSecondaryHSV;
|
: dashboardState.tempSecondaryHSV;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16,
|
||||||
|
layout.panelPaddingTop,
|
||||||
|
16,
|
||||||
|
layout.panelPaddingBottom,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -239,12 +247,12 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
SizedBox(height: layout.tabSpacing),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (lbCtx, constraints) {
|
builder: (lbCtx, constraints) {
|
||||||
const reservedBelow = 78.0;
|
final spectrumH = (constraints.maxHeight -
|
||||||
final spectrumH = (constraints.maxHeight - reservedBelow)
|
layout.reservedBelowControls)
|
||||||
.clamp(40.0, double.infinity);
|
.clamp(40.0, double.infinity);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -262,9 +270,9 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 36,
|
height: layout.hueSliderHeight,
|
||||||
child: ColorPickerSlider(
|
child: ColorPickerSlider(
|
||||||
TrackType.hue,
|
TrackType.hue,
|
||||||
currentHSV,
|
currentHSV,
|
||||||
@@ -272,14 +280,14 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
displayThumbColor: true,
|
displayThumbColor: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
ignoring: isSolid,
|
ignoring: isSolid,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
opacity: isSolid ? 0.4 : 1.0,
|
opacity: isSolid ? 0.4 : 1.0,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 26,
|
height: layout.hexRowHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@@ -411,7 +419,7 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
ignoring: isSolid,
|
ignoring: isSolid,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
@@ -463,8 +471,8 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: 5,
|
vertical: layout.compact ? 3 : 5,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
@@ -523,7 +531,7 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -554,10 +562,13 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
setPanelState(() {});
|
setPanelState(() {});
|
||||||
dashboardState.overlayEntry?.markNeedsBuild();
|
dashboardState.overlayEntry?.markNeedsBuild();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.restart_alt_rounded, size: 15),
|
icon: Icon(
|
||||||
|
Icons.restart_alt_rounded,
|
||||||
|
size: layout.compact ? 14 : 15,
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
s.reset,
|
s.reset,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: layout.compact ? 12 : 13),
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Theme.of(
|
foregroundColor: Theme.of(
|
||||||
@@ -568,7 +579,9 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
dashboardContext,
|
dashboardContext,
|
||||||
).colorScheme.onSurface.withOpacity(0.2),
|
).colorScheme.onSurface.withOpacity(0.2),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: layout.buttonVerticalPadding,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -613,7 +626,9 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
disabledForegroundColor: Theme.of(
|
disabledForegroundColor: Theme.of(
|
||||||
dashboardContext,
|
dashboardContext,
|
||||||
).colorScheme.onSurface.withOpacity(0.38),
|
).colorScheme.onSurface.withOpacity(0.38),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: layout.buttonVerticalPadding,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -622,9 +637,9 @@ class AccountColorPanel extends StatelessWidget {
|
|||||||
dashboardState.isAddingAccount
|
dashboardState.isAddingAccount
|
||||||
? s.addAccount
|
? s.addAccount
|
||||||
: s.apply,
|
: s.apply,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 14,
|
fontSize: layout.compact ? 13 : 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:sensors_plus/sensors_plus.dart';
|
|||||||
import '../../../core/constants.dart';
|
import '../../../core/constants.dart';
|
||||||
import '../../../core/l10n/locale_provider.dart';
|
import '../../../core/l10n/locale_provider.dart';
|
||||||
import '../../../core/services/card_color_service.dart';
|
import '../../../core/services/card_color_service.dart';
|
||||||
|
import '../../../core/utils/card_layout.dart';
|
||||||
|
import '../../../shared/utils/card_gradient.dart';
|
||||||
import '../../../core/services/haptic_service.dart';
|
import '../../../core/services/haptic_service.dart';
|
||||||
import '../../../shared/providers/amount_format_provider.dart';
|
import '../../../shared/providers/amount_format_provider.dart';
|
||||||
import '../../../shared/widgets/byn_sign.dart';
|
import '../../../shared/widgets/byn_sign.dart';
|
||||||
@@ -83,47 +85,6 @@ class BalanceCardState extends ConsumerState<BalanceCard>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Gradient _buildGradient(Color primary, Color secondary, GradientType type) {
|
|
||||||
final colorDark = Color.lerp(secondary, Colors.black, 0.3)!;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case GradientType.linear:
|
|
||||||
return LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: [primary, secondary, colorDark],
|
|
||||||
stops: const [0.0, 0.6, 1.0],
|
|
||||||
);
|
|
||||||
case GradientType.linearReverse:
|
|
||||||
return LinearGradient(
|
|
||||||
begin: Alignment.topRight,
|
|
||||||
end: Alignment.bottomLeft,
|
|
||||||
colors: [primary, secondary, colorDark],
|
|
||||||
stops: const [0.0, 0.6, 1.0],
|
|
||||||
);
|
|
||||||
case GradientType.radial:
|
|
||||||
return RadialGradient(
|
|
||||||
center: Alignment.center,
|
|
||||||
radius: 1.4,
|
|
||||||
colors: [primary, secondary, colorDark],
|
|
||||||
stops: const [0.0, 0.6, 1.0],
|
|
||||||
);
|
|
||||||
case GradientType.sweep:
|
|
||||||
return SweepGradient(
|
|
||||||
center: Alignment.center,
|
|
||||||
startAngle: 0.0,
|
|
||||||
endAngle: 3.14159 * 2,
|
|
||||||
colors: [primary, secondary, colorDark, secondary, primary],
|
|
||||||
stops: const [0.0, 0.25, 0.5, 0.75, 1.0],
|
|
||||||
);
|
|
||||||
case GradientType.solid:
|
|
||||||
return LinearGradient(
|
|
||||||
colors: [primary, primary, primary],
|
|
||||||
stops: const [0.0, 0.5, 1.0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
@@ -171,10 +132,10 @@ class BalanceCardState extends ConsumerState<BalanceCard>
|
|||||||
..rotateY(_tiltY * 0.42),
|
..rotateY(_tiltY * 0.42),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 180,
|
height: kBalanceCardHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
gradient: _buildGradient(primary, secondary, gradientType),
|
gradient: buildCardGradient(primary, secondary, gradientType),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.4),
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/services/card_color_service.dart';
|
import '../../../core/services/card_color_service.dart';
|
||||||
|
import '../../../core/utils/card_layout.dart';
|
||||||
import '../../../core/services/haptic_service.dart';
|
import '../../../core/services/haptic_service.dart';
|
||||||
import '../../../shared/models/account.dart';
|
import '../../../shared/models/account.dart';
|
||||||
import '../../../shared/models/transaction.dart';
|
import '../../../shared/models/transaction.dart';
|
||||||
@@ -66,7 +67,7 @@ class _BalanceCardCarouselState extends ConsumerState<BalanceCardCarousel> {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 190,
|
height: kBalanceCardCarouselHeight,
|
||||||
child: OverflowBox(
|
child: OverflowBox(
|
||||||
maxWidth: MediaQuery.of(context).size.width,
|
maxWidth: MediaQuery.of(context).size.width,
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
@@ -154,14 +155,14 @@ class _BalanceCardCarouselState extends ConsumerState<BalanceCardCarousel> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const SizedBox(
|
loading: () => const SizedBox(
|
||||||
height: 180,
|
height: kBalanceCardCarouselHeight,
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
error: (error, stack) {
|
error: (error, stack) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 180,
|
height: kBalanceCardCarouselHeight,
|
||||||
child: BalanceCard(
|
child: BalanceCard(
|
||||||
balance: widget.balance,
|
balance: widget.balance,
|
||||||
currencyInfo: widget.currencyInfo,
|
currencyInfo: widget.currencyInfo,
|
||||||
@@ -192,13 +193,13 @@ class AddAccountCard extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: const EdgeInsets.symmetric(
|
||||||
horizontal: 10,
|
horizontal: 10,
|
||||||
vertical: 10,
|
vertical: 5,
|
||||||
),
|
),
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: _DashedBorderPainter(),
|
painter: _DashedBorderPainter(),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 165,
|
height: kAddAccountCardHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface.withOpacity(0.4),
|
color: Theme.of(context).colorScheme.surface.withOpacity(0.4),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../../core/l10n/app_strings.dart';
|
import '../../../core/l10n/app_strings.dart';
|
||||||
import '../../../core/l10n/locale_provider.dart';
|
import '../../../core/l10n/locale_provider.dart';
|
||||||
import '../../../core/services/card_color_service.dart';
|
import '../../../core/services/card_color_service.dart';
|
||||||
|
import '../../../core/utils/card_layout.dart';
|
||||||
import '../../settings/provider.dart';
|
import '../../settings/provider.dart';
|
||||||
import '../provider.dart';
|
import '../provider.dart';
|
||||||
import 'balance_card.dart';
|
import 'balance_card.dart';
|
||||||
@@ -34,10 +35,11 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mq = MediaQuery.of(widget.context);
|
final mq = MediaQuery.of(widget.context);
|
||||||
final cardTop = mq.padding.top + kToolbarHeight + 16;
|
final layout = CardOverlayLayout.fromMediaQuery(mq);
|
||||||
const cardHeight = 230.0;
|
final cardTop = layout.cardTop;
|
||||||
final panelTop = cardTop + cardHeight + 65;
|
final cardHeight = layout.cardHeight;
|
||||||
const panelHeight = 410.0;
|
final panelTop = cardTop + cardHeight + layout.cardPreviewGap;
|
||||||
|
final panelHeight = layout.colorPanelHeight(mq, panelTop);
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
@@ -91,7 +93,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: _buildPanel(panelHeight),
|
child: _buildPanel(panelHeight, layout),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -132,7 +134,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPanel(double panelHeight) {
|
Widget _buildPanel(double panelHeight, CardOverlayLayout layout) {
|
||||||
return Container(
|
return Container(
|
||||||
height: panelHeight,
|
height: panelHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -182,7 +184,12 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
: dash.tempSecondaryHSV;
|
: dash.tempSecondaryHSV;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
16,
|
||||||
|
layout.panelPaddingTop,
|
||||||
|
16,
|
||||||
|
layout.panelPaddingBottom,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -341,12 +348,12 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
SizedBox(height: layout.tabSpacing),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (lbCtx, constraints) {
|
builder: (lbCtx, constraints) {
|
||||||
const reservedBelow = 78.0;
|
final spectrumH = (constraints.maxHeight -
|
||||||
final spectrumH = (constraints.maxHeight - reservedBelow)
|
layout.reservedBelowControls)
|
||||||
.clamp(40.0, double.infinity);
|
.clamp(40.0, double.infinity);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -364,9 +371,9 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 36,
|
height: layout.hueSliderHeight,
|
||||||
child: ColorPickerSlider(
|
child: ColorPickerSlider(
|
||||||
TrackType.hue,
|
TrackType.hue,
|
||||||
currentHSV,
|
currentHSV,
|
||||||
@@ -374,14 +381,14 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
displayThumbColor: true,
|
displayThumbColor: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
ignoring: isSolid,
|
ignoring: isSolid,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
opacity: isSolid ? 0.4 : 1.0,
|
opacity: isSolid ? 0.4 : 1.0,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 26,
|
height: layout.hexRowHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
@@ -499,7 +506,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
ignoring: isSolid,
|
ignoring: isSolid,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
@@ -545,8 +552,8 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
},
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: 5,
|
vertical: layout.compact ? 3 : 5,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
@@ -605,7 +612,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: layout.controlSpacing),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -633,10 +640,13 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
setPanelState(() {});
|
setPanelState(() {});
|
||||||
dash.overlayEntry?.markNeedsBuild();
|
dash.overlayEntry?.markNeedsBuild();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.restart_alt_rounded, size: 15),
|
icon: Icon(
|
||||||
|
Icons.restart_alt_rounded,
|
||||||
|
size: layout.compact ? 14 : 15,
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
s.reset,
|
s.reset,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: TextStyle(fontSize: layout.compact ? 12 : 13),
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Theme.of(
|
foregroundColor: Theme.of(
|
||||||
@@ -647,7 +657,9 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
widget.context,
|
widget.context,
|
||||||
).colorScheme.onSurface.withOpacity(0.2),
|
).colorScheme.onSurface.withOpacity(0.2),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: layout.buttonVerticalPadding,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
@@ -662,16 +674,18 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF7C6DED),
|
backgroundColor: const Color(0xFF7C6DED),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: layout.buttonVerticalPadding,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
s.apply,
|
s.apply,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 14,
|
fontSize: layout.compact ? 13 : 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-181
@@ -2,188 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
import 'app/app.dart';
|
import 'app/app.dart';
|
||||||
import 'core/services/haptic_service.dart';
|
import 'core/services/haptic_service.dart';
|
||||||
import 'data/database/app_database.dart' hide Account, Transaction;
|
import 'data/database/app_database.dart';
|
||||||
import 'data/repositories/account_repository.dart';
|
|
||||||
import 'data/repositories/transaction_repository.dart';
|
|
||||||
import 'features/dashboard/provider.dart';
|
import 'features/dashboard/provider.dart';
|
||||||
import 'shared/models/account.dart';
|
|
||||||
import 'shared/models/transaction.dart';
|
|
||||||
|
|
||||||
Future<void> seedTestData(AppDatabase database) async {
|
|
||||||
final accountRepo = AccountRepository(database);
|
|
||||||
final transactionRepo = TransactionRepository(database);
|
|
||||||
|
|
||||||
final existingAccounts = await accountRepo.getAll();
|
|
||||||
if (existingAccounts.length > 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final now = DateTime.now();
|
|
||||||
final uuid = const Uuid();
|
|
||||||
|
|
||||||
final cashId = await accountRepo.add(
|
|
||||||
Account(
|
|
||||||
id: 0,
|
|
||||||
name: 'Cash',
|
|
||||||
isMain: false,
|
|
||||||
sortOrder: 1,
|
|
||||||
currency: 'USD',
|
|
||||||
createdAt: now,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final cardId = await accountRepo.add(
|
|
||||||
Account(
|
|
||||||
id: 0,
|
|
||||||
name: 'Card',
|
|
||||||
isMain: false,
|
|
||||||
sortOrder: 2,
|
|
||||||
currency: 'USD',
|
|
||||||
createdAt: now,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final savingsId = await accountRepo.add(
|
|
||||||
Account(
|
|
||||||
id: 0,
|
|
||||||
name: 'Savings',
|
|
||||||
isMain: false,
|
|
||||||
sortOrder: 3,
|
|
||||||
currency: 'USD',
|
|
||||||
createdAt: now,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final transactions = [
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 3500.0,
|
|
||||||
category: 'Salary',
|
|
||||||
type: TransactionType.income,
|
|
||||||
date: now.subtract(const Duration(days: 28)),
|
|
||||||
note: 'Monthly salary',
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 85.50,
|
|
||||||
category: 'Groceries',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 27)),
|
|
||||||
note: 'Weekly shopping',
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 45.00,
|
|
||||||
category: 'Transportation',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 25)),
|
|
||||||
accountId: cashId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 120.00,
|
|
||||||
category: 'Utilities',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 22)),
|
|
||||||
note: 'Electricity bill',
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 200.0,
|
|
||||||
category: 'Freelance',
|
|
||||||
type: TransactionType.income,
|
|
||||||
date: now.subtract(const Duration(days: 20)),
|
|
||||||
note: 'Side project',
|
|
||||||
accountId: cashId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 500.0,
|
|
||||||
category: 'Savings',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 18)),
|
|
||||||
note: 'Monthly savings',
|
|
||||||
accountId: savingsId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 65.25,
|
|
||||||
category: 'Dining',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 15)),
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 30.00,
|
|
||||||
category: 'Entertainment',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 12)),
|
|
||||||
note: 'Movie tickets',
|
|
||||||
accountId: cashId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 95.00,
|
|
||||||
category: 'Groceries',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 10)),
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 150.0,
|
|
||||||
category: 'Bonus',
|
|
||||||
type: TransactionType.income,
|
|
||||||
date: now.subtract(const Duration(days: 8)),
|
|
||||||
note: 'Performance bonus',
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 75.00,
|
|
||||||
category: 'Shopping',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 6)),
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 40.00,
|
|
||||||
category: 'Transportation',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 4)),
|
|
||||||
accountId: cashId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 55.80,
|
|
||||||
category: 'Dining',
|
|
||||||
type: TransactionType.expense,
|
|
||||||
date: now.subtract(const Duration(days: 2)),
|
|
||||||
note: 'Dinner with friends',
|
|
||||||
accountId: cardId,
|
|
||||||
),
|
|
||||||
Transaction(
|
|
||||||
id: uuid.v4(),
|
|
||||||
amount: 100.0,
|
|
||||||
category: 'Gift',
|
|
||||||
type: TransactionType.income,
|
|
||||||
date: now.subtract(const Duration(days: 1)),
|
|
||||||
accountId: cashId,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (final transaction in transactions) {
|
|
||||||
await transactionRepo.add(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
@@ -198,8 +20,6 @@ void main() async {
|
|||||||
|
|
||||||
final database = AppDatabase();
|
final database = AppDatabase();
|
||||||
|
|
||||||
await seedTestData(database);
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../core/services/card_color_service.dart';
|
||||||
|
|
||||||
|
Gradient buildCardGradient(Color primary, Color secondary, GradientType type) {
|
||||||
|
final colorDark = Color.lerp(secondary, Colors.black, 0.3)!;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case GradientType.linear:
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [primary, secondary, colorDark],
|
||||||
|
stops: const [0.0, 0.6, 1.0],
|
||||||
|
);
|
||||||
|
case GradientType.linearReverse:
|
||||||
|
return LinearGradient(
|
||||||
|
begin: Alignment.topRight,
|
||||||
|
end: Alignment.bottomLeft,
|
||||||
|
colors: [primary, secondary, colorDark],
|
||||||
|
stops: const [0.0, 0.6, 1.0],
|
||||||
|
);
|
||||||
|
case GradientType.radial:
|
||||||
|
return RadialGradient(
|
||||||
|
center: Alignment.center,
|
||||||
|
radius: 1.4,
|
||||||
|
colors: [primary, secondary, colorDark],
|
||||||
|
stops: const [0.0, 0.6, 1.0],
|
||||||
|
);
|
||||||
|
case GradientType.sweep:
|
||||||
|
return SweepGradient(
|
||||||
|
center: Alignment.center,
|
||||||
|
startAngle: 0.0,
|
||||||
|
endAngle: 3.14159 * 2,
|
||||||
|
colors: [primary, secondary, colorDark, secondary, primary],
|
||||||
|
stops: const [0.0, 0.25, 0.5, 0.75, 1.0],
|
||||||
|
);
|
||||||
|
case GradientType.solid:
|
||||||
|
return LinearGradient(
|
||||||
|
colors: [primary, primary, primary],
|
||||||
|
stops: const [0.0, 0.5, 1.0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user