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:flutter/material.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 2 : tabController.animateTo(newIndex);
92 2 : if (pageController.hasClients) {
93 4 : tabIndex += newIndex > tabIndex ? 1 : -1;
94 2 : pageController.animateToPage(
95 1 : tabIndex,
96 : duration: const Duration(milliseconds: 300),
97 : curve: Curves.ease,
98 : );
99 4 : if (tabIndex == newIndex && widget.callback != null) {
100 3 : widget.callback!(newIndex);
101 : } else {
102 0 : timer.run(() {
103 0 : switchTab(newIndex);
104 : });
105 : }
106 0 : } else if (widget.callback != null) {
107 0 : tabIndex = newIndex;
108 0 : widget.callback!(newIndex);
109 : }
110 : });
111 : }
112 :
113 1 : @override
114 : Widget build(BuildContext context) {
115 5 : if (tabCount != widget.children.length) {
116 0 : WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => initControllers()));
117 : }
118 4 : if (initIndex != widget.focus) {
119 3 : WidgetsBinding.instance.addPostFrameCallback((_) {
120 5 : setState(() => initIndex = widget.focus);
121 3 : switchTab(widget.focus);
122 : });
123 : }
124 1 : return GestureDetector(
125 0 : onHorizontalDragEnd: (DragEndDetails details) {
126 0 : if (details.primaryVelocity! > 0) {
127 0 : switchTab(tabIndex - 1);
128 0 : } else if (details.primaryVelocity! < 0) {
129 0 : switchTab(tabIndex + 1);
130 : }
131 : },
132 1 : child: Scaffold(
133 1 : appBar: getAppBar(context),
134 1 : body: buildContent(context),
135 : ),
136 : );
137 : }
138 :
139 0 : Widget getLeftAppBar(BuildContext context) {
140 0 : final color = context.colorScheme.onInverseSurface;
141 0 : final selected = context.textTheme.bodySmall?.copyWith(color: color);
142 0 : final style = context.textTheme.bodySmall?.copyWith(color: color.withOpacity(0.6));
143 0 : return SizedBox(
144 : width: ThemeHelper.barHeight,
145 0 : child: NavigationRail(
146 0 : selectedIndex: tabIndex,
147 0 : onDestinationSelected: switchTab,
148 0 : backgroundColor: context.colorScheme.primary,
149 : indicatorColor: Colors.transparent,
150 0 : labelType: hasIcons ? NavigationRailLabelType.all : NavigationRailLabelType.selected,
151 : selectedLabelTextStyle: selected,
152 : unselectedLabelTextStyle: style,
153 : indicatorShape: InputBorder.none,
154 : minWidth: ThemeHelper.barHeight,
155 : minExtendedWidth: ThemeHelper.barHeight,
156 : groupAlignment: BorderSide.strokeAlignCenter,
157 0 : destinations: widget.tabs!
158 0 : .asMap()
159 0 : .entries
160 0 : .map(
161 0 : hasIcons
162 0 : ? (e) => NavigationRailDestination(
163 0 : icon: RotatedBox(
164 : quarterTurns: 3,
165 0 : child: TextWrapper(
166 0 : e.value.text ?? '',
167 0 : style: tabIndex == e.key ? selected : style,
168 : ),
169 : ),
170 0 : label: RotatedBox(
171 : quarterTurns: 3,
172 0 : child: e.value.icon != null
173 0 : ? Icon(
174 0 : (e.value.icon as Icon).icon,
175 0 : color: tabIndex == e.key ? color : color.withOpacity(0.6),
176 : )
177 : : null,
178 : ),
179 : )
180 0 : : (e) => NavigationRailDestination(
181 0 : icon: RotatedBox(
182 : quarterTurns: 3,
183 0 : child: Container(
184 : height: 20,
185 0 : decoration: ShapeDecoration(
186 0 : shape: UnderlineInputBorder(
187 0 : borderSide: BorderSide(
188 0 : color: color.withOpacity(tabIndex == e.key ? 1 : 0.2),
189 : width: 2,
190 : ),
191 : ),
192 : ),
193 0 : child: TextWrapper(e.value.text ?? '', style: style),
194 : ),
195 : ),
196 : label: ThemeHelper.emptyBox,
197 : ),
198 : )
199 0 : .toList(),
200 : ),
201 : );
202 : }
203 :
204 0 : Widget buildLeft(BuildContext context) {
205 0 : final indent = ThemeHelper.getIndent();
206 0 : return Row(
207 0 : children: [
208 0 : getLeftAppBar(context),
209 0 : Expanded(
210 0 : child: GestureDetector(
211 0 : onHorizontalDragEnd: (DragEndDetails details) {
212 0 : if (details.primaryVelocity! > 0) {
213 0 : switchTab(tabIndex - 1);
214 0 : } else if (details.primaryVelocity! < 0) {
215 0 : switchTab(tabIndex + 1);
216 : }
217 : },
218 0 : child: Padding(
219 0 : padding: widget.hasIndent ? EdgeInsets.fromLTRB(indent, 0, indent, 0) : EdgeInsets.zero,
220 0 : child: TabBarView(
221 0 : controller: tabController,
222 0 : children: widget.children,
223 : ),
224 : ),
225 : ),
226 : ),
227 : ],
228 : );
229 : }
230 : }
231 :
232 : class TabWidgetState extends BasicTabWidgetState {
233 0 : @override
234 : PreferredSizeWidget? getAppBar(BuildContext context) {
235 0 : return TabBar(
236 0 : controller: tabController,
237 0 : onTap: switchTab,
238 0 : tabs: widget.tabs!,
239 : );
240 : }
241 :
242 0 : @override
243 : Widget buildContent(BuildContext context) {
244 0 : return PageView(
245 0 : controller: pageController,
246 0 : onPageChanged: switchTab,
247 0 : children: widget.children,
248 : );
249 : }
250 :
251 0 : @override
252 : Widget build(BuildContext context) {
253 0 : if (widget.isLeft) {
254 0 : return buildLeft(context);
255 : }
256 0 : return super.build(context);
257 : }
258 : }
259 :
260 : class DotsWidgetState extends BasicTabWidgetState {
261 1 : @override
262 : PreferredSizeWidget? getAppBar(BuildContext context) {
263 1 : return DotsTabBarWidget(
264 1 : tabController: tabController,
265 1 : pageController: pageController,
266 1 : onTap: switchTab,
267 2 : tabList: widget.children,
268 1 : indent: ThemeHelper.getIndent(),
269 3 : width: widget.maxWidth ?? ThemeHelper.getWidth(context, 2),
270 2 : color: context.colorScheme.primary,
271 : );
272 : }
273 :
274 1 : @override
275 : Widget buildContent(BuildContext context) {
276 1 : return Transform.translate(
277 : offset: const Offset(0, -20),
278 1 : child: PageView(
279 1 : controller: pageController,
280 1 : onPageChanged: switchTab,
281 2 : children: widget.children,
282 : ),
283 : );
284 : }
285 : }
286 :
287 : class TabSecondaryWidgetState extends BasicTabWidgetState {
288 1 : @override
289 : PreferredSizeWidget? getAppBar(BuildContext context) {
290 1 : return TabBar.secondary(
291 1 : controller: tabController,
292 : // onTap: switchTab,
293 2 : tabs: widget.tabs!,
294 : );
295 : }
296 :
297 0 : @override
298 : Widget buildContent(BuildContext context) => ThemeHelper.emptyBox;
299 :
300 1 : @override
301 : Widget build(BuildContext context) {
302 1 : final indent = ThemeHelper.getIndent();
303 2 : if (widget.isLeft) {
304 0 : return buildLeft(context);
305 : }
306 1 : return Column(
307 1 : children: [
308 1 : getAppBar(context)!,
309 1 : Expanded(
310 1 : child: Padding(
311 2 : padding: widget.hasIndent ? EdgeInsets.fromLTRB(indent, 0, indent, 0) : EdgeInsets.zero,
312 1 : child: TabBarView(
313 1 : controller: tabController,
314 2 : children: widget.children,
315 : ),
316 : ),
317 : ),
318 : ],
319 : );
320 : }
321 : }
|