diff --git a/assets/images/money_back.jpg b/assets/images/money_back.jpg new file mode 100644 index 0000000..d49ca13 Binary files /dev/null and b/assets/images/money_back.jpg differ diff --git a/lib/accounts/cubit/accounts_cubit.dart b/lib/accounts/cubit/accounts_cubit.dart index 435049e..828d331 100644 --- a/lib/accounts/cubit/accounts_cubit.dart +++ b/lib/accounts/cubit/accounts_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; +import 'package:budget_app/accounts/models/accounts_view_filter.dart'; import 'package:budget_app/shared/shared.dart'; import 'package:budget_app/transactions/models/transaction.dart'; import 'package:equatable/equatable.dart'; @@ -18,11 +19,11 @@ class AccountsCubit extends Cubit { late final StreamSubscription> _accountsSubscription; AccountsCubit( - {required String categoryId, required AccountsRepository accountsRepository, + {required AccountsViewFilter filter, required AccountsRepository accountsRepository, required TransactionsRepository transactionsRepository}) : _accountsRepository = accountsRepository, _transactionsRepository = transactionsRepository, - super(AccountsState(categoryId: categoryId)) { + super(AccountsState(filter: filter)) { _transactionsSubscription = _transactionsRepository .getTransactions() .skip(1) @@ -31,31 +32,28 @@ class AccountsCubit extends Cubit { }); _accountsSubscription = _accountsRepository .getAccounts() - .skip(1) .listen((accounts) { fetchAllAccounts(); }); } Future fetchAllAccounts() async { - emit( - state.copyWith(status: DataStatus.loading)); try { final accountList = await _accountsRepository.getAccounts().first; - List filteredAccounts; - if(state.categoryId == 'all_accounts'){ - filteredAccounts = accountList; - }else { - filteredAccounts = accountList.where((acc) => acc.categoryId == state.categoryId).toList(); - } emit( - state.copyWith(status: DataStatus.success, accountList: filteredAccounts)); + state.copyWith(status: DataStatus.success, accountList: accountList)); } catch (e) { emit( state.copyWith(status: DataStatus.error, errorMessage: e.toString())); } } + Future changeExpanded(int index)async{ + var accounts = [...state.accountList]; + accounts[index] = accounts[index].copyWith(isExpanded: !accounts[index].isExpanded); + emit(state.copyWith(accountList: accounts)); + } + @override Future close() { _transactionsSubscription.cancel(); diff --git a/lib/accounts/cubit/accounts_state.dart b/lib/accounts/cubit/accounts_state.dart index 717d585..6a62f8c 100644 --- a/lib/accounts/cubit/accounts_state.dart +++ b/lib/accounts/cubit/accounts_state.dart @@ -1,31 +1,34 @@ part of 'accounts_cubit.dart'; class AccountsState extends Equatable { - final String categoryId; final DataStatus status; final List accountList; + final AccountsViewFilter filter; final String? errorMessage; const AccountsState( - {required this.categoryId, this.status = DataStatus.loading, + {this.status = DataStatus.loading, this.accountList = const [], + required this.filter, this.errorMessage}); + List get filteredAccounts => filter.applyAll(accountList); + AccountsState copyWith({ String? budgetId, - String? categoryId, DataStatus? status, List? accountList, + AccountsViewFilter? filter, String? errorMessage, }) { return AccountsState( - categoryId: categoryId ?? this.categoryId, status: status ?? this.status, accountList: accountList ?? this.accountList, + filter: filter ?? this.filter, errorMessage: errorMessage ?? this.errorMessage, ); } @override - List get props => [categoryId, status, accountList]; + List get props => [status, accountList, filter]; } diff --git a/lib/accounts/models/account.dart b/lib/accounts/models/account.dart index 78bdc2e..cea473f 100644 --- a/lib/accounts/models/account.dart +++ b/lib/accounts/models/account.dart @@ -15,6 +15,7 @@ class Account extends Equatable{ final double initialBalance; final bool includeInTotal; final String budgetId; + final bool isExpanded; const Account({ this.id, @@ -24,9 +25,34 @@ class Account extends Equatable{ required this.balance, required this.initialBalance, this.includeInTotal = true, - required this.budgetId + required this.budgetId, + this.isExpanded = false }); + Account copyWith({ + String? id, + String? name, + String? categoryId, + String? currency, + double? balance, + double? initialBalance, + bool? includeInTotal, + String? budgetId, + bool? isExpanded, +}){ + return Account( + id: id ?? this.id, + name: name ?? this.name, + categoryId: categoryId ?? this.categoryId, + currency: currency ?? this.currency, + balance: balance ?? this.balance, + initialBalance: initialBalance ?? this.initialBalance, + includeInTotal: includeInTotal ?? this.includeInTotal, + budgetId: budgetId ?? this.budgetId, + isExpanded: isExpanded ?? this.isExpanded + ); + } + String extendName(List categories){ final category = categories.where((element) => element.id == this.categoryId).first; return '${category.name} / ${this.name}'; @@ -36,5 +62,5 @@ class Account extends Equatable{ Map toJson() => _$AccountToJson(this); @override - List get props => [id, name, balance]; + List get props => [id, name, balance, isExpanded]; } diff --git a/lib/accounts/models/accounts_view_filter.dart b/lib/accounts/models/accounts_view_filter.dart new file mode 100644 index 0000000..8c6d0f6 --- /dev/null +++ b/lib/accounts/models/accounts_view_filter.dart @@ -0,0 +1,14 @@ +import 'account.dart'; + +class AccountsViewFilter { + final String filterId; + const AccountsViewFilter({required this.filterId}); + + bool apply({required Account account}) { + return account.categoryId == filterId; + } + + List applyAll(Iterable accountsList) { + return accountsList.where((acc) => apply(account: acc)).toList(); + } +} diff --git a/lib/accounts/view/accounts_page.dart b/lib/accounts/view/accounts_page.dart deleted file mode 100644 index 3a811bd..0000000 --- a/lib/accounts/view/accounts_page.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:budget_app/accounts/cubit/accounts_cubit.dart'; -import 'package:budget_app/accounts/repository/accounts_repository.dart'; -import 'package:budget_app/app/app.dart'; -import 'package:budget_app/home/cubit/home_cubit.dart'; -import 'package:budget_app/shared/shared.dart'; -import 'package:budget_app/transactions/models/transactions_view_filter.dart'; -import 'package:budget_app/transactions/repository/transactions_repository.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -import '../../transactions/view/transactions_page.dart'; - -class AccountsPage extends StatelessWidget { - const AccountsPage({Key? key}) : super(key: key); - - static const routeName = '/accounts'; - - static Route route( - {required HomeCubit homeCubit, - required String categoryId, - required DateTime dateTime}) { - return MaterialPageRoute(builder: (context) { - final appBloc = BlocProvider.of(context); - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => AccountsCubit( - categoryId: categoryId, - transactionsRepository: - context.read(), - accountsRepository: context.read()) - ..fetchAllAccounts(), - ), - BlocProvider.value(value: homeCubit), - ], - child: AccountsPage(), - ); - }); - } - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final scheme = Theme.of(context).colorScheme; - return Scaffold( - appBar: AppBar( - title: Text('Accounts'), - centerTitle: true, - ), - body: BlocBuilder( - builder: (context, state) { - return state.status == DataStatus.loading - ? Center(child: CircularProgressIndicator()) - : Column( - children: [ - SizedBox(height: 50.h,), - Expanded( - child: ListView.builder( - itemCount: state.accountList.length, - itemBuilder: (context, index) { - final acc = state.accountList[index]; - return Card( - margin: EdgeInsets.symmetric( - horizontal: 30.w, vertical: 15.h), - elevation: Theme.of(context).cardTheme.elevation, - child: ListTile( - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '\$ ${acc.balance.toString()}', - style: TextStyle( - fontSize: textTheme.titleLarge!.fontSize, - fontWeight: FontWeight.bold), - ), - SizedBox( - width: 30.w, - ), - Icon(Icons.chevron_right), - ], - ), - title: Text( - acc.name, - style: TextStyle( - fontSize: textTheme.titleLarge!.fontSize), - ), - onTap: () => - Navigator.of(context).push(TransactionsPage.route( - homeCubit: context.read(), - filter: TransactionsViewFilter( - type: TransactionsViewFilterTypes.accountId, - filterId: acc.id!), - ))), - ); - }), - ), - ], - ); - })); - } -} diff --git a/lib/accounts_list/view/accounts_list_page.dart b/lib/accounts_list/view/accounts_list_page.dart index aaddbf8..3daab3d 100644 --- a/lib/accounts_list/view/accounts_list_page.dart +++ b/lib/accounts_list/view/accounts_list_page.dart @@ -1,7 +1,6 @@ import 'package:budget_app/account_edit/bloc/account_edit_bloc.dart'; import 'package:budget_app/account_edit/view/account_edit_form.dart'; import 'package:budget_app/accounts/repository/accounts_repository.dart'; -import 'package:budget_app/app/app.dart'; import 'package:budget_app/categories/models/category.dart'; import 'package:budget_app/colors.dart'; import 'package:budget_app/home/cubit/home_cubit.dart'; @@ -17,8 +16,7 @@ class AccountsListPage extends StatelessWidget { const AccountsListPage({Key? key}) : super(key: key); static Route route( - {required HomeCubit homeCubit, - required List accountCategories}) { + {required HomeCubit homeCubit}) { return MaterialPageRoute( fullscreenDialog: true, builder: (context) { diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 9943f26..f7ae765 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -16,6 +16,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../constants/constants.dart'; import '../../subcategories/repository/subcategories_repository.dart'; import '../../theme.dart'; import '../../transactions/transaction/view/transaction_page.dart'; @@ -67,6 +68,8 @@ class _AppViewState extends State { @override Widget build(BuildContext context) { + w = MediaQuery.of(context).size.width; + h = MediaQuery.of(context).size.height; return ScreenUtilInit( designSize: const Size(1080, 2160), minTextAdapt: true, diff --git a/lib/colors.dart b/lib/colors.dart index 55f68ec..25b6417 100644 --- a/lib/colors.dart +++ b/lib/colors.dart @@ -5,6 +5,7 @@ class BudgetColors { static const Color statusBar = Color(0xFF00695C); static const Color teal900 = Color(0xFF004D40); static const Color teal600 = Color(0xFF00897B); + static const Color teal300 = Color(0xFF4DB6AC); static const Color teal50 = Color(0xFFE0F2F1); static const Color teal100 = Color(0xFFB2DFDB); static const Color teal200 = Color(0xFF80CBC4); diff --git a/lib/constants/api.dart b/lib/constants/api.dart index 8f83836..40c2b8e 100644 --- a/lib/constants/api.dart +++ b/lib/constants/api.dart @@ -1,10 +1,11 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:budget_app/shared/models/budget.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:shared_preferences/shared_preferences.dart'; -const baseURL = '10.0.2.2:8080'; +final baseURL = kIsWeb ? 'localhost:8080' : '10.0.2.2:8080'; Future> getHeaders() async { @@ -27,4 +28,4 @@ Future getBudgetId() async { budget = Budget(id: '', userId: ''); } return budget.id; -} \ No newline at end of file +} diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart new file mode 100644 index 0000000..7d0082a --- /dev/null +++ b/lib/constants/constants.dart @@ -0,0 +1,9 @@ +// defined in app.dart +import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; +import 'package:flutter/cupertino.dart'; + +late double w; +late double h; + +bool isDisplayDesktop(BuildContext context) => + getWindowType(context) >= AdaptiveWindowType.medium; diff --git a/lib/debt_payoff_planner/view/payoff_page.dart b/lib/debt_payoff_planner/view/payoff_page.dart index d5a4c78..ee86646 100644 --- a/lib/debt_payoff_planner/view/payoff_page.dart +++ b/lib/debt_payoff_planner/view/payoff_page.dart @@ -1,6 +1,9 @@ +import 'package:budget_app/colors.dart'; +import 'package:budget_app/constants/constants.dart'; import 'package:budget_app/debt_payoff_planner/cubits/strategy_cubit/strategy_cubit.dart'; import 'package:budget_app/debt_payoff_planner/repository/debts_repository.dart'; import 'package:budget_app/debt_payoff_planner/view/widgets/widgets.dart'; +import 'package:budget_app/drawer/main_drawer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -33,16 +36,15 @@ class DebtPayoffPage extends StatelessWidget { @override Widget build(BuildContext context) { - return DebtPayoffView(); + return isDisplayDesktop(context) ? DebtPayoffViewDesktop() : DebtPayoffViewMobile(); } } -class DebtPayoffView extends StatelessWidget { - const DebtPayoffView({super.key}); +class DebtPayoffViewMobile extends StatelessWidget { + const DebtPayoffViewMobile({super.key}); @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: Text('Debt payoff planner'), @@ -88,3 +90,71 @@ class DebtPayoffView extends StatelessWidget { ); } } + +class DebtPayoffViewDesktop extends StatelessWidget { + const DebtPayoffViewDesktop({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + MainDrawer(), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: w * 0.15, vertical: h * 0.05), + child: Card(borderOnForeground: true, + child: ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Scaffold( + backgroundColor: BudgetColors.teal50, + appBar: AppBar( + title: Text('Debt payoff planner'), + centerTitle: true, + actions: [ + IconButton( + onPressed: () async { + _openDialog(context: context); + }, + icon: Icon(Icons.add)), + StrategySelectButton() + ], + ), + //bottomNavigationBar: DebtController(), + body: SingleChildScrollView( + child: Column( + children: [ + DebtController(), + DebtCarousel( + onEdit: (debt) => _openDialog(debt: debt, context: context)), + DebtStrategy(), + ], + )), + ), + ), + ), + ), + ), + ], + ); + } + + void _openDialog({required BuildContext context, Debt? debt}) { + showDialog( + context: context, + builder: (_) => MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => + DebtBloc(debtsRepository: context.read()) + ..add(FormInitEvent(debt: debt)), + ), + BlocProvider.value( + value: context.read(), + ), + ], + child: DebtDialog(), + ), + ); + } +} + diff --git a/lib/drawer/main_drawer.dart b/lib/drawer/main_drawer.dart index c4fa549..447c4bf 100644 --- a/lib/drawer/main_drawer.dart +++ b/lib/drawer/main_drawer.dart @@ -1,6 +1,11 @@ import 'package:budget_app/colors.dart'; +import 'package:budget_app/constants/constants.dart'; import 'package:budget_app/debt_payoff_planner/view/payoff_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../app/bloc/app_bloc.dart'; +import '../home/view/home_page.dart'; class MainDrawer extends StatelessWidget { const MainDrawer({super.key}); @@ -30,7 +35,12 @@ class MainDrawer extends StatelessWidget { .textTheme .titleLarge! .copyWith(color: BudgetColors.teal900)), - onTap: () {}, + onTap: () { + Navigator.of(context).pushNamedAndRemoveUntil( + HomePage.routeName, + (route) => false, + ); + }, ), ListTile( leading: Icon(Icons.money_outlined, @@ -41,24 +51,20 @@ class MainDrawer extends StatelessWidget { .titleLarge! .copyWith(color: BudgetColors.teal900)), onTap: () { + if(isDisplayDesktop(context)){ + Navigator.of(context).pushAndRemoveUntil( + DebtPayoffPage.route(), + (route) => false, + ); + } Navigator.pop(context); Navigator.push(context, DebtPayoffPage.route()); }, ), ListTile( - leading: Icon(Icons.account_balance_outlined, - size: 26, color: BudgetColors.teal900), - title: Text('Accounts', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () {}, - ), - ListTile( - leading: Icon(Icons.category, + leading: Icon(Icons.settings, size: 26, color: BudgetColors.teal900), - title: Text('Categories', + title: Text('Settings', style: Theme.of(context) .textTheme .titleLarge! @@ -66,14 +72,14 @@ class MainDrawer extends StatelessWidget { onTap: () {}, ), ListTile( - leading: Icon(Icons.settings, + leading: Icon(Icons.logout, size: 26, color: BudgetColors.teal900), - title: Text('Settings', + title: Text('Log out', style: Theme.of(context) .textTheme .titleLarge! .copyWith(color: BudgetColors.teal900)), - onTap: () {}, + onTap: () {context.read().add(const AppLogoutRequested());}, ), ], ), diff --git a/lib/home/cubit/cubit.dart b/lib/home/cubit/cubit.dart new file mode 100644 index 0000000..1b704b6 --- /dev/null +++ b/lib/home/cubit/cubit.dart @@ -0,0 +1 @@ +export 'home_cubit.dart'; diff --git a/lib/home/cubit/home_cubit.dart b/lib/home/cubit/home_cubit.dart index c9cee0c..907c323 100644 --- a/lib/home/cubit/home_cubit.dart +++ b/lib/home/cubit/home_cubit.dart @@ -37,42 +37,41 @@ class HomeCubit extends Cubit { _categoriesRepository = categoriesRepository, _subcategoriesRepository = subcategoriesRepository, super(HomeState(selectedDate: DateTime.now())) { - _transactionsSubscription = - _transactionsRepository.getTransactions().listen((transactions) { - _onTransactionsChanged(transactions); - }); - _transfersSubscription = - _transactionsRepository.getTransfers().listen((transfers) { - _onTransfersChanged(transfers); - }); - _accountsSubscription = - _accountsRepository.getAccounts().skip(2).listen((accounts) { - _onAccountsChanged(accounts); - }); - _categoriesSubscription = - _categoriesRepository.getCategories().skip(1).listen((categories) { - _onCategoriesChanged(categories); - }); _init(); } Future _init() async { emit(state.copyWith(status: HomeStatus.loading)); - await Future.wait([ - _categoriesRepository.fetchAllCategories(), - //_accountsSubscription skip 1 - _accountsRepository.fetchAllAccounts(), - _subcategoriesRepository.fetchSubcategories(), - ]); - _transactionsRepository.fetchTransactions(dateTime: DateTime.now()); - _transactionsRepository.fetchTransfers(dateTime: DateTime.now()); + try { + await Future.wait([ + _categoriesRepository.fetchAllCategories(), + _subcategoriesRepository.fetchSubcategories(), + _transactionsRepository.fetchTransactions(dateTime: DateTime.now()), + _transactionsRepository.fetchTransfers(dateTime: DateTime.now()), + ]); + _transactionsSubscription = + _transactionsRepository.getTransactions().listen((transactions) { + _onTransactionsChanged(transactions); + }); + _transfersSubscription = + _transactionsRepository.getTransfers().listen((transfers) { + _onTransfersChanged(transfers); + }); + _accountsSubscription = + _accountsRepository.getAccounts().listen((accounts) { + _onAccountsChanged(accounts); + }); + _categoriesSubscription = + _categoriesRepository.getCategories().listen((categories) { + _onCategoriesChanged(categories); + }); + } catch (e) { + emit(state.copyWith(status: HomeStatus.failure, errorMessage: 'Something went wrong')); + } } Future _onTransactionsChanged(List transactions) async { - emit(state.copyWith(status: HomeStatus.loading)); - final categories = await _categoriesRepository.getCategories().first; - //_accountsSubscription skip 2 await _accountsRepository.fetchAllAccounts(); final accounts = await _accountsRepository.getAccounts().first; @@ -164,13 +163,6 @@ class HomeCubit extends Cubit { } }); } - summaries.insert( - 0, - SummaryTile( - id: state.tab.name == 'EXPENSE' ? 'all_expenses' : 'all_incomes', - name: 'All', - total: allTotal, - iconCodePoint: 62335)); return summaries; } @@ -193,13 +185,6 @@ class HomeCubit extends Cubit { total: sum, iconCodePoint: cat.iconCode!)); }); - summaries.insert( - 0, - SummaryTile( - id: 'all_accounts', - name: 'All', - total: allTotal, - iconCodePoint: 60978)); } return summaries; @@ -218,6 +203,13 @@ class HomeCubit extends Cubit { Future changeDate(DateTime dateTime) async { emit(state.copyWith(status: HomeStatus.loading)); _transactionsRepository.fetchTransactions(dateTime: dateTime); + _transactionsRepository.fetchTransfers(dateTime: dateTime); + } + + Future changeExpanded(int index)async{ + var summaryList = [...state.summaryList]; + summaryList[index] = summaryList[index].copyWith(isExpanded: !summaryList[index].isExpanded); + emit(state.copyWith(summaryList: summaryList)); } @override diff --git a/lib/home/home.dart b/lib/home/home.dart new file mode 100644 index 0000000..affde9b --- /dev/null +++ b/lib/home/home.dart @@ -0,0 +1,2 @@ +export 'view/view.dart'; +export 'cubit/cubit.dart'; diff --git a/lib/home/view/home_desktop_page.dart b/lib/home/view/home_desktop_page.dart new file mode 100644 index 0000000..e2277e1 --- /dev/null +++ b/lib/home/view/home_desktop_page.dart @@ -0,0 +1,164 @@ +import 'package:budget_app/constants/constants.dart'; +import 'package:budget_app/drawer/main_drawer.dart'; +import 'package:budget_app/home/view/widgets/accounts_summaries.dart'; +import 'package:budget_app/transactions/models/transaction_type.dart'; +import 'package:budget_app/transactions/transaction/view/transaction_page.dart'; +import 'package:budget_app/transfer/view/view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../accounts/cubit/accounts_cubit.dart'; +import '../../accounts/repository/accounts_repository.dart'; +import '../../categories/repository/categories_repository.dart'; +import '../../colors.dart'; +import '../../shared/widgets/paginator/month_paginator.dart'; +import '../../subcategories/repository/subcategories_repository.dart'; +import '../../transactions/repository/transactions_repository.dart'; +import '../../transactions/transaction/bloc/transaction_bloc.dart'; +import '../../transfer/bloc/transfer_bloc.dart'; +import '../home.dart'; + +class HomeDesktopPage extends StatelessWidget { + const HomeDesktopPage({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => + TransactionBloc( + transactionsRepository: context.read< + TransactionsRepositoryImpl>(), + categoriesRepository: context.read(), + subcategoriesRepository: context.read< + SubcategoriesRepositoryImpl>(), + accountsRepository: context.read(), + ) + ..add(TransactionFormLoaded(transactionType: TransactionType.EXPENSE)), + ), + BlocProvider( + create: (context) => + TransferBloc( + transactionsRepository: context.read(), + categoriesRepository: context.read(), + accountsRepository: context.read(), + ) + ..add(TransferFormLoaded()), + ), + ], + child: Scaffold( + backgroundColor: BudgetColors.teal50, + body: Row( + children: [MainDrawer(), _Body()], + )), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: w * 0.05, vertical: h * 0.03), + child: BlocListener( + listenWhen: (previous, current) => + previous.tab != current.tab && current.tab == HomeTab.accounts, + listener: (context, state) { + // it has been added for update accounts during first tab open + context.read().fetchAllAccounts(); + }, + child: BlocBuilder( + builder: (context, state) => + Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.all(15), + width: w * 0.3, + height: 80, + decoration: BoxDecoration( + color: BudgetColors.teal900, + borderRadius: BorderRadius.circular(15)), + child: MonthPaginator( + fontSize: 20, + color: BudgetColors.teal50, + onLeft: (date) => + context.read().changeDate(date), + onRight: (date) => + context.read().changeDate(date), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Column( + children: [ + Card( + color: BudgetColors.teal50, + elevation: 5, + child: Container( + padding: EdgeInsets.all(20), + width: w * 0.3, + height: h * 0.7, + child: state.status == HomeStatus.loading + ? Center( + child: CircularProgressIndicator()) + : state.tab == HomeTab.accounts + ? AccountsSummaries() + : CategorySummaries(), + ), + ) + ], + ), + ), + Flexible( + child: Column( + children: [ + Card( + color: BudgetColors.teal50, + elevation: 5, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 30), + width: w * 0.3, + height: h * 0.7, + child: state.tab == HomeTab.accounts + ? TransferWindow.window( + transactionType: + TransactionType.TRANSFER) + : TransactionWindow.window( + transactionType: + TransactionType.EXPENSE), + )) //TransactionsViewBody()) + ], + ), + ), + ], + ), + Container( + margin: EdgeInsets.only(top: 10), + width: w * 0.3, + decoration: BoxDecoration( + color: BudgetColors.teal900, + borderRadius: BorderRadius.circular(15)), + child: ClipPath( + clipper: ShapeBorderClipper( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15))), + child: HomeBottomNavBar( + selectedTab: state.tab, + sectionsSum: state.sectionsSum), + ), + ) + ], + )), + ), + ), + ); + } +} diff --git a/lib/home/view/home_mobile_page.dart b/lib/home/view/home_mobile_page.dart new file mode 100644 index 0000000..95faa05 --- /dev/null +++ b/lib/home/view/home_mobile_page.dart @@ -0,0 +1,89 @@ +import 'package:budget_app/colors.dart'; +import 'package:budget_app/home/view/widgets/accounts_summaries.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../accounts_list/view/accounts_list_page.dart'; +import '../../categories/view/categories_page.dart'; +import '../../drawer/main_drawer.dart'; +import '../../shared/widgets/paginator/month_paginator.dart'; +import '../../transactions/models/transaction_type.dart'; +import '../home.dart'; + +class HomeMobilePage extends StatelessWidget { + const HomeMobilePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return HomeMobileView(); + } +} + +class HomeMobileView extends StatelessWidget { + const HomeMobileView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.status == HomeStatus.failure) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Something went wrong'), + ), + ); + } + }, + child: BlocBuilder(builder: (context, state) { + return SafeArea( + child: Scaffold( + backgroundColor: BudgetColors.teal50, + appBar: AppBar( + title: state.tab != HomeTab.accounts + ? MonthPaginator( + onLeft: (date) => + context.read().changeDate(date), + onRight: (date) => + context.read().changeDate(date), + ) + : Text('Accounts'), + centerTitle: true, + actions: [ + IconButton( + key: const Key('homePage_logout_iconButton'), + icon: const Icon(Icons.create_new_folder_outlined), + onPressed: () { + switch (state.tab) { + case HomeTab.income: + Navigator.of(context).push(CategoriesPage.route( + transactionType: TransactionType.INCOME)); + break; + case HomeTab.expenses: + Navigator.of(context).push(CategoriesPage.route( + transactionType: TransactionType.EXPENSE)); + break; + case HomeTab.accounts: + Navigator.of(context).push(AccountsListPage.route( + homeCubit: context.read())); + break; + } + }, + ), + ], + ), + drawer: MainDrawer(), + floatingActionButton: + HomeFloatingActionButton(selectedTab: state.tab), + body: state.status == HomeStatus.loading + ? Center(child: CircularProgressIndicator()) + : state.tab == HomeTab.accounts + ? AccountsSummaries() + : CategorySummaries(), + bottomNavigationBar: HomeBottomNavBar( + selectedTab: state.tab, sectionsSum: state.sectionsSum))); + }), + ); + } +} diff --git a/lib/home/view/home_page.dart b/lib/home/view/home_page.dart index 0cbf837..24dba92 100644 --- a/lib/home/view/home_page.dart +++ b/lib/home/view/home_page.dart @@ -1,21 +1,14 @@ -import 'package:budget_app/accounts/repository/accounts_repository.dart'; -import 'package:budget_app/categories/repository/categories_repository.dart'; -import 'package:budget_app/drawer/main_drawer.dart'; -import 'package:budget_app/home/cubit/home_cubit.dart'; -import 'package:budget_app/home/view/widgets/categories_summary.dart'; -import 'package:budget_app/shared/models/section.dart'; -import 'package:budget_app/subcategories/repository/subcategories_repository.dart'; -import 'package:budget_app/transactions/models/transaction_type.dart'; -import 'package:budget_app/transactions/repository/transactions_repository.dart'; -import 'package:budget_app/transfer/transfer.dart'; +import 'package:budget_app/home/home.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../app/bloc/app_bloc.dart'; -import '../../colors.dart'; -import '../../shared/widgets/paginator/month_paginator.dart'; -import '../../transactions/transaction/view/transaction_page.dart'; +import '../../accounts/repository/accounts_repository.dart'; +import '../../categories/repository/categories_repository.dart'; +import '../../constants/constants.dart'; +import '../../subcategories/repository/subcategories_repository.dart'; +import '../../transactions/cubit/transactions_cubit.dart'; +import '../../transactions/models/transactions_view_filter.dart'; +import '../../transactions/repository/transactions_repository.dart'; class HomePage extends StatelessWidget { static const routeName = '/home'; @@ -24,164 +17,32 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - final appBloc = BlocProvider.of(context); - return BlocProvider( - create: (context) => HomeCubit( - accountsRepository: context.read(), - categoriesRepository: context.read(), - transactionsRepository: context.read(), - subcategoriesRepository: context.read(), - ), - child: HomeView(), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => HomeCubit( + accountsRepository: context.read(), + categoriesRepository: + context.read(), + transactionsRepository: + context.read(), + subcategoriesRepository: + context.read(), + )), + BlocProvider( + create: (context) => TransactionsCubit( + transactionsRepository: context.read(), + categoriesRepository: context.read(), + subcategoriesRepository: + context.read(), + accountsRepository: context.read(), + filter: TransactionsViewFilter( + type: TransactionsViewFilterTypes.allExpenses), + ), + ), + ], + child: isDisplayDesktop(context) ? HomeDesktopPage() : HomeMobilePage() ); - } -} - -class HomeView extends StatelessWidget { - const HomeView({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { - return SafeArea( - child: Scaffold( - appBar: AppBar( - title: state.tab != HomeTab.accounts - ? MonthPaginator( - onLeft: (date) => - context.read().changeDate(date), - onRight: (date) => - context.read().changeDate(date), - ) - : Text('Accounts'), - centerTitle: true, - actions: [ - IconButton( - key: const Key('homePage_logout_iconButton'), - icon: const Icon(Icons.exit_to_app), - onPressed: () { - context.read().add(const AppLogoutRequested()); - }, - ), - ], - ), - drawer: MainDrawer(), - floatingActionButton: _buildFAB(context, state), - body: state.status == HomeStatus.loading - ? Center(child: CircularProgressIndicator()) - : CategoriesSummary( - summaryList: state.summaryList, - dateTime: state.selectedDate), - bottomNavigationBar: _buildBottomNavigationBar(context, state)), - ); - }); } } - -FloatingActionButton _buildFAB(BuildContext context, HomeState state) { - return FloatingActionButton(backgroundColor: Theme.of(context).colorScheme.tertiary, - onPressed: () { - switch (state.tab) { - case HomeTab.expenses: - Navigator.of(context).push( - TransactionPage.route( - homeCubit: context.read(), - transactionType: TransactionType.EXPENSE), - ); - case HomeTab.income: - Navigator.of(context).push( - TransactionPage.route( - homeCubit: context.read(), - transactionType: TransactionType.INCOME), - ); - case HomeTab.accounts: - Navigator.of(context).push( - TransferPage.route(homeCubit: context.read()), - ); - } - ; - }, - child: const Icon(Icons.add, color: Colors.black,), - ); -} - -Widget _buildBottomNavigationBar(BuildContext context, HomeState state) { - final scheme = Theme.of(context).colorScheme; - return Container( - height: 230.h, - /*decoration: BoxDecoration( - color: scheme.primary, - */ /*borderRadius: BorderRadius.only( - topLeft: Radius.circular(40.h), topRight: Radius.circular(40.h)),*/ /* - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - spreadRadius: 1, - blurRadius: 1, - ) - ]),*/ - child: BottomNavigationBar( - backgroundColor: scheme.primary, - currentIndex: state.tab.index, - onTap: (value) { - context.read().setTab(value); - }, - elevation: 0, - showSelectedLabels: true, - showUnselectedLabels: false, - selectedItemColor: BudgetColors.amber800, - unselectedItemColor: scheme.surfaceVariant, - items: [ - _buildBottomNavigationBarItem( - label: 'expenses', - icon: Icons.account_balance_wallet, - color: scheme.onPrimary, - section: Section.EXPENSES, - state: state, - amount: state.sectionsSum['expenses']!, - tab: HomeTab.expenses), - _buildBottomNavigationBarItem( - label: 'income', - icon: Icons.monetization_on_outlined, - color: scheme.onPrimary, - section: Section.INCOME, - state: state, - amount: state.sectionsSum['incomes']!, - tab: HomeTab.income), - _buildBottomNavigationBarItem( - label: 'accounts', - icon: Icons.account_balance_outlined, - color: scheme.onPrimary, - section: Section.ACCOUNTS, - amount: state.sectionsSum['accounts']!, - state: state, - tab: HomeTab.accounts), - ], - ), - ); -} - -BottomNavigationBarItem _buildBottomNavigationBarItem( - {required String label, - required IconData icon, - required Color color, - required Section section, - required HomeState state, - required double amount, - required HomeTab tab}) { - return BottomNavigationBarItem( - label: label, - icon: Column( - children: [ - Icon(icon), - Text( - '\$ $amount', - style: TextStyle( - color: color, - fontWeight: - state.tab == tab ? FontWeight.bold : FontWeight.normal), - ) - ], - ), - ); -} diff --git a/lib/home/view/view.dart b/lib/home/view/view.dart new file mode 100644 index 0000000..7618297 --- /dev/null +++ b/lib/home/view/view.dart @@ -0,0 +1,4 @@ +export 'home_page.dart'; +export 'widgets/widgets.dart'; +export 'home_desktop_page.dart'; +export 'home_mobile_page.dart'; diff --git a/lib/home/view/widgets/accounts_summaries.dart b/lib/home/view/widgets/accounts_summaries.dart new file mode 100644 index 0000000..537f12b --- /dev/null +++ b/lib/home/view/widgets/accounts_summaries.dart @@ -0,0 +1,85 @@ +import 'package:budget_app/transfer/bloc/transfer_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../accounts/cubit/accounts_cubit.dart'; +import '../../../accounts/models/accounts_view_filter.dart'; +import '../../../accounts/repository/accounts_repository.dart'; +import '../../../colors.dart'; +import '../../../constants/constants.dart'; +import '../../../transactions/models/transactions_view_filter.dart'; +import '../../../transactions/repository/transactions_repository.dart'; +import '../../../transactions/view/transactions_list.dart'; + +class AccountsSummaries extends StatelessWidget { + AccountsSummaries({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => AccountsCubit( + filter: AccountsViewFilter(filterId: ''), + transactionsRepository: context.read(), + accountsRepository: context.read()) + ..fetchAllAccounts(), + child: AccountsSummariesView(), + ); + } +} + +class AccountsSummariesView extends StatelessWidget { + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final scheme = Theme.of(context).colorScheme; + return SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return ExpansionPanelList( + dividerColor: BudgetColors.teal900, + expansionCallback: (int index, bool isExpanded) { + context.read().changeExpanded(index); + if(isDisplayDesktop(context)){ + context.read().add(TransferFormLoaded()); + } + }, + children: state.accountList.map((acc) { + return ExpansionPanel( + canTapOnHeader: true, + backgroundColor: BudgetColors.teal100, + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + leading: Icon(Icons.account_balance_outlined, + color: scheme.primary), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '\$ ${acc.balance.toString()}', + style: TextStyle( + fontSize: textTheme.titleLarge!.fontSize, + fontWeight: FontWeight.bold, + color: scheme.primary), + ), + ], + ), + title: Text( + acc.name, + style: TextStyle( + fontSize: textTheme.titleLarge!.fontSize, + fontWeight: FontWeight.bold, + color: scheme.primary), + ), + ); + }, + body: TransactionsList( + filter: TransactionsViewFilter( + type: TransactionsViewFilterTypes.accountId, + filterId: acc.id)), + isExpanded: acc.isExpanded); + }).toList(), + ); + }, + )); + } +} diff --git a/lib/home/view/widgets/bottom_navbar.dart b/lib/home/view/widgets/bottom_navbar.dart new file mode 100644 index 0000000..b538e30 --- /dev/null +++ b/lib/home/view/widgets/bottom_navbar.dart @@ -0,0 +1,107 @@ +import 'package:budget_app/transactions/models/transaction_type.dart'; +import 'package:budget_app/transactions/transaction/bloc/transaction_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../colors.dart'; +import '../../../constants/constants.dart'; +import '../../../shared/models/section.dart'; +import '../../cubit/home_cubit.dart'; + +class HomeBottomNavBar extends StatelessWidget { + const HomeBottomNavBar( + {super.key, + this.selectedTab = HomeTab.expenses, + required this.sectionsSum}); + + final HomeTab selectedTab; + final Map sectionsSum; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Container( + height: 80, + child: Theme( + data: Theme.of(context).copyWith( + //splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: BottomNavigationBar( + backgroundColor: scheme.primary, + currentIndex: selectedTab.index, + onTap: (value) { + context.read().setTab(value); + if(isDisplayDesktop(context)){ + final tab = HomeTab.values[value]; + var tType = switch (tab) { + HomeTab.expenses => TransactionType.EXPENSE, + HomeTab.income => TransactionType.INCOME, + HomeTab.accounts => TransactionType.TRANSFER, + }; + context + .read() + .add(TransactionFormLoaded(transactionType: tType)); + } + }, + elevation: 0, + showSelectedLabels: true, + showUnselectedLabels: false, + selectedItemColor: BudgetColors.amber800, + unselectedItemColor: scheme.surfaceVariant, + items: [ + _buildBottomNavigationBarItem( + label: 'expenses', + icon: Icons.account_balance_wallet, + color: scheme.onPrimary, + section: Section.EXPENSES, + selectedTab: selectedTab, + amount: sectionsSum['expenses']!, + tab: HomeTab.expenses), + _buildBottomNavigationBarItem( + label: 'income', + icon: Icons.monetization_on_outlined, + color: scheme.onPrimary, + section: Section.INCOME, + selectedTab: selectedTab, + amount: sectionsSum['incomes']!, + tab: HomeTab.income), + _buildBottomNavigationBarItem( + label: 'accounts', + icon: Icons.account_balance_outlined, + color: scheme.onPrimary, + section: Section.ACCOUNTS, + amount: sectionsSum['accounts']!, + selectedTab: selectedTab, + tab: HomeTab.accounts), + ], + ), + ), + ); + } + + BottomNavigationBarItem _buildBottomNavigationBarItem( + {required String label, + required IconData icon, + required Color color, + required Section section, + required HomeTab selectedTab, + required double amount, + required HomeTab tab}) { + return BottomNavigationBarItem( + label: label, + icon: Column( + children: [ + Icon(icon), + Text( + '\$ $amount', + style: TextStyle( + color: color, + fontWeight: + selectedTab == tab ? FontWeight.bold : FontWeight.normal), + ) + ], + ), + ); + } +} diff --git a/lib/home/view/widgets/categories_summary.dart b/lib/home/view/widgets/categories_summary.dart deleted file mode 100644 index c07768f..0000000 --- a/lib/home/view/widgets/categories_summary.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:budget_app/accounts/view/accounts_page.dart'; -import 'package:budget_app/home/cubit/home_cubit.dart'; -import 'package:budget_app/shared/models/summary_tile.dart'; -import 'package:budget_app/transactions/models/transactions_view_filter.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -import '../../../transactions/view/transactions_page.dart'; - -class CategoriesSummary extends StatelessWidget { - final List summaryList; - final DateTime? dateTime; - - const CategoriesSummary({ - Key? key, - required this.summaryList, - this.dateTime, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final scheme = Theme.of(context).colorScheme; - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - SizedBox( - height: 50.h, - ), - Expanded( - child: ListView.separated( - itemCount: summaryList.length, - separatorBuilder: (context, index) => SizedBox( - height: 15.h, - ), - itemBuilder: (context, index) { - final summaryItem = summaryList[index]; - return Card( - margin: EdgeInsets.symmetric( - horizontal: 30.w, vertical: 15.h), - elevation: Theme.of(context).cardTheme.elevation, - child: ListTile( - leading: Icon( - color: scheme.primary, - IconData(summaryItem.iconCodePoint, - fontFamily: 'MaterialIcons')), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '\$ ${summaryItem.total.toString()}', - style: TextStyle( - fontSize: textTheme.titleLarge!.fontSize, - fontWeight: FontWeight.bold, - color: scheme.primary), - ), - SizedBox( - width: 30.w, - ), - Icon(Icons.chevron_right), - ], - ), - title: Text( - summaryItem.name, - style: TextStyle( - fontSize: textTheme.titleLarge!.fontSize, - color: scheme.primary), - ), - onTap: state.tab == HomeTab.accounts - ? () => Navigator.of(context).push( - AccountsPage.route( - homeCubit: - BlocProvider.of(context), - categoryId: summaryItem.id, - dateTime: dateTime ?? DateTime.now())) - : () => Navigator.of(context) - .push(TransactionsPage.route( - homeCubit: - BlocProvider.of(context), - filter: index == 0 - ? TransactionsViewFilter( - type: state.tab.index == 0 - ? TransactionsViewFilterTypes - .allExpenses - : TransactionsViewFilterTypes - .allIncomes) - : TransactionsViewFilter( - type: TransactionsViewFilterTypes - .categoryId, - filterId: summaryItem.id), - ))), - ); - }), - ), - ], - ); - }, - ); - } -} diff --git a/lib/home/view/widgets/category_summaries.dart b/lib/home/view/widgets/category_summaries.dart new file mode 100644 index 0000000..dc8f005 --- /dev/null +++ b/lib/home/view/widgets/category_summaries.dart @@ -0,0 +1,76 @@ +import 'package:budget_app/home/cubit/cubit.dart'; +import 'package:budget_app/transactions/models/transaction_type.dart'; +import 'package:budget_app/transactions/transaction/bloc/transaction_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../colors.dart'; +import '../../../constants/constants.dart'; +import '../../../transactions/models/transactions_view_filter.dart'; +import '../../../transactions/view/transactions_list.dart'; + +class CategorySummaries extends StatelessWidget { + @override + Widget build(BuildContext context) { + final textTheme = Theme + .of(context) + .textTheme; + final scheme = Theme + .of(context) + .colorScheme; + return SingleChildScrollView(child: BlocBuilder( + builder: (context, state) { + return ExpansionPanelList( + dividerColor: BudgetColors.teal900, + expansionCallback: (int index, bool isExpanded) { + final homeCubit = context.read() + ..changeExpanded(index); + if(isDisplayDesktop(context)){ + context.read().add(TransactionFormLoaded( + transactionType: homeCubit.state.tab == HomeTab.expenses + ? TransactionType.EXPENSE + : TransactionType.INCOME)); + } + }, + children: state.summaryList.map((tile) { + return ExpansionPanel( + canTapOnHeader: true, + backgroundColor: BudgetColors.teal100, + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + leading: Icon( + color: scheme.primary, + IconData(tile.iconCodePoint, + fontFamily: 'MaterialIcons')), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '\$ ${tile.total.toString()}', + style: TextStyle( + fontSize: textTheme.titleLarge!.fontSize, + fontWeight: FontWeight.bold, + color: scheme.primary), + ), + ], + ), + title: Text( + tile.name, + style: TextStyle( + fontSize: textTheme.titleLarge!.fontSize, + fontWeight: FontWeight.bold, + color: scheme.primary), + ), + ); + }, + body: TransactionsList( + filter: TransactionsViewFilter( + type: TransactionsViewFilterTypes.categoryId, + filterId: tile.id)), + isExpanded: tile.isExpanded); + }).toList(), + ); + }, + )); + } +} diff --git a/lib/home/view/widgets/home_fab.dart b/lib/home/view/widgets/home_fab.dart new file mode 100644 index 0000000..884c153 --- /dev/null +++ b/lib/home/view/widgets/home_fab.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../transactions/models/transaction_type.dart'; +import '../../../transactions/transaction/view/transaction_page.dart'; +import '../../../transfer/view/transfer_page.dart'; +import '../../cubit/home_cubit.dart'; + +class HomeFloatingActionButton extends StatelessWidget { + const HomeFloatingActionButton({super.key, required this.selectedTab}); + + final HomeTab selectedTab; + + @override + Widget build(BuildContext context) { + return FloatingActionButton( + backgroundColor: Theme.of(context).colorScheme.tertiary, + onPressed: () { + switch (selectedTab) { + case HomeTab.expenses: + Navigator.of(context).push( + TransactionPage.route( + homeCubit: context.read(), + transactionType: TransactionType.EXPENSE), + ); + case HomeTab.income: + Navigator.of(context).push( + TransactionPage.route( + homeCubit: context.read(), + transactionType: TransactionType.INCOME), + ); + case HomeTab.accounts: + Navigator.of(context).push( + TransferPage.route(homeCubit: context.read()), + ); + } + ; + }, + child: const Icon( + Icons.add, + color: Colors.black, + ), + ); + } +} diff --git a/lib/home/view/widgets/widgets.dart b/lib/home/view/widgets/widgets.dart new file mode 100644 index 0000000..f4a9b3b --- /dev/null +++ b/lib/home/view/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'home_fab.dart'; +export 'bottom_navbar.dart'; +export 'category_summaries.dart'; diff --git a/lib/login/cubit/login_cubit.dart b/lib/login/cubit/login_cubit.dart index 37594b9..b0b3fa5 100644 --- a/lib/login/cubit/login_cubit.dart +++ b/lib/login/cubit/login_cubit.dart @@ -51,21 +51,4 @@ class LoginCubit extends Cubit { emit(state.copyWith(status: FormzSubmissionStatus.failure)); } } - - Future logInWithGoogle() async { - emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); - try { - await _authenticationRepository.logInWithGoogle(); - emit(state.copyWith(status: FormzSubmissionStatus.success)); - } on LogInWithGoogleFailure catch (e) { - emit( - state.copyWith( - errorMessage: e.message, - status: FormzSubmissionStatus.failure, - ), - ); - } catch (_) { - emit(state.copyWith(status: FormzSubmissionStatus.failure)); - } - } } diff --git a/lib/login/view/login_desktop_view.dart b/lib/login/view/login_desktop_view.dart new file mode 100644 index 0000000..128ec60 --- /dev/null +++ b/lib/login/view/login_desktop_view.dart @@ -0,0 +1,154 @@ +import 'package:authentication_repository/authentication_repository.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../colors.dart'; +import '../../constants/constants.dart'; +import '../../sign_up/cubit/sign_up_cubit.dart'; +import '../../sign_up/sign_up.dart' as signup; +import '../login.dart' as login; + +class DesktopView extends StatefulWidget { + const DesktopView({super.key}); + + @override + State createState() => _DesktopViewState(); +} + +class _DesktopViewState extends State { + late int index; + + @override + void initState() { + index = 0; + super.initState(); + } + + void _changeIndex(int index) { + setState(() { + this.index = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + height: h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/money_back.jpg'), + fit: BoxFit.cover, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: w * 0.3, + child: Image.asset('assets/images/piggy_logo.png')), + SizedBox(height: h * 0.03), + Container( + width: w * 0.3, + child: Text( + textAlign: TextAlign.left, + 'Do you like saving money ? HomeBudget by Qruto.xyz is a good place to do it. You can track and see your expenses and incomes, planing your budget, sorting the transactions in different way etc.', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + ), + ], + ), + SizedBox( + width: w * 0.04, + ), + AnimatedCrossFade( + duration: Duration(milliseconds: 200), + firstChild: _loginBuilder(), + secondChild: _signupBuilder(), + crossFadeState: index == 0 + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + ) + ], + ), + ), + ); + } + + Widget _loginBuilder() { + return Container( + width: w * 0.3, + height: h * 0.43, + decoration: BoxDecoration( + color: BudgetColors.teal50.withOpacity(0.85), borderRadius: BorderRadius.circular(25)), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: w * 0.02, vertical: h * 0.06), + child: Column( + children: [ + login.EmailInput(), + SizedBox(height: h * 0.01), + login.PasswordInput(), + Expanded(child: Container()), + login.LoginButton(), + SizedBox(height: h * 0.01), + Align( + alignment: Alignment.topRight, + child: _changedButton(1, 'Register')), + //GoogleLoginButton(), + ], + ), + ), + ); + } + + Widget _signupBuilder() { + return BlocProvider( + create: (context) => + SignUpCubit(context.read()), + child: Container( + width: w * 0.3, + height: h * 0.53, + decoration: BoxDecoration( + color: BudgetColors.teal50.withOpacity(0.85), + borderRadius: BorderRadius.circular(25)), + child: Padding( + padding: + EdgeInsets.symmetric(horizontal: w * 0.02, vertical: h * 0.06), + child: Column( + children: [ + signup.EmailInput(), + SizedBox(height: h * 0.01), + signup.PasswordInput(), + SizedBox(height: h * 0.01), + signup.ConfirmPasswordInput(), + SizedBox(height: h * 0.01), + signup.SignUpButton(), + SizedBox( + height: h * 0.01, + ), + Align( + alignment: Alignment.topRight, + child: _changedButton(0, 'Login')) + ], + )), + ), + ); + } + + Widget _changedButton(int index, String text) { + return RichText( + text: TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () { + _changeIndex(index); + }, + text: text, + style: TextStyle(color: BudgetColors.teal900, fontSize: 20), + ), + ); + } +} diff --git a/lib/login/view/login_form.dart b/lib/login/view/login_form.dart deleted file mode 100644 index 522b6d6..0000000 --- a/lib/login/view/login_form.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:budget_app/colors.dart'; -import 'package:budget_app/login/login.dart'; -import 'package:budget_app/sign_up/sign_up.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:formz/formz.dart'; - -class LoginForm extends StatelessWidget { - const LoginForm({super.key}); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state.status.isFailure) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'Authentication Failure'), - ), - ); - } - }, - child: SingleChildScrollView( - child: Column( - children: [ - FilledCard(), - Padding( - padding: EdgeInsets.all(70.w), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 100.h), - _EmailInput(), - SizedBox(height: 20.h), - _PasswordInput(), - SizedBox(height: 50.h), - _LoginButton(), - SizedBox(height: 100.h), - _GoogleLoginButton(), - SizedBox(height: 100.h), - _SignUpButton() - ], - ), - ), - ], - ), - ), - ); - } -} - -enum FieldType { - email, - password, -} - -class _EmailInput extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.email != current.email, - builder: (context, state) { - return TextField( - key: const Key('loginForm_emailInput_textField'), - onChanged: (email) => context.read().emailChanged(email), - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - icon: Icon( - Icons.email, - color: Colors.orangeAccent, - ), - border: OutlineInputBorder(), - labelText: 'Email', - helperText: '', - errorText: state.email.displayError != null ? 'invalid email' : null, - ), - ); - }, - ); - } -} - -class _PasswordInput extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.password != current.password, - builder: (context, state) { - return TextField( - key: const Key('loginForm_passwordInput_textField'), - onChanged: (password) => - context.read().passwordChanged(password), - obscureText: true, - decoration: InputDecoration( - icon: Icon( - Icons.key, - color: Colors.orangeAccent, - ), - border: OutlineInputBorder(), - labelText: 'Password', - helperText: '', - errorText: - state.password.displayError != null ? 'invalid password' : null, - ), - ); - }, - ); - } -} - -class _LoginButton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.status.isInProgress - ? const CircularProgressIndicator() - : ElevatedButton( - key: const Key('loginForm_continue_raisedButton'), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - backgroundColor: - BudgetColors.amber800, - ), - onPressed: state.isValid - ? () => context.read().logInWithCredentials() - : null, - child: const Text('LOGIN'), - ); - }, - ); - } -} - -class _GoogleLoginButton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => context.read().logInWithGoogle(), - child: CircleAvatar( - minRadius: 25.0, - backgroundColor: BudgetColors.teal50, - child: Image.asset('assets/images/google.png', width: 50), - ), - /*child: Container( - child: Image.asset('assets/images/google.png', width: 50), - ),*/ - ); - } -} - -class _SignUpButton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return RichText( - text: TextSpan( - text: 'Don\'t have an account ?', - style: TextStyle(color: Colors.grey[500], fontSize: 16), - children: [ - TextSpan( - recognizer: TapGestureRecognizer() - ..onTap = - () => Navigator.of(context).pushNamed(SignUpPage.routeName), - text: ' Create', - style: TextStyle( - color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold), - ), - ], - ), - ); - } -} diff --git a/lib/login/view/login_mobile_view.dart b/lib/login/view/login_mobile_view.dart new file mode 100644 index 0000000..9a50b0d --- /dev/null +++ b/lib/login/view/login_mobile_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; + +import '../../constants/constants.dart'; +import '../../shared/widgets/filled_card.dart'; +import '../login.dart'; + +class MobileView extends StatelessWidget { + const MobileView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocListener( + listener: (context, state) { + if (state.status.isFailure) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Authentication Failure'), + ), + ); + } + }, + child: SingleChildScrollView( + child: Column( + children: [ + FilledCard(height: h * 0.35), + Padding( + padding: EdgeInsets.symmetric(vertical: h * 0.06, horizontal: w * 0.06), + child: Column( + children: [ + EmailInput(), + SizedBox(height: h * 0.01), + PasswordInput(), + SizedBox(height: h * 0.01), + LoginButton(), + SizedBox(height: h * 0.01), + Container(alignment: Alignment.topRight, child: SignUpButton()), + //GoogleLoginButton(), + ], + ), + ), + ], + ), + ), + ) + ); + } +} diff --git a/lib/login/view/login_page.dart b/lib/login/view/login_page.dart index 7ab773e..4e573b1 100644 --- a/lib/login/view/login_page.dart +++ b/lib/login/view/login_page.dart @@ -1,8 +1,7 @@ import 'package:authentication_repository/authentication_repository.dart'; -import 'package:budget_app/colors.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:budget_app/login/login.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class LoginPage extends StatelessWidget { const LoginPage({super.key}); @@ -11,11 +10,17 @@ class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: BlocProvider( - create: (_) => LoginCubit(context.read()), - child: const LoginForm(), + return BlocProvider( + create: (context) => LoginCubit(context.read()), + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 1100) { + return MobileView(); + } + return DesktopView(); + }, ), ); } } + diff --git a/lib/login/view/view.dart b/lib/login/view/view.dart index 52629a8..f0adf32 100644 --- a/lib/login/view/view.dart +++ b/lib/login/view/view.dart @@ -1,2 +1,4 @@ -export 'login_form.dart'; export 'login_page.dart'; +export 'widgets/widgets.dart'; +export 'login_mobile_view.dart'; +export 'login_desktop_view.dart'; diff --git a/lib/login/view/widgets/email_input.dart b/lib/login/view/widgets/email_input.dart new file mode 100644 index 0000000..3514820 --- /dev/null +++ b/lib/login/view/widgets/email_input.dart @@ -0,0 +1,31 @@ +import 'package:budget_app/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../cubit/login_cubit.dart'; + +class EmailInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.email != current.email, + builder: (context, state) { + return TextField( + key: const Key('loginForm_emailInput_textField'), + onChanged: (email) => context.read().emailChanged(email), + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + icon: Icon( + Icons.email, + color: BudgetColors.teal900, + ), + border: OutlineInputBorder(), + labelText: 'Email', + helperText: '', + errorText: state.email.displayError != null ? 'invalid email' : null, + ), + ); + }, + ); + } +} diff --git a/lib/login/view/widgets/login_button.dart b/lib/login/view/widgets/login_button.dart new file mode 100644 index 0000000..80c27cd --- /dev/null +++ b/lib/login/view/widgets/login_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; + +import '../../../colors.dart'; +import '../../cubit/login_cubit.dart'; + +class LoginButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.status.isInProgress + ? Container(child: const CircularProgressIndicator()) + : Container(width: double.infinity, + child: ElevatedButton( + key: const Key('loginForm_continue_raisedButton'), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + backgroundColor: + BudgetColors.amber800, + ), + onPressed: state.isValid + ? () => context.read().logInWithCredentials() + : null, + child: const Text('Login', style: TextStyle(fontSize: 20), + ), + )); + }, + ); + } +} diff --git a/lib/login/view/widgets/password_input.dart b/lib/login/view/widgets/password_input.dart new file mode 100644 index 0000000..83d6743 --- /dev/null +++ b/lib/login/view/widgets/password_input.dart @@ -0,0 +1,33 @@ +import 'package:budget_app/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../cubit/login_cubit.dart'; + +class PasswordInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.password != current.password, + builder: (context, state) { + return TextField( + key: const Key('loginForm_passwordInput_textField'), + onChanged: (password) => + context.read().passwordChanged(password), + obscureText: true, + decoration: InputDecoration( + icon: Icon( + Icons.key, + color: BudgetColors.teal900, + ), + border: OutlineInputBorder(), + labelText: 'Password', + helperText: '', + errorText: + state.password.displayError != null ? 'invalid password' : null, + ), + ); + }, + ); + } +} diff --git a/lib/login/view/widgets/signup_button.dart b/lib/login/view/widgets/signup_button.dart new file mode 100644 index 0000000..e866d44 --- /dev/null +++ b/lib/login/view/widgets/signup_button.dart @@ -0,0 +1,24 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import '../../../colors.dart'; +import '../../../sign_up/view/sign_up_page.dart'; + +class SignUpButton extends StatelessWidget { + + SignUpButton({super.key}); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = + () => Navigator.of(context).pushNamed(SignUpPage.routeName), + text: ' Register', + style: TextStyle( + color: BudgetColors.teal900, fontSize: 20), + ), + ); + } +} diff --git a/lib/login/view/widgets/widgets.dart b/lib/login/view/widgets/widgets.dart new file mode 100644 index 0000000..3e94935 --- /dev/null +++ b/lib/login/view/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'email_input.dart'; +export 'password_input.dart'; +export 'login_button.dart'; +export 'signup_button.dart'; diff --git a/lib/shared/models/summary_tile.dart b/lib/shared/models/summary_tile.dart index 5dbe7c9..dcbd1c7 100644 --- a/lib/shared/models/summary_tile.dart +++ b/lib/shared/models/summary_tile.dart @@ -4,22 +4,39 @@ import 'package:json_annotation/json_annotation.dart'; part 'summary_tile.g.dart'; @JsonSerializable() -class SummaryTile extends Equatable{ +class SummaryTile extends Equatable { final String id; final String name; final double total; final int iconCodePoint; + final bool isExpanded; - const SummaryTile({ - required this.id, - required this.name, - required this.total, - required this.iconCodePoint, - }); + const SummaryTile( + {required this.id, + required this.name, + required this.total, + required this.iconCodePoint, + this.isExpanded = false}); + + SummaryTile copyWith( + {String? id, + String? name, + double? total, + int? iconCodePoint, + bool? isExpanded}) { + return SummaryTile( + id: id ?? this.id, + name: name ?? this.name, + total: total ?? this.total, + iconCodePoint: iconCodePoint ?? this.iconCodePoint, + isExpanded: isExpanded ?? this.isExpanded); + } + + factory SummaryTile.fromJson(Map json) => + _$SummaryTileFromJson(json); - factory SummaryTile.fromJson(Map json) => _$SummaryTileFromJson(json); Map toJson() => _$SummaryTileToJson(this); @override - List get props => [id, total, name, iconCodePoint]; + List get props => [id, total, name, iconCodePoint, isExpanded]; } diff --git a/lib/shared/widgets/filled_card.dart b/lib/shared/widgets/filled_card.dart new file mode 100644 index 0000000..a8c5f13 --- /dev/null +++ b/lib/shared/widgets/filled_card.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import '../../constants/constants.dart'; + +class FilledCard extends StatelessWidget { + + final double height; + + const FilledCard({super.key, this.height = 0}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/money_back.jpg'), + fit: BoxFit.cover, + ), + ), + width: double.infinity, + height: height, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: w * 0.1, vertical: 0), + child: Image.asset('assets/images/piggy_logo.png'), + ), + ); + } +} diff --git a/lib/shared/widgets/paginator/month_paginator.dart b/lib/shared/widgets/paginator/month_paginator.dart index 913d53e..61dc783 100644 --- a/lib/shared/widgets/paginator/month_paginator.dart +++ b/lib/shared/widgets/paginator/month_paginator.dart @@ -2,10 +2,17 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class MonthPaginator extends StatefulWidget { + final Color? color; + final double? fontSize; final Function? onLeft; final Function? onRight; - const MonthPaginator({Key? key, required this.onLeft, required this.onRight}) + const MonthPaginator( + {Key? key, + this.color, + this.fontSize, + required this.onLeft, + required this.onRight}) : super(key: key); @override @@ -23,42 +30,45 @@ class _MonthPaginatorState extends State { @override Widget build(BuildContext context) { - return Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - IconButton( - icon: const Icon(Icons.arrow_circle_left_outlined), - onPressed: () { - setState(() { - _myDate = _myDate.subtract(Duration(days: 30)); - }); - widget.onLeft!(_myDate); - }, - ), - Column( - children: [ - Text(DateFormat('MMM').format(_myDate)), - Text( - DateFormat('yyyy').format(_myDate), - style: TextStyle(fontSize: 12), - ), - ], - ), - IconButton( - icon: const Icon(Icons.arrow_circle_right_outlined), - onPressed: _myDate.month >= DateTime.now().month && - _myDate.year == DateTime.now().year - ? null - : () { - setState(() { - _myDate = _myDate.add(Duration(days: 30)); - }); - widget.onRight!(_myDate); - }, - ), - ], - ), + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + icon: Icon(Icons.arrow_circle_left_outlined, color: widget.color), + onPressed: () { + setState(() { + _myDate = _myDate.subtract(Duration(days: 30)); + }); + widget.onLeft!(_myDate); + }, + ), + Column( + children: [ + Text(DateFormat('MMM').format(_myDate), + style: + TextStyle(color: widget.color, fontSize: widget.fontSize)), + Text( + DateFormat('yyyy').format(_myDate), + style: TextStyle( + fontSize: + widget.fontSize == null ? 12 : widget.fontSize! * 0.7, + color: widget.color), + ), + ], + ), + IconButton( + icon: Icon(Icons.arrow_circle_right_outlined, color: widget.color), + onPressed: _myDate.month >= DateTime.now().month && + _myDate.year == DateTime.now().year + ? null + : () { + setState(() { + _myDate = _myDate.add(Duration(days: 30)); + }); + widget.onRight!(_myDate); + }, + ), + ], ); } } diff --git a/lib/sign_up/view/desktop_view.dart b/lib/sign_up/view/desktop_view.dart new file mode 100644 index 0000000..85f32dd --- /dev/null +++ b/lib/sign_up/view/desktop_view.dart @@ -0,0 +1,72 @@ +import 'package:budget_app/sign_up/sign_up.dart'; +import 'package:flutter/material.dart'; + +import '../../colors.dart'; +import '../../constants/constants.dart'; + +class DesktopView extends StatelessWidget { + const DesktopView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Log in')), + body: Container( + height: h, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/money_back.jpg'), + fit: BoxFit.cover, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: w * 0.3, + child: Image.asset('assets/images/piggy_logo.png')), + SizedBox(height: h * 0.03), + Container( + width: w * 0.3, + child: Text( + textAlign: TextAlign.left, + 'Do you like saving money ? HomeBudget by Qruto.xyz is a good place to do it. You can track and see your expenses and incomes, planing your budget, sorting the transactions in different way etc.', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + ), + ], + ), + SizedBox( + width: 100, + ), + Container( + width: w * 0.3, + height: h * 0.49, + decoration: BoxDecoration( + color: BudgetColors.teal50, + borderRadius: BorderRadius.circular(25) + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: w * 0.02, vertical: h * 0.06), + child: Column( + children: [ + EmailInput(), + SizedBox(height: h * 0.01), + PasswordInput(), + SizedBox(height: h * 0.01), + ConfirmPasswordInput(), + SizedBox(height: h * 0.01), + SignUpButton(), + ], + ) + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/sign_up/view/mobile_view.dart b/lib/sign_up/view/mobile_view.dart new file mode 100644 index 0000000..3ac359b --- /dev/null +++ b/lib/sign_up/view/mobile_view.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; + +import '../../constants/constants.dart'; +import '../../shared/widgets/filled_card.dart'; +import '../cubit/sign_up_cubit.dart'; +import '../sign_up.dart'; + +class MobileView extends StatelessWidget { + const MobileView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Log in')), + body: BlocListener( + listener: (context, state) { + if (state.status.isSuccess) { + //Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (route) => false); + } else if (state.status.isFailure) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar(content: Text(state.errorMessage ?? 'Sign Up Failure')), + ); + } + }, + child: SingleChildScrollView( + child: Column( + children: [ + FilledCard(height: h * 0.25), + Padding( + padding: EdgeInsets.symmetric(vertical: h * 0.06, horizontal: w * 0.06), + child: Column( + children: [ + EmailInput(), + SizedBox(height: h * 0.01), + PasswordInput(), + SizedBox(height: h * 0.01), + ConfirmPasswordInput(), + SizedBox(height: h * 0.01), + SignUpButton(), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/sign_up/view/sign_up_form.dart b/lib/sign_up/view/sign_up_form.dart deleted file mode 100644 index c6cb6d2..0000000 --- a/lib/sign_up/view/sign_up_form.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'package:budget_app/colors.dart'; -import 'package:budget_app/login/login.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:budget_app/sign_up/sign_up.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:formz/formz.dart'; - -class SignUpForm extends StatelessWidget { - const SignUpForm({super.key}); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state.status.isSuccess) { - Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (route) => false); - } else if (state.status.isFailure) { - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar(content: Text(state.errorMessage ?? 'Sign Up Failure')), - ); - } - }, - child: SingleChildScrollView( - child: Column( - children: [ - FilledCard(height: 500), - Padding( - padding: EdgeInsets.all(70.w), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _EmailInput(), - SizedBox(height: 20.h), - _PasswordInput(), - SizedBox(height: 20.h), - _ConfirmPasswordInput(), - SizedBox(height: 20.h), - _SignUpButton(), - ], - ), - ), - ], - ), - ), - ); - } -} - -class FilledCard extends StatelessWidget { - - final int height; - - const FilledCard({super.key, this.height = 700}); - - @override - Widget build(BuildContext context) { - return Container( - color: BudgetColors.teal900, - width: double.infinity, - height: height.w, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 100.w, vertical: 0), - child: Image.asset('assets/images/piggy_logo.png'), - ), - ); - } -} - -class _EmailInput extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.email != current.email, - builder: (context, state) { - return TextField( - key: const Key('signUpForm_emailInput_textField'), - onChanged: (email) => context.read().emailChanged(email), - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - icon: Icon(Icons.email, color: Colors.orangeAccent,), - labelText: 'Email', - helperText: '', - border: OutlineInputBorder(), - errorText: - state.email.displayError != null ? 'invalid email' : null, - ), - ); - }, - ); - } -} - -class _PasswordInput extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.password != current.password, - builder: (context, state) { - return TextField( - key: const Key('signUpForm_passwordInput_textField'), - onChanged: (password) => - context.read().passwordChanged(password), - obscureText: true, - decoration: InputDecoration( - icon: Icon(Icons.password, color: Colors.orangeAccent,), - border: OutlineInputBorder(), - labelText: 'Password', - helperText: '', - errorText: - state.password.displayError != null ? 'invalid password' : null, - ), - ); - }, - ); - } -} - -class _ConfirmPasswordInput extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.password != current.password || - previous.confirmedPassword != current.confirmedPassword, - builder: (context, state) { - return TextField( - key: const Key('signUpForm_confirmedPasswordInput_textField'), - onChanged: (confirmPassword) => context - .read() - .confirmedPasswordChanged(confirmPassword), - obscureText: true, - decoration: InputDecoration( - icon: Icon(Icons.key, color: Colors.orangeAccent,), - border: OutlineInputBorder(), - labelText: 'Confirm password', - helperText: '', - errorText: state.confirmedPassword.displayError != null - ? 'passwords do not match' - : null, - ), - ); - }, - ); - } -} - -class _SignUpButton extends StatelessWidget { - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.status.isInProgress - ? const CircularProgressIndicator() - : ElevatedButton( - key: const Key('signUpForm_continue_raisedButton'), - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - backgroundColor: BudgetColors.amber800, - ), - onPressed: state.isValid - ? () => context.read().signUpFormSubmitted() - : null, - child: const Text('SIGN UP'), - ); - }, - ); - } -} diff --git a/lib/sign_up/view/sign_up_page.dart b/lib/sign_up/view/sign_up_page.dart index 0bdbc95..2425044 100644 --- a/lib/sign_up/view/sign_up_page.dart +++ b/lib/sign_up/view/sign_up_page.dart @@ -1,5 +1,4 @@ import 'package:authentication_repository/authentication_repository.dart'; -import 'package:budget_app/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:budget_app/sign_up/sign_up.dart'; @@ -11,12 +10,15 @@ class SignUpPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Sign Up')), - body: BlocProvider( - create: (_) => SignUpCubit(context.read()), - child: const SignUpForm(), + return BlocProvider( + create: (_) => SignUpCubit(context.read()), + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 1100) { + return MobileView(); + } + return DesktopView(); + }, ), ); } diff --git a/lib/sign_up/view/view.dart b/lib/sign_up/view/view.dart index c03f178..e16893b 100644 --- a/lib/sign_up/view/view.dart +++ b/lib/sign_up/view/view.dart @@ -1,2 +1,4 @@ -export 'sign_up_form.dart'; export 'sign_up_page.dart'; +export 'widgets/widgets.dart'; +export 'mobile_view.dart'; +export 'desktop_view.dart'; diff --git a/lib/sign_up/view/widgets/conf_password_input.dart b/lib/sign_up/view/widgets/conf_password_input.dart new file mode 100644 index 0000000..bff83e4 --- /dev/null +++ b/lib/sign_up/view/widgets/conf_password_input.dart @@ -0,0 +1,34 @@ +import 'package:budget_app/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../cubit/sign_up_cubit.dart'; + +class ConfirmPasswordInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.password != current.password || + previous.confirmedPassword != current.confirmedPassword, + builder: (context, state) { + return TextField( + key: const Key('signUpForm_confirmedPasswordInput_textField'), + onChanged: (confirmPassword) => context + .read() + .confirmedPasswordChanged(confirmPassword), + obscureText: true, + decoration: InputDecoration( + icon: Icon(Icons.password, color: BudgetColors.teal900,), + border: OutlineInputBorder(), + labelText: 'Confirm password', + helperText: '', + errorText: state.confirmedPassword.displayError != null + ? 'passwords do not match' + : null, + ), + ); + }, + ); + } +} diff --git a/lib/sign_up/view/widgets/email_input.dart b/lib/sign_up/view/widgets/email_input.dart new file mode 100644 index 0000000..26c2e51 --- /dev/null +++ b/lib/sign_up/view/widgets/email_input.dart @@ -0,0 +1,29 @@ +import 'package:budget_app/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../cubit/sign_up_cubit.dart'; + +class EmailInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.email != current.email, + builder: (context, state) { + return TextField( + key: const Key('signUpForm_emailInput_textField'), + onChanged: (email) => context.read().emailChanged(email), + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + icon: Icon(Icons.email, color: BudgetColors.teal900,), + labelText: 'Email', + helperText: '', + border: OutlineInputBorder(), + errorText: + state.email.displayError != null ? 'invalid email' : null, + ), + ); + }, + ); + } +} diff --git a/lib/sign_up/view/widgets/password_input.dart b/lib/sign_up/view/widgets/password_input.dart new file mode 100644 index 0000000..fd22969 --- /dev/null +++ b/lib/sign_up/view/widgets/password_input.dart @@ -0,0 +1,30 @@ +import 'package:budget_app/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../cubit/sign_up_cubit.dart'; + +class PasswordInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.password != current.password, + builder: (context, state) { + return TextField( + key: const Key('signUpForm_passwordInput_textField'), + onChanged: (password) => + context.read().passwordChanged(password), + obscureText: true, + decoration: InputDecoration( + icon: Icon(Icons.key, color: BudgetColors.teal900,), + border: OutlineInputBorder(), + labelText: 'Password', + helperText: '', + errorText: + state.password.displayError != null ? 'invalid password' : null, + ), + ); + }, + ); + } +} diff --git a/lib/sign_up/view/widgets/signup_button.dart b/lib/sign_up/view/widgets/signup_button.dart new file mode 100644 index 0000000..b336f46 --- /dev/null +++ b/lib/sign_up/view/widgets/signup_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; + +import '../../../colors.dart'; +import '../../cubit/sign_up_cubit.dart'; + +class SignUpButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.status.isInProgress + ? const CircularProgressIndicator() + : Container( + width: double.infinity, + child: ElevatedButton( + key: const Key('signUpForm_continue_raisedButton'), + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + backgroundColor: BudgetColors.amber800, + ), + onPressed: state.isValid + ? () => + context.read().signUpFormSubmitted() + : null, + child: const Text('Register', + style: TextStyle(fontSize: 20)))); + }, + ); + } +} diff --git a/lib/sign_up/view/widgets/widgets.dart b/lib/sign_up/view/widgets/widgets.dart new file mode 100644 index 0000000..4f9e0ee --- /dev/null +++ b/lib/sign_up/view/widgets/widgets.dart @@ -0,0 +1,4 @@ +export 'email_input.dart'; +export 'password_input.dart'; +export 'conf_password_input.dart'; +export 'signup_button.dart'; diff --git a/lib/splash/view/splash_page.dart b/lib/splash/view/splash_page.dart index 88e51a2..0aa5fda 100644 --- a/lib/splash/view/splash_page.dart +++ b/lib/splash/view/splash_page.dart @@ -1,21 +1,60 @@ import 'package:budget_app/colors.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -class SplashPage extends StatelessWidget { +class SplashPage extends StatefulWidget { const SplashPage({Key? key}) : super(key: key); static Route route() { return MaterialPageRoute(builder: (_) => const SplashPage()); } + @override + State createState() => _SplashPageState(); +} + +class _SplashPageState extends State + with SingleTickerProviderStateMixin { + late Animation animation; + late AnimationController controller; + + @override + void initState() { + super.initState(); + controller = + AnimationController(duration: const Duration(seconds: 2), vsync: this); + animation = CurvedAnimation(parent: controller, curve: Curves.bounceOut); + controller.forward(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: BudgetColors.teal900, - body: Center( + backgroundColor: BudgetColors.teal900, + body: AnimatedLogo(animation: animation,)); + } +} + +class AnimatedLogo extends AnimatedWidget { + const AnimatedLogo({super.key, required Animation animation}) + : super(listenable: animation); + + static final _opacityTween = Tween(begin: 0.1, end: 1); + static final _sizeTween = Tween(begin: 0, end: 200); + + @override + Widget build(BuildContext context) { + final animation = listenable as Animation; + return Center( + child: Opacity( + opacity: _opacityTween.evaluate(animation), child: Container( - width: 500.w, + width: _sizeTween.evaluate(animation), child: Image.asset('assets/images/logo.png'), ), ), diff --git a/lib/transactions/cubit/transactions_cubit.dart b/lib/transactions/cubit/transactions_cubit.dart index a16a2f2..3e786d3 100644 --- a/lib/transactions/cubit/transactions_cubit.dart +++ b/lib/transactions/cubit/transactions_cubit.dart @@ -49,48 +49,57 @@ class TransactionsCubit extends Cubit { final transactions = await _transactionsRepository.getTransactions().first; final transfers = await _transactionsRepository.getTransfers().first; final categories = await _categoriesRepository.getCategories().first; - final subcategories = await _subcategoriesRepository.getSubcategories().first; + final subcategories = + await _subcategoriesRepository.getSubcategories().first; final accounts = await _accountsRepository.getAccounts().first; transactions.forEach((element) { final cat = categories.where((c) => element.categoryId == c.id).first; - final subcategory = subcategories.where((sc) => element.subcategoryId == sc.id).first; + final subcategory = + subcategories.where((sc) => element.subcategoryId == sc.id).first; final acc = accounts.where((a) => element.accountId == a.id).first; - trTiles.add(element.toTile(account: acc, category: cat, subcategory: subcategory)); + trTiles.add(element.toTile( + account: acc, category: cat, subcategory: subcategory)); + }); + + transfers.forEach((element) { + final fromAcc = + accounts.where((a) => element.fromAccountId == a.id).first; + final toAcc = accounts.where((a) => element.toAccountId == a.id).first; + trTiles.addAll(element.toTiles(fromAccount: fromAcc, toAccount: toAcc)); }); - if(state.filter.type == TransactionsViewFilterTypes.accountId){ - transfers.forEach((element) { - final fromAcc = accounts.where((a) => element.fromAccountId == a.id).first; - final toAcc = accounts.where((a) => element.toAccountId == a.id).first; - trTiles.add(element.toTile(tabAccId: state.filter.filterId!, fromAccount: fromAcc, toAccount: toAcc)); - }); - } trTiles.sort( - (a, b) => a.dateTime.compareTo(b.dateTime), + (a, b) => a.dateTime.compareTo(b.dateTime), ); emit(state.copyWith( - status: TransactionsStatus.success, - transactionList: trTiles)); + status: TransactionsStatus.success, transactionList: trTiles)); + } + + Future filterChanged({required TransactionsViewFilter filter}) async { + emit(state.copyWith(filter: filter)); + _onSomethingChanged(); } Future deleteTransaction({required String transactionId}) async { - final deletedTransaction = await _transactionsRepository.deleteTransaction(transactionId); + final deletedTransaction = + await _transactionsRepository.deleteTransaction(transactionId); emit(state.copyWith(lastDeletedTransaction: () => deletedTransaction)); } Future deleteTransfer({required String transferId}) async { - final deletedTransfer = await _transactionsRepository.deleteTransfer(transferId); + final deletedTransfer = + await _transactionsRepository.deleteTransfer(transferId); emit(state.copyWith(lastDeletedTransfer: () => deletedTransfer)); } Future undoDelete() async { final transaction = state.lastDeletedTransaction; final transfer = state.lastDeletedTransfer; - if(transaction == null){ + if (transaction == null) { emit(state.copyWith(lastDeletedTransfer: () => null)); await _transactionsRepository.createTransfer(transfer!); - }else { + } else { emit(state.copyWith(lastDeletedTransaction: () => null)); await _transactionsRepository.createTransaction(transaction); } diff --git a/lib/transactions/models/transactions_view_filter.dart b/lib/transactions/models/transactions_view_filter.dart index ed59803..35eaffd 100644 --- a/lib/transactions/models/transactions_view_filter.dart +++ b/lib/transactions/models/transactions_view_filter.dart @@ -20,10 +20,14 @@ class TransactionsViewFilter { case TransactionsViewFilterTypes.allIncomes: return transactionTile.type == TransactionType.INCOME; case TransactionsViewFilterTypes.categoryId: - return transactionTile.category!.id == filterId!; + return transactionTile.category?.id == filterId!; case TransactionsViewFilterTypes.accountId: - return transactionTile.fromAccount!.id == filterId! || - transactionTile.toAccount?.id == filterId; + if(transactionTile.type == TransactionType.TRANSFER){ + return (transactionTile.fromAccount!.id == filterId && transactionTile.title == 'Transfer out') || + (transactionTile.toAccount?.id == filterId && transactionTile.title == 'Transfer in'); + }else{ + return transactionTile.fromAccount!.id == filterId; + } } } diff --git a/lib/transactions/transaction/bloc/transaction_bloc.dart b/lib/transactions/transaction/bloc/transaction_bloc.dart index 9a1fccb..a5f2c82 100644 --- a/lib/transactions/transaction/bloc/transaction_bloc.dart +++ b/lib/transactions/transaction/bloc/transaction_bloc.dart @@ -5,6 +5,7 @@ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:budget_app/accounts/repository/accounts_repository.dart'; import 'package:budget_app/categories/repository/categories_repository.dart'; import 'package:budget_app/constants/api.dart'; +import 'package:budget_app/constants/constants.dart'; import 'package:budget_app/subcategories/repository/subcategories_repository.dart'; import 'package:budget_app/transactions/models/transaction_tile.dart'; import 'package:equatable/equatable.dart'; @@ -24,7 +25,6 @@ part 'transaction_event.dart'; part 'transaction_state.dart'; class TransactionBloc extends Bloc { - final TransactionsRepository _transactionsRepository; final CategoriesRepository _categoriesRepository; final SubcategoriesRepository _subcategoriesRepository; @@ -85,11 +85,15 @@ class TransactionBloc extends Bloc { ) async { emit(state.copyWith(trStatus: TransactionStatus.loading)); final allCategories = await _categoriesRepository.getCategories().first; - final filteredCategories = allCategories.where( - (cat) => cat.transactionType == event.transactionType, - ).toList(); + final filteredCategories = allCategories + .where( + (cat) => cat.transactionType == event.transactionType, + ) + .toList(); final accounts = await _accountsRepository.getAccounts().first; - final accCategories = allCategories.where((cat) => cat.transactionType == TransactionType.ACCOUNT).toList(); + final accCategories = allCategories + .where((cat) => cat.transactionType == TransactionType.ACCOUNT) + .toList(); var subcategories = []; var category; var subcategory; @@ -98,15 +102,20 @@ class TransactionBloc extends Bloc { final tr = event.transaction; if (tr != null) { id = tr.id; - category = filteredCategories.where((cat) => cat.id == tr.category!.id).first; + category = + filteredCategories.where((cat) => cat.id == tr.category!.id).first; subcategories = await _subcategoriesRepository.getSubcategories().first; subcategory = subcategories .where((element) => element.id == tr.subcategory!.id) .first; - account = accounts.where((element) => element.id == tr.fromAccount!.id).first; + account = + accounts.where((element) => element.id == tr.fromAccount!.id).first; } + //for amount update on desktop view + await Future.delayed(Duration(milliseconds: 100)); + emit(TransactionState( - budgetId: await getBudgetId(), + budgetId: await getBudgetId(), id: id, transactionType: event.transactionType, amount: tr == null ? Amount.pure() : Amount.dirty(tr.amount.toString()), @@ -143,24 +152,39 @@ class TransactionBloc extends Bloc { Future _onCategoriesChanged(TransactionCategoriesChanged event, Emitter emit) async { - final categories = event.categories.where((cat) => cat.transactionType == state.transactionType).toList(); - final accCategories = event.categories.where((cat) => cat.transactionType == TransactionType.ACCOUNT).toList(); - emit(state.copyWith(category: () => null, categories: () => categories, accountCategories: () => accCategories,)); + final categories = event.categories + .where((cat) => cat.transactionType == state.transactionType) + .toList(); + final accCategories = event.categories + .where((cat) => cat.transactionType == TransactionType.ACCOUNT) + .toList(); + emit(state.copyWith( + category: () => null, + categories: () => categories, + accountCategories: () => accCategories, + )); } void _onCategoryChanged( TransactionCategoryChanged event, Emitter emit) async { final subcategories = await _subcategoriesRepository.getSubcategories().first; - final filteredScs = subcategories.where((sc) => sc.categoryId == event.category!.id).toList(); - emit( - state.copyWith(category: () => event.category, subcategory: () => null, subcategories: () => filteredScs)); + final filteredScs = subcategories + .where((sc) => sc.categoryId == event.category!.id) + .toList(); + emit(state.copyWith( + category: () => event.category, + subcategory: () => null, + subcategories: () => filteredScs)); } Future _onSubcategoriesChanged(TransactionSubcategoriesChanged event, Emitter emit) async { - final subcategories = event.subcategories.where((sc) => sc.categoryId == state.category!.id).toList(); - emit(state.copyWith(subcategory: () => null, subcategories: () => subcategories)); + final subcategories = event.subcategories + .where((sc) => sc.categoryId == state.category!.id) + .toList(); + emit(state.copyWith( + subcategory: () => null, subcategories: () => subcategories)); } void _onSubcategoryChanged( @@ -168,8 +192,8 @@ class TransactionBloc extends Bloc { emit(state.copyWith(subcategory: () => event.subcategory)); } - Future _onSubcategoryCreated( - TransactionSubcategoryCreated event, Emitter emit) async { + Future _onSubcategoryCreated(TransactionSubcategoryCreated event, + Emitter emit) async { final newSubcategory = Subcategory( categoryId: state.category!.id!, name: event.name, @@ -209,7 +233,9 @@ class TransactionBloc extends Bloc { try { await _transactionsRepository.createTransaction(transaction); emit(state.copyWith(status: FormzSubmissionStatus.success)); - Navigator.of(event.context!).pop(); + isDisplayDesktop(event.context!) + ? add(TransactionFormLoaded(transactionType: transaction.type!)) + : Navigator.of(event.context!).pop(); } catch (e) { emit(state.copyWith(status: FormzSubmissionStatus.failure)); } diff --git a/lib/transactions/transaction/bloc/transaction_event.dart b/lib/transactions/transaction/bloc/transaction_event.dart index 47dadee..5bfe675 100644 --- a/lib/transactions/transaction/bloc/transaction_event.dart +++ b/lib/transactions/transaction/bloc/transaction_event.dart @@ -11,7 +11,7 @@ final class TransactionFormLoaded extends TransactionEvent { final TransactionTile? transaction; final TransactionType transactionType; const TransactionFormLoaded( - {required this.transaction, required this.transactionType}); + {this.transaction, required this.transactionType}); @override List get props => [transaction, transactionType]; } diff --git a/lib/transactions/transaction/view/transaction_form.dart b/lib/transactions/transaction/view/transaction_form.dart index e74e264..ebde978 100644 --- a/lib/transactions/transaction/view/transaction_form.dart +++ b/lib/transactions/transaction/view/transaction_form.dart @@ -7,9 +7,10 @@ import 'package:budget_app/transactions/transaction/view/widgets/date_input_fiel import 'package:budget_app/transactions/transaction/view/widgets/subcategory_input_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:formz/formz.dart'; +import '../../../constants/constants.dart'; + class TransactionForm extends StatelessWidget { const TransactionForm({Key? key}) : super(key: key); @@ -19,32 +20,32 @@ class TransactionForm extends StatelessWidget { child: Column( children: [ Container( - padding: EdgeInsets.symmetric(vertical: 70.w, horizontal: 50.w), + padding: EdgeInsets.symmetric(vertical: h * 0.04, horizontal: 30), child: Column( children: [ AmountInput(), SizedBox( - height: 75.h, + height: 20, ), DateInput(), SizedBox( - height: 75.h, + height: 20, ), CategoryInput(), SizedBox( - height: 75.h, + height: 20, ), SubcategoryInput(), SizedBox( - height: 75.h, + height: 20, ), AccountInput(), SizedBox( - height: 75.h, + height: 20, ), _NotesInput(), SizedBox( - height: 75.h, + height: 20, ), _SubmitButton(), ], diff --git a/lib/transactions/transaction/view/transaction_page.dart b/lib/transactions/transaction/view/transaction_page.dart index 1232154..9da2359 100644 --- a/lib/transactions/transaction/view/transaction_page.dart +++ b/lib/transactions/transaction/view/transaction_page.dart @@ -1,5 +1,4 @@ import 'package:budget_app/accounts/repository/accounts_repository.dart'; -import 'package:budget_app/app/app.dart'; import 'package:budget_app/categories/repository/categories_repository.dart'; import 'package:budget_app/subcategories/repository/subcategories_repository.dart'; import 'package:budget_app/transactions/models/transaction_tile.dart'; @@ -21,7 +20,6 @@ class TransactionPage extends StatelessWidget { {required HomeCubit homeCubit, TransactionTile? transaction, required TransactionType transactionType}) { return MaterialPageRoute(builder: (context) { - final appBloc = BlocProvider.of(context); return MultiBlocProvider( providers: [ BlocProvider( @@ -46,7 +44,6 @@ class TransactionPage extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { return Scaffold( @@ -73,3 +70,25 @@ class TransactionPage extends StatelessWidget { return '$prefix $body'; } } + +class TransactionWindow extends StatelessWidget { + const TransactionWindow({Key? key}) : super(key: key); + + static Widget window( + {Key? key, TransactionTile? transaction, + required TransactionType transactionType}) { + return TransactionWindow(key: key,); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.trStatus == TransactionStatus.success + ? TransactionForm() + : Center(child: CircularProgressIndicator(), + ); + }, + ); + } +} diff --git a/lib/transactions/transaction/view/widgets/account_input_field.dart b/lib/transactions/transaction/view/widgets/account_input_field.dart index 6a0c143..8b7bdac 100644 --- a/lib/transactions/transaction/view/widgets/account_input_field.dart +++ b/lib/transactions/transaction/view/widgets/account_input_field.dart @@ -1,4 +1,3 @@ -import 'package:budget_app/colors.dart'; import 'package:budget_app/home/cubit/home_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,8 +15,7 @@ class AccountInput extends StatelessWidget { child: Icon(Icons.edit_note), onTap: () { Navigator.of(context).push(AccountsListPage.route( - homeCubit: context.read(), - accountCategories: state.accountCategories)); + homeCubit: context.read())); }, ), items: state.accounts.map((Account account) { diff --git a/lib/transactions/view/transactions_list.dart b/lib/transactions/view/transactions_list.dart new file mode 100644 index 0000000..6ce02b9 --- /dev/null +++ b/lib/transactions/view/transactions_list.dart @@ -0,0 +1,80 @@ +import 'package:budget_app/home/cubit/home_cubit.dart'; +import 'package:budget_app/transactions/models/transaction_type.dart'; +import 'package:budget_app/transactions/models/transactions_view_filter.dart'; +import 'package:budget_app/transactions/transaction/bloc/transaction_bloc.dart'; +import 'package:budget_app/transactions/view/widgets/transaction_list_tile.dart'; +import 'package:budget_app/transfer/bloc/transfer_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../constants/constants.dart'; +import '../../transfer/view/transfer_page.dart'; +import '../cubit/transactions_cubit.dart'; +import '../transaction/view/transaction_page.dart'; + +class TransactionsList extends StatelessWidget { + final TransactionsViewFilter filter; + + const TransactionsList({super.key, required this.filter}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + state = state.copyWith(filter: filter); + final maxHeight = h * 0.55; + return state.status == TransactionsStatus.loading + ? Center(child: CircularProgressIndicator()) + : Container( + margin: EdgeInsets.only(bottom: 10), + height: state.filteredTiles.length * 80 > maxHeight + ? maxHeight + : state.filteredTiles.length * 80, + child: ListView.separated( + itemCount: state.filteredTiles.length, + itemBuilder: (context, index) { + final tr = state.filteredTiles[index]; + return TransactionListTile( + transactionTile: tr, + onDismissed: (_) { + final trCub = context.read(); + tr.type == TransactionType.TRANSFER + ? trCub.deleteTransfer(transferId: tr.id) + : trCub.deleteTransaction(transactionId: tr.id); + }, + onTap: () => { + if (tr.type == TransactionType.TRANSFER) + { + isDisplayDesktop(context) + ? context.read().add( + TransferFormLoaded(transactionTile: tr)) : + Navigator.of(context).push( + TransferPage.route( + homeCubit: context.read(), + transactionTile: tr), + ) + } + else + { + isDisplayDesktop(context) + ? context.read().add( + TransactionFormLoaded( + transactionType: tr.type, + transaction: tr)) + : Navigator.of(context).push( + TransactionPage.route( + transaction: tr, + homeCubit: context.read(), + transactionType: tr.type), + ) + } + }, + ); + }, + separatorBuilder: (context, index) => + Divider(indent: 30, endIndent: 30)), + ); + }, + ); + } +} diff --git a/lib/transactions/view/transactions_page.dart b/lib/transactions/view/transactions_page.dart deleted file mode 100644 index 23221fd..0000000 --- a/lib/transactions/view/transactions_page.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:budget_app/accounts/repository/accounts_repository.dart'; -import 'package:budget_app/categories/repository/categories_repository.dart'; -import 'package:budget_app/home/cubit/home_cubit.dart'; -import 'package:budget_app/transactions/models/transaction_type.dart'; -import 'package:budget_app/transactions/models/transactions_view_filter.dart'; -import 'package:budget_app/transactions/repository/transactions_repository.dart'; -import 'package:budget_app/transactions/view/widgets/transaction_list_tile.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -import '../../app/bloc/app_bloc.dart'; -import '../../subcategories/repository/subcategories_repository.dart'; -import '../../transfer/view/transfer_page.dart'; -import '../cubit/transactions_cubit.dart'; -import '../transaction/view/transaction_page.dart'; - -class TransactionsPage extends StatelessWidget { - static const routeName = '/transactions'; - - static Route route( - {required HomeCubit homeCubit, required TransactionsViewFilter filter}) { - return MaterialPageRoute(builder: (context) { - final appBloc = BlocProvider.of(context); - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => TransactionsCubit( - transactionsRepository: - context.read(), - categoriesRepository: context.read(), - subcategoriesRepository: - context.read(), - accountsRepository: context.read(), - filter: filter, - ), - ), - BlocProvider.value(value: homeCubit), - ], - child: TransactionsPage(), - ); - }); - } - - const TransactionsPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; - return BlocListener( - listenWhen: (previous, current) => - (previous.lastDeletedTransaction != - current.lastDeletedTransaction && - current.lastDeletedTransaction != null) || - (previous.lastDeletedTransfer != current.lastDeletedTransfer && - current.lastDeletedTransfer != null), - listener: (context, state) { - final messenger = ScaffoldMessenger.of(context); - messenger - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - duration: Duration(seconds: 3), - content: Text(state.lastDeletedTransaction != null ? - 'Transaction has been deleted' : 'Transfer has been deleted', - ), - action: SnackBarAction( - label: 'UNDO', - onPressed: () { - messenger.hideCurrentSnackBar(); - try { - context.read().undoDelete(); - } catch (e) {} - }, - ), - ), - ); - }, - child: BlocBuilder( - builder: (context, state) { - return Scaffold( - appBar: AppBar( - centerTitle: true, - title: Text('Transactions'), - ), - body: state.status == TransactionsStatus.loading - ? Center(child: CircularProgressIndicator()) - : Column( - children: [ - SizedBox( - height: 50.h, - ), - Expanded( - child: ListView.builder( - itemCount: state.filteredTiles.length, - itemBuilder: (context, index) { - final tr = state.filteredTiles[index]; - return TransactionListTile( - transactionTile: tr, - onDismissed: (_) { - final trCub = context.read(); - tr.type == TransactionType.TRANSFER - ? trCub.deleteTransfer(transferId: tr.id) - : trCub.deleteTransaction( - transactionId: tr.id); - }, - onTap: () => { - if (tr.type == TransactionType.TRANSFER) - { - Navigator.of(context).push( - TransferPage.route( - homeCubit: context.read(), - transactionTile: tr), - ) - } - else - { - Navigator.of(context).push( - TransactionPage.route( - transaction: tr, - homeCubit: context.read(), - transactionType: tr.type), - ) - } - }, - ); - }), - ), - ], - )); - }, - )); - } -} diff --git a/lib/transactions/view/widgets/transaction_list_tile.dart b/lib/transactions/view/widgets/transaction_list_tile.dart index 862cd2e..8397d95 100644 --- a/lib/transactions/view/widgets/transaction_list_tile.dart +++ b/lib/transactions/view/widgets/transaction_list_tile.dart @@ -31,26 +31,7 @@ class TransactionListTile extends StatelessWidget { color: Color(0xAAFFFFFF), ), ), - child: Card( - margin: EdgeInsets.symmetric(horizontal: 30.w, vertical: 15.h), - elevation: Theme.of(context).cardTheme.elevation, - child: ClipPath( - clipper: ShapeBorderClipper( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - child: Container( - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: (transactionTile.type == TransactionType.EXPENSE || - transactionTile.title == - 'Transfer out') - ? Theme.of(context).colorScheme.error - : BudgetColors.teal900, - width: 20.w), - ), - ), - child: ListTile( + child: ListTile( leading: Icon(Icons.clear_all), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -87,9 +68,6 @@ class TransactionListTile extends StatelessWidget { isThreeLine: false, onTap: onTap, ), - ), - ), - ), - ); + ); } } diff --git a/lib/transfer/bloc/transfer_bloc.dart b/lib/transfer/bloc/transfer_bloc.dart index cf034cf..e90278f 100644 --- a/lib/transfer/bloc/transfer_bloc.dart +++ b/lib/transfer/bloc/transfer_bloc.dart @@ -15,6 +15,7 @@ import '../../accounts/models/account.dart'; import '../../accounts/repository/accounts_repository.dart'; import '../../categories/models/category.dart'; import '../../categories/repository/categories_repository.dart'; +import '../../constants/constants.dart'; import '../../transactions/models/transaction_type.dart'; part 'transfer_event.dart'; @@ -22,17 +23,16 @@ part 'transfer_event.dart'; part 'transfer_state.dart'; class TransferBloc extends Bloc { - final TransactionsRepository _transactionsRepository; final CategoriesRepository _categoriesRepository; late final AccountsRepository _accountsRepository; late final StreamSubscription> _accountsSubscription; late final StreamSubscription> _categoriesSubscription; - TransferBloc({ - required TransactionsRepository transactionsRepository, - required CategoriesRepository categoriesRepository, - required AccountsRepository accountsRepository}) + TransferBloc( + {required TransactionsRepository transactionsRepository, + required CategoriesRepository categoriesRepository, + required AccountsRepository accountsRepository}) : _transactionsRepository = transactionsRepository, _categoriesRepository = categoriesRepository, _accountsRepository = accountsRepository, @@ -40,67 +40,75 @@ class TransferBloc extends Bloc { on(_onEvent, transformer: sequential()); _accountsSubscription = _accountsRepository.getAccounts().skip(1).listen((accounts) { - add(TransferAccountsChanged(accounts: accounts)); - }); + add(TransferAccountsChanged(accounts: accounts)); + }); _categoriesSubscription = _categoriesRepository.getCategories().skip(1).listen((categories) { - add(TransferCategoriesChanged(categories: categories)); - }); + add(TransferCategoriesChanged(categories: categories)); + }); } - Future _onEvent(TransferEvent event, - Emitter emit) async { + Future _onEvent( + TransferEvent event, Emitter emit) async { return switch (event) { - final TransferFormLoaded e => _onFormLoaded(e, emit), - final TransferAmountChanged e => _onAmountChanged(e, emit), - final TransferDateChanged e => _onDateChanged(e, emit), - final TransferAccountsChanged e => _onAccountsChanged(e, emit), - final TransferFromAccountChanged e => _onFromAccountChanged(e, emit), - final TransferToAccountChanged e => _onToAccountChanged(e, emit), - final TransferNotesChanged e => _onNotesChanged(e, emit), + final TransferFormLoaded e => _onFormLoaded(e, emit), + final TransferAmountChanged e => _onAmountChanged(e, emit), + final TransferDateChanged e => _onDateChanged(e, emit), + final TransferAccountsChanged e => _onAccountsChanged(e, emit), + final TransferFromAccountChanged e => _onFromAccountChanged(e, emit), + final TransferToAccountChanged e => _onToAccountChanged(e, emit), + final TransferNotesChanged e => _onNotesChanged(e, emit), final TransferCategoriesChanged e => _onCategoriesChanged(e, emit), - final TransferFormSubmitted e => _onFormSubmitted(e, emit), + final TransferFormSubmitted e => _onFormSubmitted(e, emit), }; - } + } - Future _onFormLoaded(TransferFormLoaded event, - Emitter emit) async { + Future _onFormLoaded( + TransferFormLoaded event, Emitter emit) async { emit(state.copyWith(trStatus: TransferStatus.loading)); final accounts = await _accountsRepository.getAccounts().first; final accCategories = await _categoriesRepository.getCategories().first; - final filteredCategories = accCategories.where((aC) => aC.transactionType == TransactionType.ACCOUNT).toList(); + final filteredCategories = accCategories + .where((aC) => aC.transactionType == TransactionType.ACCOUNT) + .toList(); final transactionTile = event.transactionTile; + var id; + var fromAccount; + var toAccount; if (transactionTile != null) { - emit(state.copyWith( - budgetId: await getBudgetId(), - id: transactionTile.id, - amount: Amount.dirty(transactionTile.amount.toString()), - fromAccount: accounts - .where((acc) => acc.id == transactionTile.fromAccount!.id) - .first, - toAccount: accounts - .where((acc) => acc.id == transactionTile.toAccount!.id) - .first, - date: transactionTile.dateTime, - notes: transactionTile.description, - accountCategories: filteredCategories, - accounts: accounts, - isValid: true, - trStatus: TransferStatus.success,)); - } else { - emit(state.copyWith(trStatus: TransferStatus.success, - budgetId: await getBudgetId(), - accountCategories: filteredCategories, - accounts: accounts)); + id = transactionTile.id; + fromAccount = accounts + .where((acc) => acc.id == transactionTile.fromAccount!.id) + .first; + toAccount = accounts + .where((acc) => acc.id == transactionTile.toAccount!.id) + .first; } + await Future.delayed(Duration(milliseconds: 100)); + emit(state.copyWith( + budgetId: await getBudgetId(), + id: id, + amount: transactionTile == null + ? Amount.pure() + : Amount.dirty(transactionTile.amount.toString()), + trStatus: TransferStatus.success, + fromAccount: fromAccount, + toAccount: toAccount, + date: transactionTile?.dateTime ?? DateTime.now(), + notes: transactionTile?.description ?? '', + accountCategories: filteredCategories, + accounts: accounts, + isValid: transactionTile == null ? false : true, + )); } - void _onCategoriesChanged(TransferCategoriesChanged event, Emitter emit){ + void _onCategoriesChanged( + TransferCategoriesChanged event, Emitter emit) { emit(state.copyWith(accountCategories: event.categories)); } - void _onAmountChanged(TransferAmountChanged event, - Emitter emit) { + void _onAmountChanged( + TransferAmountChanged event, Emitter emit) { final amount = Amount.dirty(event.amount!); emit( state.copyWith( @@ -116,28 +124,28 @@ class TransferBloc extends Bloc { ); } - void _onAccountsChanged(TransferAccountsChanged event, - Emitter emit) { + void _onAccountsChanged( + TransferAccountsChanged event, Emitter emit) { emit(state.copyWith(accounts: event.accounts)); } - void _onFromAccountChanged(TransferFromAccountChanged event, - Emitter emit) { + void _onFromAccountChanged( + TransferFromAccountChanged event, Emitter emit) { emit(state.copyWith(fromAccount: event.account)); } - void _onToAccountChanged(TransferToAccountChanged event, - Emitter emit) { + void _onToAccountChanged( + TransferToAccountChanged event, Emitter emit) { emit(state.copyWith(toAccount: event.account)); } - void _onNotesChanged(TransferNotesChanged event, - Emitter emit) { + void _onNotesChanged( + TransferNotesChanged event, Emitter emit) { emit(state.copyWith(notes: event.notes)); } - Future _onFormSubmitted(TransferFormSubmitted event, - Emitter emit) async { + Future _onFormSubmitted( + TransferFormSubmitted event, Emitter emit) async { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); final transfer = Transfer( id: state.id, @@ -146,15 +154,18 @@ class TransferBloc extends Bloc { date: state.date ?? DateTime.now(), fromAccountId: state.fromAccount!.id!, toAccountId: state.toAccount!.id!, - notes: state.notes,); + notes: state.notes, + ); try { - if(state.id == null){ + if (state.id == null) { await _transactionsRepository.createTransfer(transfer); - }else { + } else { await _transactionsRepository.editTransfer(transfer); } emit(state.copyWith(status: FormzSubmissionStatus.success)); - Navigator.of(event.context!).pop(); + isDisplayDesktop(event.context!) + ? add(TransferFormLoaded()) + : Navigator.of(event.context!).pop(); } catch (e) { emit(state.copyWith(status: FormzSubmissionStatus.failure)); } diff --git a/lib/transfer/models/transfer.dart b/lib/transfer/models/transfer.dart index da294b8..304cc67 100644 --- a/lib/transfer/models/transfer.dart +++ b/lib/transfer/models/transfer.dart @@ -53,18 +53,27 @@ class Transfer extends Equatable { @override List get props => [id, budgetId]; - TransactionTile toTile({required String tabAccId, required Account fromAccount, required Account toAccount}) { - return TransactionTile( + List toTiles({required Account fromAccount, required Account toAccount}) { + return List.of( + [TransactionTile( id: this.id!, type: TransactionType.TRANSFER, amount: this.amount, - title: tabAccId == fromAccount.id ? 'Transfer out' : 'Transfer in', - subtitle: tabAccId == fromAccount.id - ? 'to ${toAccount.name}' - : 'from ${fromAccount.name}', + title: 'Transfer in', + subtitle: 'from ${fromAccount.name}', dateTime: date, description: this.notes!, fromAccount: fromAccount, - toAccount: toAccount); + toAccount: toAccount), + TransactionTile( + id: this.id!, + type: TransactionType.TRANSFER, + amount: this.amount, + title: 'Transfer out', + subtitle: 'to ${toAccount.name}', + dateTime: date, + description: this.notes!, + fromAccount: fromAccount, + toAccount: toAccount)]); } } diff --git a/lib/transfer/view/transfer_form.dart b/lib/transfer/view/transfer_form.dart index 7912e2d..0645d92 100644 --- a/lib/transfer/view/transfer_form.dart +++ b/lib/transfer/view/transfer_form.dart @@ -1,11 +1,10 @@ import 'package:budget_app/transfer/transfer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:formz/formz.dart'; import '../../colors.dart'; -import '../../home/cubit/home_cubit.dart'; +import '../../constants/constants.dart'; import '../widgets/to_account_input_field.dart'; class TransferForm extends StatelessWidget { @@ -17,28 +16,28 @@ class TransferForm extends StatelessWidget { child: Column( children: [ Container( - padding: EdgeInsets.symmetric(vertical: 70.w, horizontal: 50.w), + padding: EdgeInsets.symmetric(vertical: h * 0.04, horizontal: 30), child: Column( children: [ AmountInput(), SizedBox( - height: 30.h, + height: 0, ), DateInputField(), SizedBox( - height: 75.h, + height: 20, ), FromAccountInputField(), SizedBox( - height: 75.h, + height: 20, ), ToAccountInputField(), SizedBox( - height: 75.h, + height: 20, ), NotesInputField(), SizedBox( - height: 75.h, + height: 20, ), _SubmitButton(), ], @@ -53,7 +52,6 @@ class TransferForm extends StatelessWidget { class _SubmitButton extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { return state.status.isInProgress diff --git a/lib/transfer/view/transfer_page.dart b/lib/transfer/view/transfer_page.dart index 5f6d92f..365c5df 100644 --- a/lib/transfer/view/transfer_page.dart +++ b/lib/transfer/view/transfer_page.dart @@ -6,15 +6,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../accounts/repository/accounts_repository.dart'; -import '../../app/bloc/app_bloc.dart'; import '../../categories/repository/categories_repository.dart'; +import '../../transactions/models/transaction_type.dart'; class TransferPage extends StatelessWidget { const TransferPage({Key? key}) : super(key: key); static Route route({required HomeCubit homeCubit, TransactionTile? transactionTile}) { return MaterialPageRoute(builder: (context) { - final appBloc = BlocProvider.of(context); return MultiBlocProvider( providers: [ BlocProvider( @@ -35,7 +34,6 @@ class TransferPage extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { return Scaffold( @@ -51,3 +49,25 @@ class TransferPage extends StatelessWidget { ); } } + +class TransferWindow extends StatelessWidget { + const TransferWindow({Key? key}) : super(key: key); + + static Widget window( + {Key? key, TransactionTile? transaction, + required TransactionType transactionType}) { + return TransferWindow(key: key,); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return state.trStatus == TransferStatus.success + ? TransferForm() + : Center(child: CircularProgressIndicator(), + ); + }, + ); + } +} diff --git a/lib/transfer/widgets/from_account_input_field.dart b/lib/transfer/widgets/from_account_input_field.dart index 0ceed30..775367e 100644 --- a/lib/transfer/widgets/from_account_input_field.dart +++ b/lib/transfer/widgets/from_account_input_field.dart @@ -15,8 +15,7 @@ class FromAccountInputField extends StatelessWidget { child: Icon(Icons.edit_note), onTap: () { Navigator.of(context).push(AccountsListPage.route( - homeCubit: context.read(), - accountCategories: state.accountCategories)); + homeCubit: context.read())); }, ), items: state.accounts diff --git a/lib/transfer/widgets/to_account_input_field.dart b/lib/transfer/widgets/to_account_input_field.dart index 0e4a310..66bf433 100644 --- a/lib/transfer/widgets/to_account_input_field.dart +++ b/lib/transfer/widgets/to_account_input_field.dart @@ -15,8 +15,7 @@ class ToAccountInputField extends StatelessWidget { child: Icon(Icons.edit_note), onTap: () { Navigator.of(context).push(AccountsListPage.route( - homeCubit: context.read(), - accountCategories: state.accountCategories)); + homeCubit: context.read())); }, ), items: state.accounts diff --git a/packages/authentication_repository/lib/src/authentication_repository.dart b/packages/authentication_repository/lib/src/authentication_repository.dart index 26b5d37..d6a2b0b 100644 --- a/packages/authentication_repository/lib/src/authentication_repository.dart +++ b/packages/authentication_repository/lib/src/authentication_repository.dart @@ -4,7 +4,6 @@ import 'package:authentication_repository/authentication_repository.dart'; import 'package:cache/cache.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:google_sign_in/google_sign_in.dart'; import 'package:meta/meta.dart'; /// {@template sign_up_with_email_and_password_failure} @@ -155,14 +154,11 @@ class AuthenticationRepository { AuthenticationRepository({ CacheClient? cache, firebase_auth.FirebaseAuth? firebaseAuth, - GoogleSignIn? googleSignIn, }) : _cache = cache ?? CacheClient(), - _firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance, - _googleSignIn = googleSignIn ?? GoogleSignIn.standard(); + _firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance; final CacheClient _cache; final firebase_auth.FirebaseAuth _firebaseAuth; - final GoogleSignIn _googleSignIn; /// Whether or not the current environment is web /// Should only be overriden for testing purposes. Otherwise, @@ -199,13 +195,14 @@ class AuthenticationRepository { /// Creates a new user with the provided [email] and [password]. /// /// Throws a [SignUpWithEmailAndPasswordFailure] if an exception occurs. - Future signUp({required String email, required String password}) async { + Future signUp({required String email, + required String password,}) async { try { final result = await _firebaseAuth.createUserWithEmailAndPassword( email: email, password: password, ); - return await result.user!.getIdToken(); + return await result.user!.getIdToken() ?? ''; } on firebase_auth.FirebaseAuthException catch (e) { throw SignUpWithEmailAndPasswordFailure.fromCode(e.code); @@ -214,35 +211,6 @@ class AuthenticationRepository { } } - /// Starts the Sign In with Google Flow. - /// - /// Throws a [LogInWithGoogleFailure] if an exception occurs. - Future logInWithGoogle() async { - try { - late final firebase_auth.AuthCredential credential; - if (isWeb) { - final googleProvider = firebase_auth.GoogleAuthProvider(); - final userCredential = await _firebaseAuth.signInWithPopup( - googleProvider, - ); - credential = userCredential.credential!; - } else { - final googleUser = await _googleSignIn.signIn(); - final googleAuth = await googleUser!.authentication; - credential = firebase_auth.GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, - ); - } - - await _firebaseAuth.signInWithCredential(credential); - } on firebase_auth.FirebaseAuthException catch (e) { - throw LogInWithGoogleFailure.fromCode(e.code); - } catch (_) { - throw const LogInWithGoogleFailure(); - } - } - /// Signs in with the provided [email] and [password]. /// /// Throws a [LogInWithEmailAndPasswordFailure] if an exception occurs. @@ -270,7 +238,6 @@ class AuthenticationRepository { try { await Future.wait([ _firebaseAuth.signOut(), - _googleSignIn.signOut(), ]); } catch (_) { throw LogOutFailure(); @@ -284,6 +251,6 @@ extension on firebase_auth.User { id: uid, email: email, name: displayName, - photo: photoURL); + photo: photoURL,); } } diff --git a/pubspec.lock b/pubspec.lock index ab503ce..3f766fc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,18 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: a742f71d7f3484253a623b30e19256aa4668ecbb3de6ad1beb0bcf8d4777ecd8 + sha256: "5dce45a06d386358334eb1689108db6455d90ceb0d75848d5f4819283d4ee2b8" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" + adaptive_breakpoints: + dependency: "direct main" + description: + name: adaptive_breakpoints + sha256: "3aa6ef09074fe7824dfdc9a6d9f955c8a3bae0fb71c495cbd72f471a87699b4f" + url: "https://pub.dev" + source: hosted + version: "0.1.6" analyzer: dependency: transitive description: @@ -84,10 +92,10 @@ packages: dependency: transitive description: name: build - sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" build_config: dependency: transitive description: @@ -108,18 +116,18 @@ packages: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37" + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" build_runner_core: dependency: transitive description: @@ -227,10 +235,10 @@ packages: dependency: transitive description: name: dart_style - sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" diff_match_patch: dependency: transitive description: @@ -275,34 +283,34 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: f693c0aa998b1101453878951b171b69f0db5199003df1c943b33493a1de7917 + sha256: "87216661b409575ecb1a7849da38604a0abfcb6497146b66d4832cb97a4c9e0f" url: "https://pub.dev" source: hosted - version: "4.6.3" + version: "4.7.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "689ae048b78ad088ba31acdec45f5badb56201e749ed8b534947a7303ddb32aa" + sha256: "4ab0a8997994db2b76bf0652689d7908ca935a99314857c683251bc23d31c287" url: "https://pub.dev" source: hosted - version: "6.15.3" + version: "6.16.0" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: f35d637a1707afd51f30090bb5234b381d5071ccbfef09b8c393bc7c65e440cd + sha256: c74c5753855896b31536b9e151e34bf7eea143f0d9c209947b5f7ddc8e111989 url: "https://pub.dev" source: hosted - version: "5.5.3" + version: "5.6.0" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: a4a99204da264a0aa9d54a332ea0315ce7b0768075139c77abefe98093dd98be + sha256: "2e9324f719e90200dc7d3c4f5d2abc26052f9f2b995d3b6626c47a0dfe1c8192" url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.15.0" firebase_core_platform_interface: dependency: transitive description: @@ -417,10 +425,10 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: f58a17ac07d783d000786a6c313fa4a0d2ee599a346d69b24fc48fb378d5d150 + sha256: "8d60a787b29cb7d2bcf29230865f4a91f17323c6ac5b6b9027a6418e48d9ffc3" url: "https://pub.dev" source: hosted - version: "6.1.16" + version: "6.1.18" google_sign_in_ios: dependency: transitive description: @@ -513,10 +521,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "61a60716544392a82726dd0fa1dd6f5f1fd32aec66422b6e229e7b90d52325c4" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.7.1" logging: dependency: transitive description: @@ -617,10 +625,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" path_provider_linux: dependency: transitive description: @@ -669,14 +677,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" provider: dependency: transitive description: @@ -721,58 +721,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "396f85b8afc6865182610c0a2fc470853d56499f75f7499e2a73a9f0539d23d0" + sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.2.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" + sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb + sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shelf: dependency: transitive description: @@ -814,18 +814,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" source_map_stack_trace: dependency: transitive description: @@ -990,18 +990,18 @@ packages: dependency: transitive description: name: win32 - sha256: "1414f27dd781737e51afa9711f2ac2ace6ab4498ee98e20863fa5505aa00c58c" + sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee url: "https://pub.dev" source: hosted - version: "5.0.4" + version: "5.0.5" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + sha256: e0b1147eec179d3911f1f19b59206448f78195ca1d20514134e10641b7d7fbff url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8e5b359..0a5df79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: budget_app description: A Budget app. -version: 2.1.0 +version: 2.2.0 publish_to: none environment: @@ -30,6 +30,7 @@ dependencies: firebase_core: ^2.14.0 firebase_auth: ^4.6.3 carousel_slider: ^4.2.1 + adaptive_breakpoints: ^0.1.6 dev_dependencies: bloc_test: ^9.0.0