LCOV - code coverage report
Current view: top level - lib/design/wrapper - tab_widget.dart Coverage Total Hit
Test: lcov.info Lines: 49.7 % 145 72
Test Date: 2026-11-14 15:08:11 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/delayed_call.dart';
       5              : import 'package:app_finance/_configs/theme_helper.dart';
       6              : import 'package:app_finance/_ext/build_context_ext.dart';
       7              : import 'package:app_finance/design/wrapper/dots_tab_bar_widget.dart';
       8              : import 'package:app_finance/design/wrapper/text_wrapper.dart';
       9              : import 'package:material_ui/material_ui.dart';
      10              : 
      11              : enum TabType {
      12              :   primary,
      13              :   secondary,
      14              :   dots,
      15              : }
      16              : 
      17              : class TabWidget extends StatefulWidget {
      18              :   final List<Tab>? tabs;
      19              :   final List<Widget> children;
      20              :   final double? maxWidth;
      21              :   final int focus;
      22              :   final Function? callback;
      23              :   final TabType type;
      24              :   final bool isLeft;
      25              :   final bool hasIndent;
      26              : 
      27            1 :   const TabWidget({
      28              :     super.key,
      29              :     required this.children,
      30              :     this.maxWidth,
      31              :     this.callback,
      32              :     this.tabs,
      33              :     this.isLeft = false,
      34              :     this.hasIndent = true,
      35              :     this.focus = 0,
      36              :     this.type = TabType.primary,
      37            2 :   }) : assert(tabs != null || type == TabType.dots);
      38              : 
      39            1 :   @override
      40              :   // ignore: no_logic_in_create_state
      41            1 :   BasicTabWidgetState createState() => switch (type) {
      42            2 :         TabType.dots => DotsWidgetState(),
      43            2 :         TabType.secondary => TabSecondaryWidgetState(),
      44            0 :         _ => TabWidgetState(),
      45              :       };
      46              : }
      47              : 
      48              : abstract class BasicTabWidgetState extends State<TabWidget> with TickerProviderStateMixin {
      49              :   late PageController pageController;
      50              :   late TabController tabController;
      51              :   late int tabCount;
      52              :   late int tabIndex;
      53              :   late int initIndex;
      54              :   late bool hasIcons = widget.tabs?.first.icon != null;
      55              :   final timer = DelayedCall(500, preserveFirst: true);
      56              : 
      57              :   PreferredSizeWidget? getAppBar(BuildContext context);
      58              : 
      59              :   Widget buildContent(BuildContext context);
      60              : 
      61            1 :   @override
      62              :   void initState() {
      63            1 :     super.initState();
      64            1 :     initControllers();
      65              :   }
      66              : 
      67            1 :   void initControllers() {
      68            4 :     tabCount = widget.children.length;
      69            3 :     tabIndex = widget.focus;
      70            3 :     initIndex = widget.focus;
      71            3 :     pageController = PageController(initialPage: tabIndex);
      72            2 :     tabController = TabController(
      73            1 :       length: tabCount,
      74              :       vsync: this,
      75            1 :       initialIndex: tabIndex,
      76              :     );
      77              :   }
      78              : 
      79            1 :   @override
      80              :   void dispose() {
      81            2 :     pageController.dispose();
      82            2 :     tabController.dispose();
      83            1 :     super.dispose();
      84              :   }
      85              : 
      86            1 :   void switchTab(int newIndex) {
      87            5 :     if (newIndex < 0 || newIndex >= tabCount || tabIndex == newIndex) {
      88              :       return;
      89              :     }
      90            2 :     setState(() {
      91            1 :       tabIndex = newIndex;
      92            2 :       tabController.animateTo(newIndex);
      93            2 :       if (widget.isLeft) {
      94            0 :         if (widget.callback != null) {
      95            0 :           widget.callback!(newIndex);
      96              :         }
      97              :       } else {
      98            2 :         if (pageController.hasClients) {
      99            2 :           pageController.animateToPage(
     100              :             newIndex,
     101              :             duration: const Duration(milliseconds: 300),
     102              :             curve: Curves.ease,
     103              :           );
     104              :         }
     105            2 :         if (widget.callback != null) {
     106            3 :           widget.callback!(newIndex);
     107              :         }
     108              :       }
     109              :     });
     110              :   }
     111              : 
     112            1 :   @override
     113              :   Widget build(BuildContext context) {
     114            5 :     if (tabCount != widget.children.length) {
     115            0 :       WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => initControllers()));
     116              :     }
     117            4 :     if (initIndex != widget.focus) {
     118            3 :       WidgetsBinding.instance.addPostFrameCallback((_) {
     119            5 :         setState(() => initIndex = widget.focus);
     120            3 :         switchTab(widget.focus);
     121              :       });
     122              :     }
     123            1 :     return GestureDetector(
     124            0 :       onHorizontalDragEnd: (DragEndDetails details) {
     125            0 :         if (details.primaryVelocity! > 0) {
     126            0 :           switchTab(tabIndex - 1);
     127            0 :         } else if (details.primaryVelocity! < 0) {
     128            0 :           switchTab(tabIndex + 1);
     129              :         }
     130              :       },
     131            1 :       child: Scaffold(
     132            1 :         appBar: getAppBar(context),
     133            1 :         body: buildContent(context),
     134              :       ),
     135              :     );
     136              :   }
     137              : 
     138            0 :   Widget getLeftAppBar(BuildContext context) {
     139            0 :     final color = context.colorScheme.onInverseSurface;
     140            0 :     final selected = context.textTheme.bodySmall?.copyWith(color: color);
     141            0 :     final style = context.textTheme.bodySmall?.copyWith(color: color.withValues(alpha: 0.6));
     142              :     double? maxWidth;
     143            0 :     if (widget.tabs != null) {
     144            0 :       maxWidth = (ThemeHelper.getHeight(context) - widget.tabs!.length * 50) / widget.tabs!.length;
     145              :     }
     146            0 :     return SizedBox(
     147              :       width: ThemeHelper.barHeight,
     148            0 :       child: NavigationRail(
     149            0 :         selectedIndex: tabIndex,
     150            0 :         onDestinationSelected: switchTab,
     151            0 :         backgroundColor: context.colorScheme.primary,
     152              :         indicatorColor: Colors.transparent,
     153            0 :         labelType: hasIcons ? NavigationRailLabelType.all : NavigationRailLabelType.selected,
     154              :         selectedLabelTextStyle: selected,
     155              :         unselectedLabelTextStyle: style,
     156              :         indicatorShape: InputBorder.none,
     157              :         minWidth: ThemeHelper.barHeight,
     158              :         minExtendedWidth: ThemeHelper.barHeight,
     159              :         groupAlignment: BorderSide.strokeAlignCenter,
     160            0 :         destinations: widget.tabs!
     161            0 :             .asMap()
     162            0 :             .entries
     163            0 :             .map(
     164            0 :               hasIcons
     165            0 :                   ? (e) => NavigationRailDestination(
     166            0 :                         icon: RotatedBox(
     167              :                           quarterTurns: 3,
     168            0 :                           child: TextWrapper(
     169            0 :                             e.value.text ?? '',
     170            0 :                             style: tabIndex == e.key ? selected : style,
     171              :                             maxWidth: maxWidth,
     172              :                           ),
     173              :                         ),
     174            0 :                         label: RotatedBox(
     175              :                           quarterTurns: 3,
     176            0 :                           child: e.value.icon != null
     177            0 :                               ? Icon(
     178            0 :                                   (e.value.icon as Icon).icon,
     179            0 :                                   color: tabIndex == e.key ? color : color.withValues(alpha: 0.6),
     180              :                                 )
     181              :                               : null,
     182              :                         ),
     183              :                       )
     184            0 :                   : (e) => NavigationRailDestination(
     185            0 :                         icon: RotatedBox(
     186              :                           quarterTurns: 3,
     187            0 :                           child: Container(
     188              :                             height: 20,
     189            0 :                             decoration: ShapeDecoration(
     190            0 :                               shape: UnderlineInputBorder(
     191            0 :                                 borderSide: BorderSide(
     192            0 :                                   color: color.withValues(alpha: tabIndex == e.key ? 1 : 0.2),
     193              :                                   width: 2,
     194              :                                 ),
     195              :                               ),
     196              :                             ),
     197            0 :                             child: TextWrapper(e.value.text ?? '', style: style, maxWidth: maxWidth),
     198              :                           ),
     199              :                         ),
     200              :                         label: ThemeHelper.emptyBox,
     201              :                       ),
     202              :             )
     203            0 :             .toList(),
     204              :       ),
     205              :     );
     206              :   }
     207              : 
     208            0 :   Widget buildLeft(BuildContext context) {
     209            0 :     final indent = ThemeHelper.getIndent();
     210            0 :     return Row(
     211            0 :       children: [
     212            0 :         getLeftAppBar(context),
     213            0 :         Expanded(
     214            0 :           child: Padding(
     215            0 :             padding: widget.hasIndent ? EdgeInsets.fromLTRB(indent, 0, indent, 0) : EdgeInsets.zero,
     216            0 :             child: TabBarView(
     217            0 :               controller: tabController,
     218            0 :               children: widget.children,
     219              :             ),
     220              :           ),
     221              :         ),
     222              :       ],
     223              :     );
     224              :   }
     225              : }
     226              : 
     227              : class TabWidgetState extends BasicTabWidgetState {
     228            0 :   @override
     229              :   PreferredSizeWidget? getAppBar(BuildContext context) {
     230            0 :     return TabBar(
     231            0 :       controller: tabController,
     232            0 :       onTap: switchTab,
     233            0 :       tabs: widget.tabs!,
     234              :     );
     235              :   }
     236              : 
     237            0 :   @override
     238              :   Widget buildContent(BuildContext context) {
     239            0 :     return PageView(
     240            0 :       controller: pageController,
     241            0 :       onPageChanged: switchTab,
     242            0 :       children: widget.children,
     243              :     );
     244              :   }
     245              : 
     246            0 :   @override
     247              :   Widget build(BuildContext context) {
     248            0 :     if (widget.isLeft) {
     249            0 :       return buildLeft(context);
     250              :     }
     251            0 :     return super.build(context);
     252              :   }
     253              : }
     254              : 
     255              : class DotsWidgetState extends BasicTabWidgetState {
     256            1 :   @override
     257              :   PreferredSizeWidget? getAppBar(BuildContext context) {
     258            1 :     return DotsTabBarWidget(
     259            1 :       tabController: tabController,
     260            1 :       pageController: pageController,
     261            1 :       onTap: switchTab,
     262            2 :       tabList: widget.children,
     263            1 :       indent: ThemeHelper.getIndent(),
     264            3 :       width: widget.maxWidth ?? ThemeHelper.getWidth(context, 2),
     265            2 :       color: context.colorScheme.primary,
     266              :     );
     267              :   }
     268              : 
     269            1 :   @override
     270              :   Widget buildContent(BuildContext context) {
     271            1 :     return Transform.translate(
     272              :       offset: const Offset(0, -20),
     273            1 :       child: PageView(
     274            1 :         controller: pageController,
     275            1 :         onPageChanged: switchTab,
     276            2 :         children: widget.children,
     277              :       ),
     278              :     );
     279              :   }
     280              : }
     281              : 
     282              : class TabSecondaryWidgetState extends BasicTabWidgetState {
     283            1 :   @override
     284              :   PreferredSizeWidget? getAppBar(BuildContext context) {
     285            1 :     return TabBar.secondary(
     286            1 :       controller: tabController,
     287            0 :       onTap: (idx) => widget.callback != null ? widget.callback!(idx) : null,
     288            2 :       tabs: widget.tabs!,
     289              :     );
     290              :   }
     291              : 
     292            0 :   @override
     293              :   Widget buildContent(BuildContext context) => ThemeHelper.emptyBox;
     294              : 
     295            1 :   @override
     296              :   Widget build(BuildContext context) {
     297            1 :     final indent = ThemeHelper.getIndent();
     298            2 :     if (widget.isLeft) {
     299            0 :       return buildLeft(context);
     300              :     }
     301            1 :     return Column(
     302            1 :       children: [
     303            1 :         getAppBar(context)!,
     304            1 :         Expanded(
     305            1 :           child: Padding(
     306            2 :             padding: widget.hasIndent ? EdgeInsets.fromLTRB(indent, 0, indent, 0) : EdgeInsets.zero,
     307            1 :             child: TabBarView(
     308            1 :               controller: tabController,
     309            2 :               children: widget.children,
     310              :             ),
     311              :           ),
     312              :         ),
     313              :       ],
     314              :     );
     315              :   }
     316              : }
        

Generated by: LCOV version 2.0-1