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