Wearable badge

This commit is contained in:
LittleSheep 2025-03-02 21:08:38 +08:00
parent 72e6a6a1f6
commit 6e03a00280
12 changed files with 312 additions and 136 deletions

View File

@ -743,5 +743,9 @@
"fieldLocation": "Location", "fieldLocation": "Location",
"fieldLinks": "Links", "fieldLinks": "Links",
"fieldLinkName": "Name", "fieldLinkName": "Name",
"fieldLinkUrl": "URL" "fieldLinkUrl": "URL",
"screenAccountBadges": "Badges",
"accountBadges": "Badges",
"accountBadgesDescription": "View and manage your badges.",
"badgeActivated": "Activated badge {}."
} }

View File

@ -515,7 +515,7 @@
"accountBirthday": "出生于 {}", "accountBirthday": "出生于 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暂无运势记录", "accountCheckInNoRecords": "暂无运势记录",
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工", "badgeCompanyStaff": "工作人员",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "状态", "accountStatus": "状态",
"accountStatusOnline": "在线", "accountStatusOnline": "在线",
@ -741,5 +741,9 @@
"fieldLocation": "位置", "fieldLocation": "位置",
"fieldLinks": "链接", "fieldLinks": "链接",
"fieldLinkName": "名称", "fieldLinkName": "名称",
"fieldLinkUrl": "链接" "fieldLinkUrl": "链接",
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看并管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。"
} }

View File

@ -515,7 +515,7 @@
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄", "accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "工作人員",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
"accountStatusOnline": "在線", "accountStatusOnline": "在線",
@ -741,5 +741,9 @@
"fieldLocation": "位置", "fieldLocation": "位置",
"fieldLinks": "鏈接", "fieldLinks": "鏈接",
"fieldLinkName": "名稱", "fieldLinkName": "名稱",
"fieldLinkUrl": "鏈接" "fieldLinkUrl": "鏈接",
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。"
} }

View File

@ -515,7 +515,7 @@
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄", "accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "工作人員",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
"accountStatusOnline": "在線", "accountStatusOnline": "在線",
@ -741,5 +741,9 @@
"fieldLocation": "位置", "fieldLocation": "位置",
"fieldLinks": "鏈接", "fieldLinks": "鏈接",
"fieldLinkName": "名稱", "fieldLinkName": "名稱",
"fieldLinkUrl": "鏈接" "fieldLinkUrl": "鏈接",
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。"
} }

View File

@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart'; import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/account_settings.dart'; import 'package:surface/screens/account/account_settings.dart';
import 'package:surface/screens/account/badges.dart';
import 'package:surface/screens/account/factor_settings.dart'; import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/profile_edit.dart';
@ -42,8 +43,8 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation, Widget _fadeThroughTransition(
Animation<double> secondaryAnimation, Widget child) { BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition( return FadeThroughTransition(
animation: animation, animation: animation,
secondaryAnimation: secondaryAnimation, secondaryAnimation: secondaryAnimation,
@ -85,15 +86,13 @@ final _appRoutes = [
name: 'postSearch', name: 'postSearch',
builder: (context, state) => PostSearchScreen( builder: (context, state) => PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','), initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: initialCategories: state.uri.queryParameters['categories']?.split(','),
state.uri.queryParameters['categories']?.split(','),
), ),
), ),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
PostPublisherScreen(name: state.pathParameters['name']!),
), ),
GoRoute( GoRoute(
path: '/:slug', path: '/:slug',
@ -106,55 +105,61 @@ final _appRoutes = [
], ],
), ),
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
builder: (context, state) => const AccountScreen(), builder: (context, state) => const AccountScreen(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/wallet', path: '/badges',
name: 'accountWallet', name: 'accountBadges',
builder: (context, state) => const WalletScreen(), builder: (context, state) => const AccountBadgesScreen(),
),
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
), ),
GoRoute( ),
path: '/settings', GoRoute(
name: 'accountSettings', path: '/:name',
builder: (context, state) => AccountSettingsScreen(), name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
), ),
GoRoute( ),
path: '/settings/factors', ],
name: 'factorSettings', ),
builder: (context, state) => FactorSettingsScreen(),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
GoRoute(
path: '/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
]),
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
@ -217,8 +222,7 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/:alias', path: '/:alias',
name: 'realmDetail', name: 'realmDetail',
builder: (context, state) => builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
RealmDetailScreen(alias: state.pathParameters['alias']!),
), ),
], ],
), ),

