Compare commits

..

2 Commits

Author SHA1 Message Date
1a7f28703a Added theme, reworked UI 2024-02-14 23:41:50 +01:00
a51ca14041 Improved stacked graph 2024-02-11 22:49:57 +01:00
24 changed files with 472 additions and 185 deletions

View File

@@ -4,6 +4,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tunas/clients/storage/storage_client.dart';
import 'package:tunas/pages/home/home_page.dart';
import 'package:tunas/repositories/account/account_repository.dart';
import 'package:tunas/theme.dart';
class App extends StatefulWidget {
const App({super.key});
@@ -44,13 +45,9 @@ class _AppViewState extends State<AppView> {
providers: [RepositoryProvider.value(value: _accountRepository)],
child: MaterialApp(
title: 'Tunas',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 55, 55, 55),
brightness: Brightness.dark
),
useMaterial3: true
),
theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme),
themeMode: ThemeMode.dark,
initialRoute: '/home',
routes: {
'/home':(context) => const HomePage(),

View File

@@ -1,9 +1,11 @@
import 'package:equatable/equatable.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/charts/models/month_totals.dart';
import 'package:tunas/domains/transaction/models/transaction_line.dart';
import 'package:tunas/domains/charts/models/chart_item.dart';
import 'package:tunas/repositories/account/account_repository.dart';
import 'package:tunas/repositories/account/models/category.dart';
import 'package:tunas/repositories/account/models/transaction.dart';
part 'chart_event.dart';
@@ -15,20 +17,30 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
ChartBloc({required AccountRepository accountRepository}) :
_accountRepository = accountRepository, super(const ChartState()) {
on<ChartAccountLoad>(_onAccountLoad);
on<ChartTransactionsLoad>(_onChartTransactionsLoad);
on<ChartCategoriesLoad>(_onChartCategoriesLoad);
on<ChartNextYear>(_onNextYear);
on<ChartPreviousYear>(_onPreviousYear);
_accountRepository
.getTransactionsStream()
.listen((transactions) => add(ChartAccountLoad(transactions)));
.listen((transactions) => add(ChartTransactionsLoad(transactions)));
_accountRepository
.getCategoriesStream()
.listen((categories) => add(ChartCategoriesLoad(categories)));
}
_onAccountLoad(ChartAccountLoad event, Emitter<ChartState> emit) {
_onChartTransactionsLoad(ChartTransactionsLoad event, Emitter<ChartState> emit) {
ChartState localState = state.copyWith(transactions: event.transactions);
emit(_computeStateStats(localState));
}
_onChartCategoriesLoad(ChartCategoriesLoad event, Emitter<ChartState> emit) {
ChartState localState = state.copyWith(categories: Map.fromEntries(event.categories.map((category) => MapEntry(category.label, category))));
emit(_computeStateStats(localState));
}
_onNextYear(ChartNextYear event, Emitter<ChartState> emit) {
if (state.lastDate!.year >= state.currentYear + 1) {
ChartState localState = state.copyWith(currentYear: state.currentYear + 1);
@@ -100,7 +112,10 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
List<ChartItem> scopedCategoriesPositiveTotals = [];
List<ChartItem> scopedCategoriesNegativeTotals = [];
Map<int, FlSpot> scopedMonthlyTotals = {};
Map<int, Map<String, double>> scopedCategoriesMonthlyTotals = {};
Map<int, MonthTotals> scopedCategoriesMonthlyTotals = {};
Map<int, double> scopedMonthlyPostitiveTotals = {};
Map<int, double> scopedMonthlyNegativeTotals = {};
for(var transaction in state.transactions) {
globalTotal += transaction.value;
@@ -119,10 +134,17 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
scopedMonthlyTotals[transactionDateDay] = FlSpot(transactionDateDay.toDouble(), transactionLine.subTotal);
if (transaction.category.isEmpty) {
final category = state.categories[transaction.category];
if (category == null || category.saving) {
continue;
}
MonthTotals? categoryMonthTotal = scopedCategoriesMonthlyTotals[transaction.date.month];
if (categoryMonthTotal == null) {
categoryMonthTotal = MonthTotals(negatives: {}, positives: {});
scopedCategoriesMonthlyTotals[transaction.date.month] = categoryMonthTotal;
}
if (transaction.value >= 0) {
ChartItem? chartItem = scopedCategoriesPositiveTotals.firstWhere(
(item) => item.label == transaction.category,
@@ -133,6 +155,9 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
}
);
chartItem.value += transaction.value;
scopedMonthlyPostitiveTotals[transaction.date.month] = transaction.value + (scopedMonthlyPostitiveTotals[transaction.date.month] ?? 0);
categoryMonthTotal.positives[transaction.category] = transaction.value.abs() + (categoryMonthTotal.positives[transaction.category] ?? 0);
}
if (transaction.value < 0) {
@@ -146,13 +171,8 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
);
chartItem.value += transaction.value;
Map<String, double>? a = scopedCategoriesMonthlyTotals[transaction.date.month];
if (scopedCategoriesMonthlyTotals[transaction.date.month] == null) {
a = {};
}
a?[transaction.category] = transaction.value.abs() + (a[transaction.category] ?? 0);
scopedCategoriesMonthlyTotals[transaction.date.month] = a!;
scopedMonthlyNegativeTotals[transaction.date.month] = transaction.value + (scopedMonthlyPostitiveTotals[transaction.date.month] ?? 0);
categoryMonthTotal.negatives[transaction.category] = transaction.value.abs() + (categoryMonthTotal.negatives[transaction.category] ?? 0);
}
}
@@ -193,15 +213,20 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
List<ChartItem> scopedSimplifiedCategoriesPositiveTotals = [];
List<ChartItem> scopedSimplifiedCategoriesNegativeTotals = [];
scopedCategoriesPositiveTotals.sort((a, b) => a.value.compareTo(b.value));
scopedCategoriesPositiveTotalsPercents.sort((a, b) => a.value.compareTo(b.value));
scopedCategoriesPositiveTotals.sort((a, b) => b.value.compareTo(a.value));
scopedCategoriesPositiveTotalsPercents.sort((a, b) => b.value.compareTo(a.value));
scopedCategoriesNegativeTotals.sort((a, b) => a.value.compareTo(b.value));
scopedCategoriesNegativeTotalsPercents.sort((a, b) => a.value.compareTo(b.value));
scopedSimplifiedCategoriesPositiveTotals.sort((a, b) => a.value.compareTo(b.value));
scopedSimplifiedCategoriesPositiveTotalsPercents.sort((a, b) => a.value.compareTo(b.value));
scopedSimplifiedCategoriesPositiveTotals.sort((a, b) => b.value.compareTo(a.value));
scopedSimplifiedCategoriesPositiveTotalsPercents.sort((a, b) => b.value.compareTo(a.value));
scopedSimplifiedCategoriesNegativeTotals.sort((a, b) => a.value.compareTo(b.value));
scopedSimplifiedCategoriesNegativeTotalsPercents.sort((a, b) => a.value.compareTo(b.value));
for (var monthTotals in scopedCategoriesMonthlyTotals.values) {
_sortMapByValues(monthTotals.positives);
_sortMapByValues(monthTotals.negatives);
}
return state.copyWith(
scoppedProfit: scoppedTotal,
scopedCategoriesPositiveTotals: scopedCategoriesPositiveTotals,
@@ -214,6 +239,19 @@ class ChartBloc extends Bloc<ChartEvent, ChartState> {
scopedSimplifiedCategoriesNegativeTotalsPercents: scopedSimplifiedCategoriesNegativeTotalsPercents,
scopedMonthlyTotals: scopedMonthlyTotals.values.toList(),
scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals,
scopedMonthlyPostitiveTotals: scopedMonthlyPostitiveTotals,
scopedMonthlyNegativeTotals: scopedMonthlyNegativeTotals,
);
}
Map<dynamic, dynamic> _sortMapByValues(Map<dynamic, dynamic> map) {
final sortedEntries = map.entries.toList()..sort((a, b) {
var diff = b.value.compareTo(a.value);
return diff;
});
return map
..clear()
..addEntries(sortedEntries);
}
}

View File

@@ -7,13 +7,21 @@ sealed class ChartEvent extends Equatable {
List<Object> get props => [];
}
final class ChartAccountLoad extends ChartEvent {
final class ChartTransactionsLoad extends ChartEvent {
final List<Transaction> transactions;
const ChartAccountLoad(this.transactions);
const ChartTransactionsLoad(this.transactions);
@override
List<Object> get props => [transactions];
}
final class ChartCategoriesLoad extends ChartEvent {
final List<Category> categories;
const ChartCategoriesLoad(this.categories);
@override
List<Object> get props => [categories];
}
final class ChartNextYear extends ChartEvent {}
final class ChartPreviousYear extends ChartEvent {}

View File

@@ -1,6 +1,7 @@
part of 'chart_bloc.dart';
final class ChartState extends Equatable {
final Map<String, Category> categories;
final List<Transaction> transactions;
final List<TransactionLine> transactionsLines;
@@ -27,11 +28,14 @@ final class ChartState extends Equatable {
final List<ChartItem> scopedSimplifiedCategoriesNegativeTotalsPercents;
final List<FlSpot> scopedMonthlyTotals;
final Map<int, Map<String, double>> scopedCategoriesMonthlyTotals;
final Map<int, MonthTotals> scopedCategoriesMonthlyTotals;
final Map<int, double> scopedMonthlyPostitiveTotals;
final Map<int, double> scopedMonthlyNegativeTotals;
final double scoppedProfit;
const ChartState({
this.categories = const {},
this.transactions = const [],
this.transactionsLines = const [],
this.globalTotal = 0,
@@ -53,9 +57,12 @@ final class ChartState extends Equatable {
this.scopedMonthlyTotals = const [],
this.scopedCategoriesMonthlyTotals = const {},
this.scoppedProfit = 0,
this.scopedMonthlyPostitiveTotals = const {},
this.scopedMonthlyNegativeTotals = const {},
});
ChartState copyWith({
Map<String, Category>? categories,
List<Transaction>? transactions,
List<TransactionLine>? transactionsLines,
double? globalTotal,
@@ -75,10 +82,13 @@ final class ChartState extends Equatable {
List<ChartItem>? scopedSimplifiedCategoriesNegativeTotals,
List<ChartItem>? scopedSimplifiedCategoriesNegativeTotalsPercents,
List<FlSpot>? scopedMonthlyTotals,
Map<int, Map<String, double>>? scopedCategoriesMonthlyTotals,
Map<int, MonthTotals>? scopedCategoriesMonthlyTotals,
double? scoppedProfit,
Map<int, double>? scopedMonthlyPostitiveTotals,
Map<int, double>? scopedMonthlyNegativeTotals,
}) {
return ChartState(
categories: categories ?? this.categories,
transactions: transactions ?? this.transactions,
transactionsLines: transactionsLines ?? this.transactionsLines,
globalTotal: globalTotal ?? this.globalTotal,
@@ -100,11 +110,14 @@ final class ChartState extends Equatable {
scopedMonthlyTotals: scopedMonthlyTotals ?? this.scopedMonthlyTotals,
scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals ?? this.scopedCategoriesMonthlyTotals,
scoppedProfit: scoppedProfit ?? this.scoppedProfit,
scopedMonthlyPostitiveTotals: scopedMonthlyPostitiveTotals ?? this.scopedMonthlyPostitiveTotals,
scopedMonthlyNegativeTotals: scopedMonthlyNegativeTotals ?? this.scopedMonthlyNegativeTotals,
);
}
@override
List<Object> get props => [
categories,
transactions,
transactionsLines,
globalTotal,
@@ -124,6 +137,8 @@ final class ChartState extends Equatable {
scopedMonthlyTotals,
scopedCategoriesMonthlyTotals,
scoppedProfit,
scopedMonthlyPostitiveTotals,
scopedMonthlyNegativeTotals,
];
}

View File

@@ -0,0 +1,29 @@
class MonthTotals {
Map<String, double> positives;
Map<String, double> negatives;
MonthTotals({
required this.positives,
required this.negatives,
});
double maxValue() {
double max = 0.0;
if (positives.isNotEmpty) {
double localMax = positives.values.reduce((value, element) => value + element);
if (localMax > max) {
max = localMax;
}
}
if (negatives.isNotEmpty) {
double localMax2 = negatives.values.reduce((value, element) => value + element);
if (localMax2 > max) {
max = localMax2;
}
}
return max;
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
class TitledContainer extends StatelessWidget {
final String title;
final Widget child;
const TitledContainer({super.key, required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.shadow,
blurRadius: 10,
offset: const Offset(2, 2),
spreadRadius: 0.1,
blurStyle: BlurStyle.normal,
)
]
),
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w300,
fontSize: 20,
),
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
child: child,
),
],
)
);
}
}

View File

@@ -15,24 +15,25 @@ class DataPage extends StatelessWidget {
),
padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10),
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10),
child: const Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
const Text(
'Settings',
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 35,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ImportSettings(),
CategoriesSettings(),
AccountSettings(),
],
)
Expanded(
child: ListView(
children: const [
ImportSettings(),
CategoriesSettings(),
AccountSettings(),
]
)
),
]
)
)

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/account/account_bloc.dart';
import 'package:tunas/pages/common/titled_container.dart';
class AccountSettings extends StatelessWidget {
const AccountSettings({super.key});
@@ -16,34 +17,15 @@ class AccountSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) => Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(5),
builder: (context, state) => TitledContainer(
title: "Accounts",
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _computeCategoryList(state.subAccounts),
),
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
margin: const EdgeInsets.all(5),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Accounts",
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _computeCategoryList(state.subAccounts),
),
),
],
)
),
);
}

