From ad4759a157e46c4a814886c8db11a993eb721eb7 Mon Sep 17 00:00:00 2001 From: gonfff Date: Sun, 25 Jan 2026 14:35:33 +0300 Subject: [PATCH] Fix popup UI; Added scroll pickers --- ios/Runner/Info.plist | 21 -- .../widgets/add_subscription_sheet.dart | 236 +++++++++++++----- lib/presentation/widgets/currency_picker.dart | 64 ++--- pubspec.yaml | 3 +- 4 files changed, 198 insertions(+), 126 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 809c722..5f9f7b6 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -32,27 +32,6 @@ NSAllowsArbitraryLoads - NSExceptionDomains - - fc.yahoo.com - - NSExceptionAllowsInsecureHTTPLoads - - NSExceptionRequiresForwardSecrecy - - NSIncludesSubdomains - - - query1.finance.yahoo.com - - NSExceptionAllowsInsecureHTTPLoads - - NSExceptionRequiresForwardSecrecy - - NSIncludesSubdomains - - - NSUserNotificationUsageDescription We use notifications to remind you about upcoming subscription payments. diff --git a/lib/presentation/widgets/add_subscription_sheet.dart b/lib/presentation/widgets/add_subscription_sheet.dart index 26bb131..a193f85 100644 --- a/lib/presentation/widgets/add_subscription_sheet.dart +++ b/lib/presentation/widgets/add_subscription_sheet.dart @@ -5,8 +5,7 @@ import 'package:subctrl/domain/entities/tag.dart'; import 'package:subctrl/presentation/formatters/date_formatter.dart'; import 'package:subctrl/presentation/l10n/app_localizations.dart'; import 'package:subctrl/presentation/mappers/billing_cycle_labels.dart'; -import 'package:subctrl/presentation/widgets/currency_picker.dart'; -import 'package:subctrl/presentation/widgets/tag_picker.dart'; +import 'package:subctrl/presentation/utils/color_utils.dart'; class AddSubscriptionSheet extends StatefulWidget { const AddSubscriptionSheet({ @@ -120,42 +119,99 @@ class _AddSubscriptionSheetState extends State { } Future _pickCurrency(FormFieldState state) async { - final selected = await showCurrencyPicker( + if (widget.currencies.isEmpty) return; + final localizations = AppLocalizations.of(context); + var tempIndex = widget.currencies.indexWhere( + (currency) => currency.code.toUpperCase() == _currencyCode.toUpperCase(), + ); + if (tempIndex < 0) tempIndex = 0; + final controller = FixedExtentScrollController(initialItem: tempIndex); + + await showCupertinoModalPopup( context: context, - currencies: widget.currencies, - selectedCode: _currencyCode, + builder: (context) { + return CupertinoActionSheet( + title: Text(localizations.currencyLabel), + message: SizedBox( + height: 200, + child: CupertinoPicker( + itemExtent: 32, + scrollController: controller, + onSelectedItemChanged: (index) => tempIndex = index, + children: widget.currencies + .map( + (currency) => Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if ((currency.symbol ?? '').trim().isNotEmpty) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Text(currency.symbol!.trim()), + ), + Text(currency.code.toUpperCase()), + ], + ), + ), + ) + .toList(growable: false), + ), + ), + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(localizations.done), + ), + ); + }, ); - if (selected != null) { - setState(() => _currencyCode = selected.toUpperCase()); - state.didChange(_currencyCode); - } + + if (!mounted) return; + final selected = widget.currencies[tempIndex]; + setState(() => _currencyCode = selected.code.toUpperCase()); + state.didChange(_currencyCode); } Future _pickCycle(FormFieldState state) async { final localizations = AppLocalizations.of(context); - final cycle = await showCupertinoModalPopup( + final initialIndex = _orderedCycles.indexOf(_cycle); + var tempIndex = initialIndex < 0 ? 0 : initialIndex; + final controller = FixedExtentScrollController(initialItem: tempIndex); + await showCupertinoModalPopup( context: context, builder: (context) { return CupertinoActionSheet( title: Text(localizations.periodLabel), - actions: [ - for (final option in _orderedCycles) - CupertinoActionSheetAction( - onPressed: () => Navigator.of(context).pop(option), - child: Text(billingCycleLongLabel(option, localizations)), - ), - ], + message: SizedBox( + height: 200, + child: CupertinoPicker( + itemExtent: 40, + scrollController: controller, + onSelectedItemChanged: (index) => tempIndex = index, + children: _orderedCycles + .map( + (option) => Center( + child: Text( + billingCycleLongLabel(option, localizations), + style: CupertinoTheme.of( + context, + ).textTheme.textStyle.copyWith(fontSize: 19), + ), + ), + ) + .toList(growable: false), + ), + ), cancelButton: CupertinoActionSheetAction( onPressed: () => Navigator.of(context).pop(), - child: Text(localizations.settingsClose), + child: Text(localizations.done), ), ); }, ); - if (cycle != null) { - setState(() => _cycle = cycle); - state.didChange(cycle); - } + if (!mounted) return; + final selected = _orderedCycles[tempIndex]; + setState(() => _cycle = selected); + state.didChange(selected); } Future _pickPurchaseDate(FormFieldState state) async { @@ -164,37 +220,21 @@ class _AddSubscriptionSheetState extends State { context: context, builder: (context) { final localizations = AppLocalizations.of(context); - final background = CupertinoColors.systemBackground.resolveFrom( - context, - ); - return Container( - color: background, - height: 320, - child: Column( - children: [ - SizedBox( - height: 44, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - CupertinoButton( - padding: const EdgeInsets.symmetric(horizontal: 16), - onPressed: () => Navigator.of(context).pop(), - child: Text(localizations.done), - ), - ], - ), - ), - Expanded( - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.date, - initialDateTime: tempDate, - minimumDate: DateTime(DateTime.now().year - 10), - maximumDate: DateTime(DateTime.now().year + 5), - onDateTimeChanged: (value) => tempDate = value, - ), - ), - ], + return CupertinoActionSheet( + title: Text(localizations.purchaseDateLabel), + message: SizedBox( + height: 200, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: tempDate, + minimumDate: DateTime(DateTime.now().year - 10), + maximumDate: DateTime(DateTime.now().year + 5), + onDateTimeChanged: (value) => tempDate = value, + ), + ), + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(localizations.done), ), ); }, @@ -206,20 +246,73 @@ class _AddSubscriptionSheetState extends State { Future _pickTag(FormFieldState state) async { if (widget.tags.isEmpty) return; - final result = await showTagPicker( + final localizations = AppLocalizations.of(context); + final options = [ + _TagOption.none(localizations.subscriptionTagNone), + ...widget.tags.map((tag) => _TagOption.tag(tag)), + ]; + var initialIndex = options.indexWhere( + (option) => option.matches(_selectedTagId), + ); + if (initialIndex < 0) initialIndex = 0; + var tempIndex = initialIndex; + final controller = FixedExtentScrollController(initialItem: tempIndex); + + await showCupertinoModalPopup( context: context, - tags: widget.tags, - selectedTagId: _selectedTagId, + builder: (context) { + return CupertinoActionSheet( + title: Text(localizations.subscriptionTagLabel), + message: SizedBox( + height: 200, + child: CupertinoPicker( + itemExtent: 40, + scrollController: controller, + onSelectedItemChanged: (index) => tempIndex = index, + children: options + .map( + (option) => Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (option.colorHex != null) + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: colorFromHex( + option.colorHex!, + fallbackColor: const Color(0xFF000000), + ), + ), + ), + if (option.colorHex != null) const SizedBox(width: 8), + Text( + option.label, + style: CupertinoTheme.of( + context, + ).textTheme.textStyle.copyWith(fontSize: 19), + ), + ], + ), + ), + ) + .toList(growable: false), + ), + ), + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(localizations.done), + ), + ); + }, ); - if (result == null) return; + if (!mounted) return; - if (result == -1) { - setState(() => _selectedTagId = null); - state.didChange(null); - } else { - setState(() => _selectedTagId = result); - state.didChange(result); - } + final selected = options[tempIndex]; + setState(() => _selectedTagId = selected.tagId); + state.didChange(selected.tagId); } void _handleSubmit() { @@ -657,3 +750,18 @@ class _AddSubscriptionSheetState extends State { ); } } + +class _TagOption { + const _TagOption._(this.tagId, this.label, this.colorHex); + + factory _TagOption.none(String label) => _TagOption._(null, label, null); + + factory _TagOption.tag(Tag tag) => + _TagOption._(tag.id, tag.name, tag.colorHex); + + final int? tagId; + final String label; + final String? colorHex; + + bool matches(int? selectedTagId) => tagId == selectedTagId; +} diff --git a/lib/presentation/widgets/currency_picker.dart b/lib/presentation/widgets/currency_picker.dart index 4d35aa1..13ed842 100644 --- a/lib/presentation/widgets/currency_picker.dart +++ b/lib/presentation/widgets/currency_picker.dart @@ -8,6 +8,7 @@ Future showCurrencyPicker({ required BuildContext context, required List currencies, String? selectedCode, + bool showSearch = true, }) { final localizations = AppLocalizations.of(context); String query = ''; @@ -28,45 +29,26 @@ Future showCurrencyPicker({ return label.contains(normalizedQuery); }).toList(); - return Container( - height: MediaQuery.of(context).size.height * 0.7, - color: backgroundColor, - child: SafeArea( - top: false, + final visibleCurrencies = showSearch ? filtered : currencies; + return CupertinoActionSheet( + title: Text(localizations.currencyPickerTitle), + message: SizedBox( + height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - child: Row( - children: [ - Expanded( - child: Text( - localizations.currencyPickerTitle, - style: CupertinoTheme.of( - context, - ).textTheme.navTitleTextStyle, - ), - ), - CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () => Navigator.of(context).pop(), - child: Text(localizations.settingsClose), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + if (showSearch) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: CupertinoSearchTextField( + placeholder: localizations.currencySearchPlaceholder, + onChanged: (value) => + setModalState(() => query = value), + ), ), - child: CupertinoSearchTextField( - placeholder: localizations.currencySearchPlaceholder, - onChanged: (value) => setModalState(() => query = value), - ), - ), + const SizedBox(height: 8), + ], Expanded( - child: filtered.isEmpty + child: visibleCurrencies.isEmpty ? Center( child: Text( localizations.currencySearchEmpty, @@ -77,11 +59,11 @@ Future showCurrencyPicker({ ) : ListView.separated( padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + horizontal: 8, + vertical: 4, ), itemBuilder: (context, index) { - final currency = filtered[index]; + final currency = visibleCurrencies[index]; final code = currency.code; final isSelected = normalizedSelected != null && @@ -126,12 +108,16 @@ Future showCurrencyPicker({ }, separatorBuilder: (_, __) => const SizedBox(height: 8), - itemCount: filtered.length, + itemCount: visibleCurrencies.length, ), ), ], ), ), + cancelButton: CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(localizations.settingsClose), + ), ); }, ); diff --git a/pubspec.yaml b/pubspec.yaml index bce74ed..1433287 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.2+8 +version: 1.1.1+10 environment: sdk: ^3.10.3 @@ -71,7 +71,6 @@ dev_dependencies: drift_dev: ^2.18.0 flutter_launcher_icons: ^0.14.4 - # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec