diff --git a/lib/app/accounts/account_form.dart b/lib/app/accounts/account_form.dart index 69879b68..628212ed 100644 --- a/lib/app/accounts/account_form.dart +++ b/lib/app/accounts/account_form.dart @@ -3,21 +3,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:monekin/app/accounts/account_type_selector.dart'; +import 'package:monekin/app/categories/form/icon_and_color_selector.dart'; import 'package:monekin/core/database/app_db.dart'; import 'package:monekin/core/database/services/account/account_service.dart'; import 'package:monekin/core/database/services/currency/currency_service.dart'; import 'package:monekin/core/database/services/exchange-rate/exchange_rate_service.dart'; import 'package:monekin/core/database/services/transaction/transaction_service.dart'; import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/extensions/lists.extensions.dart'; import 'package:monekin/core/models/account/account.dart'; import 'package:monekin/core/models/currency/currency.dart'; import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; import 'package:monekin/core/models/supported-icon/supported_icon.dart'; -import 'package:monekin/core/presentation/app_colors.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; import 'package:monekin/core/presentation/widgets/currency_selector_modal.dart'; import 'package:monekin/core/presentation/widgets/date_form_field/date_form_field.dart'; import 'package:monekin/core/presentation/widgets/expansion_panel/single_expansion_panel.dart'; -import 'package:monekin/core/presentation/widgets/icon_selector_modal.dart'; import 'package:monekin/core/presentation/widgets/inline_info_card.dart'; import 'package:monekin/core/presentation/widgets/persistent_footer_button.dart'; import 'package:monekin/core/presentation/widgets/transaction_filter/transaction_filters.dart'; @@ -49,6 +50,7 @@ class _AccountFormPageState extends State { AccountType _type = AccountType.normal; SupportedIcon _icon = SupportedIconService.instance.defaultSupportedIcon; + Color _color = ColorHex.get(defaultColorPickerOptions.randomItem()); Currency? _currency; Currency? _userPrCurrency; @@ -94,6 +96,7 @@ class _AccountFormPageState extends State { closingDate: _closeDate, type: _type, iconId: _icon.id, + color: _color.toHex(leadingHashSign: false), currency: _currency!, iban: _ibanController.text.isEmpty ? null : _ibanController.text, description: _textController.text.isEmpty ? null : _textController.text, @@ -164,6 +167,9 @@ class _AccountFormPageState extends State { .first .then((value) { _balanceController.text = value.toString(); + + print(_accountToEdit!.color); + _color = _accountToEdit!.getComputedColor(context); }); _icon = _accountToEdit!.icon; @@ -218,256 +224,232 @@ class _AccountFormPageState extends State { ? t.account.form.edit : t.account.form.create), ), - body: widget.account != null && _accountToEdit == null - ? const LinearProgressIndicator() - : SingleChildScrollView( - child: Container( - padding: const EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Builder(builder: (context) { - final isDark = - Theme.of(context).brightness == Brightness.dark; - - return IconDisplayer( - supportedIcon: _icon, - size: 40, - isOutline: true, - outlineWidth: 1.5, - mainColor: (isDark - ? AppColors.of(context).onPrimary - : AppColors.of(context).primary) - .lighten(isDark ? 0.82 : 0), - secondaryColor: (isDark - ? AppColors.of(context).onPrimary - : AppColors.of(context).primary) - .lighten(isDark ? 0 : 0.82), - displayMode: IconDisplayMode.polygon, - onTap: () { - showIconSelectorModal( - context, - IconSelectorModal( - preselectedIconID: _icon.id, - subtitle: - t.icon_selector.select_account_icon, - onIconSelected: (selectedIcon) { - setState(() { - _icon = selectedIcon; - }); - }, - ), - ); - }, - ); - }), - const SizedBox(width: 10), - Expanded( - child: TextFormField( - controller: _nameController, - decoration: InputDecoration( - labelText: '${t.account.form.name} *', - hintText: 'Ex.: My account', - ), - validator: (value) => - fieldValidator(value, isRequired: true), - autovalidateMode: - AutovalidateMode.onUserInteraction, - textInputAction: TextInputAction.next, + body: Builder(builder: (context) { + if (widget.account != null && _accountToEdit == null) { + return const LinearProgressIndicator(); + } + + final isDark = Theme.of(context).brightness == Brightness.dark; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconAndColorSelector( + iconSelectorModalSubtitle: + t.icon_selector.select_account_icon, + iconDisplayer: IconDisplayer( + supportedIcon: _icon, + size: 36, + isOutline: true, + outlineWidth: 1.5, + mainColor: _color.lighten(0.82), + secondaryColor: _color, + displayMode: IconDisplayMode.polygon, + ), + onDataChange: ((data) { + setState(() { + _icon = data.icon; + _color = data.color; + }); + }), + data: (color: _color, icon: _icon), + ), + const SizedBox(height: 16), + TextFormField( + controller: _nameController, + decoration: InputDecoration( + labelText: '${t.account.form.name} *', + hintText: 'Ex.: My account', + ), + validator: (value) => fieldValidator(value, isRequired: true), + autovalidateMode: AutovalidateMode.onUserInteraction, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), + TextFormField( + controller: _balanceController, + decoration: InputDecoration( + labelText: widget.account != null + ? '${t.account.form.current_balance} *' + : '${t.account.form.initial_balance} *', + hintText: 'Ex.: 200', + suffixText: _currency?.symbol, + ), + keyboardType: TextInputType.number, + enabled: + !(widget.account != null && widget.account!.isClosed), + inputFormatters: decimalDigitFormatter, + validator: (value) => fieldValidator(value, + validator: ValidatorType.double, isRequired: true), + autovalidateMode: AutovalidateMode.onUserInteraction, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), + TextField( + controller: TextEditingController( + text: _currency != null + ? _currency?.name + : t.general.unspecified), + readOnly: true, + onTap: () { + if (_currency == null) return; + + showCurrencySelectorModal( + context, + CurrencySelectorModal( + preselectedCurrency: _currency!, + onCurrencySelected: (newCurrency) { + setState(() { + _currency = newCurrency; + }); + }), + ); + }, + decoration: InputDecoration( + labelText: t.currencies.currency, + suffixIcon: const Icon(Icons.arrow_drop_down), + prefixIcon: _currency != null + ? Container( + margin: const EdgeInsets.all(10), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100)), + child: SvgPicture.asset( + _currency!.currencyIconPath, + height: 25, + width: 25, + ), + ) + : null)), + const SizedBox(height: 12), + if (_currency != null) + StreamBuilder( + stream: ExchangeRateService.instance + .getLastExchangeRateOf(currencyCode: _currency!.code), + builder: (context, snapshot) { + if (snapshot.hasData || + _currency?.code == _userPrCurrency?.code) { + return Container(); + } else { + return InlineInfoCard( + text: t.account.form.currency_not_found_warn, + mode: InlineInfoCardMode.warn); + } + }), + StreamBuilder( + stream: _accountToEdit == null + ? Stream.value(true) + : TransactionService.instance + .countTransactions( + predicate: TransactionFilters( + transactionTypes: [ + TransactionType.expense, + TransactionType.income + ], + accountsIDs: [_accountToEdit!.id], ), ) - ], + .map((event) => event.numberOfRes == 0), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data! == false) { + return Container(); + } + + return Column( + children: [ + const SizedBox(height: 12), + AccountTypeSelector( + selectedType: _type, + onSelected: (newType) { + setState(() { + _type = newType; + }); + }) + ], + ); + }, + ), + const SizedBox(height: 16), + SingleExpansionPanel( + child: Column( + children: [ + const SizedBox(height: 12), + DateTimeFormField( + decoration: InputDecoration( + suffixIcon: const Icon(Icons.event), + labelText: '${t.account.date} *', + ), + initialDate: _openingDate, + dateFormat: DateFormat.yMMMd().add_jm(), + lastDate: _closeDate ?? DateTime.now(), + validator: (e) => + e == null ? t.general.validations.required : null, + onDateSelected: (DateTime value) { + setState(() { + _openingDate = value; + }); + }, ), - const SizedBox(height: 20), + const SizedBox(height: 22), + if (_accountToEdit != null && + _accountToEdit!.isClosed) ...[ + DateTimeFormField( + decoration: InputDecoration( + suffixIcon: const Icon(Icons.event), + labelText: t.account.close_date, + ), + initialDate: _closeDate, + firstDate: _openingDate, + lastDate: DateTime.now(), + dateFormat: DateFormat.yMMMd().add_jm(), + onDateSelected: (DateTime value) { + setState(() { + _closeDate = value; + }); + }, + ), + const SizedBox(height: 22), + ], TextFormField( - controller: _balanceController, + controller: _ibanController, decoration: InputDecoration( - labelText: widget.account != null - ? '${t.account.form.current_balance} *' - : '${t.account.form.initial_balance} *', - hintText: 'Ex.: 200', - suffixText: _currency?.symbol, + labelText: t.account.form.iban, ), - keyboardType: TextInputType.number, - enabled: !(widget.account != null && - widget.account!.isClosed), - inputFormatters: decimalDigitFormatter, - validator: (value) => fieldValidator(value, - validator: ValidatorType.double, isRequired: true), - autovalidateMode: AutovalidateMode.onUserInteraction, textInputAction: TextInputAction.next, ), - const SizedBox(height: 20), - TextField( - controller: TextEditingController( - text: _currency != null - ? _currency?.name - : t.general.unspecified), - readOnly: true, - onTap: () { - if (_currency == null) return; - - showCurrencySelectorModal( - context, - CurrencySelectorModal( - preselectedCurrency: _currency!, - onCurrencySelected: (newCurrency) { - setState(() { - _currency = newCurrency; - }); - }), - ); - }, - decoration: InputDecoration( - labelText: t.currencies.currency, - suffixIcon: const Icon(Icons.arrow_drop_down), - prefixIcon: _currency != null - ? Container( - margin: const EdgeInsets.all(10), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(100)), - child: SvgPicture.asset( - _currency!.currencyIconPath, - height: 25, - width: 25, - ), - ) - : null)), - const SizedBox(height: 12), - if (_currency != null) - StreamBuilder( - stream: ExchangeRateService.instance - .getLastExchangeRateOf( - currencyCode: _currency!.code), - builder: (context, snapshot) { - if (snapshot.hasData || - _currency?.code == _userPrCurrency?.code) { - return Container(); - } else { - return InlineInfoCard( - text: - t.account.form.currency_not_found_warn, - mode: InlineInfoCardMode.warn); - } - }), - StreamBuilder( - stream: _accountToEdit == null - ? Stream.value(true) - : TransactionService.instance - .countTransactions( - predicate: TransactionFilters( - transactionTypes: [ - TransactionType.expense, - TransactionType.income - ], - accountsIDs: [_accountToEdit!.id], - ), - ) - .map((event) => event.numberOfRes == 0), - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data! == false) { - return Container(); - } - - return Column( - children: [ - const SizedBox(height: 12), - AccountTypeSelector( - selectedType: _type, - onSelected: (newType) { - setState(() { - _type = newType; - }); - }) - ], - ); - }, + const SizedBox(height: 22), + TextFormField( + controller: _swiftController, + decoration: InputDecoration( + labelText: t.account.form.swift, + ), + textInputAction: TextInputAction.next, ), - const SizedBox(height: 16), - SingleExpansionPanel( - child: Column( - children: [ - const SizedBox(height: 12), - DateTimeFormField( - decoration: InputDecoration( - suffixIcon: const Icon(Icons.event), - labelText: '${t.account.date} *', - ), - initialDate: _openingDate, - dateFormat: DateFormat.yMMMd().add_jm(), - lastDate: _closeDate ?? DateTime.now(), - validator: (e) => e == null - ? t.general.validations.required - : null, - onDateSelected: (DateTime value) { - setState(() { - _openingDate = value; - }); - }, - ), - const SizedBox(height: 22), - if (_accountToEdit != null && - _accountToEdit!.isClosed) ...[ - DateTimeFormField( - decoration: InputDecoration( - suffixIcon: const Icon(Icons.event), - labelText: t.account.close_date, - ), - initialDate: _closeDate, - firstDate: _openingDate, - lastDate: DateTime.now(), - dateFormat: DateFormat.yMMMd().add_jm(), - onDateSelected: (DateTime value) { - setState(() { - _closeDate = value; - }); - }, - ), - const SizedBox(height: 22), - ], - TextFormField( - controller: _ibanController, - decoration: InputDecoration( - labelText: t.account.form.iban, - ), - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 22), - TextFormField( - controller: _swiftController, - decoration: InputDecoration( - labelText: t.account.form.swift, - ), - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 22), - TextFormField( - minLines: 2, - maxLines: 10, - controller: _textController, - decoration: InputDecoration( - labelText: t.account.form.notes, - hintText: t.account.form.notes_placeholder, - alignLabelWithHint: true, - ), - textInputAction: TextInputAction.next, - ), - const SizedBox(height: 22), - ], + const SizedBox(height: 22), + TextFormField( + minLines: 2, + maxLines: 10, + controller: _textController, + decoration: InputDecoration( + labelText: t.account.form.notes, + hintText: t.account.form.notes_placeholder, + alignLabelWithHint: true, ), + textInputAction: TextInputAction.next, ), + const SizedBox(height: 22), ], ), ), - ), + ], ), + ), + ); + }), ); } } diff --git a/lib/app/categories/form/category_form.dart b/lib/app/categories/form/category_form.dart index 50351813..936a4769 100644 --- a/lib/app/categories/form/category_form.dart +++ b/lib/app/categories/form/category_form.dart @@ -1,16 +1,16 @@ import 'package:drift/drift.dart' as drift; import 'package:flutter/material.dart'; import 'package:monekin/app/categories/form/category_form_functions.dart'; +import 'package:monekin/app/categories/form/icon_and_color_selector.dart'; import 'package:monekin/core/database/app_db.dart'; import 'package:monekin/core/database/services/category/category_service.dart'; import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/extensions/lists.extensions.dart'; import 'package:monekin/core/models/category/category.dart'; import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; import 'package:monekin/core/models/supported-icon/supported_icon.dart'; -import 'package:monekin/core/presentation/widgets/color_picker.dart'; -import 'package:monekin/core/presentation/widgets/icon_selector_modal.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; import 'package:monekin/core/presentation/widgets/persistent_footer_button.dart'; -import 'package:monekin/core/services/supported_icon/supported_icon_service.dart'; import 'package:monekin/core/utils/constants.dart'; import 'package:monekin/core/utils/text_field_utils.dart'; import 'package:monekin/core/utils/uuid.dart'; @@ -33,9 +33,8 @@ class _CategoryFormPageState extends State { final TextEditingController _nameController = TextEditingController(); - SupportedIcon _icon = SupportedIconService.instance.defaultSupportedIcon; - - String _color = '000000'; + SupportedIcon _icon = Category.unkown().icon; + String _color = defaultColorPickerOptions.randomItem(); CategoryType _type = CategoryType.E; @override @@ -211,59 +210,45 @@ class _CategoryFormPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconDisplayer( - mainColor: ColorHex.get(_color).lighten( - Theme.of(context).brightness == - Brightness.dark - ? 0.8 - : 0), - secondaryColor: ColorHex.get(_color).lighten( - Theme.of(context).brightness == - Brightness.dark - ? 0 - : 0.8), - supportedIcon: _icon, - size: 48, - isOutline: true, - outlineWidth: 1, - padding: 6, - borderRadius: 4, - onTap: () { - showIconSelectorModal( - context, - IconSelectorModal( - preselectedIconID: _icon.id, - subtitle: t - .icon_selector.select_category_icon, - onIconSelected: (selectedIcon) { - setState(() { - _icon = selectedIcon; - }); - }, - ), - ); - }, + IconAndColorSelector( + iconSelectorModalSubtitle: + t.icon_selector.select_category_icon, + iconDisplayer: IconDisplayer.fromCategory( + context, + category: Category.fromDB( + Category.unkown().copyWith( + iconId: _icon.id, + color: drift.Value(_color)), + null, ), - const SizedBox(width: 10), - Expanded( - child: TextFormField( - controller: _nameController, - maxLength: maxLabelLenghtForDisplayNames, - decoration: InputDecoration( - labelText: '${t.categories.name} *', - hintText: 'Ex.: Food', - ), - validator: (value) => - fieldValidator(value, isRequired: true), - autovalidateMode: - AutovalidateMode.onUserInteraction, - textInputAction: TextInputAction.next, - ), - ) - ], + isOutline: true, + size: 48, + padding: 6, + ), + onDataChange: ((data) { + setState(() { + _icon = data.icon; + _color = data.color.toHex(); + }); + }), + data: ( + color: ColorHex.get(_color), + icon: _icon, + ), + ), + const SizedBox(height: 20), + TextFormField( + controller: _nameController, + maxLength: maxLabelLenghtForDisplayNames, + decoration: InputDecoration( + labelText: '${t.categories.name} *', + hintText: 'Ex.: Food', + ), + validator: (value) => + fieldValidator(value, isRequired: true), + autovalidateMode: + AutovalidateMode.onUserInteraction, + textInputAction: TextInputAction.next, ), const SizedBox(height: 14), DropdownButtonFormField( @@ -293,22 +278,11 @@ class _CategoryFormPageState extends State { }); }, ), - const SizedBox(height: 24), - Text(t.icon_selector.color) + const SizedBox(height: 16), ], ), ), ), - ColorPicker( - colorOptions: colorOptions, - selectedColor: _color, - onColorSelected: (selectedColor) { - setState(() { - _color = selectedColor; - }); - }, - ), - const SizedBox(height: 6), if (widget.categoryUUID != null) ...[ Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/app/categories/form/icon_and_color_selector.dart b/lib/app/categories/form/icon_and_color_selector.dart new file mode 100644 index 00000000..0f5e887c --- /dev/null +++ b/lib/app/categories/form/icon_and_color_selector.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; +import 'package:monekin/core/models/supported-icon/supported_icon.dart'; +import 'package:monekin/core/presentation/app_colors.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker_modal.dart'; +import 'package:monekin/core/presentation/widgets/icon_selector_modal.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +class IconAndColorSelector extends StatelessWidget { + const IconAndColorSelector( + {super.key, + required this.iconSelectorModalSubtitle, + required this.iconDisplayer, + required this.onDataChange, + required this.data}); + + final String iconSelectorModalSubtitle; + + final void Function(({SupportedIcon icon, Color color}) data) onDataChange; + + final ({SupportedIcon icon, Color color}) data; + final IconDisplayer iconDisplayer; + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: AppColors.of(context).inputFill, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), + child: iconDisplayer, + ), + Flexible( + child: Column( + children: [ + Tappable( + onTap: () { + showIconSelectorModal( + context, + IconSelectorModal( + preselectedIconID: data.icon.id, + subtitle: iconSelectorModalSubtitle, + onIconSelected: (selectedIcon) { + onDataChange((color: data.color, icon: selectedIcon)); + }, + ), + ); + }, + bgColor: AppColors.of(context).inputFill, + child: ListTile( + title: Text(t.icon_selector.icon), + trailing: + const Icon(Icons.arrow_forward_ios_rounded, size: 12), + ), + ), + Divider(color: AppColors.of(context).inputFill.darken()), + Tappable( + onTap: () => showColorPickerModal( + context, + ColorPickerModal( + colorOptions: defaultColorPickerOptions, + selectedColor: data.color.toHex(), + ), + ).then((selColor) { + if (selColor == null) return; + + onDataChange((color: selColor, icon: data.icon)); + }), + bgColor: AppColors.of(context).inputFill, + child: ListTile( + title: Text(t.icon_selector.color), + trailing: Icon( + Icons.circle, + color: data.color, + ), + ), + ), + ], + ), + ) + ], + ), + ); + } +} diff --git a/lib/app/categories/subcategory_form.dart b/lib/app/categories/subcategory_form.dart index 4ac98693..f88b0ffd 100644 --- a/lib/app/categories/subcategory_form.dart +++ b/lib/app/categories/subcategory_form.dart @@ -1,4 +1,8 @@ +import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/models/category/category.dart'; +import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; import 'package:monekin/core/models/supported-icon/supported_icon.dart'; import 'package:monekin/core/presentation/widgets/bottomSheetFooter.dart'; import 'package:monekin/core/presentation/widgets/icon_selector_modal.dart'; @@ -64,7 +68,15 @@ class _SubcategoryFormDialogState extends State { body: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - InkWell( + IconDisplayer.fromCategory( + context, + category: Category.fromDB( + Category.unkown().copyWith( + color: Value(widget.color.toHex()), + iconId: _icon.id, + ), + null), + size: 32, onTap: () => showIconSelectorModal( context, IconSelectorModal( @@ -77,20 +89,8 @@ class _SubcategoryFormDialogState extends State { }, ), ), - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: _color.withOpacity(0.05), - border: Border.all( - width: 1, - color: _color, - ), - borderRadius: const BorderRadius.all(Radius.circular(6))), - child: _icon.display(size: 50, color: _color)), - ), - const SizedBox( - width: 20, ), + const SizedBox(width: 12), Expanded( child: Form( key: _formKey, diff --git a/lib/app/settings/appearance_settings_page.dart b/lib/app/settings/appearance_settings_page.dart index eb54b88a..02d903c1 100644 --- a/lib/app/settings/appearance_settings_page.dart +++ b/lib/app/settings/appearance_settings_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:monekin/core/database/services/user-setting/user_setting_service.dart'; -import 'package:monekin/core/presentation/widgets/color_picker.dart'; import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker_modal.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; import 'package:monekin/i18n/translations.g.dart'; import '../../core/presentation/app_colors.dart'; @@ -91,8 +93,6 @@ class _AdvancedSettingsPageState extends State { }); } - final ExpansionTileController expTileController = ExpansionTileController(); - @override Widget build(BuildContext context) { final t = Translations.of(context); @@ -173,10 +173,6 @@ class _AdvancedSettingsPageState extends State { subtitle: Text(t.settings.dynamic_colors_descr), value: snapshot.data!, onChanged: (bool value) { - if (value) { - expTileController.collapse(); - } - setState(() { UserSettingService.instance.setSetting( SettingKey.accentColor, @@ -200,41 +196,46 @@ class _AdvancedSettingsPageState extends State { color = ColorHex.get(snapshot.data!); } - return ExpansionTile( - title: Text(t.settings.accent_color), - subtitle: Text(t.settings.accent_color_descr), - controller: expTileController, - enabled: snapshot.data! != 'auto', - trailing: SizedBox( - height: 46, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - clipBehavior: Clip.hardEdge, - width: 46, + return Tappable( + onTap: snapshot.data! == 'auto' + ? null + : () => showColorPickerModal( + context, + ColorPickerModal( + colorOptions: [ + brandBlue.toHex(leadingHashSign: false), + ...defaultColorPickerOptions + ], + selectedColor: color.toHex(), + ), + ).then((value) { + if (value == null) return; + + setState(() { + UserSettingService.instance.setSetting( + SettingKey.accentColor, value.toHex()); + }); + }), + child: ListTile( + title: Text(t.settings.accent_color), + subtitle: Text(t.settings.accent_color_descr), + enabled: snapshot.data! != 'auto', + trailing: SizedBox( height: 46, - decoration: BoxDecoration( - color: color.withOpacity( - snapshot.data! != 'auto' ? 1 : 0.4, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + clipBehavior: Clip.hardEdge, + width: 46, + height: 46, + decoration: BoxDecoration( + color: color.withOpacity( + snapshot.data! != 'auto' ? 1 : 0.4, + ), + borderRadius: BorderRadius.circular(100), ), - borderRadius: BorderRadius.circular(100), ), ), ), - children: [ - ColorPicker( - colorOptions: [ - brandBlue.toHex(leadingHashSign: false), - ...colorOptions - ], - selectedColor: color.toHex(), - onColorSelected: (selectedColor) { - setState(() { - UserSettingService.instance.setSetting( - SettingKey.accentColor, selectedColor); - }); - }, - ), - ], ); }), ], diff --git a/lib/app/tags/tag_form_page.dart b/lib/app/tags/tag_form_page.dart index 74dbccff..eef8a48f 100644 --- a/lib/app/tags/tag_form_page.dart +++ b/lib/app/tags/tag_form_page.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:monekin/core/database/app_db.dart'; import 'package:monekin/core/database/services/tags/tags_service.dart'; import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/extensions/lists.extensions.dart'; import 'package:monekin/core/models/tags/tag.dart'; -import 'package:monekin/core/presentation/widgets/color_picker.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker.dart'; +import 'package:monekin/core/presentation/widgets/color_picker/color_picker_modal.dart'; import 'package:monekin/core/presentation/widgets/confirm_dialog.dart'; import 'package:monekin/core/presentation/widgets/persistent_footer_button.dart'; import 'package:monekin/core/utils/constants.dart'; @@ -39,7 +41,7 @@ class _TagFormPageState extends State { _descrController.value = TextEditingValue(text: widget.tag?.description ?? ''); - _color = widget.tag?.color ?? '000000'; + _color = widget.tag?.color ?? defaultColorPickerOptions.randomItem(); } submitForm() async { @@ -138,64 +140,72 @@ class _TagFormPageState extends State { ) ], body: SingleChildScrollView( + padding: const EdgeInsets.all(16), child: Column( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Tag.icon, color: _colorObj, size: 48), - const SizedBox(width: 20), - Expanded( - child: TextFormField( - controller: _nameController, - maxLength: maxLabelLenghtForDisplayNames, - decoration: InputDecoration( - labelText: '${t.tags.form.name} *', - hintText: 'Ex.: Food', - ), - validator: (value) => - fieldValidator(value, isRequired: true), - autovalidateMode: - AutovalidateMode.onUserInteraction, - textInputAction: TextInputAction.next, + Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Tag.icon, color: _colorObj, size: 48), + const SizedBox(width: 20), + Expanded( + child: TextFormField( + controller: _nameController, + maxLength: maxLabelLenghtForDisplayNames, + decoration: InputDecoration( + labelText: '${t.tags.form.name} *', + hintText: 'Ex.: Food', ), - ) - ], + validator: (value) => + fieldValidator(value, isRequired: true), + autovalidateMode: AutovalidateMode.onUserInteraction, + textInputAction: TextInputAction.next, + ), + ) + ], + ), + const SizedBox(height: 16), + TextFormField( + readOnly: true, + decoration: InputDecoration( + hintText: t.icon_selector.color, + suffixIcon: Icon(Icons.circle), + suffixIconColor: ColorHex.get(_color), ), - const SizedBox(height: 12), - TextFormField( - controller: _descrController, - maxLines: 2, - decoration: InputDecoration( - labelText: t.tags.form.description, - hintText: 'Ex.: Food', - alignLabelWithHint: true, + textInputAction: TextInputAction.next, + onTap: () => showColorPickerModal( + context, + ColorPickerModal( + colorOptions: defaultColorPickerOptions, + selectedColor: _color, ), - autovalidateMode: AutovalidateMode.onUserInteraction, - textInputAction: TextInputAction.next, + ).then((value) { + if (value == null) return; + + setState(() { + _color = value.toHex(); + }); + }), + ), + const SizedBox(height: 12), + TextFormField( + controller: _descrController, + maxLines: 2, + decoration: InputDecoration( + hintText: t.tags.form.description, + alignLabelWithHint: true, ), - const SizedBox(height: 12), - Text(t.icon_selector.color), - ], - ), + autovalidateMode: AutovalidateMode.onUserInteraction, + textInputAction: TextInputAction.next, + ), + ], ), ), - ColorPicker( - colorOptions: colorOptions, - selectedColor: _color, - onColorSelected: (selectedColor) { - setState(() { - _color = selectedColor; - }); - }, - ), ], ), ), diff --git a/lib/app/transactions/form/transaction_form.page.dart b/lib/app/transactions/form/transaction_form.page.dart index 4aefc096..960cb70a 100644 --- a/lib/app/transactions/form/transaction_form.page.dart +++ b/lib/app/transactions/form/transaction_form.page.dart @@ -23,6 +23,7 @@ import 'package:monekin/core/presentation/widgets/expansion_panel/single_expansi import 'package:monekin/core/presentation/widgets/inline_info_card.dart'; import 'package:monekin/core/presentation/widgets/number_ui_formatters/currency_displayer.dart'; import 'package:monekin/core/presentation/widgets/persistent_footer_button.dart'; +import 'package:monekin/core/presentation/widgets/tappable.dart'; import 'package:monekin/core/presentation/widgets/transaction_filter/status_filter/transaction_status_filter.dart'; import 'package:monekin/core/presentation/widgets/transaction_filter/tags_filter/tags_filter_container.dart'; import 'package:monekin/core/utils/constants.dart'; @@ -98,7 +99,7 @@ class _TransactionFormPageState extends State { onTap: () => onClick(), borderRadius: BorderRadius.circular(8), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -356,7 +357,7 @@ class _TransactionFormPageState extends State { ), ), ), - const SizedBox(height: 12), + const SizedBox(height: 16), if (widget.mode == TransactionFormMode.transfer) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), @@ -557,9 +558,9 @@ class _TransactionFormPageState extends State { buildAmountContainer(context), const SizedBox(height: 18), buildAccoutAndCategorySelectorRow(context), - const SizedBox(height: 12), + const SizedBox(height: 16), buildTransactionDateSelector(), - const SizedBox(height: 12), + const SizedBox(height: 16), buildTitleField(), ], ), @@ -601,8 +602,9 @@ class _TransactionFormPageState extends State { horizontal: 16, vertical: 4), child: Column( children: [ + const SizedBox(height: 4), buildTransactionDateSelector(), - const SizedBox(height: 12), + const SizedBox(height: 16), buildTitleField(), ], ), @@ -648,14 +650,11 @@ class _TransactionFormPageState extends State { } Widget buildAmountContainer(BuildContext context) { - return InkWell( - borderRadius: BorderRadius.circular(6), + return Tappable( + borderRadius: 12, + bgColor: currentTransactionTypeToAdd.color(context).withOpacity(0.85), onTap: () => displayAmountModal(context), - child: Container( - decoration: BoxDecoration( - color: currentTransactionTypeToAdd.color(context).withOpacity(0.85), - borderRadius: BorderRadius.circular(6), - ), + child: Padding( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -697,10 +696,14 @@ class _TransactionFormPageState extends State { DateTimeFormField( decoration: InputDecoration( suffixIcon: const Icon(Icons.event), - labelText: '${t.general.time.date} *', + labelText: recurrentRule.isNoRecurrent + ? null + : '${t.general.time.start_date} *', ), initialDate: date, - dateFormat: DateFormat.yMMMd().add_jm(), + dateFormat: date.year == currentYear + ? DateFormat.MMMMd().add_jm() + : DateFormat.yMMMd().add_jm(), validator: (e) => e == null ? t.general.validations.required : null, onDateSelected: (DateTime value) { setState(() { @@ -729,8 +732,8 @@ class _TransactionFormPageState extends State { Card buildAccoutAndCategorySelectorRow(BuildContext context) { return Card( shape: RoundedRectangleBorder( - side: BorderSide(color: Theme.of(context).dividerColor, width: 1), - borderRadius: BorderRadius.circular(6), + side: BorderSide(color: Theme.of(context).dividerColor, width: 2), + borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(0), elevation: 0, @@ -806,18 +809,12 @@ class _TransactionFormPageState extends State { child: selector( title: t.general.category, inputValue: selectedCategory?.name, - icon: selectedCategory != null - ? IconDisplayer.fromCategory( - context, - category: selectedCategory!, - size: 24, - ) - : IconDisplayer( - icon: Icons.question_mark_rounded, - mainColor: AppColors.of(context).primary, - size: 24, - borderRadius: 99999, - ), + icon: IconDisplayer.fromCategory( + context, + category: selectedCategory ?? + Category.fromDB(Category.unkown(), null), + size: 24, + ), onClick: () => selectCategory(), ), ), diff --git a/lib/app/transactions/form/widgets/interval_selector.dart b/lib/app/transactions/form/widgets/interval_selector.dart index 71b1a052..3932eae3 100644 --- a/lib/app/transactions/form/widgets/interval_selector.dart +++ b/lib/app/transactions/form/widgets/interval_selector.dart @@ -1,4 +1,3 @@ -import 'package:monekin/core/routes/route_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; diff --git a/lib/core/database/app_db.g.dart b/lib/core/database/app_db.g.dart index 6d2da593..7c9d8c88 100644 --- a/lib/core/database/app_db.g.dart +++ b/lib/core/database/app_db.g.dart @@ -480,8 +480,7 @@ class AccountInDB extends DataClass implements Insertable { map['description'] = Variable(description); } { - final converter = Accounts.$convertertype; - map['type'] = Variable(converter.toSql(type)); + map['type'] = Variable(Accounts.$convertertype.toSql(type)); } map['iconId'] = Variable(iconId); map['displayOrder'] = Variable(displayOrder); @@ -776,9 +775,7 @@ class AccountsCompanion extends UpdateCompanion { map['description'] = Variable(description.value); } if (type.present) { - final converter = Accounts.$convertertype; - - map['type'] = Variable(converter.toSql(type.value)); + map['type'] = Variable(Accounts.$convertertype.toSql(type.value)); } if (iconId.present) { map['iconId'] = Variable(iconId.value); @@ -1012,8 +1009,7 @@ class CategoryInDB extends DataClass implements Insertable { } map['displayOrder'] = Variable(displayOrder); if (!nullToAbsent || type != null) { - final converter = Categories.$convertertypen; - map['type'] = Variable(converter.toSql(type)); + map['type'] = Variable(Categories.$convertertypen.toSql(type)); } if (!nullToAbsent || parentCategoryID != null) { map['parentCategoryID'] = Variable(parentCategoryID); @@ -1208,9 +1204,8 @@ class CategoriesCompanion extends UpdateCompanion { map['displayOrder'] = Variable(displayOrder.value); } if (type.present) { - final converter = Categories.$convertertypen; - - map['type'] = Variable(converter.toSql(type.value)); + map['type'] = + Variable(Categories.$convertertypen.toSql(type.value)); } if (parentCategoryID.present) { map['parentCategoryID'] = Variable(parentCategoryID.value); @@ -1584,8 +1579,8 @@ class TransactionInDB extends DataClass implements Insertable { map['notes'] = Variable(notes); } if (!nullToAbsent || status != null) { - final converter = Transactions.$converterstatusn; - map['status'] = Variable(converter.toSql(status)); + map['status'] = + Variable(Transactions.$converterstatusn.toSql(status)); } if (!nullToAbsent || categoryID != null) { map['categoryID'] = Variable(categoryID); @@ -1598,8 +1593,8 @@ class TransactionInDB extends DataClass implements Insertable { } map['isHidden'] = Variable(isHidden); if (!nullToAbsent || intervalPeriod != null) { - final converter = Transactions.$converterintervalPeriodn; - map['intervalPeriod'] = Variable(converter.toSql(intervalPeriod)); + map['intervalPeriod'] = Variable( + Transactions.$converterintervalPeriodn.toSql(intervalPeriod)); } if (!nullToAbsent || intervalEach != null) { map['intervalEach'] = Variable(intervalEach); @@ -1954,9 +1949,8 @@ class TransactionsCompanion extends UpdateCompanion { map['notes'] = Variable(notes.value); } if (status.present) { - final converter = Transactions.$converterstatusn; - - map['status'] = Variable(converter.toSql(status.value)); + map['status'] = + Variable(Transactions.$converterstatusn.toSql(status.value)); } if (categoryID.present) { map['categoryID'] = Variable(categoryID.value); @@ -1971,10 +1965,8 @@ class TransactionsCompanion extends UpdateCompanion { map['isHidden'] = Variable(isHidden.value); } if (intervalPeriod.present) { - final converter = Transactions.$converterintervalPeriodn; - - map['intervalPeriod'] = - Variable(converter.toSql(intervalPeriod.value)); + map['intervalPeriod'] = Variable( + Transactions.$converterintervalPeriodn.toSql(intervalPeriod.value)); } if (intervalEach.present) { map['intervalEach'] = Variable(intervalEach.value); @@ -2964,8 +2956,8 @@ class BudgetInDB extends DataClass implements Insertable { map['name'] = Variable(name); map['limitAmount'] = Variable(limitAmount); if (!nullToAbsent || intervalPeriod != null) { - final converter = Budgets.$converterintervalPeriodn; - map['intervalPeriod'] = Variable(converter.toSql(intervalPeriod)); + map['intervalPeriod'] = Variable( + Budgets.$converterintervalPeriodn.toSql(intervalPeriod)); } if (!nullToAbsent || startDate != null) { map['startDate'] = Variable(startDate); @@ -3144,10 +3136,8 @@ class BudgetsCompanion extends UpdateCompanion { map['limitAmount'] = Variable(limitAmount.value); } if (intervalPeriod.present) { - final converter = Budgets.$converterintervalPeriodn; - - map['intervalPeriod'] = - Variable(converter.toSql(intervalPeriod.value)); + map['intervalPeriod'] = Variable( + Budgets.$converterintervalPeriodn.toSql(intervalPeriod.value)); } if (startDate.present) { map['startDate'] = Variable(startDate.value); @@ -3868,8 +3858,8 @@ class UserSetting extends DataClass implements Insertable { Map toColumns(bool nullToAbsent) { final map = {}; { - final converter = UserSettings.$convertersettingKey; - map['settingKey'] = Variable(converter.toSql(settingKey)); + map['settingKey'] = + Variable(UserSettings.$convertersettingKey.toSql(settingKey)); } if (!nullToAbsent || settingValue != null) { map['settingValue'] = Variable(settingValue); @@ -3973,9 +3963,8 @@ class UserSettingsCompanion extends UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (settingKey.present) { - final converter = UserSettings.$convertersettingKey; - - map['settingKey'] = Variable(converter.toSql(settingKey.value)); + map['settingKey'] = Variable( + UserSettings.$convertersettingKey.toSql(settingKey.value)); } if (settingValue.present) { map['settingValue'] = Variable(settingValue.value); @@ -4072,8 +4061,8 @@ class AppDataData extends DataClass implements Insertable { Map toColumns(bool nullToAbsent) { final map = {}; { - final converter = AppData.$converterappDataKey; - map['appDataKey'] = Variable(converter.toSql(appDataKey)); + map['appDataKey'] = + Variable(AppData.$converterappDataKey.toSql(appDataKey)); } if (!nullToAbsent || appDataValue != null) { map['appDataValue'] = Variable(appDataValue); @@ -4177,9 +4166,8 @@ class AppDataCompanion extends UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (appDataKey.present) { - final converter = AppData.$converterappDataKey; - - map['appDataKey'] = Variable(converter.toSql(appDataKey.value)); + map['appDataKey'] = Variable( + AppData.$converterappDataKey.toSql(appDataKey.value)); } if (appDataValue.present) { map['appDataValue'] = Variable(appDataValue.value); @@ -4266,6 +4254,7 @@ abstract class _$AppDB extends GeneratedDatabase { description: row.readNullable('description'), iban: row.readNullable('iban'), swift: row.readNullable('swift'), + color: row.readNullable('color'), )); } diff --git a/lib/core/database/sql/initial/tables.drift b/lib/core/database/sql/initial/tables.drift index f1e1ab31..1827834f 100644 --- a/lib/core/database/sql/initial/tables.drift +++ b/lib/core/database/sql/initial/tables.drift @@ -4,7 +4,8 @@ ---- * --- Remove the imports import './../../../models/transaction/transaction.dart'; -import './../../../models/transaction/transaction_status.dart'; +import './../../../models/transaction/transaction_status.enum.dart'; +import './../../../models/transaction/transaction_type.enum.dart'; import './../../../models/date-utils/periodicity.dart'; import './../../../models/category/category.dart'; import './../../../models/account/account.dart'; diff --git a/lib/core/extensions/lists.extensions.dart b/lib/core/extensions/lists.extensions.dart new file mode 100644 index 00000000..f12adb2a --- /dev/null +++ b/lib/core/extensions/lists.extensions.dart @@ -0,0 +1,7 @@ +import 'dart:math'; + +extension RandomListItem on List { + T randomItem() { + return this[Random().nextInt(length)]; + } +} diff --git a/lib/core/models/account/account.dart b/lib/core/models/account/account.dart index bf78c55d..a5d6a810 100644 --- a/lib/core/models/account/account.dart +++ b/lib/core/models/account/account.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:monekin/core/database/app_db.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; import 'package:monekin/core/models/supported-icon/icon_displayer.dart'; import 'package:monekin/core/models/supported-icon/supported_icon.dart'; import 'package:monekin/core/presentation/app_colors.dart'; import 'package:monekin/core/services/supported_icon/supported_icon_service.dart'; -import 'package:monekin/core/extensions/color.extensions.dart'; import 'package:monekin/i18n/translations.g.dart'; enum AccountType { @@ -63,6 +63,7 @@ class Account extends AccountInDB { super.description, super.iban, super.swift, + super.color, }) : super(currencyId: currency.code); /// Currency of all the transactions of this account. When you change this currency all transactions in this account @@ -77,7 +78,7 @@ class Account extends AccountInDB { return color != null ? ColorHex.get(color!) : Theme.of(context).brightness == Brightness.dark - ? AppColors.of(context).onPrimary + ? AppColors.of(context).primaryContainer : AppColors.of(context).primary; } diff --git a/lib/core/models/category/category.dart b/lib/core/models/category/category.dart index 471db6bd..f3a44341 100644 --- a/lib/core/models/category/category.dart +++ b/lib/core/models/category/category.dart @@ -71,11 +71,21 @@ class Category extends CategoryInDB { static Category fromDB(CategoryInDB cat, CategoryInDB? parentCategory) => Category( - id: cat.id, - displayOrder: cat.displayOrder, - name: cat.name, - iconId: cat.iconId, - parentCategory: parentCategory, - color: cat.color, - type: cat.type); + id: cat.id, + displayOrder: cat.displayOrder, + name: cat.name, + iconId: cat.iconId, + parentCategory: parentCategory, + color: cat.color, + type: cat.type, + ); + + static Category unkown() => Category( + id: 'unknown-category', + displayOrder: 1000, + iconId: SupportedIconService.instance.defaultSupportedIcon.id, + name: 'Unknown Category', + type: CategoryType.B, + color: '737373', + ); } diff --git a/lib/core/presentation/app_colors.dart b/lib/core/presentation/app_colors.dart index 65a57178..39d97dea 100644 --- a/lib/core/presentation/app_colors.dart +++ b/lib/core/presentation/app_colors.dart @@ -13,6 +13,7 @@ class AppColors extends ThemeExtension { required this.shadowColor, required this.shadowColorLight, required this.brand, + required this.inputFill, required this.primary, required this.onPrimary, required this.primaryContainer, @@ -24,6 +25,7 @@ class AppColors extends ThemeExtension { final Color danger; final Color success; final Color brand; + final Color inputFill; final Color light; final Color dark; final Color shadowColor; @@ -62,6 +64,8 @@ class AppColors extends ThemeExtension { ? const Color.fromARGB(40, 116, 116, 116) : const Color.fromARGB(44, 90, 90, 90), + inputFill: colorScheme.surfaceVariant, + // Colors from the material color scheme: primary: colorScheme.primary, onPrimary: colorScheme.onPrimary, @@ -82,6 +86,7 @@ class AppColors extends ThemeExtension { Color? success, Color? brand, Color? primary, + Color? inputFill, Color? dark, Color? light, Color? shadowColor, @@ -94,6 +99,7 @@ class AppColors extends ThemeExtension { return AppColors( danger: danger ?? this.danger, success: success ?? this.success, + inputFill: inputFill ?? this.inputFill, light: light ?? this.light, dark: dark ?? this.dark, brand: brand ?? this.brand, @@ -116,6 +122,7 @@ class AppColors extends ThemeExtension { return AppColors( danger: Color.lerp(danger, other.danger, t) ?? danger, success: Color.lerp(success, other.success, t) ?? success, + inputFill: Color.lerp(inputFill, other.inputFill, t) ?? inputFill, light: Color.lerp(light, other.light, t) ?? light, dark: Color.lerp(dark, other.dark, t) ?? dark, shadowColor: Color.lerp(shadowColor, other.shadowColor, t) ?? shadowColor, diff --git a/lib/core/presentation/theme.dart b/lib/core/presentation/theme.dart index eafd5a46..60142a00 100644 --- a/lib/core/presentation/theme.dart +++ b/lib/core/presentation/theme.dart @@ -65,8 +65,20 @@ ThemeData getThemeData( return theme.copyWith( dividerTheme: const DividerThemeData(space: 0), cardColor: theme.colorScheme.surface, - inputDecorationTheme: const InputDecorationTheme( - border: OutlineInputBorder(), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: theme.colorScheme.surfaceVariant, + isDense: true, + floatingLabelStyle: TextStyle( + backgroundColor: theme.colorScheme.background.withOpacity(0.5), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), ), floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: theme.colorScheme.primary, diff --git a/lib/core/presentation/widgets/color_picker.dart b/lib/core/presentation/widgets/color_picker.dart deleted file mode 100644 index 60e8ae8a..00000000 --- a/lib/core/presentation/widgets/color_picker.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:monekin/core/extensions/color.extensions.dart'; - -final colorOptions = [ - 'B71C1C', - 'D50000', - 'E53935', - 'EF5350', - '880E4F', - 'C51162', - 'D81B60', - 'EC407A', - '4A148C', - 'AA00FF', - '8E24AA', - 'AB47BC', - '1A237E', - '2962FF', - '2979FF', - '42A5F5', - '006064', - '00897B', - '00BFA5', - '4DB6AC', - '1B5E20', - '388E3C', - '8BC34A', - 'D4E157', - 'BF360C', - 'F4511E', - 'FB8C00', - 'FFA726', - 'E65100', - 'FFA000', - 'FFAB00', - 'FFCA28', - '546E7A', - '90A4AE', - '795548', - '757575', -]; - -class ColorPicker extends StatefulWidget { - const ColorPicker({ - super.key, - required this.colorOptions, - this.selectedColor, - this.onColorSelected, - this.padding = const EdgeInsets.only(top: 8, bottom: 16), - }); - - final List colorOptions; - - final String? selectedColor; - - final EdgeInsets padding; - - final void Function(String selectedColor)? onColorSelected; - - @override - State createState() => _ColorPickerState(); -} - -class _ColorPickerState extends State { - late String? _color; - - late ScrollController _scrollController; - - @override - void initState() { - super.initState(); - - _color = widget.selectedColor; - _scrollController = ScrollController(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 46 + widget.padding.top + widget.padding.bottom, - child: Scrollbar( - controller: _scrollController, - child: Padding( - padding: widget.padding, - child: ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - controller: _scrollController, - itemCount: widget.colorOptions.length, - itemBuilder: (BuildContext context, int index) { - final colorItem = widget.colorOptions[index]; - - return Container( - clipBehavior: Clip.hardEdge, - width: 46, - height: 46, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - ), - margin: EdgeInsets.only( - left: index == 0 ? 16 : 4, - right: index == widget.colorOptions.length - 1 ? 16 : 4), - child: Stack( - children: [ - DecoratedBox( - decoration: BoxDecoration( - color: ColorHex.get(colorItem), - ), - child: Material( - color: Colors.transparent, - child: InkWell(onTap: () { - setState(() { - _color = colorItem; - }); - - if (widget.onColorSelected != null) { - widget.onColorSelected!(colorItem); - } - }), - ), - ), - if (_color != null && - ColorHex.get('#$colorItem') == ColorHex.get(_color!)) - const DecoratedBox( - decoration: BoxDecoration( - color: Color.fromARGB(47, 255, 255, 255), - ), - child: Center( - child: Icon( - Icons.check, - color: Colors.white, - ))) - ], - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/lib/core/presentation/widgets/color_picker/color_picker.dart b/lib/core/presentation/widgets/color_picker/color_picker.dart new file mode 100644 index 00000000..4d2a6087 --- /dev/null +++ b/lib/core/presentation/widgets/color_picker/color_picker.dart @@ -0,0 +1,38 @@ +final defaultColorPickerOptions = [ + 'B71C1C', + 'D50000', + 'E53935', + 'EF5350', + '880E4F', + 'C51162', + 'D81B60', + 'EC407A', + '4A148C', + 'AA00FF', + '8E24AA', + 'AB47BC', + '1A237E', + '2962FF', + '2979FF', + '42A5F5', + '006064', + '00897B', + '00BFA5', + '4DB6AC', + '1B5E20', + '388E3C', + '8BC34A', + 'D4E157', + 'BF360C', + 'F4511E', + 'FB8C00', + 'FFA726', + 'E65100', + 'FFA000', + 'FFAB00', + 'FFCA28', + '546E7A', + '90A4AE', + '795548', + '757575', +]; diff --git a/lib/core/presentation/widgets/color_picker/color_picker_modal.dart b/lib/core/presentation/widgets/color_picker/color_picker_modal.dart new file mode 100644 index 00000000..27292a98 --- /dev/null +++ b/lib/core/presentation/widgets/color_picker/color_picker_modal.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:monekin/core/extensions/color.extensions.dart'; +import 'package:monekin/core/presentation/widgets/modal_container.dart'; +import 'package:monekin/i18n/translations.g.dart'; + +Future showColorPickerModal( + BuildContext context, ColorPickerModal component) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (context) { + return component; + }, + ); +} + +class ColorPickerModal extends StatelessWidget { + const ColorPickerModal( + {super.key, required this.colorOptions, this.selectedColor}); + + final List colorOptions; + + final String? selectedColor; + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + + return DraggableScrollableSheet( + expand: false, + maxChildSize: 0.65, + minChildSize: 0.45, + initialChildSize: 0.65, + builder: (context, scrollController) { + return ModalContainer( + title: t.icon_selector.select_color, + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + controller: scrollController, + child: Align( + alignment: Alignment.center, + heightFactor: 1, + child: Wrap( + runAlignment: WrapAlignment.center, + spacing: 6, + runSpacing: 12, + children: List.generate(colorOptions.length, (index) { + final colorItem = colorOptions[index]; + + return Container( + clipBehavior: Clip.hardEdge, + width: 52, + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999)), + child: Stack( + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: ColorHex.get(colorItem), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => Navigator.pop( + context, + ColorHex.get('#$colorItem'), + ), + ), + ), + ), + if (selectedColor != null && + ColorHex.get('#$colorItem') == + ColorHex.get(selectedColor!)) + const DecoratedBox( + decoration: BoxDecoration( + color: Color.fromARGB(47, 255, 255, 255), + ), + child: Center( + child: Icon( + Icons.check, + color: Colors.white, + ), + ), + ) + ], + ), + ); + }), + ), + ), + ), + ); + }); + } +} diff --git a/lib/core/presentation/widgets/currency_selector_modal.dart b/lib/core/presentation/widgets/currency_selector_modal.dart index d9f643b7..7f65de97 100644 --- a/lib/core/presentation/widgets/currency_selector_modal.dart +++ b/lib/core/presentation/widgets/currency_selector_modal.dart @@ -100,12 +100,13 @@ class _CurrencySelectorModalState extends State { ), TextField( decoration: InputDecoration( + filled: false, + isDense: false, hintText: t.currencies.search_placeholder, labelText: t.currencies.search_title, + floatingLabelStyle: const TextStyle(height: -0.0005), prefixIcon: const Icon(Icons.search), border: const UnderlineInputBorder(), - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), onChanged: (value) { CurrencyService.instance diff --git a/lib/core/presentation/widgets/dates/date_period_modal.dart b/lib/core/presentation/widgets/dates/date_period_modal.dart index 8ae5ec4c..215832b0 100644 --- a/lib/core/presentation/widgets/dates/date_period_modal.dart +++ b/lib/core/presentation/widgets/dates/date_period_modal.dart @@ -117,6 +117,7 @@ class _DatePeriodModalState extends State { textAlign: TextAlign.center, decoration: const InputDecoration( border: UnderlineInputBorder(), + filled: false, counterText: '', ), inputFormatters: [ @@ -218,6 +219,7 @@ class _DatePeriodModalState extends State { textAlign: TextAlign.center, decoration: InputDecoration( border: const UnderlineInputBorder(), + filled: false, hintText: '-- ${t.general.unspecified} --', ), onTap: () { diff --git a/lib/i18n/strings_en.json b/lib/i18n/strings_en.json index 31f32338..e5b39074 100644 --- a/lib/i18n/strings_en.json +++ b/lib/i18n/strings_en.json @@ -226,8 +226,9 @@ "ICON-SELECTOR": { "name": "Name:", "icon": "Icon", - "color": "Colour", + "color": "Color", "select-icon": "Select an icon", + "select-color": "Select a color", "select-account-icon": "Identify your account", "select-category-icon": "Identify your category", "SCOPES": { diff --git a/lib/i18n/strings_es.json b/lib/i18n/strings_es.json index 65e7ab49..493c4507 100644 --- a/lib/i18n/strings_es.json +++ b/lib/i18n/strings_es.json @@ -232,6 +232,7 @@ "icon": "Icono", "color": "Color", "select-icon": "Selecciona un icono", + "select-color": "Selecciona un color", "select-account-icon": "Identifica tu cuenta", "select-category-icon": "Identifica tu categoría", "SCOPES": { diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart index 99525fe5..604ccf93 100644 --- a/lib/i18n/translations.g.dart +++ b/lib/i18n/translations.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1033 (516 per locale) +/// Strings: 1035 (517 per locale) /// -/// Built on 2024-04-30 at 17:28 UTC +/// Built on 2024-05-01 at 14:05 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -308,8 +308,9 @@ class _TranslationsIconSelectorEn { // Translations String get name => 'Name:'; String get icon => 'Icon'; - String get color => 'Colour'; + String get color => 'Color'; String get select_icon => 'Select an icon'; + String get select_color => 'Select a color'; String get select_account_icon => 'Identify your account'; String get select_category_icon => 'Identify your category'; late final _TranslationsIconSelectorScopesEn scopes = _TranslationsIconSelectorScopesEn._(_root); @@ -1544,6 +1545,7 @@ class _TranslationsIconSelectorEs implements _TranslationsIconSelectorEn { @override String get icon => 'Icono'; @override String get color => 'Color'; @override String get select_icon => 'Selecciona un icono'; + @override String get select_color => 'Selecciona un color'; @override String get select_account_icon => 'Identifica tu cuenta'; @override String get select_category_icon => 'Identifica tu categoría'; @override late final _TranslationsIconSelectorScopesEs scopes = _TranslationsIconSelectorScopesEs._(_root); @@ -2833,8 +2835,9 @@ extension on Translations { case 'stats.finance_health_breakdown': return 'Breakdown'; case 'icon_selector.name': return 'Name:'; case 'icon_selector.icon': return 'Icon'; - case 'icon_selector.color': return 'Colour'; + case 'icon_selector.color': return 'Color'; case 'icon_selector.select_icon': return 'Select an icon'; + case 'icon_selector.select_color': return 'Select a color'; case 'icon_selector.select_account_icon': return 'Identify your account'; case 'icon_selector.select_category_icon': return 'Identify your category'; case 'icon_selector.scopes.transport': return 'Transport'; @@ -3428,6 +3431,7 @@ extension on _TranslationsEs { case 'icon_selector.icon': return 'Icono'; case 'icon_selector.color': return 'Color'; case 'icon_selector.select_icon': return 'Selecciona un icono'; + case 'icon_selector.select_color': return 'Selecciona un color'; case 'icon_selector.select_account_icon': return 'Identifica tu cuenta'; case 'icon_selector.select_category_icon': return 'Identifica tu categoría'; case 'icon_selector.scopes.transport': return 'Transporte';