LCOV - code coverage report
Current view: top level - lib/pages/home - home_page.dart Coverage Total Hit
Test: lcov.info Lines: 53.3 % 165 88
Test Date: 2026-11-14 15:04:37 Functions: - 0 0

            Line data    Source code
       1              : // Copyright 2023 The terCAD team. All rights reserved.
       2              : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
       3              : 
       4              : import 'package:app_finance/_classes/controller/payments_controller.dart';
       5              : import 'package:app_finance/_classes/herald/app_design.dart';
       6              : import 'package:app_finance/_classes/herald/app_start_of_month.dart';
       7              : import 'package:app_finance/_classes/storage/app_data.dart';
       8              : import 'package:app_finance/_classes/herald/app_locale.dart';
       9              : import 'package:app_finance/_classes/structure/currency/exchange.dart';
      10              : import 'package:app_finance/_classes/storage/app_preferences.dart';
      11              : import 'package:app_finance/_configs/screen_helper.dart';
      12              : import 'package:app_finance/_configs/theme_helper.dart';
      13              : import 'package:app_finance/_classes/structure/navigation/app_route.dart';
      14              : import 'package:app_finance/_ext/date_time_ext.dart';
      15              : import 'package:app_finance/components/_core/component_data.dart';
      16              : import 'package:app_finance/components/_core/components_builder.dart';
      17              : import 'package:app_finance/components/_core/list_component_registry.dart';
      18              : import 'package:app_finance/components/component_chart.dart';
      19              : import 'package:app_finance/components/widgets/account_flow_chart.dart';
      20              : import 'package:app_finance/components/widgets/budget_forecast_chart.dart';
      21              : import 'package:app_finance/components/component_recent.dart';
      22              : import 'package:app_finance/components/widgets/bill_ytd_chart.dart';
      23              : import 'package:app_finance/pages/_interfaces/abstract_page_state.dart';
      24              : import 'package:app_finance/pages/home/home_edit_page.dart';
      25              : import 'package:app_finance/pages/home/widgets/security_bio_tab.dart';
      26              : import 'package:app_finance/pages/home/widgets/security_tab.dart';
      27              : import 'package:app_finance/pages/start/start_page.dart';
      28              : import 'package:app_finance/design/wrapper/grid_layer.dart';
      29              : import 'package:app_finance/pages/home/widgets/init_tab.dart';
      30              : import 'package:app_finance/design/button/toolbar_button_widget.dart';
      31              : import 'package:app_finance/components/widgets/account_widget.dart';
      32              : import 'package:app_finance/components/widgets/bill_widget.dart';
      33              : import 'package:app_finance/components/widgets/budget_widget.dart';
      34              : import 'package:app_finance/pages/home/widgets/goal_widget.dart';
      35              : import 'package:flutter/foundation.dart';
      36              : import 'package:material_ui/material_ui.dart';
      37              : import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
      38              : import 'package:flutter_svg/flutter_svg.dart';
      39              : import 'package:package_info_plus/package_info_plus.dart';
      40              : import 'package:provider/provider.dart';
      41              : import 'package:simple_totp_auth/simple_totp_auth.dart';
      42              : 
      43              : class HomePage extends StatefulWidget {
      44            2 :   const HomePage({super.key});
      45              : 
      46            6 :   @override
      47            6 :   HomePageState createState() => HomePageState();
      48              : }
      49              : 
      50              : class HomePageState extends AbstractPageState<HomePage> {
      51              :   String? toExpand;
      52              :   String secret = '';
      53              :   bool isEditMode = false;
      54              :   bool isOtpPassed = false;
      55              :   late String version;
      56              : 
      57            6 :   @override
      58              :   initState() {
      59            6 :     super.initState();
      60           12 :     toExpand = AppPreferences.get(AppPreferences.prefExpand);
      61           12 :     version = AppPreferences.get(AppPreferences.prefVersion) ?? '';
      62           12 :     secret = AppPreferences.get(AppPreferences.prefPrivacyKey) ?? '';
      63           12 :     if (secret.isEmpty) {
      64           12 :       secret = TOTP.generateSecret();
      65           12 :       AppPreferences.set(AppPreferences.prefPrivacyKey, secret);
      66              :     }
      67           12 :     PackageInfo.fromPlatform().then((PackageInfo value) {
      68            0 :       if (version != value.version) {
      69            0 :         WidgetsBinding.instance.addPostFrameCallback((_) => showModalBottomSheet(
      70            0 :               context: context,
      71            0 :               builder: (BuildContext context) => buildReleaseHelper(context, version),
      72              :             ));
      73            0 :         AppPreferences.set(AppPreferences.prefVersion, value.version);
      74              :       }
      75              :     });
      76              :   }
      77              : 
      78            0 :   Widget buildReleaseHelper(BuildContext context, String version) => buildHelper(
      79              :         context,
      80              :         type: 'upgrade',
      81            0 :         builder: (context, snapshot) {
      82            0 :           if (snapshot.hasData) {
      83            0 :             String data = snapshot.data!;
      84            0 :             if (version.isNotEmpty) {
      85            0 :               data = data.split('### $version')[0];
      86              :             }
      87            0 :             return Directionality(
      88            0 :               textDirection: AppDesign.getAlignment<TextDirection>(),
      89            0 :               child: Markdown(data: data),
      90              :             );
      91              :           }
      92            0 :           return Container();
      93              :         },
      94              :       );
      95              : 
      96            0 :   @override
      97            0 :   String getTitle() => AppLocale.labels.appTitle;
      98              : 
      99            6 :   @override
     100              :   Widget buildBottomBar(BuildContext context, BoxConstraints constraints) {
     101           12 :     return ThemeHelper.isWearable ? ThemeHelper.emptyBox : super.buildBottomBar(context, constraints);
     102              :   }
     103              : 
     104            0 :   @override
     105              :   Widget? buildRightBar(BuildContext context, BoxConstraints constraints) {
     106            0 :     return ThemeHelper.isWearable ? null : super.buildRightBar(context, constraints);
     107              :   }
     108              : 
     109            6 :   @override
     110              :   Widget? getBarLeading(NavigatorState nav) {
     111            6 :     if (ScreenHelper.state().isWide) {
     112              :       return ThemeHelper.emptyBox;
     113              :     }
     114            6 :     return Builder(
     115            6 :       builder: (BuildContext context) {
     116            6 :         final scaffold = Scaffold.of(context);
     117            6 :         return ToolbarButtonWidget(
     118              :           icon: Icons.menu,
     119           12 :           tooltip: AppLocale.labels.navigationTooltip,
     120            0 :           onPressed: () => AppDesign.isRightToLeft() ? scaffold.openEndDrawer() : scaffold.openDrawer(),
     121              :         );
     122              :       },
     123              :     );
     124              :   }
     125              : 
     126            6 :   @override
     127              :   List<Widget> getBarActions(NavigatorState nav) {
     128            6 :     final isWide = ScreenHelper.state().isWide;
     129            6 :     return [
     130            6 :       ToolbarButtonWidget(
     131              :         isWide: isWide,
     132              :         icon: Icons.app_registration_outlined,
     133           12 :         tooltip: AppLocale.labels.customizeTooltip,
     134            0 :         onPressed: () => setState(() => isEditMode = true),
     135              :       ),
     136           18 :       if (![TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android].contains(defaultTargetPlatform))
     137            0 :         ToolbarButtonWidget(
     138              :           isWide: isWide,
     139              :           icon: Icons.switch_access_shortcut_add_outlined,
     140            0 :           tooltip: AppLocale.labels.subscriptionTooltip,
     141            0 :           onPressed: () => nav.pushNamed(AppRoute.subscriptionRoute),
     142              :         ),
     143              :     ];
     144              :   }
     145              : 
     146            6 :   @override
     147              :   Widget getBarTitle(BuildContext context, [bool isBottom = false]) {
     148            6 :     return SvgPicture.asset(
     149              :       'assets/images/fingrom.svg',
     150              :       height: isBottom ? 20 : 40,
     151              :       alignment: Alignment.centerLeft,
     152           12 :       semanticsLabel: AppLocale.labels.appTitle,
     153              :     );
     154              :   }
     155              : 
     156            6 :   @override
     157           12 :   String getButtonName() => AppLocale.labels.addMainTooltip;
     158              : 
     159            6 :   @override
     160              :   Widget buildButton(BuildContext context, BoxConstraints constraints) {
     161            6 :     NavigatorState nav = Navigator.of(context);
     162            6 :     return FloatingActionButton(
     163              :       heroTag: 'home_page',
     164            6 :       mini: ThemeHelper.isWearable,
     165            2 :       onPressed: () => nav.pushNamed(AppRoute.billAddRoute),
     166            6 :       tooltip: getButtonName(),
     167              :       child: const Icon(Icons.add),
     168              :     );
     169              :   }
     170              : 
     171            6 :   @override
     172              :   Widget build(BuildContext context) {
     173           12 :     Provider.of<AppLocale>(context, listen: false).updateState(context);
     174            6 :     if (isEditMode) {
     175            0 :       return HomeEditPage(callback: () => setState(() => isEditMode = false));
     176              :     }
     177            6 :     if (AppPreferences.get(AppPreferences.prefPrivacyPolicy) == null) {
     178              :       return const StartPage();
     179              :     }
     180           12 :     return Consumer<AppData>(builder: (context, appState, _) {
     181           12 :       if (AppPreferences.get(AppPreferences.prefIsBio) == AppPreferences.isActive && !isOtpPassed) {
     182            0 :         return SecurityBioTab(
     183            0 :           onSuccess: () => setState(() => isOtpPassed = true),
     184              :         );
     185           12 :       } else if (AppPreferences.get(AppPreferences.prefIsOTP) == AppPreferences.isActive && !isOtpPassed) {
     186            0 :         return SecurityTab(
     187            0 :           onSuccess: () => setState(() => isOtpPassed = true),
     188              :         );
     189              :       }
     190            6 :       if (appState.isLoading) {
     191              :         return const InitTab();
     192              :       }
     193           24 :       WidgetsBinding.instance.addPostFrameCallback((_) => PaymentsController(appState));
     194            6 :       return super.build(context);
     195              :     });
     196              :   }
     197              : 
     198            0 :   Widget buildContentWideScreen() {
     199            0 :     return ComponentsBuilder([
     200            0 :       {
     201            0 :         componentData.key: ComponentRegistry.chart.toString(),
     202              :         componentData.startX: 0.0,
     203              :         componentData.startY: 4.0,
     204              :         componentData.endX: 4.0,
     205              :         componentData.endY: 7.0,
     206              :         componentData.order: 0,
     207            0 :         componentData.type: ComponentChartType.accountFlow.toString(),
     208              :       },
     209            0 :       {
     210            0 :         componentData.key: ComponentRegistry.chart.toString(),
     211              :         componentData.startX: 4.0,
     212              :         componentData.startY: 4.0,
     213              :         componentData.endX: 8.0,
     214              :         componentData.endY: 7.0,
     215              :         componentData.order: 1,
     216            0 :         componentData.type: ComponentChartType.billYtd.toString()
     217              :       },
     218            0 :       {
     219            0 :         componentData.key: ComponentRegistry.chart.toString(),
     220              :         componentData.startX: 8.0,
     221              :         componentData.startY: 4.0,
     222              :         componentData.endX: 12.0,
     223              :         componentData.endY: 7.0,
     224              :         componentData.order: 2,
     225            0 :         componentData.type: ComponentChartType.budgetForecast.toString()
     226              :       },
     227            0 :       {
     228            0 :         componentData.key: ComponentRegistry.summary.toString(),
     229              :         componentData.startX: 0,
     230              :         componentData.startY: 0,
     231              :         componentData.endX: 12.0,
     232              :         componentData.endY: 4.0,
     233              :         componentData.order: 3,
     234            0 :         componentData.type: ComponentRecentType.account.toString()
     235              :       }
     236              :     ]);
     237              :   }
     238              : 
     239            0 :   Widget buildContentSmallScreen() {
     240            0 :     return ComponentsBuilder([
     241            0 :       {
     242            0 :         componentData.key: ComponentRegistry.recent.toString(),
     243              :         componentData.startX: 0,
     244              :         componentData.startY: 0,
     245              :         componentData.endX: 3.0,
     246              :         componentData.endY: 2.0,
     247              :         componentData.order: 0,
     248            0 :         componentData.type: ComponentRecentType.account.toString(),
     249              :         ComponentRecent.count: 0
     250              :       },
     251            0 :       {
     252            0 :         componentData.key: ComponentRegistry.recent.toString(),
     253              :         componentData.startX: 3.0,
     254              :         componentData.startY: 0.0,
     255              :         componentData.endX: 6.0,
     256              :         componentData.endY: 2.0,
     257              :         componentData.order: 1,
     258            0 :         componentData.type: ComponentRecentType.budget.toString(),
     259              :         ComponentRecent.count: 0
     260              :       },
     261            0 :       {
     262            0 :         componentData.key: ComponentRegistry.recent.toString(),
     263              :         componentData.startX: 0.0,
     264              :         componentData.startY: 2.0,
     265              :         componentData.endX: 6.0,
     266              :         componentData.endY: 12.0,
     267              :         componentData.order: 2,
     268            0 :         componentData.type: ComponentRecentType.bill.toString(),
     269              :         ComponentRecent.count: 10
     270              :       }
     271              :     ]);
     272              :   }
     273              : 
     274            6 :   @override
     275              :   Widget buildContent(BuildContext context, BoxConstraints constraints) {
     276            6 :     final data = ComponentsBuilder.getData(context);
     277            6 :     bool isWide = ScreenHelper.state().isWide;
     278            6 :     final countHeight = ThemeHelper.getHeightCount(context, constraints);
     279            6 :     final countWidth = ThemeHelper.getWidthCount(constraints, context);
     280            0 :     if (data != null && data.isNotEmpty) {
     281            0 :       return ComponentsBuilder(data);
     282            6 :     } else if (countHeight > 3 && isWide) {
     283            0 :       return buildContentWideScreen();
     284            6 :     } else if (countHeight < 3 && countWidth == 1 && !ThemeHelper.isHorizontal(constraints)) {
     285            0 :       return buildContentSmallScreen();
     286              :     }
     287            6 :     double indent = ThemeHelper.getIndent();
     288            6 :     EdgeInsets margin = EdgeInsets.only(top: indent);
     289            6 :     bool isVertical = countWidth == 1 && !ThemeHelper.isWearable;
     290            6 :     double width = ThemeHelper.getWidth(context, 3, constraints);
     291           24 :     double partWidth = width / countWidth - indent * (countWidth - 1);
     292           18 :     DateTime curr = DateTime.now().getStartingDay(AppStartOfMonth.get());
     293              : 
     294            6 :     final billWidget = BillWidget(
     295              :       margin: margin,
     296           24 :       title: '${AppLocale.labels.billHeadline}, ${curr.fullMonth()}',
     297           12 :       state: state.get(AppDataType.bills),
     298              :       limit: 7,
     299              :       route: AppRoute.billRoute,
     300           12 :       tooltip: AppLocale.labels.billTooltip,
     301              :       width: isVertical ? width : partWidth,
     302              :       hasExpand: isVertical,
     303            6 :       toExpand: toExpand,
     304            0 :       callback: (v) => setState(() => toExpand = v),
     305              :     );
     306            6 :     final accountWidget = AccountWidget(
     307              :       margin: margin,
     308           30 :       title: '${AppLocale.labels.accountHeadline}, ${AppLocale.labels.total}',
     309           12 :       state: state.get(AppDataType.accounts),
     310              :       limit: 7,
     311              :       route: AppRoute.accountRoute,
     312           12 :       tooltip: AppLocale.labels.accountTooltip,
     313              :       width: isVertical ? width : partWidth,
     314              :       hasExpand: isVertical,
     315            6 :       toExpand: toExpand,
     316            0 :       callback: (v) => setState(() => toExpand = v),
     317           18 :     )..exchange = Exchange(store: super.state);
     318            6 :     final budgetWidget = BudgetWidget(
     319              :       margin: margin,
     320           30 :       title: '${AppLocale.labels.budgetHeadline}, ${AppLocale.labels.left}',
     321           12 :       state: state.get(AppDataType.budgets),
     322              :       limit: 7,
     323              :       route: AppRoute.budgetRoute,
     324           12 :       tooltip: AppLocale.labels.budgetTooltip,
     325              :       width: isVertical ? width : partWidth,
     326              :       hasExpand: isVertical,
     327            6 :       toExpand: toExpand,
     328            0 :       callback: (v) => setState(() => toExpand = v),
     329           18 :     )..exchange = Exchange(store: super.state);
     330              : 
     331            6 :     return GridLayer(
     332              :       padding: indent,
     333              :       width: width,
     334              :       crossAxisCount: countWidth,
     335              :       strategy: switch (countWidth) {
     336            6 :         4 => [
     337            0 :             [0, 6],
     338            0 :             [2, 5],
     339            0 :             [3, 4],
     340            0 :             [1, 7]
     341              :           ],
     342            6 :         3 => [
     343            0 :             [2],
     344            0 :             [3],
     345            0 :             [0, 1]
     346              :           ],
     347           12 :         2 => [
     348            6 :             [2, 3],
     349            6 :             [0, 1]
     350              :           ],
     351            0 :         _ => [
     352            0 :             [0, 1, 2, 3]
     353              :           ]
     354              :       },
     355            6 :       children: [
     356            6 :         countHeight > 3
     357              :             ? isWide
     358            0 :                 ? Expanded(
     359            0 :                     child: Padding(
     360            0 :                       padding: EdgeInsets.only(top: indent / 2),
     361              :                       child: const ComponentRecent({'type': ComponentRecentType.goals, 'count': 7}),
     362              :                     ),
     363              :                   )
     364            6 :                 : GoalWidget(
     365              :                     width: isVertical ? width : partWidth,
     366           12 :                     state: super.state.getList(AppDataType.goals),
     367              :                   )
     368              :             : ThemeHelper.emptyBox,
     369              :         billWidget,
     370              :         accountWidget,
     371              :         budgetWidget,
     372            0 :         () => const Expanded(
     373              :               child: Column(
     374              :                 children: [
     375              :                   Expanded(child: ThemeHelper.emptyBox),
     376              :                   AccountFlowChart(),
     377              :                   ThemeHelper.hIndent6x,
     378              :                 ],
     379              :               ),
     380              :             ),
     381            0 :         () => const Expanded(
     382              :               child: Column(
     383              :                 children: [
     384              :                   Expanded(child: ThemeHelper.emptyBox),
     385              :                   BudgetForecastChart(),
     386              :                   ThemeHelper.hIndent6x,
     387              :                 ],
     388              :               ),
     389              :             ),
     390            0 :         () => const Expanded(
     391              :               child: Column(
     392              :                 children: [
     393              :                   Expanded(child: ThemeHelper.emptyBox),
     394              :                   BillYtdChart(),
     395              :                   ThemeHelper.hIndent6x,
     396              :                 ],
     397              :               ),
     398              :             ),
     399            0 :         () => const Expanded(
     400              :               child: ComponentRecent({'type': ComponentRecentType.invoice, 'count': 7}),
     401              :             ),
     402              :       ],
     403              :     );
     404              :   }
     405              : }
        

Generated by: LCOV version 2.0-1