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

Generated by: LCOV version 1.14