Skip to content

Commit 3ab0739

Browse files
apskhemdtscalac
andauthored
feat(cat-voices): Add roles chooser panel widget (#916)
* feat: initial commit * feat: preview card with state * fix: icon * feat: roles chooser panel * feat: localization * refactor: move l10n items * feat: role images * feat: adjusting learn more button * refactor: move value * docs: add initial docs * docs: fix minor * feat: view only * feat: role summary panel * chore: remove unused imports * fix: cspell * refactor: rename example category to role panels * refactor: minor example * fix: selection * docs: minor fix * refactor: organizing widgets * refactor: change map to set for selection * refactor: leaner * refactor: rename from panel to container instead * refactor: rename url example * docs: fix data change * refactor: roles mapping * feat: role extension icon * chore: remove spacebetween * docs: minor fix * fix: l10n * chore: lintfix --------- Co-authored-by: Dominik Toton <[email protected]>
1 parent 860abd3 commit 3ab0739

24 files changed

+576
-3
lines changed

catalyst_voices/lib/common/ext/account_role_ext.dart

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
12
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
23
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
34
import 'package:flutter/material.dart';
@@ -13,4 +14,10 @@ extension AccountRoleExt on AccountRole {
1314
return context.l10n.drep;
1415
}
1516
}
17+
18+
String get icon => switch (this) {
19+
AccountRole.voter => VoicesAssets.images.roleVoter.path,
20+
AccountRole.proposer => VoicesAssets.images.roleProposer.path,
21+
AccountRole.drep => VoicesAssets.images.roleDrep.path,
22+
};
1623
}

