This commit is contained in:
2026-06-13 22:43:06 +03:00
parent 2682c92f23
commit 8fb785944c
29 changed files with 671 additions and 322 deletions
+212 -78
View File
@@ -35,77 +35,75 @@ final storageServiceProvider = Provider<StorageService>((ref) {
});
final transactionsProvider =
StateNotifierProvider<TransactionsNotifier, AsyncValue<List<Transaction>>>((
ref,
) {
final repository = ref.watch(transactionRepositoryProvider);
return TransactionsNotifier(repository);
});
AsyncNotifierProvider<TransactionsNotifier, List<Transaction>>(
TransactionsNotifier.new,
);
class TransactionsNotifier
extends StateNotifier<AsyncValue<List<Transaction>>> {
final TransactionRepository _repository;
class TransactionsNotifier extends AsyncNotifier<List<Transaction>> {
@override
Future<List<Transaction>> build() async {
final repository = ref.watch(transactionRepositoryProvider);
final result = await repository.getAll();
TransactionsNotifier(this._repository) : super(const AsyncValue.loading()) {
_load();
}
Future<void> _load() async {
state = const AsyncValue.loading();
final result = await _repository.getAll();
state = result.isSuccess
? AsyncValue.data(result.dataOrNull!)
: AsyncValue.error(result.errorOrNull!, StackTrace.current);
if (result.isSuccess) {
return result.dataOrNull!;
} else {
throw result.errorOrNull!;
}
}
Future<Result<void>> add(Transaction transaction) async {
final result = await _repository.add(transaction);
final repository = ref.read(transactionRepositoryProvider);
final result = await repository.add(transaction);
if (result.isSuccess) {
await _load();
ref.invalidateSelf();
}
return result;
}
Future<Result<void>> update(Transaction transaction) async {
final result = await _repository.update(transaction);
Future<Result<void>> updateTransaction(Transaction transaction) async {
final repository = ref.read(transactionRepositoryProvider);
final result = await repository.update(transaction);
if (result.isSuccess) {
await _load();
ref.invalidateSelf();
}
return result;
}
Future<Result<void>> delete(String id) async {
final result = await _repository.delete(id);
final repository = ref.read(transactionRepositoryProvider);
final result = await repository.delete(id);
if (result.isSuccess) {
await _load();
ref.invalidateSelf();
}
return result;
}
Future<void> restore(Transaction transaction) async {
await _repository.add(transaction);
await _load();
final repository = ref.read(transactionRepositoryProvider);
await repository.add(transaction);
ref.invalidateSelf();
}
Future<void> clearAll() async {
await _repository.deleteAll();
final repository = ref.read(transactionRepositoryProvider);
await repository.deleteAll();
state = const AsyncValue.data([]);
}
Future<void> refresh() async {
await _load();
ref.invalidateSelf();
}
}
final transferPairsProvider = Provider<Map<String, Transaction>>((ref) {
final txs = ref.watch(transactionsProvider).valueOrNull ?? [];
final txs = ref.watch(transactionsProvider).value ?? [];
final transfers = txs.where((t) => t.category == 'Transfer').toList();
final Map<String, Transaction> pairs = {};
@@ -131,23 +129,46 @@ final transferPairsProvider = Provider<Map<String, Transaction>>((ref) {
return pairs;
});
final searchQueryProvider = StateProvider<String>((ref) => '');
final searchQueryProvider = NotifierProvider<_SearchQueryNotifier, String>(
_SearchQueryNotifier.new,
);
class _SearchQueryNotifier extends Notifier<String> {
@override
String build() => '';
void set(String v) => state = v;
}
enum TransactionFilter { all, income, expense, transfer }
enum TimeFilter { allTime, lastMonth }
final transactionFilterProvider = StateProvider<TransactionFilter>(
(ref) => TransactionFilter.all,
final transactionFilterProvider = NotifierProvider<_TransactionFilterNotifier, TransactionFilter>(
_TransactionFilterNotifier.new,
);
final timeFilterProvider = StateProvider<TimeFilter>(
(ref) => TimeFilter.lastMonth,
class _TransactionFilterNotifier extends Notifier<TransactionFilter> {
@override
TransactionFilter build() => TransactionFilter.all;
void set(TransactionFilter v) => state = v;
}
final timeFilterProvider = NotifierProvider<_TimeFilterNotifier, TimeFilter>(
_TimeFilterNotifier.new,
);
class _TimeFilterNotifier extends Notifier<TimeFilter> {
@override
TimeFilter build() => TimeFilter.lastMonth;
void set(TimeFilter v) => state = v;
}
final accountFilteredTransactionsProvider = Provider<List<Transaction>>((ref) {
final txsAsync = ref.watch(transactionsProvider);
final txs = txsAsync.valueOrNull ?? [];
final txs = txsAsync.value ?? [];
final activeAccount = ref.watch(activeAccountProvider);
if (activeAccount == null) {
@@ -158,7 +179,7 @@ final accountFilteredTransactionsProvider = Provider<List<Transaction>>((ref) {
});
final globalTotalBalanceProvider = Provider<double>((ref) {
final txs = ref.watch(transactionsProvider).valueOrNull ?? [];
final txs = ref.watch(transactionsProvider).value ?? [];
final exchangeService = ref.watch(exchangeRateServiceProvider);
final targetCurrency = ref.watch(currencyProvider).code;
@@ -181,7 +202,7 @@ final totalBalanceProvider = Provider<double>((ref) {
String targetCurrency = globalCurrency;
if (index > 0) {
final accounts = accountsAsync.valueOrNull ?? [];
final accounts = accountsAsync.value ?? [];
if (index <= accounts.length) {
targetCurrency = accounts[index - 1].currency;
}
@@ -211,7 +232,7 @@ final totalIncomeProvider = Provider<double>((ref) {
String targetCurrency = globalCurrency;
if (index > 0) {
final accounts = accountsAsync.valueOrNull ?? [];
final accounts = accountsAsync.value ?? [];
if (index <= accounts.length) {
targetCurrency = accounts[index - 1].currency;
}
@@ -237,7 +258,7 @@ final totalExpenseProvider = Provider<double>((ref) {
String targetCurrency = globalCurrency;
if (index > 0) {
final accounts = accountsAsync.valueOrNull ?? [];
final accounts = accountsAsync.value ?? [];
if (index <= accounts.length) {
targetCurrency = accounts[index - 1].currency;
}
@@ -267,7 +288,7 @@ final currentMonthExpenseProvider = Provider<double>((ref) {
String targetCurrency = globalCurrency;
if (index > 0) {
final accounts = accountsAsync.valueOrNull ?? [];
final accounts = accountsAsync.value ?? [];
if (index <= accounts.length) {
targetCurrency = accounts[index - 1].currency;
}
@@ -349,7 +370,16 @@ final accountsProvider = StreamProvider<List<Account>>((ref) async* {
}
});
final activeAccountIndexProvider = StateProvider<int>((ref) => 0);
final activeAccountIndexProvider = NotifierProvider<_ActiveAccountIndexNotifier, int>(
_ActiveAccountIndexNotifier.new,
);
class _ActiveAccountIndexNotifier extends Notifier<int> {
@override
int build() => 0;
void set(int v) => state = v;
}
final activeAccountProvider = Provider<Account?>((ref) {
final index = ref.watch(activeAccountIndexProvider);
@@ -387,52 +417,37 @@ class CardColors {
}
final cardColorsProvider =
StateNotifierProvider<CardColorsNotifier, CardColors>((ref) {
final notifier = CardColorsNotifier();
notifier.setupThemeListener(ref);
return notifier;
});
NotifierProvider<CardColorsNotifier, CardColors>(
CardColorsNotifier.new,
);
final accountCardColorsProvider =
StateNotifierProvider.family<CardColorsNotifier, CardColors, int>((
ref,
accountId,
) {
final notifier = CardColorsNotifier(accountId: accountId);
notifier.setupThemeListener(ref);
return notifier;
});
class CardColorsNotifier extends StateNotifier<CardColors> {
final int? accountId;
CardColorsNotifier({this.accountId})
: super(
const CardColors(
CardColorService.defaultPrimary,
CardColorService.defaultSecondary,
CardColorService.defaultGradientLight,
CardColorService.defaultGradientDark,
),
) {
_load();
}
NotifierProvider.family<AccountCardColorsNotifier, CardColors, int>(
(accountId) => AccountCardColorsNotifier(accountId),
);
class CardColorsNotifier extends Notifier<CardColors> {
int _loadGeneration = 0;
void setupThemeListener(Ref ref) {
@override
CardColors build() {
ref.listen<ThemeMode>(themeProvider, (previous, next) {
if (previous != null) {
_onThemeChanged(previous, next);
}
});
_load();
return const CardColors(
CardColorService.defaultPrimary,
CardColorService.defaultSecondary,
CardColorService.defaultGradientLight,
CardColorService.defaultGradientDark,
);
}
Future<void> _load() async {
final currentGeneration = ++_loadGeneration;
final (c1, c2, lightG, darkG) = await CardColorService.load(
accountId: accountId,
);
final (c1, c2, lightG, darkG) = await CardColorService.load();
if (currentGeneration != _loadGeneration) return;
state = CardColors(c1, c2, lightG, darkG);
}
@@ -450,7 +465,6 @@ class CardColorsNotifier extends StateNotifier<CardColors> {
secondary,
lightGradient,
darkGradient,
accountId: accountId,
);
}
@@ -473,7 +487,127 @@ class CardColorsNotifier extends StateNotifier<CardColors> {
secondary,
CardColorService.defaultGradientLight,
CardColorService.defaultGradientDark,
accountId: accountId,
);
}
void _onThemeChanged(ThemeMode previous, ThemeMode next) {
final previousBrightness = _resolve(previous);
final nextBrightness = _resolve(next);
if (previousBrightness == nextBrightness) return;
final oldDefaults = _defaultsFor(previousBrightness);
final newDefaults = _defaultsFor(nextBrightness);
final isUsingOldDefaults =
state.primary == oldDefaults.primary &&
state.secondary == oldDefaults.secondary &&
state.gradientTypeForBrightness(previousBrightness) ==
oldDefaults.gradient;
if (isUsingOldDefaults) {
_loadGeneration++;
state = CardColors(
newDefaults.primary,
newDefaults.secondary,
state.lightGradientType,
state.darkGradientType,
);
}
}
Brightness _resolve(ThemeMode mode) {
if (mode == ThemeMode.system) {
return WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
return mode == ThemeMode.dark ? Brightness.dark : Brightness.light;
}
({Color primary, Color secondary, GradientType gradient}) _defaultsFor(
Brightness brightness,
) {
return brightness == Brightness.dark
? (
primary: CardColorService.defaultPrimary,
secondary: CardColorService.defaultSecondary,
gradient: CardColorService.defaultGradientDark,
)
: (
primary: CardColorService.defaultPrimaryLight,
secondary: CardColorService.defaultSecondaryLight,
gradient: CardColorService.defaultGradientLight,
);
}
}
class AccountCardColorsNotifier extends Notifier<CardColors> {
AccountCardColorsNotifier(this._accountId);
final int _accountId;
int _loadGeneration = 0;
@override
CardColors build() {
ref.listen<ThemeMode>(themeProvider, (previous, next) {
if (previous != null) {
_onThemeChanged(previous, next);
}
});
_load(_accountId);
return const CardColors(
CardColorService.defaultPrimary,
CardColorService.defaultSecondary,
CardColorService.defaultGradientLight,
CardColorService.defaultGradientDark,
);
}
Future<void> _load(int accountId) async {
final currentGeneration = ++_loadGeneration;
final (c1, c2, lightG, darkG) = await CardColorService.load(
accountId: accountId,
);
if (currentGeneration != _loadGeneration) return;
state = CardColors(c1, c2, lightG, darkG);
}
Future<void> save(
Color primary,
Color secondary,
GradientType lightGradient,
GradientType darkGradient,
) async {
_loadGeneration++;
state = CardColors(primary, secondary, lightGradient, darkGradient);
await CardColorService.save(
primary,
secondary,
lightGradient,
darkGradient,
accountId: _accountId,
);
}
Future<void> reset(bool isDark) async {
final primary = isDark
? CardColorService.defaultPrimary
: CardColorService.defaultPrimaryLight;
final secondary = isDark
? CardColorService.defaultSecondary
: CardColorService.defaultSecondaryLight;
_loadGeneration++;
state = CardColors(
primary,
secondary,
CardColorService.defaultGradientLight,
CardColorService.defaultGradientDark,
);
await CardColorService.save(
primary,
secondary,
CardColorService.defaultGradientLight,
CardColorService.defaultGradientDark,
accountId: _accountId,
);
}
+1 -1
View File
@@ -280,7 +280,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final activeIndex = ref.watch(activeAccountIndexProvider);
final accountsAsync = ref.watch(accountsProvider);
final accountCount = accountsAsync.valueOrNull?.length ?? 0;
final accountCount = accountsAsync.value?.length ?? 0;
final isOnAddAccountPage =
accountCount < 5 && activeIndex == accountCount + 1;
@@ -377,7 +377,7 @@ class _AccountEditorOverlayState extends State<AccountEditorOverlay> {
mainAxisSize: MainAxisSize.min,
children: [
if (!dash.isAddingAccount &&
(ref.watch(accountsProvider).valueOrNull?.length ?? 0) >
(ref.watch(accountsProvider).value?.length ?? 0) >
1) ...[
GestureDetector(
onTap: () => setState(() => _showDeleteDialog = true),
@@ -588,7 +588,7 @@ class AccountColorPanel extends StatelessWidget {
final accounts =
ProviderScope.containerOf(
dashboardContext,
).read(accountsProvider).valueOrNull ??
).read(accountsProvider).value ??
[];
if (isDuplicateName(
@@ -56,7 +56,7 @@ class AccountDeleteDialog extends ConsumerWidget {
onConfirm();
final txs = ref.read(transactionsProvider).valueOrNull ?? [];
final txs = ref.read(transactionsProvider).value ?? [];
final accountTxs = txs
.where((t) => t.accountId == accountId)
.toList();
@@ -145,15 +145,12 @@ class BalanceCardState extends ConsumerState<BalanceCard>
.toList();
final textColorMode = ref.watch(cardTextColorProvider);
final Color onCard;
switch (textColorMode) {
case CardTextColorMode.white:
onCard = Colors.white;
case CardTextColorMode.black:
onCard = Colors.black;
case CardTextColorMode.adaptive:
onCard = primary.computeLuminance() > 0.3 ? Colors.black : Colors.white;
}
final Color onCard = switch (textColorMode) {
CardTextColorMode.white => Colors.white,
CardTextColorMode.black => Colors.black,
CardTextColorMode.adaptive => primary.computeLuminance() > 0.3 ? Colors.black : Colors.white,
_ => Colors.white,
};
return GestureDetector(
onLongPress: () {
@@ -75,7 +75,7 @@ class _BalanceCardCarouselState extends ConsumerState<BalanceCardCarousel> {
Clip.none,
itemCount: totalPages,
onPageChanged: (index) {
ref.read(activeAccountIndexProvider.notifier).state = index;
ref.read(activeAccountIndexProvider.notifier).set(index);
if (ref.read(hapticEnabledProvider)) {
HapticService.light();
}
@@ -103,7 +103,7 @@ class _BalanceCardCarouselState extends ConsumerState<BalanceCardCarousel> {
);
final txs =
ref.watch(transactionsProvider).valueOrNull ?? [];
ref.watch(transactionsProvider).value ?? [];
final accountTxs = txs
.where((t) => t.accountId == account.id)
.toList();
@@ -23,15 +23,15 @@ class FilterChips extends ConsumerWidget {
_FilterChip(
label: strings.filterAllTime,
isSelected: timeFilter == TimeFilter.allTime,
onTap: () => ref.read(timeFilterProvider.notifier).state =
TimeFilter.allTime,
onTap: () => ref.read(timeFilterProvider.notifier).set(
TimeFilter.allTime),
),
const SizedBox(width: 6),
_FilterChip(
label: strings.filterMonth,
isSelected: timeFilter == TimeFilter.lastMonth,
onTap: () => ref.read(timeFilterProvider.notifier).state =
TimeFilter.lastMonth,
onTap: () => ref.read(timeFilterProvider.notifier).set(
TimeFilter.lastMonth),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -46,32 +46,32 @@ class FilterChips extends ConsumerWidget {
_FilterChip(
label: strings.filterAll,
isSelected: typeFilter == TransactionFilter.all,
onTap: () => ref.read(transactionFilterProvider.notifier).state =
TransactionFilter.all,
onTap: () => ref.read(transactionFilterProvider.notifier).set(
TransactionFilter.all),
),
const SizedBox(width: 6),
_FilterChip(
label: strings.filterIncome,
isSelected: typeFilter == TransactionFilter.income,
color: AppColors.income,
onTap: () => ref.read(transactionFilterProvider.notifier).state =
TransactionFilter.income,
onTap: () => ref.read(transactionFilterProvider.notifier).set(
TransactionFilter.income),
),
const SizedBox(width: 6),
_FilterChip(
label: strings.filterExpense,
isSelected: typeFilter == TransactionFilter.expense,
color: AppColors.expense,
onTap: () => ref.read(transactionFilterProvider.notifier).state =
TransactionFilter.expense,
onTap: () => ref.read(transactionFilterProvider.notifier).set(
TransactionFilter.expense),
),
const SizedBox(width: 6),
_FilterChip(
label: strings.filterTransfer,
isSelected: typeFilter == TransactionFilter.transfer,
color: Colors.blueAccent,
onTap: () => ref.read(transactionFilterProvider.notifier).state =
TransactionFilter.transfer,
onTap: () => ref.read(transactionFilterProvider.notifier).set(
TransactionFilter.transfer),
),
],
),
@@ -37,7 +37,7 @@ class SearchBar extends StatelessWidget {
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
onPressed: () {
controller.clear();
ref.read(searchQueryProvider.notifier).state = '';
ref.read(searchQueryProvider.notifier).set('');
},
)
: null,
@@ -63,7 +63,7 @@ class SearchBar extends StatelessWidget {
vertical: 12,
),
),
onChanged: (v) => ref.read(searchQueryProvider.notifier).state = v,
onChanged: (v) => ref.read(searchQueryProvider.notifier).set(v),
);
}
}
@@ -56,7 +56,7 @@ class TransactionTile extends ConsumerWidget {
: 0.0;
final displaySymbol = currencyMap[displayCurrency]?.symbol ?? '';
final accounts = ref.watch(accountsProvider).valueOrNull ?? [];
final accounts = ref.watch(accountsProvider).value ?? [];
final txAccount = accounts.firstWhereOrNull(
(a) => a.id == transaction.accountId,
);