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/payments_controller.dart';
5 : import 'package:app_finance/_classes/herald/app_design.dart';
6 : import 'package:app_finance/_classes/herald/app_start_of_month.dart';
7 : import 'package:app_finance/_classes/storage/app_data.dart';
8 : import 'package:app_finance/_classes/herald/app_locale.dart';
9 : import 'package:app_finance/_classes/structure/currency/exchange.dart';
10 : import 'package:app_finance/_classes/storage/app_preferences.dart';
11 : import 'package:app_finance/_configs/screen_helper.dart';
12 : import 'package:app_finance/_configs/theme_helper.dart';
13 : import 'package:app_finance/_classes/structure/navigation/app_route.dart';
14 : import 'package:app_finance/_ext/date_time_ext.dart';
15 : import 'package:app_finance/components/_core/component_data.dart';
16 : import 'package:app_finance/components/_core/components_builder.dart';
17 : import 'package:app_finance/components/_core/list_component_registry.dart';
18 : import 'package:app_finance/components/component_chart.dart';
19 : import 'package:app_finance/components/widgets/account_flow_chart.dart';
20 : import 'package:app_finance/components/widgets/budget_forecast_chart.dart';
21 : import 'package:app_finance/components/component_recent.dart';
22 : import 'package:app_finance/components/widgets/bill_ytd_chart.dart';
23 : import 'package:app_finance/pages/_interfaces/abstract_page_state.dart';
24 : import 'package:app_finance/pages/home/home_edit_page.dart';
25 : import 'package:app_finance/pages/home/widgets/security_bio_tab.dart';
26 : import 'package:app_finance/pages/home/widgets/security_tab.dart';
27 : import 'package:app_finance/pages/start/start_page.dart';
28 : import 'package:app_finance/design/wrapper/grid_layer.dart';
29 : import 'package:app_finance/pages/home/widgets/init_tab.dart';
30 : import 'package:app_finance/design/button/toolbar_button_widget.dart';
31 : import 'package:app_finance/components/widgets/account_widget.dart';
32 : import 'package:app_finance/components/widgets/bill_widget.dart';
33 : import 'package:app_finance/components/widgets/budget_widget.dart';
34 : import 'package:app_finance/pages/home/widgets/goal_widget.dart';
35 : import 'package:flutter/foundation.dart';
36 : import 'package:material_ui/material_ui.dart';
37 : import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
38 : import 'package:flutter_svg/flutter_svg.dart';
39 : import 'package:package_info_plus/package_info_plus.dart';
40 : import 'package:provider/provider.dart';
41 : import 'package:simple_totp_auth/simple_totp_auth.dart';
42 :
43 : class HomePage extends StatefulWidget {
44 1 : const HomePage({super.key});
45 :
46 1 : @override
47 1 : HomePageState createState() => HomePageState();
48 : }
49 :
50 : class HomePageState extends AbstractPageState<HomePage> {
51 : String? toExpand;
52 : String secret = '';
53 : bool isEditMode = false;
54 : bool isOtpPassed = false;
55 : late String version;
56 :
57 1 : @override
58 : initState() {
59 1 : super.initState();
60 2 : toExpand = AppPreferences.get(AppPreferences.prefExpand);
61 2 : version = AppPreferences.get(AppPreferences.prefVersion) ?? '';
62 2 : secret = AppPreferences.get(AppPreferences.prefPrivacyKey) ?? '';
63 2 : if (secret.isEmpty) {
64 2 : secret = TOTP.generateSecret();
65 2 : AppPreferences.set(AppPreferences.prefPrivacyKey, secret);
66 : }
67 2 : PackageInfo.fromPlatform().then((PackageInfo value) {
68 0 : if (version != value.version) {
69 0 : WidgetsBinding.instance.addPostFrameCallback((_) => showModalBottomSheet(
70 0 : context: context,
71 0 : builder: (BuildContext context) => buildReleaseHelper(context, version),
72 : ));
73 0 : AppPreferences.set(AppPreferences.prefVersion, value.version);
74 : }
75 : });
76 : }
77 :
78 0 : Widget buildReleaseHelper(BuildContext context, String version) => buildHelper(
79 : context,
80 : type: 'upgrade',
81 0 : builder: (context, snapshot) {
82 0 : if (snapshot.hasData) {
83 0 : String data = snapshot.data!;
84 0 : if (version.isNotEmpty) {
85 0 : data = data.split('### $version')[0];
86 : }
87 0 : return Directionality(
88 0 : textDirection: AppDesign.getAlignment<TextDirection>(),
89 0 : child: Markdown(data: data),
90 : );
91 : }
92 0 : return Container();
93 : },
94 : );
95 :
96 0 : @override
97 0 : String getTitle() => AppLocale.labels.appTitle;
98 :
99 1 : @override
100 : Widget buildBottomBar(BuildContext context, BoxConstraints constraints) {
101 2 : return ThemeHelper.isWearable ? ThemeHelper.emptyBox : super.buildBottomBar(context, constraints);
102 : }
103 :
104 0 : @override
105 : Widget? buildRightBar(BuildContext context, BoxConstraints constraints) {
106 0 : return ThemeHelper.isWearable ? null : super.buildRightBar(context, constraints);
107 : }
108 :
109 1 : @override
110 : Widget? getBarLeading(NavigatorState nav) {
111 1 : if (ScreenHelper.state().isWide) {
112 : return ThemeHelper.emptyBox;
113 : }
114 1 : return Builder(
115 1 : builder: (BuildContext context) {
116 1 : final scaffold = Scaffold.of(context);
117 1 : return ToolbarButtonWidget(
118 : icon: Icons.menu,
119 2 : tooltip: AppLocale.labels.navigationTooltip,
120 3 : onPressed: () => AppDesign.isRightToLeft() ? scaffold.openEndDrawer() : scaffold.openDrawer(),
121 : );
122 : },
123 : );
124 : }
125 :
126 1 : @override
127 : List<Widget> getBarActions(NavigatorState nav) {
128 1 : final isWide = ScreenHelper.state().isWide;
129 1 : return [
130 1 : ToolbarButtonWidget(
131 : isWide: isWide,
132 : icon: Icons.app_registration_outlined,
133 2 : tooltip: AppLocale.labels.customizeTooltip,
134 0 : onPressed: () => setState(() => isEditMode = true),
135 : ),
136 3 : if (![TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android].contains(defaultTargetPlatform))
137 0 : ToolbarButtonWidget(
138 : isWide: isWide,
139 : icon: Icons.switch_access_shortcut_add_outlined,
140 0 : tooltip: AppLocale.labels.subscriptionTooltip,
141 0 : onPressed: () => nav.pushNamed(AppRoute.subscriptionRoute),
142 : ),
143 : ];
144 : }
145 :
146 1 : @override
147 : Widget getBarTitle(BuildContext context, [bool isBottom = false]) {
148 1 : return SvgPicture.asset(
149 : 'assets/images/fingrom.svg',
150 : height: isBottom ? 20 : 40,
151 : alignment: Alignment.centerLeft,
152 2 : semanticsLabel: AppLocale.labels.appTitle,
153 : );
154 : }
155 :
156 1 : @override
157 2 : String getButtonName() => AppLocale.labels.addMainTooltip;
158 :
159 1 : @override
160 : Widget buildButton(BuildContext context, BoxConstraints constraints) {
161 1 : NavigatorState nav = Navigator.of(context);
162 1 : return FloatingActionButton(
163 : heroTag: 'home_page',
164 1 : mini: ThemeHelper.isWearable,
165 2 : onPressed: () => nav.pushNamed(AppRoute.billAddRoute),
166 1 : tooltip: getButtonName(),
167 : child: const Icon(Icons.add),
168 : );
169 : }
170 :
171 1 : @override
172 : Widget build(BuildContext context) {
173 2 : Provider.of<AppLocale>(context, listen: false).updateState(context);
174 1 : if (isEditMode) {
175 0 : return HomeEditPage(callback: () => setState(() => isEditMode = false));
176 : }
177 1 : if (AppPreferences.get(AppPreferences.prefPrivacyPolicy) == null) {
178 : return const StartPage();
179 : }
180 2 : return Consumer<AppData>(builder: (context, appState, _) {
181 2 : if (AppPreferences.get(AppPreferences.prefIsBio) == AppPreferences.isActive && !isOtpPassed) {
182 0 : return SecurityBioTab(
183 0 : onSuccess: () => setState(() => isOtpPassed = true),
184 : );
185 2 : } else if (AppPreferences.get(AppPreferences.prefIsOTP) == AppPreferences.isActive && !isOtpPassed) {
186 0 : return SecurityTab(
187 0 : onSuccess: () => setState(() => isOtpPassed = true),
188 : );
189 : }
190 1 : if (appState.isLoading) {
191 : return const InitTab();
192 : }
193 4 : WidgetsBinding.instance.addPostFrameCallback((_) => PaymentsController(appState));
194 1 : return super.build(context);
195 : });
196 : }
197 :
198 0 : Widget buildContentWideScreen() {
199 0 : return ComponentsBuilder([
200 0 : {
201 0 : componentData.key: ComponentRegistry.chart.toString(),
202 : componentData.startX: 0.0,
203 : componentData.startY: 4.0,
204 : componentData.endX: 4.0,
205 : componentData.endY: 7.0,
206 : componentData.order: 0,
207 0 : componentData.type: ComponentChartType.accountFlow.toString(),
208 : },
209 0 : {
210 0 : componentData.key: ComponentRegistry.chart.toString(),
211 : componentData.startX: 4.0,
212 : componentData.startY: 4.0,
213 : componentData.endX: 8.0,
214 : componentData.endY: 7.0,
215 : componentData.order: 1,
216 0 : componentData.type: ComponentChartType.billYtd.toString()
217 : },
218 0 : {
219 0 : componentData.key: ComponentRegistry.chart.toString(),
220 : componentData.startX: 8.0,
221 : componentData.startY: 4.0,
222 : componentData.endX: 12.0,
223 : componentData.endY: 7.0,
224 : componentData.order: 2,
225 0 : componentData.type: ComponentChartType.budgetForecast.toString()
226 : },
227 0 : {
228 0 : componentData.key: ComponentRegistry.summary.toString(),
229 : componentData.startX: 0,
230 : componentData.startY: 0,
231 : componentData.endX: 12.0,
232 : componentData.endY: 4.0,
233 : componentData.order: 3,
234 0 : componentData.type: ComponentRecentType.account.toString()
235 : }
236 : ]);
237 : }
238 :
239 0 : Widget buildContentSmallScreen() {
240 0 : return ComponentsBuilder([
241 0 : {
242 0 : componentData.key: ComponentRegistry.recent.toString(),
243 : componentData.startX: 0,
244 : componentData.startY: 0,
245 : componentData.endX: 3.0,
246 : componentData.endY: 2.0,
247 : componentData.order: 0,
248 0 : componentData.type: ComponentRecentType.account.toString(),
249 : ComponentRecent.count: 0
250 : },
251 0 : {
252 0 : componentData.key: ComponentRegistry.recent.toString(),
253 : componentData.startX: 3.0,
254 : componentData.startY: 0.0,
255 : componentData.endX: 6.0,
256 : componentData.endY: 2.0,
257 : componentData.order: 1,
258 0 : componentData.type: ComponentRecentType.budget.toString(),
259 : ComponentRecent.count: 0
260 : },
261 0 : {
262 0 : componentData.key: ComponentRegistry.recent.toString(),
263 : componentData.startX: 0.0,
264 : componentData.startY: 2.0,
265 : componentData.endX: 6.0,
266 : componentData.endY: 12.0,
267 : componentData.order: 2,
268 0 : componentData.type: ComponentRecentType.bill.toString(),
269 : ComponentRecent.count: 10
270 : }
271 : ]);
272 : }
273 :
274 1 : @override
275 : Widget buildContent(BuildContext context, BoxConstraints constraints) {
276 1 : final data = ComponentsBuilder.getData(context);
277 1 : bool isWide = ScreenHelper.state().isWide;
278 1 : final countHeight = ThemeHelper.getHeightCount(context, constraints);
279 1 : final countWidth = ThemeHelper.getWidthCount(constraints, context);
280 1 : if (data != null && data.isNotEmpty) {
281 0 : return ComponentsBuilder(data);
282 1 : } else if (countHeight > 3 && isWide) {
283 0 : return buildContentWideScreen();
284 1 : } else if (countHeight < 3 && countWidth == 1 && !ThemeHelper.isHorizontal(constraints)) {
285 0 : return buildContentSmallScreen();
286 : }
287 1 : double indent = ThemeHelper.getIndent();
288 1 : EdgeInsets margin = EdgeInsets.only(top: indent);
289 1 : bool isVertical = countWidth == 1 && !ThemeHelper.isWearable;
290 1 : double width = ThemeHelper.getWidth(context, 3, constraints);
291 4 : double partWidth = width / countWidth - indent * (countWidth - 1);
292 3 : DateTime curr = DateTime.now().getStartingDay(AppStartOfMonth.get());
293 :
294 1 : final billWidget = BillWidget(
295 : margin: margin,
296 4 : title: '${AppLocale.labels.billHeadline}, ${curr.fullMonth()}',
297 2 : state: state.get(AppDataType.bills),
298 : limit: 7,
299 : route: AppRoute.billRoute,
300 2 : tooltip: AppLocale.labels.billTooltip,
301 : width: isVertical ? width : partWidth,
302 : hasExpand: isVertical,
303 1 : toExpand: toExpand,
304 0 : callback: (v) => setState(() => toExpand = v),
305 : );
306 1 : final accountWidget = AccountWidget(
307 : margin: margin,
308 5 : title: '${AppLocale.labels.accountHeadline}, ${AppLocale.labels.total}',
309 2 : state: state.get(AppDataType.accounts),
310 : limit: 7,
311 : route: AppRoute.accountRoute,
312 2 : tooltip: AppLocale.labels.accountTooltip,
313 : width: isVertical ? width : partWidth,
314 : hasExpand: isVertical,
315 1 : toExpand: toExpand,
316 0 : callback: (v) => setState(() => toExpand = v),
317 3 : )..exchange = Exchange(store: super.state);
318 1 : final budgetWidget = BudgetWidget(
319 : margin: margin,
320 5 : title: '${AppLocale.labels.budgetHeadline}, ${AppLocale.labels.left}',
321 2 : state: state.get(AppDataType.budgets),
322 : limit: 7,
323 : route: AppRoute.budgetRoute,
324 2 : tooltip: AppLocale.labels.budgetTooltip,
325 : width: isVertical ? width : partWidth,
326 : hasExpand: isVertical,
327 1 : toExpand: toExpand,
328 0 : callback: (v) => setState(() => toExpand = v),
329 3 : )..exchange = Exchange(store: super.state);
330 :
331 1 : return GridLayer(
332 : padding: indent,
333 : width: width,
334 : crossAxisCount: countWidth,
335 : strategy: switch (countWidth) {
336 1 : 4 => [
337 0 : [0, 6],
338 0 : [2, 5],
339 0 : [3, 4],
340 0 : [1, 7]
341 : ],
342 1 : 3 => [
343 0 : [2],
344 0 : [3],
345 0 : [0, 1]
346 : ],
347 2 : 2 => [
348 1 : [2, 3],
349 1 : [0, 1]
350 : ],
351 0 : _ => [
352 0 : [0, 1, 2, 3]
353 : ]
354 : },
355 1 : children: [
356 1 : countHeight > 3
357 : ? isWide
358 0 : ? Expanded(
359 0 : child: Padding(
360 0 : padding: EdgeInsets.only(top: indent / 2),
361 : child: const ComponentRecent({'type': ComponentRecentType.goals, 'count': 7}),
362 : ),
363 : )
364 1 : : GoalWidget(
365 : width: isVertical ? width : partWidth,
366 2 : state: super.state.getList(AppDataType.goals),
367 : )
368 : : ThemeHelper.emptyBox,
369 : billWidget,
370 : accountWidget,
371 : budgetWidget,
372 0 : () => const Expanded(
373 : child: Column(
374 : children: [
375 : Expanded(child: ThemeHelper.emptyBox),
376 : AccountFlowChart(),
377 : ThemeHelper.hIndent6x,
378 : ],
379 : ),
380 : ),
381 0 : () => const Expanded(
382 : child: Column(
383 : children: [
384 : Expanded(child: ThemeHelper.emptyBox),
385 : BudgetForecastChart(),
386 : ThemeHelper.hIndent6x,
387 : ],
388 : ),
389 : ),
390 0 : () => const Expanded(
391 : child: Column(
392 : children: [
393 : Expanded(child: ThemeHelper.emptyBox),
394 : BillYtdChart(),
395 : ThemeHelper.hIndent6x,
396 : ],
397 : ),
398 : ),
399 0 : () => const Expanded(
400 : child: ComponentRecent({'type': ComponentRecentType.invoice, 'count': 7}),
401 : ),
402 : ],
403 : );
404 : }
405 : }
|