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 : }
|