LCOV - code coverage report
Current view: top level - lib/pages/_interfaces - abstract_page_state.dart Coverage Total Hit
Test: lcov.info Lines: 59.5 % 168 100
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/herald/app_design.dart';
       5              : import 'package:app_finance/_classes/herald/app_locale.dart';
       6              : import 'package:app_finance/_classes/herald/app_zoom.dart';
       7              : import 'package:app_finance/_classes/structure/navigation/app_menu.dart';
       8              : import 'package:app_finance/_classes/storage/app_data.dart';
       9              : import 'package:app_finance/_classes/structure/navigation/app_route.dart';
      10              : import 'package:app_finance/_configs/screen_helper.dart';
      11              : import 'package:app_finance/_configs/theme_helper.dart';
      12              : import 'package:app_finance/_ext/build_context_ext.dart';
      13              : import 'package:app_finance/design/wrapper/markdown_builder_wrapper.dart';
      14              : import 'package:app_finance/pages/_interfaces/widgets/menu_widget.dart';
      15              : import 'package:app_finance/design/wrapper/input_controller_wrapper.dart';
      16              : import 'package:app_finance/design/wrapper/row_widget.dart';
      17              : import 'package:app_finance/design/wrapper/text_wrapper.dart';
      18              : import 'package:app_finance/design/button/toolbar_button_widget.dart';
      19              : import 'package:flutter/foundation.dart';
      20              : import 'package:material_ui/material_ui.dart';
      21              : import 'package:flutter_grid_layout/flutter_grid_layout.dart';
      22              : import 'package:provider/provider.dart';
      23              : 
      24              : abstract class AbstractPageState<T extends StatefulWidget> extends State<T> {
      25              :   late AppData state;
      26              : 
      27              :   int selectedMenu = 0;
      28              : 
      29              :   String getTitle();
      30              : 
      31              :   String getButtonName();
      32              : 
      33            4 :   String? getHelperName() => null;
      34              : 
      35              :   Widget buildButton(BuildContext context, BoxConstraints constraints);
      36              : 
      37              :   Widget buildContent(BuildContext context, BoxConstraints constraints);
      38              : 
      39            4 :   Widget? getBarLeading(NavigatorState nav) {
      40            4 :     return ToolbarButtonWidget(
      41            4 :       isWide: ScreenHelper.state().isWide,
      42            8 :       tooltip: AppLocale.labels.backTooltip,
      43            0 :       onPressed: () => nav.canPop() ? nav.pop() : nav.pushNamed(AppRoute.homeRoute),
      44              :       icon: Icons.arrow_back,
      45              :     );
      46              :   }
      47              : 
      48            0 :   Widget buildHelper(BuildContext context,
      49              :       {String? type, Widget Function(BuildContext, AsyncSnapshot<String>)? builder}) {
      50            0 :     final locale = AppLocale.labels.localeName;
      51            0 :     type ??= getHelperName();
      52            0 :     return Container(
      53              :       width: double.infinity,
      54            0 :       color: context.colorScheme.surface,
      55            0 :       child: SafeArea(
      56              :         top: false,
      57            0 :         child: Column(
      58            0 :           children: [
      59            0 :             Transform.translate(
      60              :               offset: const Offset(-10, -24),
      61            0 :               child: Align(
      62              :                 alignment: Alignment.topRight,
      63            0 :                 child: FloatingActionButton(
      64              :                   heroTag: 'helper',
      65              :                   mini: true,
      66            0 :                   tooltip: AppLocale.labels.closeTooltip,
      67            0 :                   onPressed: () => Navigator.pop(context),
      68            0 :                   child: Icon(
      69              :                     Icons.close,
      70              :                     color: Colors.white70,
      71            0 :                     semanticLabel: AppLocale.labels.closeTooltip,
      72              :                   ),
      73              :                 ),
      74              :               ),
      75              :             ),
      76            0 :             Expanded(
      77            0 :               child: MarkdownBuilderWrapper(
      78            0 :                 url: './assets/l10n/${type}_$locale.md',
      79              :                 builder: builder,
      80              :               ),
      81              :             ),
      82              :           ],
      83              :         ),
      84              :       ),
      85              :     );
      86              :   }
      87              : 
      88            4 :   List<Widget> getBarActions(NavigatorState nav) {
      89            4 :     final isWide = ScreenHelper.state().isWide;
      90            4 :     return [
      91            4 :       if (getHelperName() != null)
      92            0 :         ToolbarButtonWidget(
      93              :           isWide: isWide,
      94            0 :           tooltip: AppLocale.labels.helpTooltip,
      95            0 :           onPressed: () => showModalBottomSheet(
      96            0 :             context: context,
      97            0 :             backgroundColor: context.colorScheme.surface,
      98            0 :             builder: buildHelper,
      99              :           ),
     100              :           icon: Icons.contact_support_outlined,
     101            0 :           color: isWide ? context.colorScheme.onSurface : Colors.white70,
     102            0 :           borderColor: isWide ? context.colorScheme.onSurface : Colors.white70,
     103            0 :           semanticLabel: AppLocale.labels.helpTooltip,
     104              :         ),
     105              :       if (!isWide)
     106            4 :         Builder(
     107            4 :           builder: (context) {
     108            4 :             final scaffold = Scaffold.of(context);
     109            4 :             return ToolbarButtonWidget(
     110              :               icon: Icons.menu,
     111              :               color: Colors.white70,
     112            8 :               tooltip: AppLocale.labels.navigationTooltip,
     113            0 :               onPressed: () => AppDesign.isRightToLeft() ? scaffold.openEndDrawer() : scaffold.openDrawer(),
     114              :             );
     115              :           },
     116              :         )
     117              :     ];
     118              :   }
     119              : 
     120            4 :   Widget getBarTitle(BuildContext context, [bool isBottom = false]) {
     121            4 :     final isWide = ScreenHelper.state().isWide;
     122            4 :     return TextWrapper(
     123            4 :       getTitle(),
     124            4 :       style: TextStyle(color: isWide ? context.colorScheme.onSurface : Colors.white70),
     125              :     );
     126              :   }
     127              : 
     128            0 :   AppBar? buildBar(BuildContext context, BoxConstraints constraints) {
     129            0 :     final nav = Navigator.of(context);
     130            0 :     final isWide = ScreenHelper.state().isWide;
     131            0 :     return AppBar(
     132            0 :       title: Center(child: getBarTitle(context)),
     133              :       toolbarHeight: ThemeHelper.barHeight,
     134              :       shape: isWide
     135            0 :           ? UnderlineInputBorder(
     136            0 :               borderSide: BorderSide(color: context.colorScheme.primary),
     137              :               borderRadius: BorderRadius.zero,
     138              :             )
     139              :           : null,
     140            0 :       backgroundColor: isWide ? context.colorScheme.inverseSurface.withValues(alpha: 0.4) : context.colorScheme.primary,
     141            0 :       leading: getBarLeading(nav),
     142              :       leadingWidth: isWide ? ThemeHelper.menuWidth : null,
     143            0 :       actions: getBarActions(nav),
     144              :     );
     145              :   }
     146              : 
     147            0 :   Widget? buildRightBar(BuildContext context, BoxConstraints constraints) {
     148            0 :     final nav = Navigator.of(context);
     149            0 :     return Align(
     150              :       alignment: Alignment.topRight,
     151            0 :       child: Container(
     152            0 :         color: context.colorScheme.primary,
     153              :         width: ThemeHelper.barHeight,
     154            0 :         child: Column(
     155            0 :           children: [
     156            0 :             getBarLeading(nav) ?? ThemeHelper.emptyBox,
     157            0 :             ...getBarActions(nav),
     158            0 :             ThemeHelper.hIndent,
     159            0 :             RotatedBox(
     160              :               quarterTurns: 3,
     161            0 :               child: SizedBox(width: constraints.maxHeight / 2.5, child: getBarTitle(context)),
     162              :             ),
     163              :           ],
     164              :         ),
     165              :       ),
     166              :     );
     167              :   }
     168              : 
     169            6 :   Widget buildBottomBar(BuildContext context, BoxConstraints constraints) {
     170            6 :     final theme = Theme.of(context);
     171            6 :     final nav = Navigator.of(context);
     172            6 :     final actions = getBarActions(nav);
     173           12 :     final btnWidth = 50.0 * actions.length;
     174           18 :     final titleWidth = ThemeHelper.getWidth(context, 0, null, false) / 2 - 80;
     175           12 :     final hasTooltip = getButtonName().isNotEmpty;
     176           30 :     final showTooltip = hasTooltip && (constraints.maxWidth - titleWidth - btnWidth - 50 > 125);
     177            6 :     return Container(
     178              :       padding: EdgeInsets.zero,
     179              :       clipBehavior: Clip.none,
     180              :       height: ThemeHelper.barHeight,
     181           12 :       color: theme.colorScheme.primary,
     182            6 :       child: RowWidget(
     183            6 :         maxWidth: constraints.maxWidth,
     184              :         indent: 0,
     185            6 :         chunk: [50, hasTooltip ? titleWidth : null, hasTooltip ? 64 : 0, hasTooltip ? null : 0, btnWidth],
     186            6 :         children: [
     187           12 :           [getBarLeading(nav) ?? ThemeHelper.emptyBox],
     188            6 :           [
     189            6 :             Center(
     190              :               heightFactor: 1.9,
     191            6 :               child: getBarTitle(context, true),
     192              :             ),
     193              :           ],
     194              :           const [ThemeHelper.emptyBox],
     195            6 :           [
     196              :             if (showTooltip)
     197            6 :               Padding(
     198           12 :                 padding: EdgeInsets.only(top: ThemeHelper.getIndent(0.5)),
     199            6 :                 child: TextWrapper(
     200            6 :                   getButtonName(),
     201              :                   maxLines: 2,
     202           12 :                   style: theme.textTheme.bodySmall
     203           24 :                       ?.copyWith(color: theme.colorScheme.onInverseSurface.withValues(alpha: 0.6)),
     204              :                 ),
     205              :               ),
     206              :           ],
     207            6 :           [
     208            6 :             RowWidget(
     209           12 :               chunk: List.filled(actions.length, null),
     210              :               maxWidth: btnWidth,
     211              :               indent: 0,
     212           24 :               children: actions.map((e) => [e]).toList(),
     213              :             ),
     214              :           ],
     215              :         ],
     216              :       ),
     217              :     );
     218              :   }
     219              : 
     220            6 :   Widget? buildNavigation() {
     221            6 :     return ListView.separated(
     222              :       scrollDirection: Axis.vertical,
     223              :       addSemanticIndexes: true,
     224           12 :       padding: EdgeInsets.symmetric(vertical: ThemeHelper.getIndent(4)),
     225            1 :       separatorBuilder: (context, index) => ThemeHelper.hIndent2x,
     226           12 :       itemCount: AppMenu.get().length,
     227            2 :       itemBuilder: (context, index) => MenuWidget(
     228              :         index: index,
     229            4 :         setState: () => setState(() => selectedMenu = index),
     230            1 :         selectedIndex: selectedMenu,
     231              :       ),
     232              :     );
     233              :   }
     234              : 
     235            6 :   Drawer? buildDrawer() {
     236            6 :     return Drawer(
     237              :       elevation: 0,
     238            6 :       shape: Border.all(width: 0),
     239            6 :       child: ScreenHelper.state().isWide
     240            0 :           ? buildNavigation()
     241            6 :           : InputControllerWrapper(
     242            6 :               child: buildNavigation() ?? ThemeHelper.emptyBox,
     243              :             ),
     244              :     );
     245              :   }
     246              : 
     247            6 :   @override
     248              :   Widget build(BuildContext context) {
     249           12 :     final scale = context.watch<AppZoom>().value;
     250            6 :     return Scaffold(
     251           18 :       appBar: AppBar(backgroundColor: context.colorScheme.primary, toolbarHeight: 0),
     252            6 :       body: Container(
     253           12 :         color: context.colorScheme.primary,
     254           18 :         child: SafeArea(child: Consumer<AppData>(builder: (context, appState, _) {
     255            6 :           state = appState;
     256           12 :           return LayoutBuilder(builder: (context, constraints) {
     257            6 :             final display = ScreenHelper.getInstance(context, constraints);
     258              :             final isBottom = display.isBottom && !display.isRight;
     259            6 :             final hasKeyboard = ThemeHelper.isKeyboardVisible(context, constraints);
     260            6 :             final height = constraints.maxHeight;
     261              :             final blockHeight =
     262           24 :                 height / scale - (display.isRight ? 0 : ThemeHelper.barHeight + ThemeHelper.getIndent());
     263           12 :             double width = constraints.maxWidth / scale;
     264              :             Widget? rightBar;
     265              :             Widget? leftBar;
     266              :             if (display.isRight) {
     267            0 :               rightBar = buildRightBar(context, constraints);
     268              :               if (rightBar != null) {
     269            0 :                 width -= ThemeHelper.barHeight;
     270              :               }
     271              :             } else if (display.isWide) {
     272            0 :               leftBar = buildNavigation();
     273              :               if (leftBar != null) {
     274            0 :                 width -= ThemeHelper.menuWidth;
     275              :               }
     276              :             }
     277            6 :             if (width < 0) {
     278              :               width = 0;
     279              :             }
     280           30 :             final dx = (constraints.maxWidth - constraints.maxWidth / scale) / 2;
     281           18 :             final dy = (height - height / scale) / 2;
     282            6 :             return Scaffold(
     283            0 :               appBar: display.isBottom ? null : buildBar(context, constraints),
     284            6 :               drawer: buildDrawer(),
     285            6 :               endDrawer: buildDrawer(),
     286              :               floatingActionButtonLocation: isBottom ? FloatingActionButtonLocation.centerDocked : null,
     287              :               floatingActionButton: isBottom
     288              :                   ? hasKeyboard
     289            0 :                       ? Transform.translate(
     290              :                           offset: const Offset(0, 12),
     291            0 :                           child: SizedBox(
     292            0 :                             height: ThemeHelper.barHeight * 1.2,
     293            0 :                             child: buildButton(context, constraints),
     294              :                           ),
     295              :                         )
     296           12 :                       : defaultTargetPlatform == TargetPlatform.iOS
     297            0 :                           ? buildButton(context, constraints)
     298            6 :                           : Container(
     299           12 :                               margin: EdgeInsets.only(bottom: ThemeHelper.getIndent()),
     300            6 :                               child: buildButton(context, constraints),
     301              :                             )
     302            0 :                   : buildButton(context, constraints),
     303              :               resizeToAvoidBottomInset: true,
     304            6 :               body: InputControllerWrapper(
     305            6 :                 child: GridContainer(
     306            6 :                   alignment: AppDesign.getAlignment<MainAxisAlignment>(),
     307              :                   rows: const [ThemeHelper.menuWidth, null, ThemeHelper.barHeight],
     308              :                   columns: const [null, ThemeHelper.barHeight],
     309            6 :                   children: [
     310              :                     if (leftBar != null)
     311            0 :                       GridItem(
     312              :                         order: 2,
     313              :                         start: const Size(0, 0),
     314              :                         end: const Size(1, 2),
     315            0 :                         child: Container(
     316            0 :                           color: context.colorScheme.inversePrimary.withValues(alpha: 0.2),
     317              :                           width: ThemeHelper.menuWidth,
     318              :                           height: double.infinity,
     319            0 :                           child: buildNavigation(),
     320              :                         ),
     321              :                       ),
     322            6 :                     GridItem(
     323              :                       order: 1,
     324            6 :                       start: Size(leftBar != null ? 1 : 0, 0),
     325            6 :                       end: Size(rightBar != null ? 2 : 3, rightBar == null && display.isBottom ? 1 : 2),
     326            6 :                       child: OverflowBox(
     327              :                         alignment: Alignment.topLeft,
     328              :                         minWidth: width,
     329              :                         maxWidth: width,
     330              :                         minHeight: blockHeight,
     331              :                         maxHeight: blockHeight,
     332            6 :                         child: Transform.translate(
     333            6 :                           offset: Offset(dx, dy),
     334            6 :                           child: Transform.scale(
     335              :                             scale: scale,
     336            6 :                             child: buildContent(context, constraints),
     337              :                           ),
     338              :                         ),
     339              :                       ),
     340              :                     ),
     341              :                     if (rightBar != null)
     342            0 :                       GridItem(
     343              :                         order: 2,
     344              :                         start: const Size(2, 0),
     345              :                         end: const Size(3, 2),
     346              :                         child: rightBar,
     347              :                       ),
     348              :                     if (rightBar == null && display.isBottom)
     349            6 :                       GridItem(
     350              :                         order: 2,
     351              :                         start: const Size(0, 1),
     352              :                         end: const Size(3, 2),
     353            6 :                         child: buildBottomBar(context, constraints),
     354              :                       ),
     355              :                   ],
     356              :                 ),
     357              :               ),
     358              :             );
     359              :           });
     360              :         })),
     361              :       ),
     362              :     );
     363              :   }
     364              : }
        

Generated by: LCOV version 2.0-1