View File

@ -173,6 +173,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('accountWallet'); GoRouter.of(context).pushNamed('accountWallet');
}, },
), ),
ListTile(
title: Text('accountBadges').tr(),
subtitle: Text('accountBadgesDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.award_star),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountBadges');
},
),
ListTile( ListTile(
title: Text('accountSettings').tr(), title: Text('accountSettings').tr(),
subtitle: Text('accountSettingsSubtitle').tr(), subtitle: Text('accountSettingsSubtitle').tr(),

View File

@ -0,0 +1,135 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart' show kBadgesMeta;
import 'package:surface/theme.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountBadgesScreen extends StatefulWidget {
const AccountBadgesScreen({super.key});
@override
State<AccountBadgesScreen> createState() => _AccountBadgesScreenState();
}
class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
bool _isBusy = false;
List<SnAccountBadge>? _badges;
Future<void> _fetchBadges() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/badges/me');
if (!mounted) return;
setState(
() => _badges = List<SnAccountBadge>.from(
resp.data?.map((e) => SnAccountBadge.fromJson(e)) ?? [],
),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
bool _isActivating = false;
Future<void> _activateBadge(SnAccountBadge badge) async {
try {
setState(() => _isActivating = true);
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/badges/${badge.id}/active');
if (!mounted) return;
context.showSnackbar('badgeActivated'.tr(args: [(kBadgesMeta[badge.type]?.$1 ?? 'unknown').tr()]));
await _fetchBadges();
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isActivating = false);
}
}
@override
void initState() {
super.initState();
_fetchBadges();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(
title: Text('screenAccountBadges').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
if (_badges != null)
Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator(
onRefresh: _fetchBadges,
child: ListView.builder(
itemCount: _badges!.length,
itemBuilder: (context, idx) {
final badge = _badges![idx];
return ListTile(
title: Text(
kBadgesMeta[badge.type]?.$1 ?? 'unknown',
).tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (badge.metadata['title'] != null)
Text(badge.metadata['title']).fontSize(14).bold()
else
Text(
'#${badge.id.toString().padLeft(8, '0')}',
style: GoogleFonts.robotoMono(),
).fontSize(14).bold(),
Text(
DateFormat('y/M/d').format(badge.createdAt),
)
],
),
trailing: IconButton(
icon: const Icon(Symbols.check),
onPressed: (badge.isActive || _isActivating)
? null
: () {
_activateBadge(badge);
},
),
leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
color: badge.metadata['color'] != null
? HexColor.fromHex(badge.metadata['color']!)
: kBadgesMeta[badge.type]?.$3,
fill: 1,
),
);
},
),
),
),
),
],
),
);
}
}

View File

