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/focus_controller.dart';
5 : import 'package:app_finance/_classes/herald/app_budget_positive.dart';
6 : import 'package:app_finance/_classes/herald/app_design.dart';
7 : import 'package:app_finance/_classes/herald/app_locale.dart';
8 : import 'package:app_finance/_classes/herald/app_palette.dart';
9 : import 'package:app_finance/_classes/herald/app_start_of_month.dart';
10 : import 'package:app_finance/_classes/herald/app_theme.dart';
11 : import 'package:app_finance/_classes/herald/app_start_of_week.dart';
12 : import 'package:app_finance/_classes/herald/app_zoom.dart';
13 : import 'package:app_finance/_classes/storage/app_data.dart';
14 : import 'package:app_finance/_classes/storage/app_preferences.dart';
15 : import 'package:app_finance/_classes/structure/currency/exchange.dart';
16 : import 'package:app_finance/_configs/design_type.dart';
17 : import 'package:app_finance/design/form/list_selector_item.dart';
18 : import 'package:app_finance/_configs/custom_color_scheme.dart';
19 : import 'package:app_finance/_configs/theme_helper.dart';
20 : import 'package:app_finance/_ext/build_context_ext.dart';
21 : import 'package:app_finance/_ext/color_ext.dart';
22 : import 'package:app_finance/design/wrapper/input_wrapper.dart';
23 : import 'package:app_finance/l10n/index.dart';
24 : import 'package:app_finance/design/button/link_widget.dart';
25 : import 'package:app_finance/design/form/color_selector.dart';
26 : import 'package:app_finance/pages/start/widgets/abstract_tab.dart';
27 : import 'package:app_finance/design/wrapper/row_widget.dart';
28 : import 'package:app_finance/design/wrapper/single_scroll_wrapper.dart';
29 : import 'package:app_finance/design/wrapper/table_widget.dart';
30 : import 'package:flutter/foundation.dart';
31 : import 'package:material_ui/material_ui.dart';
32 : import 'package:flutter_currency_picker/flutter_currency_picker.dart';
33 : import 'package:intl/intl.dart';
34 : import 'package:provider/provider.dart';
35 :
36 : class SettingTab extends AbstractTab {
37 2 : const SettingTab({
38 : super.key,
39 : required super.setState,
40 : required super.setButton,
41 : super.isFirstBoot = true,
42 : });
43 :
44 0 : @override
45 0 : SettingTabState createState() => SettingTabState();
46 : }
47 :
48 : class SettingTabState<T extends SettingTab> extends AbstractTabState<T> {
49 : late FocusController controller;
50 :
51 : late AppTheme theme;
52 : late AppZoom zoom;
53 : late AppStartOfWeek startOfWeek;
54 : late AppStartOfMonth startOfMonth;
55 : late AppPalette palette;
56 : late AppLocale locale;
57 : late AppDesign design;
58 : late AppData state;
59 : late AppBudgetPositive budgetPositive;
60 :
61 : Currency? currency;
62 : bool isEncrypted = false;
63 : bool hasEncrypted = false;
64 : String brightness = '0';
65 : String colorMode = AppPalette.state;
66 :
67 : var paletteState = (
68 : light: AppDefaultColors.fromString(AppPalette.light),
69 : dark: AppDefaultColors.fromString(AppPalette.dark),
70 : );
71 :
72 : final languages = languageList.map((e) => ListSelectorItem(id: e.id, name: e.name)).toList();
73 :
74 0 : @override
75 : void initState() {
76 0 : super.initState();
77 0 : controller = FocusController();
78 0 : final doEncrypt = AppPreferences.get(AppPreferences.prefDoEncrypt);
79 0 : hasEncrypted = doEncrypt != null;
80 0 : isEncrypted = doEncrypt == 'true' || doEncrypt == null;
81 0 : AppPreferences.set(AppPreferences.prefDoEncrypt, isEncrypted ? 'true' : 'false');
82 0 : brightness = AppPreferences.get(AppPreferences.prefTheme) ?? brightness;
83 0 : colorMode = AppPreferences.get(AppPreferences.prefColor) ?? colorMode;
84 0 : currency = CurrencyProvider.find(AppPreferences.get(AppPreferences.prefCurrency) ?? '');
85 : }
86 :
87 0 : @override
88 : void dispose() {
89 0 : controller.dispose();
90 0 : super.dispose();
91 : }
92 :
93 0 : void saveEncryption(newValue) {
94 0 : if (hasEncrypted) {
95 : return;
96 : }
97 0 : setState(() => isEncrypted = newValue);
98 0 : AppPreferences.set(AppPreferences.prefDoEncrypt, isEncrypted ? 'true' : 'false');
99 : }
100 :
101 0 : Future<void> saveCurrency(Currency? value) async {
102 : if (value != null) {
103 0 : await AppPreferences.set(AppPreferences.prefCurrency, value.code);
104 : Exchange.defaultCurrency = value;
105 : CurrencyDefaults.defaultCurrency = value;
106 0 : await state.restate();
107 0 : if (!mounted) {
108 : return;
109 : }
110 0 : setState(() => currency = value);
111 : }
112 : }
113 :
114 0 : Future<void> saveStartOfMonth(String? value) async {
115 : if (value != null) {
116 0 : await startOfMonth.set(int.tryParse(value) ?? 1);
117 0 : await state.restate();
118 0 : if (!mounted) {
119 : return;
120 : }
121 0 : setState(() => {});
122 : }
123 : }
124 :
125 0 : Future<void> saveTheme(String value) async {
126 0 : await theme.setTheme(value);
127 0 : if (!mounted) {
128 : return;
129 : }
130 0 : setState(() => brightness = value);
131 : }
132 :
133 0 : Future<void> saveColor(String value) async {
134 0 : await palette.setMode(value);
135 0 : if (!mounted) {
136 : return;
137 : }
138 0 : setState(() => colorMode = value);
139 : }
140 :
141 0 : Future<void> saveLocale(String value) async {
142 0 : await locale.set(value);
143 0 : await design.set(AppDesign.fromLocale(AppLocale.fromCode(value)));
144 0 : if (value == 'ar') {
145 0 : await startOfWeek.set(int.parse(AppStartOfWeek.SATURDAY));
146 : }
147 0 : if (!mounted) {
148 : return;
149 : }
150 0 : setState(() {});
151 : }
152 :
153 0 : Future<void> saveDesign(String value) async {
154 0 : await design.set(value);
155 0 : if (!mounted) {
156 : return;
157 : }
158 0 : setState(() {});
159 : }
160 :
161 0 : Future<void> changePalette(_) async {
162 0 : await palette.set(paletteState.light, paletteState.dark);
163 0 : if (!mounted) {
164 : return;
165 : }
166 0 : setState(() => paletteState);
167 : }
168 :
169 0 : Future<void> initCurrencyFromLocale(String locale) async {
170 0 : final format = NumberFormat.simpleCurrency(locale: locale);
171 0 : final code = format.currencyName ?? 'EUR';
172 0 : await AppPreferences.set(AppPreferences.prefCurrency, code);
173 0 : if (!mounted) {
174 : return;
175 : }
176 0 : setState(() => currency = CurrencyProvider.find(code));
177 0 : Exchange.defaultCurrency = currency;
178 0 : CurrencyDefaults.defaultCurrency = currency;
179 : }
180 :
181 0 : @override
182 0 : String getButtonTitle() => AppLocale.labels.saveSettingsTooltip;
183 :
184 0 : @override
185 : Widget buildContent(BuildContext context, BoxConstraints constraints) {
186 0 : theme = Provider.of<AppTheme>(context, listen: false);
187 0 : zoom = Provider.of<AppZoom>(context, listen: false);
188 0 : startOfWeek = Provider.of<AppStartOfWeek>(context, listen: true);
189 0 : startOfMonth = Provider.of<AppStartOfMonth>(context, listen: true);
190 0 : palette = Provider.of<AppPalette>(context, listen: false);
191 0 : locale = Provider.of<AppLocale>(context, listen: false);
192 0 : design = Provider.of<AppDesign>(context, listen: false);
193 0 : budgetPositive = Provider.of<AppBudgetPositive>(context, listen: false);
194 0 : state = Provider.of<AppData>(context, listen: true);
195 0 : final textTheme = context.textTheme;
196 0 : final indent = ThemeHelper.getIndent(2);
197 0 : final width = ThemeHelper.getMaxWidth(context, constraints) - 2 * indent;
198 0 : if (currency == null) {
199 0 : WidgetsBinding.instance.addPostFrameCallback((_) => initCurrencyFromLocale(AppLocale.code));
200 : return ThemeHelper.emptyBox;
201 : }
202 0 : return SingleScrollWrapper(
203 0 : controller: controller,
204 0 : child: Column(
205 0 : crossAxisAlignment: AppDesign.getAlignment(),
206 0 : children: [
207 : ThemeHelper.hIndent2x,
208 0 : InputWrapper.select(
209 0 : title: AppLocale.labels.language,
210 0 : tooltip: AppLocale.labels.language,
211 : value: AppLocale.code,
212 0 : options: languages,
213 0 : onChange: (v) => saveLocale(v ?? AppLocale.code),
214 : ),
215 0 : InputWrapper.select(
216 0 : title: AppLocale.labels.design,
217 0 : tooltip: AppLocale.labels.design,
218 0 : value: AppDesign.get().name,
219 0 : options: [
220 0 : ListSelectorItem(id: AppDesignType.global.name, name: AppLocale.labels.designGlobal),
221 0 : ListSelectorItem(id: AppDesignType.asiaGeneral.name, name: AppLocale.labels.designAsiaGeneral),
222 0 : ListSelectorItem(id: AppDesignType.rtlGeneral.name, name: AppLocale.labels.designRtlGeneral),
223 0 : ListSelectorItem(id: AppDesignType.germany.name, name: AppLocale.labels.designGermany),
224 0 : ].cast<ListSelectorItem>(),
225 0 : onChange: (v) => saveDesign(AppDesign.find(v)?.name ?? AppDesignType.global.name),
226 : ),
227 0 : InputWrapper.currency(
228 0 : title: AppLocale.labels.currencyDefault,
229 0 : tooltip: AppLocale.labels.currencyDefault,
230 0 : value: currency,
231 0 : onChange: saveCurrency,
232 : ),
233 0 : InputWrapper.select(
234 0 : title: AppLocale.labels.dayStartOfWeek,
235 0 : tooltip: AppLocale.labels.dayStartOfWeek,
236 0 : value: startOfWeek.value.toString(),
237 0 : options: [
238 0 : ListSelectorItem(id: '6', name: AppLocale.labels.daySaturday),
239 0 : ListSelectorItem(id: '0', name: AppLocale.labels.daySunday),
240 0 : ListSelectorItem(id: '1', name: AppLocale.labels.dayMonday),
241 0 : ].cast<ListSelectorItem>(),
242 0 : onChange: (v) => startOfWeek.set(int.tryParse(v ?? '1') ?? 1),
243 : ),
244 0 : InputWrapper.select(
245 0 : title: AppLocale.labels.dayStartOfMonth,
246 0 : tooltip: AppLocale.labels.dayStartOfMonth,
247 0 : value: startOfMonth.value.toString(),
248 0 : options: List.generate(31, (index) {
249 0 : String key = (index + 1).toString();
250 0 : return ListSelectorItem(id: key, name: key);
251 0 : }).cast<ListSelectorItem>(),
252 0 : onChange: (v) => saveStartOfMonth(v ?? '1'),
253 : ),
254 0 : InputWrapper.select(
255 0 : title: AppLocale.labels.budgetPositive,
256 0 : tooltip: AppLocale.labels.budgetPositive,
257 0 : value: budgetPositive.value.toString(),
258 0 : options: [
259 0 : ListSelectorItem(id: '0', name: AppLocale.labels.budgetPositiveOn),
260 0 : ListSelectorItem(id: '1', name: AppLocale.labels.budgetPositiveOff),
261 0 : ].cast<ListSelectorItem>(),
262 0 : onChange: (v) {
263 0 : budgetPositive.set(int.tryParse(v) ?? 0);
264 0 : state.updateTotals([AppDataType.budgets]);
265 : },
266 : ),
267 0 : if (kDebugMode) ...[
268 0 : Row(
269 0 : mainAxisAlignment: AppDesign.getAlignment<MainAxisAlignment>(),
270 0 : children: [
271 0 : Text(
272 0 : AppLocale.labels.encryptionMode,
273 0 : style: textTheme.bodyLarge,
274 : ),
275 0 : Switch(
276 0 : value: isEncrypted,
277 0 : onChanged: saveEncryption,
278 : ),
279 0 : Expanded(
280 0 : child: hasEncrypted ? Text(AppLocale.labels.hasEncrypted) : ThemeHelper.emptyBox,
281 : ),
282 : ],
283 : ),
284 : ThemeHelper.hIndent2x,
285 : ],
286 0 : InputWrapper.select(
287 0 : title: AppLocale.labels.brightnessTheme,
288 0 : value: brightness,
289 0 : tooltip: AppLocale.labels.brightnessTheme,
290 0 : options: [
291 0 : ListSelectorItem(id: '0', name: AppLocale.labels.systemMode),
292 0 : ListSelectorItem(id: '1', name: AppLocale.labels.lightMode),
293 0 : ListSelectorItem(id: '2', name: AppLocale.labels.darkMode),
294 0 : ].cast<ListSelectorItem>(),
295 0 : onChange: (v) => saveTheme(v ?? '0'),
296 : ),
297 0 : InputWrapper.select(
298 0 : title: AppLocale.labels.colorTheme,
299 0 : value: colorMode,
300 0 : tooltip: AppLocale.labels.colorTheme,
301 0 : options: [
302 0 : ListSelectorItem(id: AppColors.colorApp, name: AppLocale.labels.colorApp),
303 0 : ListSelectorItem(id: AppColors.colorSystem, name: AppLocale.labels.colorSystem),
304 0 : ListSelectorItem(id: AppColors.colorUser, name: AppLocale.labels.colorUser),
305 0 : ].cast<ListSelectorItem>(),
306 0 : onChange: (v) => saveColor(v ?? AppColors.colorApp),
307 : ),
308 0 : if (colorMode == AppColors.colorUser) ...[
309 0 : TableWidget(
310 : width: width,
311 0 : shadowColor: context.colorScheme.onSurface.withValues(alpha: 0.05),
312 : chunk: const [120, null, null],
313 0 : data: [
314 0 : [
315 0 : Text(AppLocale.labels.colorType),
316 0 : Row(
317 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
318 0 : children: [
319 0 : Text(AppLocale.labels.colorLight),
320 0 : IconButton(
321 0 : onPressed: () =>
322 0 : changePalette(paletteState = (light: AppDefaultColors(), dark: paletteState.dark)),
323 : icon: const Icon(Icons.restore),
324 0 : tooltip: AppLocale.labels.colorRestore,
325 : ),
326 : ],
327 : ),
328 0 : Row(
329 : mainAxisAlignment: MainAxisAlignment.spaceBetween,
330 0 : children: [
331 0 : Text(AppLocale.labels.colorDark),
332 0 : IconButton(
333 0 : onPressed: () =>
334 0 : changePalette(paletteState = (light: paletteState.light, dark: AppDarkColors())),
335 : icon: const Icon(Icons.restore),
336 0 : tooltip: AppLocale.labels.colorRestore,
337 : ),
338 : ],
339 : ),
340 : ],
341 0 : [
342 0 : Text(AppLocale.labels.colorBackground),
343 0 : ColorSelector(
344 0 : value: paletteState.light.background.toMaterialColor,
345 0 : setState: (v) => changePalette(paletteState.light.background = v)),
346 0 : ColorSelector(
347 0 : value: paletteState.dark.background.toMaterialColor,
348 0 : setState: (v) => changePalette(paletteState.dark.background = v)),
349 : ],
350 0 : [
351 0 : Text(AppLocale.labels.colorPrimary),
352 0 : ColorSelector(
353 0 : value: paletteState.light.primary.toMaterialColor,
354 0 : setState: (v) => changePalette(paletteState.light.primary = v)),
355 0 : ColorSelector(
356 0 : value: paletteState.dark.primary.toMaterialColor,
357 0 : setState: (v) => changePalette(paletteState.dark.primary = v)),
358 : ],
359 0 : [
360 0 : Text(AppLocale.labels.colorInversePrimary),
361 0 : ColorSelector(
362 0 : value: paletteState.light.inversePrimary.toMaterialColor,
363 0 : setState: (v) => changePalette(paletteState.light.inversePrimary = v)),
364 0 : ColorSelector(
365 0 : value: paletteState.dark.inversePrimary.toMaterialColor,
366 0 : setState: (v) => changePalette(paletteState.dark.inversePrimary = v)),
367 : ],
368 0 : [
369 0 : Text(AppLocale.labels.colorInverseSurface),
370 0 : ColorSelector(
371 0 : value: paletteState.light.inverseSurface.toMaterialColor,
372 0 : setState: (v) => changePalette(paletteState.light.inverseSurface = v)),
373 0 : ColorSelector(
374 0 : value: paletteState.dark.inverseSurface.toMaterialColor,
375 0 : setState: (v) => changePalette(paletteState.dark.inverseSurface = v)),
376 : ],
377 0 : [
378 0 : Text(AppLocale.labels.colorOnInverseSurface),
379 0 : ColorSelector(
380 0 : value: paletteState.light.onInverseSurface.toMaterialColor,
381 0 : setState: (v) => changePalette(paletteState.light.onInverseSurface = v)),
382 0 : ColorSelector(
383 0 : value: paletteState.dark.onInverseSurface.toMaterialColor,
384 0 : setState: (v) => changePalette(paletteState.dark.onInverseSurface = v)),
385 : ],
386 0 : [
387 0 : Text(AppLocale.labels.colorSecondary),
388 0 : ColorSelector(
389 0 : value: paletteState.light.secondary.toMaterialColor,
390 0 : setState: (v) => changePalette(paletteState.light.secondary = v)),
391 0 : ColorSelector(
392 0 : value: paletteState.dark.secondary.toMaterialColor,
393 0 : setState: (v) => changePalette(paletteState.dark.secondary = v)),
394 : ],
395 0 : [
396 0 : Text(AppLocale.labels.colorOnSecondary),
397 0 : ColorSelector(
398 0 : value: paletteState.light.onSecondary.toMaterialColor,
399 0 : setState: (v) => changePalette(paletteState.light.onSecondary = v)),
400 0 : ColorSelector(
401 0 : value: paletteState.dark.onSecondary.toMaterialColor,
402 0 : setState: (v) => changePalette(paletteState.dark.onSecondary = v)),
403 : ],
404 0 : [
405 0 : Text(AppLocale.labels.colorOnSecondaryContainer),
406 0 : ColorSelector(
407 0 : value: paletteState.light.onSecondaryContainer.toMaterialColor,
408 0 : setState: (v) => changePalette(paletteState.light.onSecondaryContainer = v)),
409 0 : ColorSelector(
410 0 : value: paletteState.dark.onSecondaryContainer.toMaterialColor,
411 0 : setState: (v) => changePalette(paletteState.dark.onSecondaryContainer = v)),
412 : ],
413 : ],
414 : ),
415 : ThemeHelper.hIndent4x,
416 : ],
417 0 : RowWidget(
418 : indent: indent,
419 : maxWidth: width,
420 0 : chunk: [null, indent + ThemeHelper.getTextWidth(Text(AppLocale.labels.reset))],
421 0 : children: [
422 0 : [
423 0 : Text(
424 0 : AppLocale.labels.zoomState,
425 0 : style: textTheme.bodyLarge,
426 : ),
427 : ],
428 0 : [
429 0 : Align(
430 : alignment: Alignment.centerRight,
431 0 : child: LinkWidget(AppLocale.labels.reset, onTap: () => zoom.set(1)),
432 : ),
433 : ],
434 : ],
435 : ),
436 0 : Container(
437 0 : color: context.colorScheme.fieldBackground,
438 0 : child: Slider(
439 0 : value: zoom.value,
440 0 : onChanged: zoom.set,
441 : min: AppZoom.min,
442 : max: AppZoom.max,
443 : divisions: 15,
444 0 : label: zoom.value.toStringAsFixed(2),
445 : ),
446 : ),
447 0 : ThemeHelper.hIndent2x,
448 : ],
449 : ),
450 : );
451 : }
452 : }
|