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