Improved json auto save & budget mobile UI
This commit is contained in:
@@ -46,6 +46,8 @@ class _AppViewState extends State<AppView> {
|
|||||||
|
|
||||||
_transactionsRepository.loadTransactions();
|
_transactionsRepository.loadTransactions();
|
||||||
_metadataRepository.loadMetadata();
|
_metadataRepository.loadMetadata();
|
||||||
|
|
||||||
|
_metadataRepository.getSettingsStream().listen((event) => setState(() {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -66,11 +68,11 @@ class _AppViewState extends State<AppView> {
|
|||||||
darkColorScheme = darkDynamic.harmonized();
|
darkColorScheme = darkDynamic.harmonized();
|
||||||
} else {
|
} else {
|
||||||
lightColorScheme = ColorScheme.fromSeed(
|
lightColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: const Color.fromARGB(1, 5, 236, 55),
|
seedColor: const Color.fromARGB(255, 103, 6, 231),
|
||||||
);
|
);
|
||||||
|
|
||||||
darkColorScheme = ColorScheme.fromSeed(
|
darkColorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: const Color.fromARGB(1, 5, 236, 55),
|
seedColor: const Color.fromARGB(255, 103, 6, 231),
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,6 +81,7 @@ class _AppViewState extends State<AppView> {
|
|||||||
title: 'Tunas',
|
title: 'Tunas',
|
||||||
theme: ThemeData(colorScheme: lightColorScheme),
|
theme: ThemeData(colorScheme: lightColorScheme),
|
||||||
darkTheme: ThemeData(colorScheme: darkColorScheme),
|
darkTheme: ThemeData(colorScheme: darkColorScheme),
|
||||||
|
themeMode: _metadataRepository.getSettings().themeMode,
|
||||||
initialRoute: '/home',
|
initialRoute: '/home',
|
||||||
routes: {
|
routes: {
|
||||||
'/home':(context) => const HomePage(),
|
'/home':(context) => const HomePage(),
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
|||||||
on<AccountEditLabel>(_onAccountEditLabel);
|
on<AccountEditLabel>(_onAccountEditLabel);
|
||||||
on<AccountEditSaving>(_onAccountEditSaving);
|
on<AccountEditSaving>(_onAccountEditSaving);
|
||||||
on<AccountEditColor>(_onAccountEditColor);
|
on<AccountEditColor>(_onAccountEditColor);
|
||||||
|
on<ClearData>(_onClearData);
|
||||||
|
|
||||||
_metadataRepository
|
_metadataRepository
|
||||||
.getAccountsStream()
|
.getAccountsStream()
|
||||||
@@ -209,4 +210,9 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
|||||||
_metadataRepository.saveAccounts(accounts);
|
_metadataRepository.saveAccounts(accounts);
|
||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onClearData(ClearData event, Emitter<AccountState> emit) {
|
||||||
|
_metadataRepository.deleteMetadata();
|
||||||
|
_transactionsRepository.deleteTransactions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ sealed class AccountEvent extends Equatable {
|
|||||||
List<Object> get props => [];
|
List<Object> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ClearData extends AccountEvent {
|
||||||
|
const ClearData();
|
||||||
|
}
|
||||||
|
|
||||||
final class AccountImportCSV extends AccountEvent {
|
final class AccountImportCSV extends AccountEvent {
|
||||||
const AccountImportCSV();
|
const AccountImportCSV();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,10 +226,14 @@ class BudgetBloc extends Bloc<BudgetEvent, BudgetState> {
|
|||||||
|
|
||||||
BudgetState _computeState(List<Budget> budgets, double? initialBudget) {
|
BudgetState _computeState(List<Budget> budgets, double? initialBudget) {
|
||||||
final compareResult = _computeCompareBudget(state.budgets, state.compareYear, state.compareMonth);
|
final compareResult = _computeCompareBudget(state.budgets, state.compareYear, state.compareMonth);
|
||||||
|
|
||||||
|
final budgetValues = budgets.map((budget) => budget.value);
|
||||||
|
final budgetReducedValues = budgetValues.isEmpty ? 0 : budgetValues.reduce((value, element) => value + element);
|
||||||
|
|
||||||
return state.copyWith(
|
return state.copyWith(
|
||||||
budgets: budgets,
|
budgets: budgets,
|
||||||
initialBudget: (initialBudget ?? state.initialBudget),
|
initialBudget: (initialBudget ?? state.initialBudget),
|
||||||
remainingBudget: (initialBudget ?? state.initialBudget) - budgets.map((budget) => budget.value).reduce((value, element) => value + element),
|
remainingBudget: (initialBudget ?? state.initialBudget) - budgetReducedValues,
|
||||||
compareBudgets: compareResult.$1,
|
compareBudgets: compareResult.$1,
|
||||||
otherBudgets: compareResult.$2,
|
otherBudgets: compareResult.$2,
|
||||||
);
|
);
|
||||||
|
|||||||
36
lib/domains/settings/settings_bloc.dart
Normal file
36
lib/domains/settings/settings_bloc.dart
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/metadata_repository.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/settings.dart';
|
||||||
|
|
||||||
|
part 'settings_event.dart';
|
||||||
|
part 'settings_state.dart';
|
||||||
|
|
||||||
|
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
|
||||||
|
final MetadataRepository _metadataRepository;
|
||||||
|
|
||||||
|
SettingsBloc({required MetadataRepository metadataRepository}) : _metadataRepository = metadataRepository, super(const SettingsState()) {
|
||||||
|
on<SettingsLoad>(_onSettingsLoad);
|
||||||
|
on<SetThemeMode>(_onSetThemeMode);
|
||||||
|
|
||||||
|
_metadataRepository
|
||||||
|
.getSettingsStream()
|
||||||
|
.listen((settings) => add(SettingsLoad(settings)));
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onSettingsLoad(SettingsLoad event, Emitter<SettingsState> emit) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
themeMode: event.settings.themeMode,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onSetThemeMode(SetThemeMode event, Emitter<SettingsState> emit) {
|
||||||
|
_metadataRepository.saveSettings(Settings(themeMode: event.themeMode));
|
||||||
|
emit(state.copyWith(
|
||||||
|
themeMode: event.themeMode,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
lib/domains/settings/settings_event.dart
Normal file
20
lib/domains/settings/settings_event.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
part of 'settings_bloc.dart';
|
||||||
|
|
||||||
|
sealed class SettingsEvent extends Equatable {
|
||||||
|
const SettingsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SettingsLoad extends SettingsEvent {
|
||||||
|
final Settings settings;
|
||||||
|
|
||||||
|
const SettingsLoad(this.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SetThemeMode extends SettingsEvent {
|
||||||
|
final ThemeMode themeMode;
|
||||||
|
|
||||||
|
const SetThemeMode(this.themeMode);
|
||||||
|
}
|
||||||
17
lib/domains/settings/settings_state.dart
Normal file
17
lib/domains/settings/settings_state.dart
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
part of 'settings_bloc.dart';
|
||||||
|
|
||||||
|
class SettingsState {
|
||||||
|
final ThemeMode themeMode;
|
||||||
|
|
||||||
|
const SettingsState({
|
||||||
|
this.themeMode = ThemeMode.system,
|
||||||
|
});
|
||||||
|
|
||||||
|
SettingsState copyWith({
|
||||||
|
ThemeMode? themeMode,
|
||||||
|
}) {
|
||||||
|
return SettingsState(
|
||||||
|
themeMode: themeMode ?? this.themeMode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
lib/pages/budgets/widgets/budget_cards.dart
Normal file
124
lib/pages/budgets/widgets/budget_cards.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/budget.dart';
|
||||||
|
|
||||||
|
class BudgetCards extends StatelessWidget {
|
||||||
|
const BudgetCards({super.key});
|
||||||
|
|
||||||
|
List<Widget> _computeBudgetsCompare(BuildContext context, List<Budget> targetBudgets, List<Budget> realBudgets) {
|
||||||
|
List<Widget> list = [
|
||||||
|
const Text('Budget'),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
width: 2,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
list.addAll(targetBudgets.map((budget) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 5),
|
||||||
|
Text(budget.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${NumberFormat("#00").format(budget.value.abs())}/${NumberFormat("#00 €").format(realBudgets.firstWhere((rbudget) => rbudget.label == budget.label).value.abs())}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'NovaMono',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _computeOtherBudgets(BuildContext context, List<Budget> otherBudgets) {
|
||||||
|
List<Widget> list = [
|
||||||
|
const Text('Hors budget'),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
width: 2,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
list.addAll(otherBudgets.map((budget) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 5),
|
||||||
|
Text(budget.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
NumberFormat("#00 €").format(budget.value.abs()),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'NovaMono',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool smallVerticalScreen = MediaQuery.sizeOf(context).width < 800;
|
||||||
|
return BlocBuilder<BudgetBloc, BudgetState>(
|
||||||
|
builder: (context, state) => Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: smallVerticalScreen ? null : 300,
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _computeBudgetsCompare(context, state.budgets, state.compareBudgets),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
width: smallVerticalScreen ? null : 300,
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
color: Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: _computeOtherBudgets(context, state.otherBudgets),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,135 +1,50 @@
|
|||||||
import 'package:fl_chart/fl_chart.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tunas/domains/budget/budget_bloc.dart';
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
|
import 'package:tunas/pages/budgets/widgets/budget_cards.dart';
|
||||||
|
import 'package:tunas/pages/budgets/widgets/budget_compare_selector.dart';
|
||||||
|
import 'package:tunas/pages/budgets/widgets/budget_radar.dart';
|
||||||
import 'package:tunas/pages/common/titled_container.dart';
|
import 'package:tunas/pages/common/titled_container.dart';
|
||||||
import 'package:tunas/repositories/metadata/models/budget.dart';
|
|
||||||
|
|
||||||
class BudgetComparator extends StatelessWidget {
|
class BudgetComparator extends StatelessWidget {
|
||||||
const BudgetComparator({super.key});
|
const BudgetComparator({super.key});
|
||||||
|
|
||||||
List<RadarDataSet> _computeDataSet(BuildContext context, List<Budget> targetBudgets, List<Budget> realBudgets) {
|
|
||||||
RadarDataSet targetDataSet = RadarDataSet(
|
|
||||||
fillColor: Theme.of(context).colorScheme.primary.withAlpha(100),
|
|
||||||
borderColor: Theme.of(context).colorScheme.primary,
|
|
||||||
entryRadius: 5,
|
|
||||||
dataEntries: targetBudgets.map((budget) => RadarEntry(value: budget.value)).toList(),
|
|
||||||
);
|
|
||||||
if (realBudgets.isNotEmpty) {
|
|
||||||
targetDataSet.dataEntries.add(const RadarEntry(value: 0));
|
|
||||||
}
|
|
||||||
RadarDataSet realDataSet = RadarDataSet(
|
|
||||||
fillColor: Theme.of(context).colorScheme.onPrimary.withAlpha(100),
|
|
||||||
borderColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
entryRadius: 8,
|
|
||||||
dataEntries: realBudgets.map((budget) => RadarEntry(value: budget.value)).toList(),
|
|
||||||
);
|
|
||||||
return realBudgets.isEmpty ? [RadarDataSet(dataEntries: [const RadarEntry(value: 1), const RadarEntry(value: 2), const RadarEntry(value: 3)])] : [realDataSet, targetDataSet];
|
|
||||||
}
|
|
||||||
|
|
||||||
RadarChartTitle _computeDataSetTitle(int index, List<Budget> realBudgets) {
|
|
||||||
return realBudgets.isEmpty ? const RadarChartTitle(text: 'No data') : RadarChartTitle(text: realBudgets[index].label);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _computeOtherBudgets(BuildContext context, List<Budget> otherBudgets) {
|
|
||||||
List<Widget> list = [
|
|
||||||
const Text('Hors budget'),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
width: 2,
|
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
list.addAll(otherBudgets.map((budget) => Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(width: 5),
|
|
||||||
Text(budget.label),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
NumberFormat("#00 €").format(budget.value.abs()),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'NovaMono',
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)));
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
bool smallVerticalScreen = MediaQuery.sizeOf(context).width < 800;
|
||||||
return BlocBuilder<BudgetBloc, BudgetState>(
|
return BlocBuilder<BudgetBloc, BudgetState>(
|
||||||
builder: (context, state) => TitledContainer(
|
builder: (context, state) => TitledContainer(
|
||||||
title: 'Compare',
|
title: 'Compare',
|
||||||
child: Column(
|
child: smallVerticalScreen
|
||||||
|
? const Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
BudgetCompareSelector(),
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
SizedBox(
|
||||||
children: [
|
height: 500,
|
||||||
IconButton(
|
child: BudgetRadar(),
|
||||||
onPressed: () => context.read<BudgetBloc>().add(BudgetComparePrevious()),
|
|
||||||
icon: const Icon(Icons.skip_previous)
|
|
||||||
),
|
|
||||||
Text('${NumberFormat('00', 'fr_FR').format(state.compareMonth)} - ${state.compareYear}'),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => context.read<BudgetBloc>().add(BudgetCompareNext()),
|
|
||||||
icon: const Icon(Icons.skip_next)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 500,
|
||||||
|
child: BudgetCards(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const Column(
|
||||||
|
children: [
|
||||||
|
BudgetCompareSelector(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
BudgetCards(),
|
||||||
height: 300,
|
BudgetRadar(),
|
||||||
width: 250,
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.vertical,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: _computeOtherBudgets(context, state.otherBudgets),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1.3,
|
|
||||||
child: RadarChart(
|
|
||||||
RadarChartData(
|
|
||||||
titlePositionPercentageOffset: 0.15,
|
|
||||||
tickBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 1),
|
|
||||||
gridBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 2),
|
|
||||||
radarBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 3),
|
|
||||||
radarBackgroundColor: Theme.of(context).colorScheme.secondaryContainer.withAlpha(100),
|
|
||||||
radarShape: RadarShape.circle,
|
|
||||||
dataSets: _computeDataSet(context, state.budgets, state.compareBudgets),
|
|
||||||
getTitle: (index, angle) => _computeDataSetTitle(index, state.compareBudgets),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
]
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
lib/pages/budgets/widgets/budget_compare_selector.dart
Normal file
28
lib/pages/budgets/widgets/budget_compare_selector.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
|
|
||||||
|
class BudgetCompareSelector extends StatelessWidget {
|
||||||
|
const BudgetCompareSelector({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<BudgetBloc, BudgetState>(
|
||||||
|
builder: (context, state) => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetComparePrevious()),
|
||||||
|
icon: const Icon(Icons.skip_previous)
|
||||||
|
),
|
||||||
|
Text('${NumberFormat('00', 'fr_FR').format(state.compareMonth)} - ${state.compareYear}'),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetCompareNext()),
|
||||||
|
icon: const Icon(Icons.skip_next)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
lib/pages/budgets/widgets/budget_line.dart
Normal file
124
lib/pages/budgets/widgets/budget_line.dart
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
|
import 'package:tunas/domains/category/category_bloc.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/budget.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/category.dart';
|
||||||
|
|
||||||
|
class BudgetLine extends StatelessWidget {
|
||||||
|
final Budget budget;
|
||||||
|
|
||||||
|
const BudgetLine({super.key, required this.budget});
|
||||||
|
|
||||||
|
Widget _largeScreenLine(BuildContext context, List<Category> categories, double initialBudget, double remainingBudget) {
|
||||||
|
return Row (
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: budget.label,
|
||||||
|
onChanged: (value) => context.read<BudgetBloc>().add(BudgetSetLabel(budget, value!)),
|
||||||
|
items: categories.map((e) => DropdownMenuItem(value: e.label, child: Text(e.label))).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 30),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetSetValue(budget, budget.value.round().toDouble() - 1)),
|
||||||
|
icon: const Icon(Icons.remove_circle)
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
NumberFormat('####000 €', 'fr_FR').format(budget.value),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'NovaMono',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetSetValue(budget, budget.value.round().toDouble() + 1)),
|
||||||
|
icon: const Icon(Icons.add_circle)
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Slider(
|
||||||
|
min: 0,
|
||||||
|
max: initialBudget,
|
||||||
|
label: budget.value.round().toString(),
|
||||||
|
value: budget.value,
|
||||||
|
secondaryTrackValue: remainingBudget + budget.value,
|
||||||
|
onChanged: (value) => context.read<BudgetBloc>().add(BudgetSetValue(budget, value.round().toDouble())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetRemove(budget)),
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _smallScreenLine(BuildContext context, List<Category> categories, double initialBudget, double remainingBudget) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: budget.label,
|
||||||
|
onChanged: (value) => context.read<BudgetBloc>().add(BudgetSetLabel(budget, value!)),
|
||||||
|
items: categories.map((e) => DropdownMenuItem(value: e.label, child: Text(e.label))).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 30),
|
||||||
|
Text(
|
||||||
|
NumberFormat('####000 €', 'fr_FR').format(budget.value),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'NovaMono',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetSetValue(budget, budget.value.round().toDouble() - 1)),
|
||||||
|
icon: const Icon(Icons.remove_circle)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetSetValue(budget, budget.value.round().toDouble() + 1)),
|
||||||
|
icon: const Icon(Icons.add_circle)
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.read<BudgetBloc>().add(BudgetRemove(budget)),
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
max: initialBudget,
|
||||||
|
label: budget.value.round().toString(),
|
||||||
|
value: budget.value,
|
||||||
|
secondaryTrackValue: remainingBudget + budget.value,
|
||||||
|
onChanged: (value) => context.read<BudgetBloc>().add(BudgetSetValue(budget, value.round().toDouble())),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final categoryState = context.watch<CategoryBloc>().state;
|
||||||
|
bool smallVerticalScreen = MediaQuery.sizeOf(context).width < 800;
|
||||||
|
return BlocBuilder<BudgetBloc, BudgetState>(
|
||||||
|
builder: (context, state) => smallVerticalScreen
|
||||||
|
? _smallScreenLine(context, categoryState.categories, state.initialBudget, state.remainingBudget)
|
||||||
|
: _largeScreenLine(context, categoryState.categories, state.initialBudget, state.remainingBudget),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:tunas/domains/budget/budget_bloc.dart';
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
import 'package:tunas/domains/category/category_bloc.dart';
|
import 'package:tunas/domains/category/category_bloc.dart';
|
||||||
|
import 'package:tunas/pages/budgets/widgets/budget_line.dart';
|
||||||
import 'package:tunas/pages/common/titled_container.dart';
|
import 'package:tunas/pages/common/titled_container.dart';
|
||||||
import 'package:tunas/repositories/metadata/models/budget.dart';
|
import 'package:tunas/repositories/metadata/models/budget.dart';
|
||||||
|
|
||||||
@@ -10,7 +11,6 @@ class BudgetMaker extends StatelessWidget {
|
|||||||
const BudgetMaker({super.key});
|
const BudgetMaker({super.key});
|
||||||
|
|
||||||
List<Widget> _computeBudgetLines(BuildContext context, List<Budget> budgets, double initialBudget, double remainingBudget) {
|
List<Widget> _computeBudgetLines(BuildContext context, List<Budget> budgets, double initialBudget, double remainingBudget) {
|
||||||
final categoryState = context.watch<CategoryBloc>().state;
|
|
||||||
List<Widget> list = [];
|
List<Widget> list = [];
|
||||||
|
|
||||||
list.add(
|
list.add(
|
||||||
@@ -38,50 +38,7 @@ class BudgetMaker extends StatelessWidget {
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
list.addAll(budgets.map((budget) => Row(
|
list.addAll(budgets.map((budget) => BudgetLine(budget: budget)).toList());
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: DropdownButtonFormField<String>(
|
|
||||||
value: budget.label,
|
|
||||||
onChanged: (value) => context.read<BudgetBloc>().add(BudgetSetLabel(budget, value!)),
|
|
||||||
items: categoryState.categories.map((e) => DropdownMenuItem(value: e.label, child: Text(e.label))).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Text(
|
|
||||||
NumberFormat('####000 €', 'fr_FR').format(budget.value),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontFamily: 'NovaMono',
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 15,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: SliderTheme(
|
|
||||||
data: const SliderThemeData(),
|
|
||||||
child: Slider(
|
|
||||||
min: 0,
|
|
||||||
max: initialBudget,
|
|
||||||
label: budget.value.round().toString(),
|
|
||||||
value: budget.value,
|
|
||||||
secondaryTrackValue: remainingBudget + budget.value,
|
|
||||||
onChanged: (value) => context.read<BudgetBloc>().add(BudgetSetValue(budget, value.round().toDouble())),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () => context.read<BudgetBloc>().add(BudgetRemove(budget)),
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)).toList());
|
|
||||||
|
|
||||||
list.add(
|
list.add(
|
||||||
Container(
|
Container(
|
||||||
@@ -99,9 +56,10 @@ class BudgetMaker extends StatelessWidget {
|
|||||||
|
|
||||||
list.add(
|
list.add(
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
' Remaining ${NumberFormat('#####00 €', 'fr_FR').format(remainingBudget)} out of ${NumberFormat('#####00 €', 'fr_FR').format(initialBudget)}',
|
'${NumberFormat('#####00 €', 'fr_FR').format(remainingBudget)} remaining ',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'NovaMono',
|
fontFamily: 'NovaMono',
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
67
lib/pages/budgets/widgets/budget_radar.dart
Normal file
67
lib/pages/budgets/widgets/budget_radar.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/budget.dart';
|
||||||
|
|
||||||
|
class BudgetRadar extends StatelessWidget {
|
||||||
|
const BudgetRadar({super.key});
|
||||||
|
|
||||||
|
List<RadarDataSet> _computeDataSet(BuildContext context, List<Budget> targetBudgets, List<Budget> realBudgets) {
|
||||||
|
if (realBudgets.isEmpty || targetBudgets.isEmpty) {
|
||||||
|
return [RadarDataSet(dataEntries: [const RadarEntry(value: 1), const RadarEntry(value: 2), const RadarEntry(value: 3)])];
|
||||||
|
}
|
||||||
|
|
||||||
|
RadarDataSet targetDataSet = RadarDataSet(
|
||||||
|
fillColor: Theme.of(context).colorScheme.primary.withAlpha(100),
|
||||||
|
borderColor: Theme.of(context).colorScheme.primary,
|
||||||
|
entryRadius: 5,
|
||||||
|
dataEntries: targetBudgets.map((budget) => RadarEntry(value: budget.value)).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (realBudgets.isNotEmpty) {
|
||||||
|
targetDataSet.dataEntries.add(const RadarEntry(value: 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
RadarDataSet realDataSet = RadarDataSet(
|
||||||
|
fillColor: Theme.of(context).colorScheme.onPrimary.withAlpha(100),
|
||||||
|
borderColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
entryRadius: 8,
|
||||||
|
dataEntries: realBudgets.map((budget) => RadarEntry(value: budget.value)).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return [realDataSet, targetDataSet];
|
||||||
|
}
|
||||||
|
|
||||||
|
RadarChartTitle _computeDataSetTitle(int index, List<Budget> realBudgets) {
|
||||||
|
return RadarChartTitle(text: realBudgets[index].label);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool canShowData(List<Budget> targetBudgets, List<Budget> realBudgets) {
|
||||||
|
return realBudgets.length >= 3 && targetBudgets.length >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<BudgetBloc, BudgetState>(
|
||||||
|
builder: (context, state) => canShowData(state.budgets, state.compareBudgets) ? Expanded(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.3,
|
||||||
|
child: RadarChart(
|
||||||
|
RadarChartData(
|
||||||
|
titlePositionPercentageOffset: 0.15,
|
||||||
|
tickBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 1),
|
||||||
|
gridBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 2),
|
||||||
|
radarBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 3),
|
||||||
|
radarBackgroundColor: Theme.of(context).colorScheme.secondaryContainer.withAlpha(100),
|
||||||
|
radarShape: RadarShape.circle,
|
||||||
|
dataSets: _computeDataSet(context, state.budgets, state.compareBudgets),
|
||||||
|
getTitle: (index, angle) => _computeDataSetTitle(index, state.compareBudgets),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: const Text('No data to show'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:tunas/pages/data/widgets/account_settings.dart';
|
import 'package:tunas/pages/data/widgets/account_settings.dart';
|
||||||
import 'package:tunas/pages/data/widgets/categories_settings.dart';
|
import 'package:tunas/pages/data/widgets/categories_settings.dart';
|
||||||
import 'package:tunas/pages/data/widgets/import_settings.dart';
|
import 'package:tunas/pages/data/widgets/import_settings.dart';
|
||||||
|
import 'package:tunas/pages/data/widgets/settings_settings.dart';
|
||||||
|
|
||||||
class DataPage extends StatelessWidget {
|
class DataPage extends StatelessWidget {
|
||||||
const DataPage({super.key});
|
const DataPage({super.key});
|
||||||
@@ -25,6 +26,7 @@ class DataPage extends StatelessWidget {
|
|||||||
fontSize: 35,
|
fontSize: 35,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SettingsSettings(),
|
||||||
ImportSettings(),
|
ImportSettings(),
|
||||||
AccountSettings(),
|
AccountSettings(),
|
||||||
CategoriesSettings(),
|
CategoriesSettings(),
|
||||||
|
|||||||
@@ -12,25 +12,36 @@ class ImportSettings extends StatelessWidget {
|
|||||||
builder: (context, state) => TitledContainer(
|
builder: (context, state) => TitledContainer(
|
||||||
title: "Import",
|
title: "Import",
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
FilledButton(
|
FilledButton.icon(
|
||||||
|
onPressed: () => context.read<AccountBloc>().add(const ClearData()),
|
||||||
|
label: const Text('ClearData'),
|
||||||
|
icon: const Icon(Icons.delete_forever),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
FilledButton.tonalIcon(
|
||||||
onPressed: () => context.read<AccountBloc>().add(const AccountImportCSV()),
|
onPressed: () => context.read<AccountBloc>().add(const AccountImportCSV()),
|
||||||
child: const Text('Import CSV')
|
label: const Text('Import CSV'),
|
||||||
|
icon: const Icon(Icons.upload_file),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
FilledButton(
|
FilledButton.tonalIcon(
|
||||||
onPressed: () => context.read<AccountBloc>().add(const AccountImportJSON()),
|
onPressed: () => context.read<AccountBloc>().add(const AccountImportJSON()),
|
||||||
child: const Text('Import JSON')
|
label: const Text('Import JSON'),
|
||||||
|
icon: const Icon(Icons.upload_file),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
FilledButton(
|
FilledButton.tonalIcon(
|
||||||
onPressed: () => context.read<AccountBloc>().add(const AccountExportCSV()),
|
onPressed: () => context.read<AccountBloc>().add(const AccountExportCSV()),
|
||||||
child: const Text('Export CSV')
|
label: const Text('Export CSV'),
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
FilledButton(
|
FilledButton.tonalIcon(
|
||||||
onPressed: () => context.read<AccountBloc>().add(const AccountExportJSON()),
|
onPressed: () => context.read<AccountBloc>().add(const AccountExportJSON()),
|
||||||
child: const Text('Export JSON')
|
label: const Text('Export JSON'),
|
||||||
|
icon: const Icon(Icons.download),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
39
lib/pages/data/widgets/settings_settings.dart
Normal file
39
lib/pages/data/widgets/settings_settings.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:tunas/domains/settings/settings_bloc.dart';
|
||||||
|
import 'package:tunas/pages/common/titled_container.dart';
|
||||||
|
|
||||||
|
class SettingsSettings extends StatelessWidget {
|
||||||
|
const SettingsSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<SettingsBloc, SettingsState>(
|
||||||
|
builder: (context, state) => TitledContainer(
|
||||||
|
title: "Theme",
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SegmentedButton<ThemeMode>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: ThemeMode.system,
|
||||||
|
icon: Icon(Icons.settings)
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: ThemeMode.light,
|
||||||
|
icon: Icon(Icons.light_mode)
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: ThemeMode.dark,
|
||||||
|
icon: Icon(Icons.dark_mode)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {state.themeMode},
|
||||||
|
onSelectionChanged: (themeMode) => context.read<SettingsBloc>().add(SetThemeMode(themeMode.first)),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:tunas/domains/account/account_bloc.dart';
|
|||||||
import 'package:tunas/domains/budget/budget_bloc.dart';
|
import 'package:tunas/domains/budget/budget_bloc.dart';
|
||||||
import 'package:tunas/domains/category/category_bloc.dart';
|
import 'package:tunas/domains/category/category_bloc.dart';
|
||||||
import 'package:tunas/domains/charts/chart_bloc.dart';
|
import 'package:tunas/domains/charts/chart_bloc.dart';
|
||||||
|
import 'package:tunas/domains/settings/settings_bloc.dart';
|
||||||
import 'package:tunas/domains/transaction/transaction_bloc.dart';
|
import 'package:tunas/domains/transaction/transaction_bloc.dart';
|
||||||
import 'package:tunas/pages/budgets/budgets_page.dart';
|
import 'package:tunas/pages/budgets/budgets_page.dart';
|
||||||
import 'package:tunas/pages/data/data_page.dart';
|
import 'package:tunas/pages/data/data_page.dart';
|
||||||
@@ -95,6 +96,7 @@ class HomePage extends StatelessWidget {
|
|||||||
BlocProvider(create: (context) => CategoryBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context), transactionsRepository: RepositoryProvider.of<TransactionsRepository>(context))),
|
BlocProvider(create: (context) => CategoryBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context), transactionsRepository: RepositoryProvider.of<TransactionsRepository>(context))),
|
||||||
BlocProvider(create: (context) => BudgetBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context), transactionsRepository: RepositoryProvider.of<TransactionsRepository>(context))),
|
BlocProvider(create: (context) => BudgetBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context), transactionsRepository: RepositoryProvider.of<TransactionsRepository>(context))),
|
||||||
BlocProvider(create: (context) => ChartBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context), transactionsRepository: RepositoryProvider.of<TransactionsRepository>(context))),
|
BlocProvider(create: (context) => ChartBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context), transactionsRepository: RepositoryProvider.of<TransactionsRepository>(context))),
|
||||||
|
BlocProvider(create: (context) => SettingsBloc(metadataRepository: RepositoryProvider.of<MetadataRepository>(context))),
|
||||||
],
|
],
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 4,
|
length: 4,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:tunas/repositories/metadata/models/budget.dart';
|
|||||||
import 'package:tunas/repositories/metadata/models/category.dart';
|
import 'package:tunas/repositories/metadata/models/category.dart';
|
||||||
import 'package:tunas/repositories/metadata/models/account.dart';
|
import 'package:tunas/repositories/metadata/models/account.dart';
|
||||||
import 'package:tunas/repositories/metadata/models/metadata.dart';
|
import 'package:tunas/repositories/metadata/models/metadata.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/settings.dart';
|
||||||
|
|
||||||
class MetadataRepository {
|
class MetadataRepository {
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ class MetadataRepository {
|
|||||||
final _categoriesController = BehaviorSubject<List<Category>>.seeded(const []);
|
final _categoriesController = BehaviorSubject<List<Category>>.seeded(const []);
|
||||||
final _budgetController = BehaviorSubject<List<Budget>>.seeded(const []);
|
final _budgetController = BehaviorSubject<List<Budget>>.seeded(const []);
|
||||||
final _accountController = BehaviorSubject<List<Account>>.seeded(const []);
|
final _accountController = BehaviorSubject<List<Account>>.seeded(const []);
|
||||||
|
final _settingsController = BehaviorSubject<Settings>.seeded(const Settings());
|
||||||
|
|
||||||
MetadataRepository({
|
MetadataRepository({
|
||||||
required jsonRepository,
|
required jsonRepository,
|
||||||
@@ -40,6 +42,14 @@ class MetadataRepository {
|
|||||||
return _accountController.asBroadcastStream();
|
return _accountController.asBroadcastStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Settings getSettings() {
|
||||||
|
return _settingsController.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Settings> getSettingsStream() {
|
||||||
|
return _settingsController.asBroadcastStream();
|
||||||
|
}
|
||||||
|
|
||||||
void loadMetadata() async {
|
void loadMetadata() async {
|
||||||
Metadata metadata = await _jsonRepository.loadJson(Metadata(), MetadataFactory());
|
Metadata metadata = await _jsonRepository.loadJson(Metadata(), MetadataFactory());
|
||||||
_broadcastMetadata(metadata);
|
_broadcastMetadata(metadata);
|
||||||
@@ -69,6 +79,14 @@ class MetadataRepository {
|
|||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Settings saveSettings(Settings settings) {
|
||||||
|
Metadata metadata = _constructMetadataFromControllers();
|
||||||
|
metadata.settings = settings;
|
||||||
|
_jsonRepository.saveJson(metadata);
|
||||||
|
_settingsController.add(settings);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
void deleteMetadata() {
|
void deleteMetadata() {
|
||||||
Metadata metadata = Metadata();
|
Metadata metadata = Metadata();
|
||||||
_jsonRepository.saveJson(metadata);
|
_jsonRepository.saveJson(metadata);
|
||||||
@@ -79,6 +97,7 @@ class MetadataRepository {
|
|||||||
_categoriesController.add(metadata.categories);
|
_categoriesController.add(metadata.categories);
|
||||||
_budgetController.add(metadata.budgets);
|
_budgetController.add(metadata.budgets);
|
||||||
_accountController.add(metadata.accounts);
|
_accountController.add(metadata.accounts);
|
||||||
|
_settingsController.add(metadata.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
Metadata _constructMetadataFromControllers() {
|
Metadata _constructMetadataFromControllers() {
|
||||||
@@ -86,6 +105,7 @@ class MetadataRepository {
|
|||||||
categories: _categoriesController.value,
|
categories: _categoriesController.value,
|
||||||
budgets: _budgetController.value,
|
budgets: _budgetController.value,
|
||||||
accounts: _accountController.value,
|
accounts: _accountController.value,
|
||||||
|
settings: _settingsController.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,16 +2,19 @@ import 'package:tunas/repositories/metadata/models/budget.dart';
|
|||||||
import 'package:tunas/repositories/metadata/models/category.dart';
|
import 'package:tunas/repositories/metadata/models/category.dart';
|
||||||
import 'package:tunas/repositories/json/models/json.dart';
|
import 'package:tunas/repositories/json/models/json.dart';
|
||||||
import 'package:tunas/repositories/metadata/models/account.dart';
|
import 'package:tunas/repositories/metadata/models/account.dart';
|
||||||
|
import 'package:tunas/repositories/metadata/models/settings.dart';
|
||||||
|
|
||||||
class Metadata implements Json {
|
class Metadata implements Json {
|
||||||
List<Budget> budgets;
|
List<Budget> budgets;
|
||||||
List<Category> categories;
|
List<Category> categories;
|
||||||
List<Account> accounts;
|
List<Account> accounts;
|
||||||
|
Settings settings;
|
||||||
|
|
||||||
Metadata({
|
Metadata({
|
||||||
this.budgets = const [],
|
this.budgets = const [],
|
||||||
this.categories = const [],
|
this.categories = const [],
|
||||||
this.accounts = const [],
|
this.accounts = const [],
|
||||||
|
this.settings = const Settings(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
19
lib/repositories/metadata/models/settings.dart
Normal file
19
lib/repositories/metadata/models/settings.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
final ThemeMode themeMode;
|
||||||
|
|
||||||
|
const Settings({
|
||||||
|
this.themeMode = ThemeMode.system,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Settings.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Settings(
|
||||||
|
themeMode: ThemeMode.values.byName(json['themeMode']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> toJson() => {
|
||||||
|
'themeMode': themeMode.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user