Skip to content

Commit b3899f0

Browse files
authored
Fix popup UI; Added scroll pickers (#17)
1 parent a3aeefe commit b3899f0

4 files changed

Lines changed: 198 additions & 126 deletions

File tree

ios/Runner/Info.plist

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,6 @@
3232
<dict>
3333
<key>NSAllowsArbitraryLoads</key>
3434
<false/>
35-
<key>NSExceptionDomains</key>
36-
<dict>
37-
<key>fc.yahoo.com</key>
38-
<dict>
39-
<key>NSExceptionAllowsInsecureHTTPLoads</key>
40-
<false/>
41-
<key>NSExceptionRequiresForwardSecrecy</key>
42-
<false/>
43-
<key>NSIncludesSubdomains</key>
44-
<true/>
45-
</dict>
46-
<key>query1.finance.yahoo.com</key>
47-
<dict>
48-
<key>NSExceptionAllowsInsecureHTTPLoads</key>
49-
<false/>
50-
<key>NSExceptionRequiresForwardSecrecy</key>
51-
<false/>
52-
<key>NSIncludesSubdomains</key>
53-
<true/>
54-
</dict>
55-
</dict>
5635
</dict>
5736
<key>NSUserNotificationUsageDescription</key>
5837
<string>We use notifications to remind you about upcoming subscription payments.</string>

lib/presentation/widgets/add_subscription_sheet.dart

Lines changed: 172 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import 'package:subctrl/domain/entities/tag.dart';
55
import 'package:subctrl/presentation/formatters/date_formatter.dart';
66
import 'package:subctrl/presentation/l10n/app_localizations.dart';
77
import 'package:subctrl/presentation/mappers/billing_cycle_labels.dart';
8-
import 'package:subctrl/presentation/widgets/currency_picker.dart';
9-
import 'package:subctrl/presentation/widgets/tag_picker.dart';
8+
import 'package:subctrl/presentation/utils/color_utils.dart';
109

1110
class AddSubscriptionSheet extends StatefulWidget {
1211
const AddSubscriptionSheet({
@@ -120,42 +119,99 @@ class _AddSubscriptionSheetState extends State<AddSubscriptionSheet> {
120119
}
121120

122121
Future<void> _pickCurrency(FormFieldState<String> state) async {
123-
final selected = await showCurrencyPicker(
122+
if (widget.currencies.isEmpty) return;
123+
final localizations = AppLocalizations.of(context);
124+
var tempIndex = widget.currencies.indexWhere(
125+
(currency) => currency.code.toUpperCase() == _currencyCode.toUpperCase(),
126+
);
127+
if (tempIndex < 0) tempIndex = 0;
128+
final controller = FixedExtentScrollController(initialItem: tempIndex);
129+
130+
await showCupertinoModalPopup<void>(
124131
context: context,
125-
currencies: widget.currencies,
126-
selectedCode: _currencyCode,
132+
builder: (context) {
133+
return CupertinoActionSheet(
134+
title: Text(localizations.currencyLabel),
135+
message: SizedBox(
136+
height: 200,
137+
child: CupertinoPicker(
138+
itemExtent: 32,
139+
scrollController: controller,
140+
onSelectedItemChanged: (index) => tempIndex = index,
141+
children: widget.currencies
142+
.map(
143+
(currency) => Center(
144+
child: Row(
145+
mainAxisSize: MainAxisSize.min,
146+
children: [
147+
if ((currency.symbol ?? '').trim().isNotEmpty)
148+
Padding(
149+
padding: const EdgeInsets.only(right: 8),
150+
child: Text(currency.symbol!.trim()),
151+
),
152+
Text(currency.code.toUpperCase()),
153+
],
154+
),
155+
),
156+
)
157+
.toList(growable: false),
158+
),
159+
),
160+
cancelButton: CupertinoActionSheetAction(
161+
onPressed: () => Navigator.of(context).pop(),
162+
child: Text(localizations.done),
163+
),
164+
);
165+
},
127166
);
128-
if (selected != null) {
129-
setState(() => _currencyCode = selected.toUpperCase());
130-
state.didChange(_currencyCode);
131-
}
167+
168+
if (!mounted) return;
169+
final selected = widget.currencies[tempIndex];
170+
setState(() => _currencyCode = selected.code.toUpperCase());
171+
state.didChange(_currencyCode);
132172
}
133173

134174
Future<void> _pickCycle(FormFieldState<BillingCycle> state) async {
135175
final localizations = AppLocalizations.of(context);
136-
final cycle = await showCupertinoModalPopup<BillingCycle>(
176+
final initialIndex = _orderedCycles.indexOf(_cycle);
177+
var tempIndex = initialIndex < 0 ? 0 : initialIndex;
178+
final controller = FixedExtentScrollController(initialItem: tempIndex);
179+
await showCupertinoModalPopup<void>(
137180
context: context,
138181
builder: (context) {
139182
return CupertinoActionSheet(
140183
title: Text(localizations.periodLabel),
141-
actions: [
142-
for (final option in _orderedCycles)
143-
CupertinoActionSheetAction(
144-
onPressed: () => Navigator.of(context).pop(option),
145-
child: Text(billingCycleLongLabel(option, localizations)),
146-
),
147-
],
184+
message: SizedBox(
185+
height: 200,
186+
child: CupertinoPicker(
187+
itemExtent: 40,
188+
scrollController: controller,
189+
onSelectedItemChanged: (index) => tempIndex = index,
190+
children: _orderedCycles
191+
.map(
192+
(option) => Center(
193+
child: Text(
194+
billingCycleLongLabel(option, localizations),
195+
style: CupertinoTheme.of(
196+
context,
197+
).textTheme.textStyle.copyWith(fontSize: 19),
198+
),
199+
),
200+
)
201+
.toList(growable: false),
202+
),
203+
),
148204
cancelButton: CupertinoActionSheetAction(
149205
onPressed: () => Navigator.of(context).pop(),
150-
child: Text(localizations.settingsClose),
206+
child: Text(localizations.done),
151207
),
152208
);
153209
},
154210
);
155-
if (cycle != null) {
156-
setState(() => _cycle = cycle);
157-
state.didChange(cycle);
158-
}
211+
if (!mounted) return;
212+
final selected = _orderedCycles[tempIndex];
213+
setState(() => _cycle = selected);
214+
state.didChange(selected);
159215
}
160216

161217
Future<void> _pickPurchaseDate(FormFieldState<DateTime?> state) async {
@@ -164,37 +220,21 @@ class _AddSubscriptionSheetState extends State<AddSubscriptionSheet> {
164220
context: context,
165221
builder: (context) {
166222
final localizations = AppLocalizations.of(context);
167-
final background = CupertinoColors.systemBackground.resolveFrom(
168-
context,
169-
);
170-
return Container(
171-
color: background,
172-
height: 320,
173-
child: Column(
174-
children: [
175-
SizedBox(
176-
height: 44,
177-
child: Row(
178-
mainAxisAlignment: MainAxisAlignment.end,
179-
children: [
180-
CupertinoButton(
181-
padding: const EdgeInsets.symmetric(horizontal: 16),
182-
onPressed: () => Navigator.of(context).pop(),
183-
child: Text(localizations.done),
184-
),
185-
],
186-
),
187-
),
188-
Expanded(
189-
child: CupertinoDatePicker(
190-
mode: CupertinoDatePickerMode.date,
191-
initialDateTime: tempDate,
192-
minimumDate: DateTime(DateTime.now().year - 10),
193-
maximumDate: DateTime(DateTime.now().year + 5),
194-
onDateTimeChanged: (value) => tempDate = value,
195-
),
196-
),
197-
],
223+
return CupertinoActionSheet(
224+
title: Text(localizations.purchaseDateLabel),
225+
message: SizedBox(
226+
height: 200,
227+
child: CupertinoDatePicker(
228+
mode: CupertinoDatePickerMode.date,
229+
initialDateTime: tempDate,
230+
minimumDate: DateTime(DateTime.now().year - 10),
231+
maximumDate: DateTime(DateTime.now().year + 5),
232+
onDateTimeChanged: (value) => tempDate = value,
233+
),
234+
),
235+
cancelButton: CupertinoActionSheetAction(
236+
onPressed: () => Navigator.of(context).pop(),
237+
child: Text(localizations.done),
198238
),
199239
);
200240
},
@@ -206,20 +246,73 @@ class _AddSubscriptionSheetState extends State<AddSubscriptionSheet> {
206246

207247
Future<void> _pickTag(FormFieldState<int?> state) async {
208248
if (widget.tags.isEmpty) return;
209-
final result = await showTagPicker(
249+
final localizations = AppLocalizations.of(context);
250+
final options = [
251+
_TagOption.none(localizations.subscriptionTagNone),
252+
...widget.tags.map((tag) => _TagOption.tag(tag)),
253+
];
254+
var initialIndex = options.indexWhere(
255+
(option) => option.matches(_selectedTagId),
256+
);
257+
if (initialIndex < 0) initialIndex = 0;
258+
var tempIndex = initialIndex;
259+
final controller = FixedExtentScrollController(initialItem: tempIndex);
260+
261+
await showCupertinoModalPopup<void>(
210262
context: context,
211-
tags: widget.tags,
212-
selectedTagId: _selectedTagId,
263+
builder: (context) {
264+
return CupertinoActionSheet(
265+
title: Text(localizations.subscriptionTagLabel),
266+
message: SizedBox(
267+
height: 200,
268+
child: CupertinoPicker(
269+
itemExtent: 40,
270+
scrollController: controller,
271+
onSelectedItemChanged: (index) => tempIndex = index,
272+
children: options
273+
.map(
274+
(option) => Center(
275+
child: Row(
276+
mainAxisSize: MainAxisSize.min,
277+
children: [
278+
if (option.colorHex != null)
279+
Container(
280+
width: 12,
281+
height: 12,
282+
decoration: BoxDecoration(
283+
shape: BoxShape.circle,
284+
color: colorFromHex(
285+
option.colorHex!,
286+
fallbackColor: const Color(0xFF000000),
287+
),
288+
),
289+
),
290+
if (option.colorHex != null) const SizedBox(width: 8),
291+
Text(
292+
option.label,
293+
style: CupertinoTheme.of(
294+
context,
295+
).textTheme.textStyle.copyWith(fontSize: 19),
296+
),
297+
],
298+
),
299+
),
300+
)
301+
.toList(growable: false),
302+
),
303+
),
304+
cancelButton: CupertinoActionSheetAction(
305+
onPressed: () => Navigator.of(context).pop(),
306+
child: Text(localizations.done),
307+
),
308+
);
309+
},
213310
);
214-
if (result == null) return;
311+
215312
if (!mounted) return;
216-
if (result == -1) {
217-
setState(() => _selectedTagId = null);
218-
state.didChange(null);
219-
} else {
220-
setState(() => _selectedTagId = result);
221-
state.didChange(result);
222-
}
313+
final selected = options[tempIndex];
314+
setState(() => _selectedTagId = selected.tagId);
315+
state.didChange(selected.tagId);
223316
}
224317

225318
void _handleSubmit() {
@@ -657,3 +750,18 @@ class _AddSubscriptionSheetState extends State<AddSubscriptionSheet> {
657750
);
658751
}
659752
}
753+
754+
class _TagOption {
755+
const _TagOption._(this.tagId, this.label, this.colorHex);
756+
757+
factory _TagOption.none(String label) => _TagOption._(null, label, null);
758+
759+
factory _TagOption.tag(Tag tag) =>
760+
_TagOption._(tag.id, tag.name, tag.colorHex);
761+
762+
final int? tagId;
763+
final String label;
764+
final String? colorHex;
765+
766+
bool matches(int? selectedTagId) => tagId == selectedTagId;
767+
}

0 commit comments

Comments
 (0)