LCOV - code coverage report
Current view: top level - lib/pages/start/widgets - setting_tab.dart Coverage Total Hit
Test: lcov.info Lines: 0.4 % 267 1
Test Date: 2026-11-14 15:04:37 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.0-1