@ -20,6 +20,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:surface/theme.dart';
const Map<String, (String, IconData, Color)> kBadgesMeta = { const Map<String, (String, IconData, Color)> kBadgesMeta = {
'company.staff': ( 'company.staff': (
@ -43,8 +44,7 @@ class UserScreen extends StatefulWidget {
State<UserScreen> createState() => _UserScreenState(); State<UserScreen> createState() => _UserScreenState();
} }
class _UserScreenState extends State<UserScreen> class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
SnAccount? _account; SnAccount? _account;
@ -70,8 +70,7 @@ class _UserScreenState extends State<UserScreen>
Future<void> _getCheckInRecords() async { Future<void> _getCheckInRecords() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
setState(() { setState(() {
_records = List.from( _records = List.from(
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [], resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
@ -104,8 +103,7 @@ class _UserScreenState extends State<UserScreen>
Future<void> _fetchPublishers() async { Future<void> _fetchPublishers() async {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}');
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
_publishers = List<SnPublisher>.from( _publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [], resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
); );
@ -151,8 +149,7 @@ class _UserScreenState extends State<UserScreen>
'related': _account!.name, 'related': _account!.name,
}); });
if (!mounted) return; if (!mounted) return;
context.showSnackbar( context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -168,11 +165,9 @@ class _UserScreenState extends State<UserScreen>
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship( await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return; if (!mounted) return;
context.showSnackbar( context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -198,14 +193,12 @@ class _UserScreenState extends State<UserScreen>
double _appBarBlur = 0.0; double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
setState(() { setState(() {
_appBarBlur = _appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
}); });
} }
@ -273,20 +266,18 @@ class _UserScreenState extends State<UserScreen>
text: TextSpan(children: [ text: TextSpan(children: [
TextSpan( TextSpan(
text: _account!.nick, text: _account!.nick,
style: style: Theme.of(context).textTheme.titleLarge!.copyWith(
Theme.of(context).textTheme.titleLarge!.copyWith( color: Colors.white,
color: Colors.white, shadows: labelShadows,
shadows: labelShadows, ),
),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: '@${_account!.name}', text: '@${_account!.name}',
style: style: Theme.of(context).textTheme.bodySmall!.copyWith(
Theme.of(context).textTheme.bodySmall!.copyWith( color: Colors.white,
color: Colors.white, shadows: labelShadows,
shadows: labelShadows, ),
),
), ),
]), ]),
), ),
@ -354,8 +345,7 @@ class _UserScreenState extends State<UserScreen>
PopupMenuButton( PopupMenuButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
style: ButtonStyle( style: ButtonStyle(
visualDensity: visualDensity: VisualDensity(horizontal: -4, vertical: -4),
VisualDensity(horizontal: -4, vertical: -4),
), ),
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
@ -415,9 +405,7 @@ class _UserScreenState extends State<UserScreen>
Symbols.circle, Symbols.circle,
fill: 1, fill: 1,
size: 16, size: 16,
color: (_status?.isOnline ?? false) color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
? Colors.green
: Colors.grey,
).padding(all: 4), ).padding(all: 4),
const Gap(8), const Gap(8),
Text( Text(
@ -427,9 +415,7 @@ class _UserScreenState extends State<UserScreen>
: 'accountStatusOffline'.tr() : 'accountStatusOffline'.tr()
: 'loading'.tr(), : 'loading'.tr(),
), ),
if (_status != null && if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
!_status!.isOnline &&
_status!.lastSeenAt != null)
Text( Text(
'accountStatusLastSeen'.tr(args: [ 'accountStatusLastSeen'.tr(args: [
_status!.lastSeenAt != null _status!.lastSeenAt != null
@ -450,13 +436,12 @@ class _UserScreenState extends State<UserScreen>
richMessage: TextSpan( richMessage: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: kBadgesMeta[ele.type]?.$1.tr() ?? text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr(),
'unknown'.tr()), ),
if (ele.metadata['title'] != null) if (ele.metadata['title'] != null)
TextSpan( TextSpan(
text: '\n${ele.metadata['title']}', text: '\n${ele.metadata['title']}',
style: const TextStyle( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold),
), ),
TextSpan(text: '\n'), TextSpan(text: '\n'),
TextSpan( TextSpan(
@ -465,9 +450,10 @@ class _UserScreenState extends State<UserScreen>
], ],
), ),
child: Icon( child: Icon(
kBadgesMeta[ele.type]?.$2 ?? kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
Symbols.question_mark, color: ele.metadata['color'] != null
color: kBadgesMeta[ele.type]?.$3, ? HexColor.fromHex(ele.metadata['color']!)
: kBadgesMeta[ele.type]?.$3,
fill: 1, fill: 1,
), ),
), ),
@ -482,9 +468,7 @@ class _UserScreenState extends State<UserScreen>
children: [ children: [
const Icon(Symbols.calendar_add_on), const Icon(Symbols.calendar_add_on),
const Gap(8), const Gap(8),
Text('publisherJoinedAt').tr(args: [ Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
DateFormat('y/M/d').format(_account!.createdAt)
]),
], ],
), ),
Row( Row(
@ -517,24 +501,17 @@ class _UserScreenState extends State<UserScreen>
children: [ children: [
const Icon(Symbols.star), const Icon(Symbols.star),
const Gap(8), const Gap(8),
Text( Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
const Gap(8), const Gap(8),
Text(calcLevelUpProgressLevel( Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
_account?.profile?.experience ?? 0))
.fontSize(11)
.opacity(0.5),
const Gap(8), const Gap(8),
Container( Container(
width: double.infinity, width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160), constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: calcLevelUpProgress( value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
_account?.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
.colorScheme
.surfaceContainer,
).alignment(Alignment.centerLeft), ).alignment(Alignment.centerLeft),
), ),
], ],
@ -554,11 +531,7 @@ class _UserScreenState extends State<UserScreen>
return Text( return Text(
'accountCheckInNoRecords', 'accountCheckInNoRecords',
textAlign: TextAlign.center, textAlign: TextAlign.center,
) ).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
.tr()
.fontWeight(FontWeight.bold)
.center()
.padding(horizontal: 20, vertical: 8);
} }
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
@ -579,11 +552,7 @@ class _UserScreenState extends State<UserScreen>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('accountBadge') Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
SizedBox( SizedBox(
height: 80, height: 80,
width: double.infinity, width: double.infinity,
@ -597,9 +566,10 @@ class _UserScreenState extends State<UserScreen>
child: Card( child: Card(
child: ListTile( child: ListTile(
leading: Icon( leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
Symbols.question_mark, color: badge.metadata['color'] != null
color: kBadgesMeta[badge.type]?.$3, ? HexColor.fromHex(badge.metadata['color']!)
: kBadgesMeta[badge.type]?.$3,
fill: 1, fill: 1,
), ),
title: Text( title: Text(
@ -608,8 +578,7 @@ class _UserScreenState extends State<UserScreen>
subtitle: badge.metadata['title'] != null subtitle: badge.metadata['title'] != null
? Text(badge.metadata['title']) ? Text(badge.metadata['title'])
: Text( : Text(
DateFormat('y/M/d') DateFormat('y/M/d').format(badge.createdAt),
.format(badge.createdAt),
), ),
), ),
), ),
@ -705,8 +674,7 @@ class CheckInRecordChart extends StatelessWidget {
), ),
) )
.toList(), .toList(),
getTooltipColor: (_) => getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
), ),
titlesData: FlTitlesData( titlesData: FlTitlesData(

View File

@ -89,3 +89,20 @@ Future<ThemeData> createAppTheme(
), ),
); );
} }
extension HexColor on Color {
/// String is in the format "aabbcc" or "ffaabbcc" with an optional leading "#".
static Color fromHex(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 6 || hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
/// Prefixes a hash sign if [leadingHashSign] is set to `true` (default is `true`).
String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}'
'${alpha.toRadixString(16).padLeft(2, '0')}'
'${red.toRadixString(16).padLeft(2, '0')}'
'${green.toRadixString(16).padLeft(2, '0')}'
'${blue.toRadixString(16).padLeft(2, '0')}';
}

