✨ 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/attachment.dart';
|
||||||
import 'package:solian/models/pagination.dart';
|
import 'package:solian/models/pagination.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/providers/account_status.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.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/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:solian/widgets/posts/post_list.dart';
|
import 'package:solian/widgets/posts/post_list.dart';
|
||||||
@ -143,7 +145,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
return Material(
|
return Material(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 2,
|
length: 3,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return [
|
return [
|
||||||
@ -211,6 +213,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
),
|
),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
|
Tab(text: 'profilePage'.tr),
|
||||||
Tab(text: 'profilePosts'.tr),
|
Tab(text: 'profilePosts'.tr),
|
||||||
Tab(text: 'profileAlbum'.tr),
|
Tab(text: 'profileAlbum'.tr),
|
||||||
],
|
],
|
||||||
@ -221,6 +224,24 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
body: TabBarView(
|
body: TabBarView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: [
|
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(
|
RefreshIndicator(
|
||||||
onRefresh: () => Future.wait([
|
onRefresh: () => Future.wait([
|
||||||
_postController.reloadAllOver(),
|
_postController.reloadAllOver(),
|
||||||
|
@ -136,10 +136,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'today'.tr,
|
DateTime.now().day == DateTime.now().toUtc().day
|
||||||
|
? 'today'.tr
|
||||||
|
: 'yesterday'.tr,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
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),
|
).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
|
||||||
Card(
|
Card(
|
||||||
@ -502,16 +504,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'dashboardFooter'.tr,
|
'dashboardFooter'.tr,
|
||||||
style: [const Locale('zh', 'CN'), const Locale('zh', 'HK')]
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
.contains(Get.deviceLocale)
|
|
||||||
? GoogleFonts.notoSerifHk(
|
|
||||||
color: _unFocusColor,
|
|
||||||
fontSize: 12,
|
|
||||||
)
|
|
||||||
: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).paddingAll(8),
|
).paddingAll(8),
|
||||||
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
|
||||||
class TitleShell extends StatelessWidget {
|
class TitleShell extends StatelessWidget {
|
||||||
final bool showAppBar;
|
final bool showAppBar;
|
||||||
@ -32,6 +33,12 @@ class TitleShell extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
centerTitle: isCenteredTitle,
|
centerTitle: isCenteredTitle,
|
||||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||||
|
actions: [
|
||||||
|
const BackgroundStateWidget(),
|
||||||
|
SizedBox(
|
||||||
|
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: child,
|
body: child,
|
||||||
|
@ -10,6 +10,7 @@ const i18nEnglish = {
|
|||||||
'draft': 'Draft',
|
'draft': 'Draft',
|
||||||
'dashboard': 'Dashboard',
|
'dashboard': 'Dashboard',
|
||||||
'today': 'Today',
|
'today': 'Today',
|
||||||
|
'yesterday': 'Yesterday',
|
||||||
'draftSave': 'Save',
|
'draftSave': 'Save',
|
||||||
'draftBox': 'Draft Box',
|
'draftBox': 'Draft Box',
|
||||||
'more': 'More',
|
'more': 'More',
|
||||||
@ -33,6 +34,7 @@ const i18nEnglish = {
|
|||||||
'dailySignTier4': 'Everything will be awesome',
|
'dailySignTier4': 'Everything will be awesome',
|
||||||
'dashboardFooter': 'Don\'t be serious, just for fun.',
|
'dashboardFooter': 'Don\'t be serious, just for fun.',
|
||||||
'visitProfilePage': 'Visit Profile Page',
|
'visitProfilePage': 'Visit Profile Page',
|
||||||
|
'profilePage': 'Page',
|
||||||
'profilePosts': 'Posts',
|
'profilePosts': 'Posts',
|
||||||
'profileAlbum': 'Album',
|
'profileAlbum': 'Album',
|
||||||
'chat': 'Chat',
|
'chat': 'Chat',
|
||||||
@ -400,4 +402,18 @@ const i18nEnglish = {
|
|||||||
'collapse': 'Collapse',
|
'collapse': 'Collapse',
|
||||||
'expand': 'Expand',
|
'expand': 'Expand',
|
||||||
'typingMessage': '@user are typing...',
|
'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': '移除链接',
|
'unlink': '移除链接',
|
||||||
'dashboard': '仪表盘',
|
'dashboard': '仪表盘',
|
||||||
'today': '今日',
|
'today': '今日',
|
||||||
|
'yesterday': '昨日',
|
||||||
'feedSearch': '搜索资讯',
|
'feedSearch': '搜索资讯',
|
||||||
'feedSearchWithTag': '检索带有 #@key 标签的资讯',
|
'feedSearchWithTag': '检索带有 #@key 标签的资讯',
|
||||||
'feedSearchWithCategory': '检索位于分类 @category 的资讯',
|
'feedSearchWithCategory': '检索位于分类 @category 的资讯',
|
||||||
@ -41,6 +42,7 @@ const i18nSimplifiedChinese = {
|
|||||||
'dailySignTier4': '诸事皆宜',
|
'dailySignTier4': '诸事皆宜',
|
||||||
'dashboardFooter': '占卜多少沾点玩,人生还得靠实力',
|
'dashboardFooter': '占卜多少沾点玩,人生还得靠实力',
|
||||||
'visitProfilePage': '造访个人主页',
|
'visitProfilePage': '造访个人主页',
|
||||||
|
'profilePage': '主页',
|
||||||
'profilePosts': '帖子',
|
'profilePosts': '帖子',
|
||||||
'profileAlbum': '相簿',
|
'profileAlbum': '相簿',
|
||||||
'chat': '聊天',
|
'chat': '聊天',
|
||||||
@ -370,4 +372,18 @@ const i18nSimplifiedChinese = {
|
|||||||
'collapse': '折叠',
|
'collapse': '折叠',
|
||||||
'expand': '展开',
|
'expand': '展开',
|
||||||
'typingMessage': '@user 正在输入中…',
|
'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:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/models/account_status.dart';
|
import 'package:solian/models/account_status.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/account_status.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_avatar.dart';
|
||||||
import 'package:solian/widgets/account/account_badge.dart';
|
import 'package:solian/widgets/account/account_badge.dart';
|
||||||
import 'package:solian/widgets/account/account_status_action.dart';
|
import 'package:solian/widgets/account/account_status_action.dart';
|
||||||
@ -18,6 +20,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
final String nick;
|
final String nick;
|
||||||
final String? desc;
|
final String? desc;
|
||||||
final Account? detail;
|
final Account? detail;
|
||||||
|
final AccountProfile? profile;
|
||||||
final List<AccountBadge>? badges;
|
final List<AccountBadge>? badges;
|
||||||
final List<Widget>? extraWidgets;
|
final List<Widget>? extraWidgets;
|
||||||
|
|
||||||
@ -33,6 +36,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
required this.desc,
|
required this.desc,
|
||||||
required this.badges,
|
required this.badges,
|
||||||
this.detail,
|
this.detail,
|
||||||
|
this.profile,
|
||||||
this.status,
|
this.status,
|
||||||
this.extraWidgets,
|
this.extraWidgets,
|
||||||
this.onEditStatus,
|
this.onEditStatus,
|
||||||
@ -130,7 +134,10 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(info.$3),
|
Text(
|
||||||
|
info.$3,
|
||||||
|
style: const TextStyle(height: 1),
|
||||||
|
).paddingOnly(bottom: 3),
|
||||||
if (!status.isOnline && status.lastSeenAt != null)
|
if (!status.isOnline && status.lastSeenAt != null)
|
||||||
Opacity(
|
Opacity(
|
||||||
opacity: 0.75,
|
opacity: 0.75,
|
||||||
@ -182,6 +189,63 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddingSymmetric(horizontal: 16),
|
).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(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Card(
|
child: Card(
|
||||||
|
@ -99,6 +99,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
nick: _userinfo!.nick,
|
nick: _userinfo!.nick,
|
||||||
desc: _userinfo!.description,
|
desc: _userinfo!.description,
|
||||||
detail: _userinfo!,
|
detail: _userinfo!,
|
||||||
|
profile: _userinfo!.profile,
|
||||||
badges: _userinfo!.badges,
|
badges: _userinfo!.badges,
|
||||||
status:
|
status:
|
||||||
Get.find<StatusProvider>().getSomeoneStatus(_userinfo!.name),
|
Get.find<StatusProvider>().getSomeoneStatus(_userinfo!.name),
|
||||||
|
@ -347,8 +347,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: element.file.length(),
|
future: element.file.length(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData)
|
if (!snapshot.hasData) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
return Text(
|
return Text(
|
||||||
_formatBytes(snapshot.data!),
|
_formatBytes(snapshot.data!),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
@ -131,7 +131,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
|
|||||||
reservedSize: 28,
|
reservedSize: 28,
|
||||||
interval: 86400000,
|
interval: 86400000,
|
||||||
getTitlesWidget: (value, _) => Text(
|
getTitlesWidget: (value, _) => Text(
|
||||||
DateFormat('MM/dd').format(
|
DateFormat('dd').format(
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
value.toInt(),
|
value.toInt(),
|
||||||
),
|
),
|
||||||
@ -231,7 +231,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
|
|||||||
reservedSize: 28,
|
reservedSize: 28,
|
||||||
interval: 86400000,
|
interval: 86400000,
|
||||||
getTitlesWidget: (value, _) => Text(
|
getTitlesWidget: (value, _) => Text(
|
||||||
DateFormat('MM/dd').format(
|
DateFormat('dd').format(
|
||||||
DateTime.fromMillisecondsSinceEpoch(
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
value.toInt(),
|
value.toInt(),
|
||||||
),
|
),
|
||||||
|
Loading…
Reference in New Issue
Block a user