diff --git a/lib/providers/experience.dart b/lib/providers/experience.dart new file mode 100644 index 0000000..192fa54 --- /dev/null +++ b/lib/providers/experience.dart @@ -0,0 +1,51 @@ +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; + +class ExperienceProvider extends GetxController { + static List 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 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)}'; + } +} diff --git a/lib/screens/account/profile_page.dart b/lib/screens/account/profile_page.dart index 45faed6..80735ed 100644 --- a/lib/screens/account/profile_page.dart +++ b/lib/screens/account/profile_page.dart @@ -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 { 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 { ), bottom: TabBar( tabs: [ + Tab(text: 'profilePage'.tr), Tab(text: 'profilePosts'.tr), Tab(text: 'profileAlbum'.tr), ], @@ -221,6 +224,24 @@ class _AccountProfilePageState extends State { 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() + .getSomeoneStatus(_userinfo!.name), + detail: _userinfo, + profile: _userinfo!.profile, + extraWidgets: const [], + ), + ], + ), RefreshIndicator( onRefresh: () => Future.wait([ _postController.reloadAllOver(), diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index dda2c01..a5cb515 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -136,10 +136,12 @@ class _DashboardScreenState extends State { 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 { ), 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), diff --git a/lib/shells/title_shell.dart b/lib/shells/title_shell.dart index fa35e4c..1fa6820 100644 --- a/lib/shells/title_shell.dart +++ b/lib/shells/title_shell.dart @@ -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, diff --git a/lib/translations/en_us.dart b/lib/translations/en_us.dart index b0c7a91..e18866c 100644 --- a/lib/translations/en_us.dart +++ b/lib/translations/en_us.dart @@ -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', }; diff --git a/lib/translations/zh_cn.dart b/lib/translations/zh_cn.dart index a04e6ba..1099a5a 100644 --- a/lib/translations/zh_cn.dart +++ b/lib/translations/zh_cn.dart @@ -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': '万古流芳', }; diff --git a/lib/widgets/account/account_heading.dart b/lib/widgets/account/account_heading.dart index c9a4b1f..ffd4305 100644 --- a/lib/widgets/account/account_heading.dart +++ b/lib/widgets/account/account_heading.dart @@ -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? badges; final List? 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( diff --git a/lib/widgets/account/account_profile_popup.dart b/lib/widgets/account/account_profile_popup.dart index 65ce616..2d9c08f 100644 --- a/lib/widgets/account/account_profile_popup.dart +++ b/lib/widgets/account/account_profile_popup.dart @@ -99,6 +99,7 @@ class _AccountProfilePopupState extends State { nick: _userinfo!.nick, desc: _userinfo!.description, detail: _userinfo!, + profile: _userinfo!.profile, badges: _userinfo!.badges, status: Get.find().getSomeoneStatus(_userinfo!.name), diff --git a/lib/widgets/attachments/attachment_editor.dart b/lib/widgets/attachments/attachment_editor.dart index 4929d8a..32b350a 100644 --- a/lib/widgets/attachments/attachment_editor.dart +++ b/lib/widgets/attachments/attachment_editor.dart @@ -347,8 +347,9 @@ class _AttachmentEditorPopupState extends State { 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, diff --git a/lib/widgets/daily_sign/history_chart.dart b/lib/widgets/daily_sign/history_chart.dart index 0067f84..cc028aa 100644 --- a/lib/widgets/daily_sign/history_chart.dart +++ b/lib/widgets/daily_sign/history_chart.dart @@ -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(), ),