From cd2a507b7f98db75c0699c009e929b6b73da554a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 7 Sep 2025 16:00:30 +0800 Subject: [PATCH] :sparkles: Account region settings --- assets/i18n/en-US.json | 3 ++ assets/i18n/zh-CN.json | 11 +++--- lib/models/account.dart | 1 + lib/models/account.freezed.dart | 39 ++++++++++++---------- lib/models/account.g.dart | 2 ++ lib/models/auth.freezed.dart | 10 ++---- lib/screens/account/me/profile_update.dart | 29 ++++++++++++++++ lib/widgets/check_in.dart | 35 +++++++++++++++---- 8 files changed, 92 insertions(+), 38 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 4027864f..6aaf1d66 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -195,6 +195,7 @@ "checkInResultLevel2": "A Normal Day", "checkInResultLevel3": "Good Luck", "checkInResultLevel4": "Best Luck", + "checkInResultLevel5": "Happy Birthday 🥳", "checkInActivityTitle": "{} checked in on {} and got a {}", "eventCalander": "Event Calander", "eventCalanderEmpty": "No events on that day.", @@ -228,6 +229,8 @@ "settings": "Settings", "language": "Language", "accountLanguageHint": "This language will be used for email and push notifications.", + "region": "Region", + "accountRegionHint": "This region will be used for content delivery and localization.", "settingsDisplayLanguage": "Display Language", "languageFollowSystem": "Follow System", "postsCreatedCount": "Posts", diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 5fe9a354..4800b3cf 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -158,11 +158,12 @@ "checkIn": "签到", "checkInNone": "尚未签到", "checkInNoneHint": "通过签到获取您的财富提示和每日奖励。", - "checkInResultLevel0": "最差运气", - "checkInResultLevel1": "坏运气", - "checkInResultLevel2": "一个普通的日常", - "checkInResultLevel3": "好运", - "checkInResultLevel4": "最佳运气", + "checkInResultLevel0": "大凶", + "checkInResultLevel1": "凶", + "checkInResultLevel2": "中平", + "checkInResultLevel3": "吉", + "checkInResultLevel4": "大吉", + "checkInResultLevel5": "生日快乐 🥳", "checkInActivityTitle": "{} 在 {} 签到并获得了 {}", "eventCalander": "活动日历", "eventCalanderEmpty": "该日无活动。", diff --git a/lib/models/account.dart b/lib/models/account.dart index 541656ca..f1b01d17 100644 --- a/lib/models/account.dart +++ b/lib/models/account.dart @@ -13,6 +13,7 @@ sealed class SnAccount with _$SnAccount { required String name, required String nick, required String language, + required String region, required bool isSuperuser, required String? automatedId, required SnAccountProfile profile, diff --git a/lib/models/account.freezed.dart b/lib/models/account.freezed.dart index b288b06d..7c944e04 100644 --- a/lib/models/account.freezed.dart +++ b/lib/models/account.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$SnAccount { - String get id; String get name; String get nick; String get language; bool get isSuperuser; String? get automatedId; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; String get name; String get nick; String get language; String get region; bool get isSuperuser; String? get automatedId; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnAccount /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $SnAccountCopyWith get copyWith => _$SnAccountCopyWithImpl @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.region, region) || other.region == region)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,name,nick,language,region,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt); @override String toString() { - return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, region: $region, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -48,7 +48,7 @@ abstract mixin class $SnAccountCopyWith<$Res> { factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl; @useResult $Res call({ - String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -65,12 +65,13 @@ class _$SnAccountCopyWithImpl<$Res> /// Create a copy of SnAccount /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? region = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable +as String,region: null == region ? _self.region : region // ignore: cast_nullable_to_non_nullable as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable @@ -182,10 +183,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnAccount() when $default != null: -return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return orElse(); } @@ -203,10 +204,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { case _SnAccount(): -return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);} +return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);} } /// A variant of `when` that fallback to returning `null` /// @@ -220,10 +221,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { case _SnAccount() when $default != null: -return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: +return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return null; } @@ -235,13 +236,14 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, @JsonSerializable() class _SnAccount implements SnAccount { - const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.automatedId, required this.profile, required this.perkSubscription, final List badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges; + const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.region, required this.isSuperuser, required this.automatedId, required this.profile, required this.perkSubscription, final List badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges; factory _SnAccount.fromJson(Map json) => _$SnAccountFromJson(json); @override final String id; @override final String name; @override final String nick; @override final String language; +@override final String region; @override final bool isSuperuser; @override final String? automatedId; @override final SnAccountProfile profile; @@ -270,16 +272,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.region, region) || other.region == region)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,name,nick,language,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt); +int get hashCode => Object.hash(runtimeType,id,name,nick,language,region,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt); @override String toString() { - return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, region: $region, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } @@ -290,7 +292,7 @@ abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Re factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl; @override @useResult $Res call({ - String id, String name, String nick, String language, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -307,12 +309,13 @@ class __$SnAccountCopyWithImpl<$Res> /// Create a copy of SnAccount /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? region = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_SnAccount( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable +as String,region: null == region ? _self.region : region // ignore: cast_nullable_to_non_nullable as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/account.g.dart b/lib/models/account.g.dart index 9d8def6c..7b6e9806 100644 --- a/lib/models/account.g.dart +++ b/lib/models/account.g.dart @@ -11,6 +11,7 @@ _SnAccount _$SnAccountFromJson(Map json) => _SnAccount( name: json['name'] as String, nick: json['nick'] as String, language: json['language'] as String, + region: json['region'] as String, isSuperuser: json['is_superuser'] as bool, automatedId: json['automated_id'] as String?, profile: SnAccountProfile.fromJson(json['profile'] as Map), @@ -39,6 +40,7 @@ Map _$SnAccountToJson(_SnAccount instance) => 'name': instance.name, 'nick': instance.nick, 'language': instance.language, + 'region': instance.region, 'is_superuser': instance.isSuperuser, 'automated_id': instance.automatedId, 'profile': instance.profile.toJson(), diff --git a/lib/models/auth.freezed.dart b/lib/models/auth.freezed.dart index 0756a7fd..1f69e1ea 100644 --- a/lib/models/auth.freezed.dart +++ b/lib/models/auth.freezed.dart @@ -376,10 +376,7 @@ return $default(_that);case _: final _that = this; switch (_that) { case _GeoIpLocation(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} +return $default(_that);} } /// A variant of `map` that fallback to returning `null`. /// @@ -438,10 +435,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_ @optionalTypeArgs TResult when(TResult Function( double latitude, double longitude, String countryCode, String country, String city) $default,) {final _that = this; switch (_that) { case _GeoIpLocation(): -return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: - throw StateError('Unexpected subclass'); - -} +return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);} } /// A variant of `when` that fallback to returning `null` /// diff --git a/lib/screens/account/me/profile_update.dart b/lib/screens/account/me/profile_update.dart index 071912c6..41cd808a 100644 --- a/lib/screens/account/me/profile_update.dart +++ b/lib/screens/account/me/profile_update.dart @@ -21,6 +21,7 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; +const kServerSupportedRegions = ['US', 'JP', 'CN']; class UpdateProfileScreen extends HookConsumerWidget { const UpdateProfileScreen({super.key}); @@ -97,6 +98,7 @@ class UpdateProfileScreen extends HookConsumerWidget { final usernameController = useTextEditingController(text: user.value!.name); final nicknameController = useTextEditingController(text: user.value!.nick); final language = useState(user.value!.language); + final region = useState(user.value!.region); final links = useState>(user.value!.profile.links); void updateBasicInfo() async { @@ -111,6 +113,7 @@ class UpdateProfileScreen extends HookConsumerWidget { 'name': usernameController.text, 'nick': nicknameController.text, 'language': language.value, + 'region': region.value, }, ); final userNotifier = ref.read(userInfoProvider.notifier); @@ -291,6 +294,32 @@ class UpdateProfileScreen extends HookConsumerWidget { ], ), ), + DropdownButtonFormField2( + decoration: InputDecoration( + labelText: 'region'.tr(), + helperText: 'accountRegionHint'.tr(), + ), + items: [ + ...kServerSupportedRegions.map( + (e) => DropdownMenuItem(value: e, child: Text(e)), + ), + if (!kServerSupportedRegions.contains(region.value)) + DropdownMenuItem( + value: region.value, + child: Text(region.value), + ), + ], + value: region.value, + onChanged: (value) { + region.value = value ?? region.value; + }, + customButton: Row( + children: [ + Expanded(child: Text(region.value)), + Icon(Symbols.arrow_drop_down), + ], + ), + ), Align( alignment: Alignment.centerRight, child: TextButton.icon( diff --git a/lib/widgets/check_in.dart b/lib/widgets/check_in.dart index 1a39cb80..74fbdf06 100644 --- a/lib/widgets/check_in.dart +++ b/lib/widgets/check_in.dart @@ -113,13 +113,34 @@ class CheckInWidget extends HookConsumerWidget { Text( 'checkInResultLevel${result.level}', ).tr().fontSize(15).bold(), - Text( - result.tips - .map( - (e) => '${e.isPositive ? '宜' : '忌'} ${e.title}', - ) - .join(' · '), - ).fontSize(11), + Wrap( + children: + result.tips + .map((e) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + e.isPositive + ? Symbols.thumb_up + : Symbols.thumb_down, + size: 12, + ), + const Gap(4), + Text(e.title).fontSize(11), + ], + ); + }) + .toList() + .expand( + (widget) => [ + widget, + Text(' · ').fontSize(11), + ], + ) + .toList() + ..removeLast(), + ), ], ); },