✨ User experience & level
This commit is contained in:
		
							
								
								
									
										51
									
								
								lib/providers/experience.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								lib/providers/experience.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
|  | ||||
| class ExperienceProvider extends GetxController { | ||||
|   static List<int> experienceToLevelRequirements = [ | ||||
|     0, // Level 0 | ||||
|     100, // Level 1 | ||||
|     400, // Level 2 | ||||
|     900, // Level 3 | ||||
|     1600, // Level 4 | ||||
|     2500, // Level 5 | ||||
|     3600, // Level 6 | ||||
|     4900, // Level 7 | ||||
|     6400, // Level 8 | ||||
|     8100, // Level 9 | ||||
|     10000, // Level 10 | ||||
|     12100, // Level 11 | ||||
|     14400, // Level 12 | ||||
|     36800 // Level 13 | ||||
|   ]; | ||||
|  | ||||
|   static List<String> levelLabelMapping = | ||||
|       List.generate(experienceToLevelRequirements.length, (x) => 'userLevel$x'); | ||||
|  | ||||
|   static (int level, String label) getLevelFromExp(int experience) { | ||||
|     final exp = experienceToLevelRequirements.reversed | ||||
|         .firstWhere((x) => x <= experience); | ||||
|     final idx = experienceToLevelRequirements.indexOf(exp); | ||||
|     return (idx, levelLabelMapping[idx]); | ||||
|   } | ||||
|  | ||||
|   static double calcLevelUpProgress(int experience) { | ||||
|     final exp = experienceToLevelRequirements.reversed | ||||
|         .firstWhere((x) => x <= experience); | ||||
|     final idx = experienceToLevelRequirements.indexOf(exp); | ||||
|     if (idx + 1 >= experienceToLevelRequirements.length) return 1; | ||||
|     final nextExp = experienceToLevelRequirements[idx + 1]; | ||||
|     return exp / nextExp; | ||||
|   } | ||||
|  | ||||
|   static String calcLevelUpProgressLevel(int experience) { | ||||
|     final exp = experienceToLevelRequirements.reversed | ||||
|         .firstWhere((x) => x <= experience); | ||||
|     final idx = experienceToLevelRequirements.indexOf(exp); | ||||
|     if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity'; | ||||
|     final nextExp = experienceToLevelRequirements[idx + 1]; | ||||
|     final formatter = | ||||
|         NumberFormat.compactCurrency(symbol: '', decimalDigits: 1); | ||||
|     return '${formatter.format(exp)}/${formatter.format(nextExp)}'; | ||||
|   } | ||||
| } | ||||
| @@ -8,10 +8,12 @@ import 'package:solian/models/account.dart'; | ||||
| import 'package:solian/models/attachment.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/models/post.dart'; | ||||
| import 'package:solian/providers/account_status.dart'; | ||||
| import 'package:solian/providers/relation.dart'; | ||||
| import 'package:solian/services.dart'; | ||||
| import 'package:solian/theme.dart'; | ||||
| import 'package:solian/widgets/account/account_avatar.dart'; | ||||
| import 'package:solian/widgets/account/account_heading.dart'; | ||||
| import 'package:solian/widgets/app_bar_leading.dart'; | ||||
| import 'package:solian/widgets/attachments/attachment_list.dart'; | ||||
| import 'package:solian/widgets/posts/post_list.dart'; | ||||
| @@ -143,7 +145,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> { | ||||
|     return Material( | ||||
|       color: Theme.of(context).colorScheme.surface, | ||||
|       child: DefaultTabController( | ||||
|         length: 2, | ||||
|         length: 3, | ||||
|         child: NestedScrollView( | ||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|             return [ | ||||
| @@ -211,6 +213,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> { | ||||
|                 ), | ||||
|                 bottom: TabBar( | ||||
|                   tabs: [ | ||||
|                     Tab(text: 'profilePage'.tr), | ||||
|                     Tab(text: 'profilePosts'.tr), | ||||
|                     Tab(text: 'profileAlbum'.tr), | ||||
|                   ], | ||||
| @@ -221,6 +224,24 @@ class _AccountProfilePageState extends State<AccountProfilePage> { | ||||
|           body: TabBarView( | ||||
|             physics: const NeverScrollableScrollPhysics(), | ||||
|             children: [ | ||||
|               Column( | ||||
|                 children: [ | ||||
|                   const Gap(16), | ||||
|                   AccountHeadingWidget( | ||||
|                     name: _userinfo!.name, | ||||
|                     nick: _userinfo!.nick, | ||||
|                     desc: _userinfo!.description, | ||||
|                     badges: _userinfo!.badges, | ||||
|                     banner: _userinfo!.banner, | ||||
|                     avatar: _userinfo!.avatar, | ||||
|                     status: Get.find<StatusProvider>() | ||||
|                         .getSomeoneStatus(_userinfo!.name), | ||||
|                     detail: _userinfo, | ||||
|                     profile: _userinfo!.profile, | ||||
|                     extraWidgets: const [], | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               RefreshIndicator( | ||||
|                 onRefresh: () => Future.wait([ | ||||
|                   _postController.reloadAllOver(), | ||||
|   | ||||
| @@ -136,10 +136,12 @@ class _DashboardScreenState extends State<DashboardScreen> { | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Text( | ||||
|                 'today'.tr, | ||||
|                 DateTime.now().day == DateTime.now().toUtc().day | ||||
|                     ? 'today'.tr | ||||
|                     : 'yesterday'.tr, | ||||
|                 style: Theme.of(context).textTheme.headlineSmall, | ||||
|               ), | ||||
|               Text(DateFormat('yyyy/MM/dd').format(DateTime.now())), | ||||
|               Text(DateFormat('yyyy/MM/dd').format(DateTime.now().toUtc())), | ||||
|             ], | ||||
|           ).paddingOnly(top: 8, left: 18, right: 18, bottom: 12), | ||||
|           Card( | ||||
| @@ -502,16 +504,7 @@ class _DashboardScreenState extends State<DashboardScreen> { | ||||
|               ), | ||||
|               Text( | ||||
|                 'dashboardFooter'.tr, | ||||
|                 style: [const Locale('zh', 'CN'), const Locale('zh', 'HK')] | ||||
|                         .contains(Get.deviceLocale) | ||||
|                     ? GoogleFonts.notoSerifHk( | ||||
|                         color: _unFocusColor, | ||||
|                         fontSize: 12, | ||||
|                       ) | ||||
|                     : TextStyle( | ||||
|                         color: _unFocusColor, | ||||
|                         fontSize: 12, | ||||
|                       ), | ||||
|                 style: TextStyle(color: _unFocusColor, fontSize: 12), | ||||
|               ) | ||||
|             ], | ||||
|           ).paddingAll(8), | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:solian/theme.dart'; | ||||
| import 'package:solian/widgets/app_bar_title.dart'; | ||||
| import 'package:solian/widgets/app_bar_leading.dart'; | ||||
| import 'package:solian/widgets/current_state_action.dart'; | ||||
|  | ||||
| class TitleShell extends StatelessWidget { | ||||
|   final bool showAppBar; | ||||
| @@ -32,6 +33,12 @@ class TitleShell extends StatelessWidget { | ||||
|               ), | ||||
|               centerTitle: isCenteredTitle, | ||||
|               toolbarHeight: SolianTheme.toolbarHeight(context), | ||||
|               actions: [ | ||||
|                 const BackgroundStateWidget(), | ||||
|                 SizedBox( | ||||
|                   width: SolianTheme.isLargeScreen(context) ? 8 : 16, | ||||
|                 ), | ||||
|               ], | ||||
|             ) | ||||
|           : null, | ||||
|       body: child, | ||||
|   | ||||
| @@ -10,6 +10,7 @@ const i18nEnglish = { | ||||
|   'draft': 'Draft', | ||||
|   'dashboard': 'Dashboard', | ||||
|   'today': 'Today', | ||||
|   'yesterday': 'Yesterday', | ||||
|   'draftSave': 'Save', | ||||
|   'draftBox': 'Draft Box', | ||||
|   'more': 'More', | ||||
| @@ -33,6 +34,7 @@ const i18nEnglish = { | ||||
|   'dailySignTier4': 'Everything will be awesome', | ||||
|   'dashboardFooter': 'Don\'t be serious, just for fun.', | ||||
|   'visitProfilePage': 'Visit Profile Page', | ||||
|   'profilePage': 'Page', | ||||
|   'profilePosts': 'Posts', | ||||
|   'profileAlbum': 'Album', | ||||
|   'chat': 'Chat', | ||||
| @@ -400,4 +402,18 @@ const i18nEnglish = { | ||||
|   'collapse': 'Collapse', | ||||
|   'expand': 'Expand', | ||||
|   'typingMessage': '@user are typing...', | ||||
|   'userLevel0': 'Newbie', | ||||
|   'userLevel1': 'Novice', | ||||
|   'userLevel2': 'Apprentice', | ||||
|   'userLevel3': 'Explorer', | ||||
|   'userLevel4': 'Adventurer', | ||||
|   'userLevel5': 'Warrior', | ||||
|   'userLevel6': 'Knight', | ||||
|   'userLevel7': 'Champion', | ||||
|   'userLevel8': 'Hero', | ||||
|   'userLevel9': 'Master', | ||||
|   'userLevel10': 'Grandmaster', | ||||
|   'userLevel11': 'Legend', | ||||
|   'userLevel12': 'Mythic', | ||||
|   'userLevel13': 'Immortal', | ||||
| }; | ||||
|   | ||||
| @@ -26,6 +26,7 @@ const i18nSimplifiedChinese = { | ||||
|   'unlink': '移除链接', | ||||
|   'dashboard': '仪表盘', | ||||
|   'today': '今日', | ||||
|   'yesterday': '昨日', | ||||
|   'feedSearch': '搜索资讯', | ||||
|   'feedSearchWithTag': '检索带有 #@key 标签的资讯', | ||||
|   'feedSearchWithCategory': '检索位于分类 @category 的资讯', | ||||
| @@ -41,6 +42,7 @@ const i18nSimplifiedChinese = { | ||||
|   'dailySignTier4': '诸事皆宜', | ||||
|   'dashboardFooter': '占卜多少沾点玩,人生还得靠实力', | ||||
|   'visitProfilePage': '造访个人主页', | ||||
|   'profilePage': '主页', | ||||
|   'profilePosts': '帖子', | ||||
|   'profileAlbum': '相簿', | ||||
|   'chat': '聊天', | ||||
| @@ -370,4 +372,18 @@ const i18nSimplifiedChinese = { | ||||
|   'collapse': '折叠', | ||||
|   'expand': '展开', | ||||
|   'typingMessage': '@user 正在输入中…', | ||||
|   'userLevel0': '不慕名利', | ||||
|   'userLevel1': '初出茅庐', | ||||
|   'userLevel2': '小试牛刀', | ||||
|   'userLevel3': '磨杵成针', | ||||
|   'userLevel4': '披荆斩棘', | ||||
|   'userLevel5': '力挽狂澜', | ||||
|   'userLevel6': '一骑当千', | ||||
|   'userLevel7': '所向披靡', | ||||
|   'userLevel8': '气吞山河', | ||||
|   'userLevel9': '登峰造极', | ||||
|   'userLevel10': '出神入化', | ||||
|   'userLevel11': '名垂千古', | ||||
|   'userLevel12': '独占鳌头', | ||||
|   'userLevel13': '万古流芳', | ||||
| }; | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:get/get.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:intl/intl.dart'; | ||||
| import 'package:solian/models/account.dart'; | ||||
| import 'package:solian/models/account_status.dart'; | ||||
| import 'package:solian/platform.dart'; | ||||
| import 'package:solian/providers/account_status.dart'; | ||||
| import 'package:solian/providers/experience.dart'; | ||||
| import 'package:solian/widgets/account/account_avatar.dart'; | ||||
| import 'package:solian/widgets/account/account_badge.dart'; | ||||
| import 'package:solian/widgets/account/account_status_action.dart'; | ||||
| @@ -18,6 +20,7 @@ class AccountHeadingWidget extends StatelessWidget { | ||||
|   final String nick; | ||||
|   final String? desc; | ||||
|   final Account? detail; | ||||
|   final AccountProfile? profile; | ||||
|   final List<AccountBadge>? badges; | ||||
|   final List<Widget>? extraWidgets; | ||||
|  | ||||
| @@ -33,6 +36,7 @@ class AccountHeadingWidget extends StatelessWidget { | ||||
|     required this.desc, | ||||
|     required this.badges, | ||||
|     this.detail, | ||||
|     this.profile, | ||||
|     this.status, | ||||
|     this.extraWidgets, | ||||
|     this.onEditStatus, | ||||
| @@ -130,7 +134,10 @@ class AccountHeadingWidget extends StatelessWidget { | ||||
|                         child: Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           children: [ | ||||
|                             Text(info.$3), | ||||
|                             Text( | ||||
|                               info.$3, | ||||
|                               style: const TextStyle(height: 1), | ||||
|                             ).paddingOnly(bottom: 3), | ||||
|                             if (!status.isOnline && status.lastSeenAt != null) | ||||
|                               Opacity( | ||||
|                                 opacity: 0.75, | ||||
| @@ -182,6 +189,63 @@ class AccountHeadingWidget extends StatelessWidget { | ||||
|                 ), | ||||
|               ), | ||||
|             ).paddingSymmetric(horizontal: 16), | ||||
|           if (profile != null) | ||||
|             Card( | ||||
|               child: ListTile( | ||||
|                 shape: const RoundedRectangleBorder( | ||||
|                   borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                 ), | ||||
|                 visualDensity: | ||||
|                     const VisualDensity(horizontal: -4, vertical: -2), | ||||
|                 title: Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     Text( | ||||
|                       ExperienceProvider.getLevelFromExp( | ||||
|                         profile!.experience ?? 0, | ||||
|                       ).$2.tr, | ||||
|                     ), | ||||
|                     const Gap(4), | ||||
|                     Badge( | ||||
|                       label: Text( | ||||
|                         'Lv${ExperienceProvider.getLevelFromExp( | ||||
|                           profile!.experience ?? 0, | ||||
|                         ).$1}', | ||||
|                         style: GoogleFonts.dosis( | ||||
|                           fontWeight: FontWeight.bold, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ).paddingOnly(top: 1), | ||||
|                   ], | ||||
|                 ), | ||||
|                 subtitle: SizedBox( | ||||
|                   height: 20, | ||||
|                   child: Row( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       Expanded( | ||||
|                         child: LinearProgressIndicator( | ||||
|                           borderRadius: const BorderRadius.all( | ||||
|                             Radius.circular(8), | ||||
|                           ), | ||||
|                           value: ExperienceProvider.calcLevelUpProgress( | ||||
|                             profile!.experience ?? 0, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                       const Gap(8), | ||||
|                       Text( | ||||
|                         '${ExperienceProvider.calcLevelUpProgressLevel(profile!.experience ?? 0)} EXP', | ||||
|                         style: GoogleFonts.robotoMono( | ||||
|                           fontSize: 10, | ||||
|                           height: 0.5, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ).paddingSymmetric(horizontal: 16), | ||||
|           SizedBox( | ||||
|             width: double.infinity, | ||||
|             child: Card( | ||||
|   | ||||
| @@ -99,6 +99,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> { | ||||
|             nick: _userinfo!.nick, | ||||
|             desc: _userinfo!.description, | ||||
|             detail: _userinfo!, | ||||
|             profile: _userinfo!.profile, | ||||
|             badges: _userinfo!.badges, | ||||
|             status: | ||||
|                 Get.find<StatusProvider>().getSomeoneStatus(_userinfo!.name), | ||||
|   | ||||
| @@ -347,8 +347,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> { | ||||
|                             FutureBuilder( | ||||
|                               future: element.file.length(), | ||||
|                               builder: (context, snapshot) { | ||||
|                                 if (!snapshot.hasData) | ||||
|                                 if (!snapshot.hasData) { | ||||
|                                   return const SizedBox.shrink(); | ||||
|                                 } | ||||
|                                 return Text( | ||||
|                                   _formatBytes(snapshot.data!), | ||||
|                                   style: Theme.of(context).textTheme.bodySmall, | ||||
|   | ||||
| @@ -131,7 +131,7 @@ class DailySignHistoryChartDialog extends StatelessWidget { | ||||
|                             reservedSize: 28, | ||||
|                             interval: 86400000, | ||||
|                             getTitlesWidget: (value, _) => Text( | ||||
|                               DateFormat('MM/dd').format( | ||||
|                               DateFormat('dd').format( | ||||
|                                 DateTime.fromMillisecondsSinceEpoch( | ||||
|                                   value.toInt(), | ||||
|                                 ), | ||||
| @@ -231,7 +231,7 @@ class DailySignHistoryChartDialog extends StatelessWidget { | ||||
|                             reservedSize: 28, | ||||
|                             interval: 86400000, | ||||
|                             getTitlesWidget: (value, _) => Text( | ||||
|                               DateFormat('MM/dd').format( | ||||
|                               DateFormat('dd').format( | ||||
|                                 DateTime.fromMillisecondsSinceEpoch( | ||||
|                                   value.toInt(), | ||||
|                                 ), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user