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 2 : 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 2 : @override
40 : // ignore: no_logic_in_create_state
41 2 : BasicTabWidgetState createState() => switch (type) {
42 2 : TabType.dots => DotsWidgetState(),
43 4 : 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 2 : @override
62 : void initState() {
63 2 : super.initState();
64 2 : initControllers();
65 : }
66 :
67 2 : void initControllers() {
68 8 : tabCount = widget.children.length;
69 6 : tabIndex = widget.focus;
70 6 : initIndex = widget.focus;
71 6 : pageController = PageController(initialPage: tabIndex);
72 4 : tabController = TabController(
73 2 : length: tabCount,
74 : vsync: this,
75 2 : initialIndex: tabIndex,
76 : );
77 : }
78 :
79 2 : @override
80 : void dispose() {
81 4 : pageController.dispose();
82 4 : tabController.dispose();
83 2 : super.dispose();
84 : }
85 :
86 0 : void switchTab(int newIndex) {
87 0 : if (newIndex < 0 || newIndex >= tabCount || tabIndex == newIndex) {
88 : return;
89 : }
90 0 : setState(() {
91 0 : tabIndex = newIndex;
92 0 : tabController.animateTo(newIndex);
93 0 : if (widget.isLeft) {
94 0 : if (widget.callback != null) {
95 0 : widget.callback!(newIndex);
96 : }
97 : } else {
98 0 : if (pageController.hasClients) {
99 0 : pageController.animateToPage(
100 : newIndex,
101 : duration: const Duration(milliseconds: 300),
102 : curve: Curves.ease,
103 : );
104 : }
105 0 : if (widget.callback != null) {
106 0 : widget.callback!(newIndex);
107 : }
108 : }
109 : });
110 : }
111 :
112 0 : @override
113 : Widget build(BuildContext context) {
114 0 : if (tabCount != widget.children.length) {
115 0 : WidgetsBinding.instance.addPostFrameCallback((_) => setState(() => initControllers()));
116 : }
117 0 : if (initIndex != widget.focus) {
118 0 : WidgetsBinding.instance.addPostFrameCallback((_) {
119 0 : setState(() => initIndex = widget.focus);
120 0 : switchTab(widget.focus);
121 : });
122 : }
123 0 : 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 0 : child: Scaffold(
132 0 : appBar: getAppBar(context),
133 0 : 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 0 : @override
257 : PreferredSizeWidget? getAppBar(BuildContext context) {
258 0 : return DotsTabBarWidget(
259 0 : tabController: tabController,
260 0 : pageController: pageController,
261 0 : onTap: switchTab,
262 0 : tabList: widget.children,
263 0 : indent: ThemeHelper.getIndent(),
264 0 : width: widget.maxWidth ?? ThemeHelper.getWidth(context, 2),
265 0 : color: context.colorScheme.primary,
266 : );
267 : }
268 :
269 0 : @override
270 : Widget buildContent(BuildContext context) {
271 0 : return Transform.translate(
272 : offset: const Offset(0, -20),
273 0 : child: PageView(
274 0 : controller: pageController,
275 0 : onPageChanged: switchTab,
276 0 : children: widget.children,
277 : ),
278 : );
279 : }
280 : }
281 :
282 : class TabSecondaryWidgetState extends BasicTabWidgetState {
283 2 : @override
284 : PreferredSizeWidget? getAppBar(BuildContext context) {
285 2 : return TabBar.secondary(
286 2 : controller: tabController,
287 0 : onTap: (idx) => widget.callback != null ? widget.callback!(idx) : null,
288 4 : tabs: widget.tabs!,
289 : );
290 : }
291 :
292 0 : @override
293 : Widget buildContent(BuildContext context) => ThemeHelper.emptyBox;
294 :
295 2 : @override
296 : Widget build(BuildContext context) {
297 2 : final indent = ThemeHelper.getIndent();
298 4 : if (widget.isLeft) {
299 0 : return buildLeft(context);
300 : }
301 2 : return Column(
302 2 : children: [
303 2 : getAppBar(context)!,
304 2 : Expanded(
305 2 : child: Padding(
306 4 : padding: widget.hasIndent ? EdgeInsets.fromLTRB(indent, 0, indent, 0) : EdgeInsets.zero,
307 2 : child: TabBarView(
308 2 : controller: tabController,
309 4 : children: widget.children,
310 : ),
311 : ),
312 : ),
313 : ],
314 : );
315 : }
316 : }
|