catalyst_voices/lib/widgets/buttons/voices_segmented_button.dart

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ class VoicesSegmentedButton<T extends Object> extends StatelessWidget {
5454
/// Should insert leading check icon into selected [segments].
5555
final bool showSelectedIcon;
5656

57+
/// Customizes this button's appearance.
58+
final ButtonStyle? style;
59+
5760
/// Default constructor.
5861
const VoicesSegmentedButton({
5962
super.key,
@@ -63,6 +66,7 @@ class VoicesSegmentedButton<T extends Object> extends StatelessWidget {
6366
this.multiSelectionEnabled = false,
6467
this.emptySelectionAllowed = false,
6568
this.showSelectedIcon = true,
69+
this.style,
6670
});
6771

6872
@override
@@ -74,6 +78,7 @@ class VoicesSegmentedButton<T extends Object> extends StatelessWidget {
7478
multiSelectionEnabled: multiSelectionEnabled,
7579
emptySelectionAllowed: emptySelectionAllowed,
7680
showSelectedIcon: showSelectedIcon,
81+
style: style,
7782
);
7883
}
7984
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import 'package:catalyst_voices/widgets/common/grayscale_filter.dart';
2+
import 'package:catalyst_voices/widgets/widgets.dart';
3+
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
4+
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
5+
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
6+
import 'package:flutter/material.dart';
7+
8+
/// A role chooser card, responsible as a building block
9+
/// for any role selection, and available in both
10+
/// interactive and view-only mode.
11+
class RoleChooserCard extends StatelessWidget {
12+
/// The current displaying value.
13+
final bool value;
14+
15+
/// Needs to be a rasterized image.
16+
final String imageUrl;
17+
18+
/// The text label displaying on the card.
19+
final String label;
20+
21+
/// Locks the value and shows it as default, only the selected value appears.
22+
final bool lockValueAsDefault;
23+
24+
/// Hides the "Learn More" link.
25+
final bool isLearnMoreHidden;
26+
27+
/// Toggles view-only mode.
28+
final bool isViewOnly;
29+
30+
/// A callback triggered when the role selection changes.
31+
final ValueChanged<bool>? onChanged;
32+
33+
/// A callback triggered when the "Learn More" link is clicked.
34+
final VoidCallback? onLearnMore;
35+
36+
const RoleChooserCard({
37+
super.key,
38+
required this.value,
39+
required this.imageUrl,
40+
required this.label,
41+
this.lockValueAsDefault = false,
42+
this.isLearnMoreHidden = false,
43+
this.isViewOnly = false,
44+
this.onChanged,
45+
this.onLearnMore,
46+
});
47+
48+
@override
49+
Widget build(BuildContext context) {
50+
return Container(
51+
height: 100,
52+
padding: EdgeInsets.all(isViewOnly ? 8 : 12),
53+
decoration: isViewOnly
54+
? null
55+
: BoxDecoration(
56+
border: Border.all(
57+
color: Theme.of(context).colors.outlineBorderVariant!,
58+
width: 1,
59+
),
60+
borderRadius: BorderRadius.circular(8),
61+
),
62+
child: Row(
63+
children: [
64+
Column(
65+
children: [
66+
GrayscaleFilter(
67+
image: CatalystImage.asset(
68+
imageUrl,
69+
width: 70,
70+
height: 70,
71+
fit: BoxFit.cover,
72+
),
73+
),
74+
],
75+
),
76+
const SizedBox(width: 12),
77+
Expanded(
78+
child: Column(
79+
children: [
80+
Row(
81+
children: [
82+
Expanded(
83+
child: Text(
84+
overflow: TextOverflow.ellipsis,
85+
label,
86+
style: Theme.of(context).textTheme.titleSmall,
87+
),
88+
),
89+
if (!isLearnMoreHidden) ...[
90+
const SizedBox(width: 10),
91+
_LearnMoreText(
92+
onTap: onLearnMore,
93+
),
94+
],
95+
],
96+
),
97+
const SizedBox(height: 8),
98+
Row(
99+
children: [
100+
if (isViewOnly)
101+
_DisplayingValueAsChips(
102+
value: value,
103+
lockValueAsDefault: lockValueAsDefault,
104+
)
105+
else
106+
_DisplayingValueAsSegmentedButton(
107+
value: value,
108+
lockValueAsDefault: lockValueAsDefault,
109+
onChanged: onChanged,
110+
),
111+
],
112+
),
113+
],
114+
),
115+
),
116+
],
117+
),
118+
);
119+
}
120+
}
121+
122+
class _LearnMoreText extends StatelessWidget {
123+
final VoidCallback? onTap;
124+
125+
const _LearnMoreText({
126+
this.onTap,
127+
});
128+
129+
@override
130+
Widget build(BuildContext context) {
131+
return LinkText(
132+
context.l10n.learnMore,
133+
onTap: onTap,
134+
style: Theme.of(context).textTheme.labelMedium,
135+
underline: false,
136+
);
137+
}
138+
}
139+
140+
class _DisplayingValueAsChips extends StatelessWidget {
141+
final bool value;
142+
final bool lockValueAsDefault;
143+
144+
const _DisplayingValueAsChips({
145+
required this.value,
146+
required this.lockValueAsDefault,
147+
});
148+
149+
@override
150+
Widget build(BuildContext context) {
151+
return Wrap(
152+
spacing: 5,
153+
runSpacing: 5,
154+
children: [
155+
VoicesChip.round(
156+
content: Text(
157+
value ? context.l10n.yes : context.l10n.no,
158+
style: TextStyle(
159+
color: value
160+
? Theme.of(context).colors.successContainer
161+
: Theme.of(context).colors.errorContainer,
162+
),
163+
),
164+
padding: const EdgeInsets.symmetric(
165+
horizontal: 12,
166+
vertical: 4,
167+
),
168+
backgroundColor: value
169+
? Theme.of(context).colors.success
170+
: Theme.of(context).colors.iconsError,
171+
),
172+
if (lockValueAsDefault)
173+
VoicesChip.round(
174+
content: Text(
175+
context.l10n.defaultRole,
176+
style: TextStyle(
177+
color: Theme.of(context).colors.iconsPrimary,
178+
),
179+
),
180+
padding: const EdgeInsets.symmetric(
181+
horizontal: 20,
182+
vertical: 4,
183+
),
184+
backgroundColor: Theme.of(context).colors.iconsForeground,
185+
),
186+
],
187+
);
188+
}
189+
}
190+
191+
class _DisplayingValueAsSegmentedButton extends StatelessWidget {
192+
final bool value;
193+
final bool lockValueAsDefault;
194+
final ValueChanged<bool>? onChanged;
195+
196+
const _DisplayingValueAsSegmentedButton({
197+
required this.value,
198+
required this.lockValueAsDefault,
199+
this.onChanged,
200+
});
201+
202+
@override
203+
Widget build(BuildContext context) {
204+
return Expanded(
205+
child: VoicesSegmentedButton<bool>(
206+
segments: lockValueAsDefault
207+
? [
208+
ButtonSegment(
209+
value: value,
210+
label: Text(
211+
[
212+
if (value) context.l10n.yes else context.l10n.no,
213+
'(${context.l10n.defaultRole})',
214+
].join(' '),
215+
),
216+
icon: Icon(
217+
value ? Icons.check : Icons.block,
218+
),
219+
),
220+
]
221+
: [
222+
ButtonSegment(
223+
value: true,
224+
label: Text(context.l10n.yes),
225+
icon: value ? const Icon(Icons.check) : null,
226+
),
227+
ButtonSegment(
228+
value: false,
229+
label: Text(context.l10n.no),
230+
icon: !value ? const Icon(Icons.block) : null,
231+
),
232+
],
233+
style: SegmentedButton.styleFrom(
234+
backgroundColor: Colors.transparent,
235+
foregroundColor: Theme.of(context).colors.textOnPrimary,
236+
selectedForegroundColor: value
237+
? Theme.of(context).colors.successContainer
238+
: Theme.of(context).colors.errorContainer,
239+
selectedBackgroundColor: value
240+
? Theme.of(context).colors.success
241+
: Theme.of(context).colors.iconsError,
242+
),
243+
showSelectedIcon: false,
244+
selected: {value},
245+
onChanged: (selected) => onChanged?.call(selected.first),
246+
),
247+
);
248+
}
249+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:flutter/material.dart';
2+
3+
/// A constant grayscale [ColorFilter] used to de-saturate an image.
4+
const _grayscaleFilter = ColorFilter.matrix([
5+
0.2126,
6+
0.7152,
7+
0.0722,
8+
0,
9+
0,
10+
0.2126,
11+
0.7152,
12+
0.0722,
13+
0,
14+
0,
15+
0.2126,
16+
0.7152,
17+
0.0722,
18+
0,
19+
0,
20+
0,
21+
0,
22+
0,
23+
1,
24+
0,
25+
]);
26+
27+
/// Applies grayscale filter to a widget.
28+
class GrayscaleFilter extends StatelessWidget {
29+
final Widget image;
30+
31+
const GrayscaleFilter({
32+
super.key,
33+
required this.image,
34+
});
35+
36+
@override
37+
Widget build(BuildContext context) {
38+
return ColorFiltered(
39+
colorFilter: _grayscaleFilter,
40+
child: image,
41+
);
42+
}
43+
}

catalyst_voices/lib/widgets/common/link_text.dart

+7-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class LinkText extends StatelessWidget {
1111
/// The text to be displayed.
1212
final String data;
1313

14+
/// Displays the text with underline.
15+
final bool underline;
16+
1417
/// An optional TextStyle to customize the appearance of the text.
1518
final TextStyle? style;
1619

@@ -21,6 +24,7 @@ class LinkText extends StatelessWidget {
2124
const LinkText(
2225
this.data, {
2326
super.key,
27+
this.underline = true,
2428
this.style,
2529
this.onTap,
2630
});
@@ -32,9 +36,9 @@ class LinkText extends StatelessWidget {
3236

3337
final effectiveStyle = (style ?? const TextStyle()).copyWith(
3438
color: color,
35-
decoration: TextDecoration.underline,
36-
decorationColor: color,
37-
decorationStyle: TextDecorationStyle.solid,
39+
decoration: underline ? TextDecoration.underline : null,
40+
decorationColor: underline ? color : null,
41+
decorationStyle: underline ? TextDecorationStyle.solid : null,
3842
);
3943

4044
return GestureDetector(

0 commit comments

Comments
 (0)