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/herald/app_design.dart';
5 : import 'package:app_finance/_classes/herald/app_locale.dart';
6 : import 'package:app_finance/_classes/herald/app_zoom.dart';
7 : import 'package:app_finance/_classes/structure/navigation/app_menu.dart';
8 : import 'package:app_finance/_classes/storage/app_data.dart';
9 : import 'package:app_finance/_classes/structure/navigation/app_route.dart';
10 : import 'package:app_finance/_configs/screen_helper.dart';
11 : import 'package:app_finance/_configs/theme_helper.dart';
12 : import 'package:app_finance/_ext/build_context_ext.dart';
13 : import 'package:app_finance/design/wrapper/markdown_builder_wrapper.dart';
14 : import 'package:app_finance/pages/_interfaces/widgets/menu_widget.dart';
15 : import 'package:app_finance/design/wrapper/input_controller_wrapper.dart';
16 : import 'package:app_finance/design/wrapper/row_widget.dart';
17 : import 'package:app_finance/design/wrapper/text_wrapper.dart';
18 : import 'package:app_finance/design/button/toolbar_button_widget.dart';
19 : import 'package:flutter/foundation.dart';
20 : import 'package:material_ui/material_ui.dart';
21 : import 'package:flutter_grid_layout/flutter_grid_layout.dart';
22 : import 'package:provider/provider.dart';
23 :
24 : abstract class AbstractPageState<T extends StatefulWidget> extends State<T> {
25 : late AppData state;
26 :
27 : int selectedMenu = 0;
28 :
29 : String getTitle();
30 :
31 : String getButtonName();
32 :
33 4 : String? getHelperName() => null;
34 :
35 : Widget buildButton(BuildContext context, BoxConstraints constraints);
36 :
37 : Widget buildContent(BuildContext context, BoxConstraints constraints);
38 :
39 4 : Widget? getBarLeading(NavigatorState nav) {
40 4 : return ToolbarButtonWidget(
41 4 : isWide: ScreenHelper.state().isWide,
42 8 : tooltip: AppLocale.labels.backTooltip,
43 0 : onPressed: () => nav.canPop() ? nav.pop() : nav.pushNamed(AppRoute.homeRoute),
44 : icon: Icons.arrow_back,
45 : );
46 : }
47 :
48 0 : Widget buildHelper(BuildContext context,
49 : {String? type, Widget Function(BuildContext, AsyncSnapshot<String>)? builder}) {
50 0 : final locale = AppLocale.labels.localeName;
51 0 : type ??= getHelperName();
52 0 : return Container(
53 : width: double.infinity,
54 0 : color: context.colorScheme.surface,
55 0 : child: SafeArea(
56 : top: false,
57 0 : child: Column(
58 0 : children: [
59 0 : Transform.translate(
60 : offset: const Offset(-10, -24),
61 0 : child: Align(
62 : alignment: Alignment.topRight,
63 0 : child: FloatingActionButton(
64 : heroTag: 'helper',
65 : mini: true,
66 0 : tooltip: AppLocale.labels.closeTooltip,
67 0 : onPressed: () => Navigator.pop(context),
68 0 : child: Icon(
69 : Icons.close,
70 : color: Colors.white70,
71 0 : semanticLabel: AppLocale.labels.closeTooltip,
72 : ),
73 : ),
74 : ),
75 : ),
76 0 : Expanded(
77 0 : child: MarkdownBuilderWrapper(
78 0 : url: './assets/l10n/${type}_$locale.md',
79 : builder: builder,
80 : ),
81 : ),
82 : ],
83 : ),
84 : ),
85 : );
86 : }
87 :
88 4 : List<Widget> getBarActions(NavigatorState nav) {
89 4 : final isWide = ScreenHelper.state().isWide;
90 4 : return [
91 4 : if (getHelperName() != null)
92 0 : ToolbarButtonWidget(
93 : isWide: isWide,
94 0 : tooltip: AppLocale.labels.helpTooltip,
95 0 : onPressed: () => showModalBottomSheet(
96 0 : context: context,
97 0 : backgroundColor: context.colorScheme.surface,
98 0 : builder: buildHelper,
99 : ),
100 : icon: Icons.contact_support_outlined,
101 0 : color: isWide ? context.colorScheme.onSurface : Colors.white70,
102 0 : borderColor: isWide ? context.colorScheme.onSurface : Colors.white70,
103 0 : semanticLabel: AppLocale.labels.helpTooltip,
104 : ),
105 : if (!isWide)
106 4 : Builder(
107 4 : builder: (context) {
108 4 : final scaffold = Scaffold.of(context);
109 4 : return ToolbarButtonWidget(
110 : icon: Icons.menu,
111 : color: Colors.white70,
112 8 : tooltip: AppLocale.labels.navigationTooltip,
113 0 : onPressed: () => AppDesign.isRightToLeft() ? scaffold.openEndDrawer() : scaffold.openDrawer(),
114 : );
115 : },
116 : )
117 : ];
118 : }
119 :
120 4 : Widget getBarTitle(BuildContext context, [bool isBottom = false]) {
121 4 : final isWide = ScreenHelper.state().isWide;
122 4 : return TextWrapper(
123 4 : getTitle(),
124 4 : style: TextStyle(color: isWide ? context.colorScheme.onSurface : Colors.white70),
125 : );
126 : }
127 :
128 0 : AppBar? buildBar(BuildContext context, BoxConstraints constraints) {
129 0 : final nav = Navigator.of(context);
130 0 : final isWide = ScreenHelper.state().isWide;
131 0 : return AppBar(
132 0 : title: Center(child: getBarTitle(context)),
133 : toolbarHeight: ThemeHelper.barHeight,
134 : shape: isWide
135 0 : ? UnderlineInputBorder(
136 0 : borderSide: BorderSide(color: context.colorScheme.primary),
137 : borderRadius: BorderRadius.zero,
138 : )
139 : : null,
140 0 : backgroundColor: isWide ? context.colorScheme.inverseSurface.withValues(alpha: 0.4) : context.colorScheme.primary,
141 0 : leading: getBarLeading(nav),
142 : leadingWidth: isWide ? ThemeHelper.menuWidth : null,
143 0 : actions: getBarActions(nav),
144 : );
145 : }
146 :
147 0 : Widget? buildRightBar(BuildContext context, BoxConstraints constraints) {
148 0 : final nav = Navigator.of(context);
149 0 : return Align(
150 : alignment: Alignment.topRight,
151 0 : child: Container(
152 0 : color: context.colorScheme.primary,
153 : width: ThemeHelper.barHeight,
154 0 : child: Column(
155 0 : children: [
156 0 : getBarLeading(nav) ?? ThemeHelper.emptyBox,
157 0 : ...getBarActions(nav),
158 0 : ThemeHelper.hIndent,
159 0 : RotatedBox(
160 : quarterTurns: 3,
161 0 : child: SizedBox(width: constraints.maxHeight / 2.5, child: getBarTitle(context)),
162 : ),
163 : ],
164 : ),
165 : ),
166 : );
167 : }
168 :
169 6 : Widget buildBottomBar(BuildContext context, BoxConstraints constraints) {
170 6 : final theme = Theme.of(context);
171 6 : final nav = Navigator.of(context);
172 6 : final actions = getBarActions(nav);
173 12 : final btnWidth = 50.0 * actions.length;
174 18 : final titleWidth = ThemeHelper.getWidth(context, 0, null, false) / 2 - 80;
175 12 : final hasTooltip = getButtonName().isNotEmpty;
176 30 : final showTooltip = hasTooltip && (constraints.maxWidth - titleWidth - btnWidth - 50 > 125);
177 6 : return Container(
178 : padding: EdgeInsets.zero,
179 : clipBehavior: Clip.none,
180 : height: ThemeHelper.barHeight,
181 12 : color: theme.colorScheme.primary,
182 6 : child: RowWidget(
183 6 : maxWidth: constraints.maxWidth,
184 : indent: 0,
185 6 : chunk: [50, hasTooltip ? titleWidth : null, hasTooltip ? 64 : 0, hasTooltip ? null : 0, btnWidth],
186 6 : children: [
187 12 : [getBarLeading(nav) ?? ThemeHelper.emptyBox],
188 6 : [
189 6 : Center(
190 : heightFactor: 1.9,
191 6 : child: getBarTitle(context, true),
192 : ),
193 : ],
194 : const [ThemeHelper.emptyBox],
195 6 : [
196 : if (showTooltip)
197 6 : Padding(
198 12 : padding: EdgeInsets.only(top: ThemeHelper.getIndent(0.5)),
199 6 : child: TextWrapper(
200 6 : getButtonName(),
201 : maxLines: 2,
202 12 : style: theme.textTheme.bodySmall
203 24 : ?.copyWith(color: theme.colorScheme.onInverseSurface.withValues(alpha: 0.6)),
204 : ),
205 : ),
206 : ],
207 6 : [
208 6 : RowWidget(
209 12 : chunk: List.filled(actions.length, null),
210 : maxWidth: btnWidth,
211 : indent: 0,
212 24 : children: actions.map((e) => [e]).toList(),
213 : ),
214 : ],
215 : ],
216 : ),
217 : );
218 : }
219 :
220 6 : Widget? buildNavigation() {
221 6 : return ListView.separated(
222 : scrollDirection: Axis.vertical,
223 : addSemanticIndexes: true,
224 12 : padding: EdgeInsets.symmetric(vertical: ThemeHelper.getIndent(4)),
225 1 : separatorBuilder: (context, index) => ThemeHelper.hIndent2x,
226 12 : itemCount: AppMenu.get().length,
227 2 : itemBuilder: (context, index) => MenuWidget(
228 : index: index,
229 4 : setState: () => setState(() => selectedMenu = index),
230 1 : selectedIndex: selectedMenu,
231 : ),
232 : );
233 : }
234 :
235 6 : Drawer? buildDrawer() {
236 6 : return Drawer(
237 : elevation: 0,
238 6 : shape: Border.all(width: 0),
239 6 : child: ScreenHelper.state().isWide
240 0 : ? buildNavigation()
241 6 : : InputControllerWrapper(
242 6 : child: buildNavigation() ?? ThemeHelper.emptyBox,
243 : ),
244 : );
245 : }
246 :
247 6 : @override
248 : Widget build(BuildContext context) {
249 12 : final scale = context.watch<AppZoom>().value;
250 6 : return Scaffold(
251 18 : appBar: AppBar(backgroundColor: context.colorScheme.primary, toolbarHeight: 0),
252 6 : body: Container(
253 12 : color: context.colorScheme.primary,
254 18 : child: SafeArea(child: Consumer<AppData>(builder: (context, appState, _) {
255 6 : state = appState;
256 12 : return LayoutBuilder(builder: (context, constraints) {
257 6 : final display = ScreenHelper.getInstance(context, constraints);
258 : final isBottom = display.isBottom && !display.isRight;
259 6 : final hasKeyboard = ThemeHelper.isKeyboardVisible(context, constraints);
260 6 : final height = constraints.maxHeight;
261 : final blockHeight =
262 24 : height / scale - (display.isRight ? 0 : ThemeHelper.barHeight + ThemeHelper.getIndent());
263 12 : double width = constraints.maxWidth / scale;
264 : Widget? rightBar;
265 : Widget? leftBar;
266 : if (display.isRight) {
267 0 : rightBar = buildRightBar(context, constraints);
268 : if (rightBar != null) {
269 0 : width -= ThemeHelper.barHeight;
270 : }
271 : } else if (display.isWide) {
272 0 : leftBar = buildNavigation();
273 : if (leftBar != null) {
274 0 : width -= ThemeHelper.menuWidth;
275 : }
276 : }
277 6 : if (width < 0) {
278 : width = 0;
279 : }
280 30 : final dx = (constraints.maxWidth - constraints.maxWidth / scale) / 2;
281 18 : final dy = (height - height / scale) / 2;
282 6 : return Scaffold(
283 0 : appBar: display.isBottom ? null : buildBar(context, constraints),
284 6 : drawer: buildDrawer(),
285 6 : endDrawer: buildDrawer(),
286 : floatingActionButtonLocation: isBottom ? FloatingActionButtonLocation.centerDocked : null,
287 : floatingActionButton: isBottom
288 : ? hasKeyboard
289 0 : ? Transform.translate(
290 : offset: const Offset(0, 12),
291 0 : child: SizedBox(
292 0 : height: ThemeHelper.barHeight * 1.2,
293 0 : child: buildButton(context, constraints),
294 : ),
295 : )
296 12 : : defaultTargetPlatform == TargetPlatform.iOS
297 0 : ? buildButton(context, constraints)
298 6 : : Container(
299 12 : margin: EdgeInsets.only(bottom: ThemeHelper.getIndent()),
300 6 : child: buildButton(context, constraints),
301 : )
302 0 : : buildButton(context, constraints),
303 : resizeToAvoidBottomInset: true,
304 6 : body: InputControllerWrapper(
305 6 : child: GridContainer(
306 6 : alignment: AppDesign.getAlignment<MainAxisAlignment>(),
307 : rows: const [ThemeHelper.menuWidth, null, ThemeHelper.barHeight],
308 : columns: const [null, ThemeHelper.barHeight],
309 6 : children: [
310 : if (leftBar != null)
311 0 : GridItem(
312 : order: 2,
313 : start: const Size(0, 0),
314 : end: const Size(1, 2),
315 0 : child: Container(
316 0 : color: context.colorScheme.inversePrimary.withValues(alpha: 0.2),
317 : width: ThemeHelper.menuWidth,
318 : height: double.infinity,
319 0 : child: buildNavigation(),
320 : ),
321 : ),
322 6 : GridItem(
323 : order: 1,
324 6 : start: Size(leftBar != null ? 1 : 0, 0),
325 6 : end: Size(rightBar != null ? 2 : 3, rightBar == null && display.isBottom ? 1 : 2),
326 6 : child: OverflowBox(
327 : alignment: Alignment.topLeft,
328 : minWidth: width,
329 : maxWidth: width,
330 : minHeight: blockHeight,
331 : maxHeight: blockHeight,
332 6 : child: Transform.translate(
333 6 : offset: Offset(dx, dy),
334 6 : child: Transform.scale(
335 : scale: scale,
336 6 : child: buildContent(context, constraints),
337 : ),
338 : ),
339 : ),
340 : ),
341 : if (rightBar != null)
342 0 : GridItem(
343 : order: 2,
344 : start: const Size(2, 0),
345 : end: const Size(3, 2),
346 : child: rightBar,
347 : ),
348 : if (rightBar == null && display.isBottom)
349 6 : GridItem(
350 : order: 2,
351 : start: const Size(0, 1),
352 : end: const Size(3, 2),
353 6 : child: buildBottomBar(context, constraints),
354 : ),
355 : ],
356 : ),
357 : ),
358 : );
359 : });
360 : })),
361 : ),
362 : );
363 : }
364 : }
|