View File

@@ -1,21 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/category/category_bloc.dart';
import 'package:tunas/pages/common/titled_container.dart';
import 'package:tunas/repositories/account/models/category.dart';
class CategoriesSettings extends StatelessWidget {
const CategoriesSettings({super.key});
List<Widget> _computeCategoryList(List<Category> categories) {
List<Widget> _computeCategoryList(BuildContext context, List<Category> categories) {
return categories.map((category) => Row(
children: [
Container(
height: 10,
width: 10,
IconButton(
onPressed: () {},
icon: const Icon(Icons.palette),
color: category.rgbToColor(),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.savings),
color: category.saving ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error,
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.foundation),
color: category.essential ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error,
),
Container(width: 5),
Text(category.label),
Expanded(
child: Text(category.label)
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.edit),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.delete),
),
],
)).toList();
}
@@ -23,35 +44,16 @@ class CategoriesSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<CategoryBloc, CategoryState>(
builder: (context, state) => Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(5),
builder: (context, state) => TitledContainer(
title: "Categories",
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _computeCategoryList(context, state.categories),
),
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
margin: const EdgeInsets.all(5),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Categories",
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _computeCategoryList(state.categories),
),
),
],
)
),
)
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/account/account_bloc.dart';
import 'package:tunas/pages/common/titled_container.dart';
class ImportSettings extends StatelessWidget {
const ImportSettings({super.key});
@@ -8,25 +9,10 @@ class ImportSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) => Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(5)
),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
margin: const EdgeInsets.all(5),
builder: (context, state) => TitledContainer(
title: "Import",
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Import",
style: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(height: 10),
FilledButton(
onPressed: () => context.read<AccountBloc>().add(const AccountImportCSV()),
child: const Text('Import CSV')

View File

@@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tunas/domains/charts/chart_bloc.dart';
import 'package:tunas/pages/stats/widgets/account_counters.dart';
import 'package:tunas/pages/stats/widgets/categories_totals_chart.dart';
import 'package:tunas/pages/stats/widgets/main_counter.dart';
import 'package:tunas/pages/stats/widgets/global_counter.dart';
import 'package:tunas/pages/stats/widgets/monthly_categories_total_chart.dart';
import 'package:tunas/pages/stats/widgets/monthly_total_chart.dart';
import 'package:tunas/pages/stats/widgets/global_total_chart.dart';
import 'package:tunas/pages/stats/widgets/profit_indicator.dart';
import 'package:tunas/pages/stats/widgets/year_selector.dart';
import 'package:tunas/repositories/account/account_repository.dart';
@@ -31,7 +31,7 @@ class StatsPage extends StatelessWidget {
children: [
Expanded(
flex: 2,
child: MainCounter(value: state.globalTotal)
child: GlobalCounter(value: state.globalTotal)
),
Expanded(
flex: 1,

View File

@@ -6,7 +6,7 @@ class AccountCounter extends StatelessWidget {
const AccountCounter({super.key, required this.accountsTotals});
List<Row> _renderAccountTotals() {
List<Row> _renderAccountTotals(BuildContext context) {
return accountsTotals.entries.toList().map((entry) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -21,7 +21,7 @@ class AccountCounter extends StatelessWidget {
style: TextStyle(
fontFamily: 'NovaMono',
fontSize: 15,
color: entry.value > 0 ? const Color.fromARGB(255, 0, 255, 8) : Colors.red
color: entry.value > 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error
)),
],
)).toList();
@@ -33,13 +33,22 @@ class AccountCounter extends StatelessWidget {
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blue
borderRadius: BorderRadius.circular(15),
color: Theme.of(context).colorScheme.primaryContainer,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.shadow,
blurRadius: 10,
offset: const Offset(2, 2),
spreadRadius: 0.1,
blurStyle: BlurStyle.normal,
)
]
),
alignment: Alignment.centerRight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: _renderAccountTotals(),
children: _renderAccountTotals(context),
),
);
}

View File

@@ -21,9 +21,9 @@ class CategoriesTotalsChart extends StatelessWidget {
fontSize: 15,
fontWeight: FontWeight.w300
),
titlePositionPercentageOffset: 0.8,
titlePositionPercentageOffset: 0.5,
borderSide: const BorderSide(width: 0),
radius: 150,
radius: 40,
color: categoriesColors[item.label]
))
.toList();
@@ -39,7 +39,10 @@ class CategoriesTotalsChart extends StatelessWidget {
Container(
height: 10,
width: 10,
color: categoriesColors[item.label],
decoration: BoxDecoration(
color: categoriesColors[item.label],
borderRadius: BorderRadius.circular(15)
),
),
Container(width: 5),
Text(item.label),
@@ -60,12 +63,21 @@ class CategoriesTotalsChart extends StatelessWidget {
return BlocBuilder<CategoryBloc, CategoryState>(
builder: (context, state) => Container(
height: 320,
width: 600,
width: 500,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blue
borderRadius: BorderRadius.circular(15),
color: Theme.of(context).colorScheme.primaryContainer,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.shadow,
blurRadius: 10,
offset: const Offset(2, 2),
spreadRadius: 0.1,
blurStyle: BlurStyle.normal,
)
]
),
child: Row(
children: [
@@ -76,8 +88,8 @@ class CategoriesTotalsChart extends StatelessWidget {
borderData: FlBorderData(
show: false
),
centerSpaceRadius: 0,
sectionsSpace: 2
centerSpaceRadius: 50,
sectionsSpace: 4
)
),
),
@@ -87,7 +99,7 @@ class CategoriesTotalsChart extends StatelessWidget {
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blueGrey
color: Theme.of(context).colorScheme.secondaryContainer
),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class GlobalCounter extends StatelessWidget {
final double value;
const GlobalCounter({super.key, required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Theme.of(context).colorScheme.primaryContainer,
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.shadow,
blurRadius: 10,
offset: const Offset(2, 2),
spreadRadius: 0.1,
blurStyle: BlurStyle.normal,
)
]
),
alignment: Alignment.centerRight,
child: Text(
NumberFormat('000.00 €', 'fr_FR').format(value),
style: TextStyle(
fontFamily: 'NovaMono',
fontSize: 60,
fontWeight: FontWeight.w500,
color: value > 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error
),
),
);
}
}

