✨ User experience & level
This commit is contained in:
parent
1fd042bcae
commit
d8b2c7f81e
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(),
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user