View File

@ -105,6 +105,7 @@ abstract class SnAccountBadge with _$SnAccountBadge {
required dynamic deletedAt, required dynamic deletedAt,
required String type, required String type,
required int accountId, required int accountId,
@Default(false) bool isActive,
@Default({}) Map<String, dynamic> metadata, @Default({}) Map<String, dynamic> metadata,
}) = _SnAccountBadge; }) = _SnAccountBadge;

View File

@ -1835,6 +1835,7 @@ mixin _$SnAccountBadge {
dynamic get deletedAt; dynamic get deletedAt;
String get type; String get type;
int get accountId; int get accountId;
bool get isActive;
Map<String, dynamic> get metadata; Map<String, dynamic> get metadata;
/// Create a copy of SnAccountBadge /// Create a copy of SnAccountBadge
@ -1862,6 +1863,8 @@ mixin _$SnAccountBadge {
(identical(other.type, type) || other.type == type) && (identical(other.type, type) || other.type == type) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
const DeepCollectionEquality().equals(other.metadata, metadata)); const DeepCollectionEquality().equals(other.metadata, metadata));
} }
@ -1875,11 +1878,12 @@ mixin _$SnAccountBadge {
const DeepCollectionEquality().hash(deletedAt), const DeepCollectionEquality().hash(deletedAt),
type, type,
accountId, accountId,
isActive,
const DeepCollectionEquality().hash(metadata)); const DeepCollectionEquality().hash(metadata));
@override @override
String toString() { String toString() {
return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, metadata: $metadata)'; return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, isActive: $isActive, metadata: $metadata)';
} }
} }
@ -1896,6 +1900,7 @@ abstract mixin class $SnAccountBadgeCopyWith<$Res> {
dynamic deletedAt, dynamic deletedAt,
String type, String type,
int accountId, int accountId,
bool isActive,
Map<String, dynamic> metadata}); Map<String, dynamic> metadata});
} }
@ -1918,6 +1923,7 @@ class _$SnAccountBadgeCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? type = null, Object? type = null,
Object? accountId = null, Object? accountId = null,
Object? isActive = null,
Object? metadata = null, Object? metadata = null,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
@ -1945,6 +1951,10 @@ class _$SnAccountBadgeCopyWithImpl<$Res>
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
isActive: null == isActive
? _self.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool,
metadata: null == metadata metadata: null == metadata
? _self.metadata ? _self.metadata
: metadata // ignore: cast_nullable_to_non_nullable : metadata // ignore: cast_nullable_to_non_nullable
@ -1963,6 +1973,7 @@ class _SnAccountBadge implements SnAccountBadge {
required this.deletedAt, required this.deletedAt,
required this.type, required this.type,
required this.accountId, required this.accountId,
this.isActive = false,
final Map<String, dynamic> metadata = const {}}) final Map<String, dynamic> metadata = const {}})
: _metadata = metadata; : _metadata = metadata;
factory _SnAccountBadge.fromJson(Map<String, dynamic> json) => factory _SnAccountBadge.fromJson(Map<String, dynamic> json) =>
@ -1980,6 +1991,9 @@ class _SnAccountBadge implements SnAccountBadge {
final String type; final String type;
@override @override
final int accountId; final int accountId;
@override
@JsonKey()
final bool isActive;
final Map<String, dynamic> _metadata; final Map<String, dynamic> _metadata;
@override @override
@JsonKey() @JsonKey()
@ -2018,6 +2032,8 @@ class _SnAccountBadge implements SnAccountBadge {
(identical(other.type, type) || other.type == type) && (identical(other.type, type) || other.type == type) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.isActive, isActive) ||
other.isActive == isActive) &&
const DeepCollectionEquality().equals(other._metadata, _metadata)); const DeepCollectionEquality().equals(other._metadata, _metadata));
} }
@ -2031,11 +2047,12 @@ class _SnAccountBadge implements SnAccountBadge {
const DeepCollectionEquality().hash(deletedAt), const DeepCollectionEquality().hash(deletedAt),
type, type,
accountId, accountId,
isActive,
const DeepCollectionEquality().hash(_metadata)); const DeepCollectionEquality().hash(_metadata));
@override @override
String toString() { String toString() {
return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, metadata: $metadata)'; return 'SnAccountBadge(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, accountId: $accountId, isActive: $isActive, metadata: $metadata)';
} }
} }
@ -2054,6 +2071,7 @@ abstract mixin class _$SnAccountBadgeCopyWith<$Res>
dynamic deletedAt, dynamic deletedAt,
String type, String type,
int accountId, int accountId,
bool isActive,
Map<String, dynamic> metadata}); Map<String, dynamic> metadata});
} }
@ -2076,6 +2094,7 @@ class __$SnAccountBadgeCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? type = null, Object? type = null,
Object? accountId = null, Object? accountId = null,
Object? isActive = null,
Object? metadata = null, Object? metadata = null,
}) { }) {
return _then(_SnAccountBadge( return _then(_SnAccountBadge(
@ -2103,6 +2122,10 @@ class __$SnAccountBadgeCopyWithImpl<$Res>
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
isActive: null == isActive
? _self.isActive
: isActive // ignore: cast_nullable_to_non_nullable
as bool,
metadata: null == metadata metadata: null == metadata
? _self._metadata ? _self._metadata
: metadata // ignore: cast_nullable_to_non_nullable : metadata // ignore: cast_nullable_to_non_nullable

View File

@ -187,6 +187,7 @@ _SnAccountBadge _$SnAccountBadgeFromJson(Map<String, dynamic> json) =>
deletedAt: json['deleted_at'], deletedAt: json['deleted_at'],
type: json['type'] as String, type: json['type'] as String,
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
isActive: json['is_active'] as bool? ?? false,
metadata: json['metadata'] as Map<String, dynamic>? ?? const {}, metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
); );
@ -198,6 +199,7 @@ Map<String, dynamic> _$SnAccountBadgeToJson(_SnAccountBadge instance) =>
'deleted_at': instance.deletedAt, 'deleted_at': instance.deletedAt,
'type': instance.type, 'type': instance.type,
'account_id': instance.accountId, 'account_id': instance.accountId,
'is_active': instance.isActive,
'metadata': instance.metadata, 'metadata': instance.metadata,
}; };