View File

@@ -18,9 +18,7 @@ class GlobalTotalChart extends StatelessWidget {
return Padding(
padding: const EdgeInsets.only(
left: 12,
bottom: 12,
right: 20,
top: 20,
),
child: AspectRatio(
@@ -31,7 +29,7 @@ class GlobalTotalChart extends StatelessWidget {
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
maxContentWidth: 100,
tooltipBgColor: Colors.black,
tooltipBgColor: Theme.of(context).colorScheme.primaryContainer,
getTooltipItems: (touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
final textStyle = TextStyle(
@@ -52,13 +50,18 @@ class GlobalTotalChart extends StatelessWidget {
),
lineBarsData: [
LineChartBarData(
color: Colors.pink,
color: Theme.of(context).colorScheme.primary,
spots: monthlyTotals,
isCurved: true,
isStrokeCapRound: true,
barWidth: 3,
belowBarData: BarAreaData(
show: false,
show: true,
gradient: LinearGradient(
colors: [Theme.of(context).colorScheme.primary, Theme.of(context).colorScheme.secondary]
.map((color) => color.withOpacity(0.2))
.toList(),
),
),
dotData: const FlDotData(show: false),
),

View File

@@ -1,30 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MainCounter extends StatelessWidget {
final double value;
const MainCounter({super.key, required this.value});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: Colors.blue
),
alignment: Alignment.centerRight,
child: Text(
NumberFormat('000.00 €', 'fr_FR').format(value),
style: TextStyle(
fontFamily: 'NovaMono',
fontSize: 60,
fontWeight: FontWeight.w500,
color: value > 0 ? const Color.fromARGB(255, 0, 255, 8) : Colors.red
),
),
);
}
}

View File

@@ -1,22 +1,28 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:tunas/domains/category/category_bloc.dart';
import 'package:tunas/domains/charts/models/month_totals.dart';
class MonthlyCategoriesTotalChart extends StatelessWidget {
final Map<int, Map<String, double>> categoriesMonthlyTotals;
final Map<int, MonthTotals> categoriesMonthlyTotals;
const MonthlyCategoriesTotalChart({super.key, required this.categoriesMonthlyTotals});
BarChartRodData _computeStack(double barsWidth, MapEntry<int, Map<String, double>> entry, Map<String, Color> categoriesColors) {
BarChartRodData _computeStack(double barsWidth, Map<String, double> entry, Map<String, Color> categoriesColors) {
var subcounter = 0.0;
var a = entry.value.entries.map((subEntry) => BarChartRodStackItem(subcounter, subcounter += subEntry.value, categoriesColors[subEntry.key] ?? Colors.red)).toList();
var items = entry.entries.map((subEntry) => BarChartRodStackItem(
subcounter, subcounter += subEntry.value,
categoriesColors[subEntry.key] ?? Colors.red,
)).toList();
return BarChartRodData(
color: Colors.transparent,
fromY: 0,
toY: subcounter,
width: barsWidth,
borderRadius: BorderRadius.zero,
rodStackItems: a
borderRadius: BorderRadius.circular(3),
rodStackItems: items
);
}
@@ -26,7 +32,11 @@ class MonthlyCategoriesTotalChart extends StatelessWidget {
return BarChartGroupData(
x: entry.key,
barsSpace: barsSpace,
barRods: [_computeStack(barsWidth, entry, categoriesColors)]
barRods: [
_computeStack(barsWidth, entry.value.positives, categoriesColors),
_computeStack(barsWidth, entry.value.negatives, categoriesColors),
],
showingTooltipIndicators: [0, 1]
);
})
.toList();
@@ -46,7 +56,7 @@ class MonthlyCategoriesTotalChart extends StatelessWidget {
double _computeMaxValue() {
double max = 0.0;
categoriesMonthlyTotals.forEach((monthKey, value) {
double localMax = value.values.reduce((value, element) => value + element);
double localMax = value.maxValue();
if (localMax > max) {
max = localMax;
}
@@ -61,8 +71,8 @@ class MonthlyCategoriesTotalChart extends StatelessWidget {
aspectRatio: 1.66,
child: LayoutBuilder(
builder: (context, constraints) {
final barsSpace = 4.0 * constraints.maxWidth / 100;
final barsWidth = 8.0 * constraints.maxWidth / 130;
final barsSpace = 4.0 * constraints.maxWidth / 300;
final barsWidth = 8.0 * constraints.maxWidth / 500;
return BarChart(
BarChartData(
@@ -78,6 +88,41 @@ class MonthlyCategoriesTotalChart extends StatelessWidget {
getTitlesWidget: _computeBottomTitles
)
)
),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: false,
touchTooltipData: BarTouchTooltipData(
tooltipBgColor: Colors.transparent,
tooltipMargin: 0,
getTooltipItem:(group, groupIndex, rod, rodIndex) {
String value = NumberFormat("#00").format(rod.toY);
Color color = Colors.black;
if (rodIndex == 0) {
value = "+$value";
color = Theme.of(context).colorScheme.primary;
} else {
value = "-$value";
color = Theme.of(context).colorScheme.error;
}
return BarTooltipItem(
value,
TextStyle(
fontWeight: FontWeight.bold,
color: color,
fontSize: 12,
fontFamily: 'NovaMono',
shadows: const [
Shadow(
color: Colors.black26,
blurRadius: 12,
)
],
),
);
}
)
)
)
);

View File

@@ -13,7 +13,7 @@ class ProfitIndicator extends StatelessWidget {
margin: const EdgeInsets.fromLTRB(0, 0, 20, 0),
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
decoration: BoxDecoration(
color: Colors.blue,
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(5)
),
child: Text(
@@ -22,7 +22,7 @@ class ProfitIndicator extends StatelessWidget {
fontFamily: 'NovaMono',
fontSize: 20,
fontWeight: FontWeight.w500,
color: profit > 0 ? const Color.fromARGB(255, 0, 255, 8) : Colors.red
color: profit > 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error
),
)
);

View File

@@ -12,8 +12,8 @@ class YearSelector extends StatelessWidget {
margin: const EdgeInsets.fromLTRB(20, 0, 0, 0),
padding: const EdgeInsets.fromLTRB(5, 0, 5, 0),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(5)
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(5),
),
child: Row(
children: [

View File

@@ -48,13 +48,13 @@ class TransactionLine extends StatelessWidget {
Text(
NumberFormat(transaction.value > 0 ? '+#######.00 €' : '#######.00 €', 'fr_FR').format(transaction.value),
style: TextStyle(
color: transaction.value > 0 ? Colors.green : Colors.red
color: transaction.value > 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error
)
),
Text(
NumberFormat('#######.00 €', 'fr_FR').format(subTotal),
style: TextStyle(
color: subTotal > 0 ? Colors.green : Colors.red
color: subTotal > 0 ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.error
)
),
],

View File

@@ -3,22 +3,30 @@ import 'dart:ui';
class Category {
String label;
String color;
bool essential;
bool saving;
Category({
this.label = '',
this.color = '',
this.essential = false,
this.saving = false,
});
factory Category.fromJson(Map<String, dynamic> json) {
return Category(
label: json['label'],
color: json['color']
color: json['color'],
essential: bool.parse(json['essential']),
saving: bool.parse(json['saving']),
);
}
Map<String, String> toJson() => {
'label': label,
'color': color,
'essential': essential.toString(),
'saving': saving.toString(),
};
Color rgbToColor() {

69
lib/theme.dart Normal file
View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
const lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF006C45),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFF51FFB1),
onPrimaryContainer: Color(0xFF002112),
secondary: Color(0xFF4E6355),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFD0E8D7),
onSecondaryContainer: Color(0xFF0B1F15),
tertiary: Color(0xFF3C6472),
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFFBFE9F9),
onTertiaryContainer: Color(0xFF001F28),
error: Color(0xFFBA1A1A),
errorContainer: Color(0xFFFFDAD6),
onError: Color(0xFFFFFFFF),
onErrorContainer: Color(0xFF410002),
background: Color(0xFFFBFDF8),
onBackground: Color(0xFF191C1A),
surface: Color(0xFFFBFDF8),
onSurface: Color(0xFF191C1A),
surfaceVariant: Color(0xFFDCE5DC),
onSurfaceVariant: Color(0xFF404943),
outline: Color(0xFF707972),
onInverseSurface: Color(0xFFEFF1ED),
inverseSurface: Color(0xFF2E312E),
inversePrimary: Color(0xFF21E297),
shadow: Color(0xFF000000),
surfaceTint: Color(0xFF006C45),
outlineVariant: Color(0xFFC0C9C1),
scrim: Color(0xFF000000),
);
const darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFF21E297),
onPrimary: Color(0xFF003822),
primaryContainer: Color(0xFF005233),
onPrimaryContainer: Color(0xFF51FFB1),
secondary: Color(0xFFB4CCBB),
onSecondary: Color(0xFF203529),
secondaryContainer: Color(0xFF364B3E),
onSecondaryContainer: Color(0xFFD0E8D7),
tertiary: Color(0xFFA4CDDD),
onTertiary: Color(0xFF043542),
tertiaryContainer: Color(0xFF234C59),
onTertiaryContainer: Color(0xFFBFE9F9),
error: Color(0xFFFFB4AB),
errorContainer: Color(0xFF93000A),
onError: Color(0xFF690005),
onErrorContainer: Color(0xFFFFDAD6),
background: Color(0xFF191C1A),
onBackground: Color(0xFFE1E3DF),
surface: Color(0xFF191C1A),
onSurface: Color(0xFFE1E3DF),
surfaceVariant: Color(0xFF404943),
onSurfaceVariant: Color(0xFFC0C9C1),
outline: Color(0xFF8A938B),
onInverseSurface: Color(0xFF191C1A),
inverseSurface: Color(0xFFE1E3DF),
inversePrimary: Color(0xFF006C45),
shadow: Color(0xFF000000),
surfaceTint: Color(0xFF21E297),
outlineVariant: Color(0xFF404943),
scrim: Color(0xFF000000),
);

View File

@@ -121,6 +121,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.65.0"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
flutter:
dependency: "direct main"
description: flutter
@@ -130,10 +146,10 @@ packages:
dependency: "direct main"
description:
name: flutter_bloc
sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1"
url: "https://pub.dev"
source: hosted
version: "8.1.3"
version: "8.1.4"
flutter_keyboard_visibility:
dependency: transitive
description:

View File

@@ -15,7 +15,7 @@ dependencies:
cupertino_icons: ^1.0.2
fl_chart: ^0.65.0
logging: ^1.2.0
flutter_bloc: ^8.1.3
flutter_bloc: ^8.1.4
path_provider: ^2.1.1
equatable: ^2.0.5
rxdart: ^0.27.7
@@ -27,6 +27,7 @@ dependencies:
formz: ^0.6.1
uuid: ^4.3.2
flutter_typeahead: ^5.2.0
flex_color_picker: ^3.3.1
dev_dependencies:
flutter_test: