Files
PiliPlus/lib/pages/mine/view.dart
2026-04-14 13:46:47 +08:00

573 lines
19 KiB
Dart

import 'dart:async';
import 'package:PiliPlus/common/assets.dart';
import 'package:PiliPlus/common/style.dart';
import 'package:PiliPlus/common/widgets/flutter/list_tile.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/nav_bar_config.dart';
import 'package:PiliPlus/models_new/fav/fav_folder/list.dart';
import 'package:PiliPlus/pages/common/common_page.dart';
import 'package:PiliPlus/pages/home/view.dart';
import 'package:PiliPlus/pages/login/controller.dart';
import 'package:PiliPlus/pages/main/controller.dart';
import 'package:PiliPlus/pages/mine/controller.dart';
import 'package:PiliPlus/pages/mine/widgets/item.dart';
import 'package:PiliPlus/utils/extension/get_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart' hide ListTile;
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
class MinePage extends StatefulWidget {
const MinePage({super.key, this.showBackBtn = false});
final bool showBackBtn;
@override
State<MinePage> createState() => _MediaPageState();
}
class _MediaPageState extends CommonPageState<MinePage>
with AutomaticKeepAliveClientMixin {
final MineController controller = Get.putOrFind(MineController.new);
late final MainController _mainController = Get.find<MainController>();
@override
bool get wantKeepAlive => true;
bool get checkPage =>
_mainController.navigationBars[0] != NavigationBarType.mine &&
_mainController.selectedIndex.value == 0;
@override
bool onNotificationType1(UserScrollNotification notification) {
if (checkPage) {
return false;
}
return super.onNotificationType1(notification);
}
@override
bool onNotificationType2(ScrollNotification notification) {
if (checkPage) {
return false;
}
return super.onNotificationType2(notification);
}
@override
Widget build(BuildContext context) {
super.build(context);
final theme = Theme.of(context);
final secondary = theme.colorScheme.secondary;
return Column(
children: [
Padding(
padding: const .symmetric(vertical: 10),
child: _buildHeaderActions,
),
Expanded(
child: Material(
type: .transparency,
child: refreshIndicator(
onRefresh: controller.onRefresh,
child: onBuild(
listView(
padding: const .only(bottom: 100),
physics: const AlwaysScrollableScrollPhysics(),
children: [
_buildUserInfo(theme, secondary),
_buildActions(secondary),
Obx(
() => controller.loadingState.value is Loading
? const SizedBox.shrink()
: _buildFav(theme, secondary),
),
],
),
),
),
),
),
],
);
}
Widget _buildActions(Color primary) {
return Row(
mainAxisAlignment: .spaceEvenly,
children: controller.list
.map(
(e) => Flexible(
child: InkWell(
onTap: e.onTap,
borderRadius: Style.mdRadius,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 80),
child: AspectRatio(
aspectRatio: 1,
child: Column(
spacing: 6,
mainAxisSize: .min,
mainAxisAlignment: .center,
children: [
Icon(size: e.size, e.icon, color: primary),
Text(
e.title,
style: const TextStyle(fontSize: 13),
),
],
),
),
),
),
),
)
.toList(),
);
}
Widget get _buildHeaderActions {
const iconSize = 22.0;
const padding = EdgeInsets.all(8);
const style = ButtonStyle(tapTargetSize: .shrinkWrap);
return Row(
spacing: 5,
mainAxisAlignment: .end,
children: [
if (widget.showBackBtn)
const Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(left: 8),
child: BackButton(),
),
),
),
if (!_mainController.hasHome) ...[
IconButton(
iconSize: iconSize,
padding: padding,
style: style,
tooltip: '搜索',
onPressed: () => Get.toNamed('/search'),
icon: const Icon(Icons.search),
),
msgBadge(_mainController),
],
if (GStorage.reply != null)
IconButton(
iconSize: iconSize,
padding: padding,
style: style,
tooltip: '评论记录',
onPressed: () => Get.toNamed('/myReply'),
icon: const Icon(Icons.message_outlined),
),
Obx(
() {
final anonymity = MineController.anonymity.value;
return IconButton(
iconSize: iconSize,
padding: padding,
style: style,
tooltip: "${anonymity ? '退出' : '进入'}无痕模式",
onPressed: MineController.onChangeAnonymity,
icon: anonymity
? const Icon(MdiIcons.incognito)
: const Icon(MdiIcons.incognitoOff),
);
},
),
IconButton(
iconSize: iconSize,
padding: padding,
style: style,
tooltip: '设置账号模式',
onPressed: () => LoginPageController.switchAccountDialog(context),
icon: const Icon(Icons.switch_account_outlined),
),
Obx(
() {
return IconButton(
iconSize: iconSize,
padding: padding,
style: style,
tooltip: '切换至${controller.nextThemeType.desc}主题',
onPressed: controller.onChangeTheme,
icon: controller.themeType.value.icon,
);
},
),
IconButton(
iconSize: iconSize,
padding: padding,
style: style,
tooltip: '设置',
onPressed: () => Get.toNamed('/setting', preventDuplicates: false),
icon: const Icon(Icons.settings_outlined),
),
const SizedBox(width: 16),
],
);
}
Widget _buildUserInfo(ThemeData theme, Color secondary) {
final style = TextStyle(
fontSize: theme.textTheme.titleMedium!.fontSize,
fontWeight: FontWeight.bold,
);
final labelStyle = theme.textTheme.labelMedium!.copyWith(
color: theme.colorScheme.outline,
);
final coinLabelStyle = TextStyle(
fontSize: theme.textTheme.labelMedium!.fontSize,
color: theme.colorScheme.outline,
);
final coinValStyle = TextStyle(
fontSize: theme.textTheme.labelMedium!.fontSize,
fontWeight: FontWeight.bold,
color: secondary,
);
return Obx(() {
final userInfo = controller.userInfo.value;
final levelInfo = userInfo.levelInfo;
final hasLevel = levelInfo != null;
final isVip = userInfo.vipStatus != null && userInfo.vipStatus! > 0;
final userStat = controller.userStat.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
behavior: .opaque,
onTap: controller.onLogin,
onLongPress: () {
Feedback.forLongPress(context);
controller.onLogin(true);
},
onSecondaryTap: PlatformUtils.isMobile
? null
: () => controller.onLogin(true),
child: Row(
mainAxisSize: .min,
children: [
const SizedBox(width: 20),
userInfo.face != null
? Stack(
clipBehavior: .none,
children: [
NetworkImgLayer(
src: userInfo.face,
type: .avatar,
width: 55,
height: 55,
),
if (isVip)
Positioned(
right: -1,
bottom: -2,
child: Image.asset(
Assets.vipIcon,
height: 19,
cacheHeight: 19.cacheSize(context),
semanticLabel: "大会员",
),
),
],
)
: ClipOval(
child: Image.asset(
width: 55,
height: 55,
cacheHeight: 55.cacheSize(context),
Assets.avatarPlaceHolder,
semanticLabel: "默认头像",
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: .min,
mainAxisAlignment: .center,
crossAxisAlignment: .start,
children: [
Row(
spacing: 6,
children: [
Flexible(
child: Text(
userInfo.uname ?? '点击登录',
style: theme.textTheme.titleMedium!.copyWith(
height: 1,
color: isVip && userInfo.vipType == 2
? theme.colorScheme.vipColor
: null,
),
maxLines: 1,
overflow: .ellipsis,
),
),
Image.asset(
Utils.levelName(
levelInfo?.currentLevel ?? 0,
isSeniorMember: userInfo.isSeniorMember == 1,
),
height: 10,
cacheHeight: 10.cacheSize(context),
),
],
),
const SizedBox(height: 8),
Text.rich(
TextSpan(
children: [
TextSpan(
text: '硬币 ',
style: coinLabelStyle,
),
TextSpan(
text: userInfo.money?.toString() ?? '-',
style: coinValStyle,
),
TextSpan(
text: " 经验 ",
style: coinLabelStyle,
),
TextSpan(
text: levelInfo?.currentExp?.toString() ?? '-',
style: coinValStyle,
),
TextSpan(
text: "/${levelInfo?.nextExp ?? '-'}",
style: coinLabelStyle,
),
],
),
),
const SizedBox(height: 4),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 225),
child: LinearProgressIndicator(
minHeight: 2.25,
value: hasLevel
? levelInfo.currentExp! / levelInfo.nextExp!
: 0,
backgroundColor: theme.colorScheme.outline.withValues(
alpha: 0.4,
),
valueColor: AlwaysStoppedAnimation<Color>(secondary),
stopIndicatorColor: Colors.transparent,
),
),
],
),
),
const SizedBox(width: 20),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: .spaceEvenly,
children: [
_btn(
count: userStat.dynamicCount,
countStyle: style,
name: '动态',
labelStyle: labelStyle,
onTap: () => controller.push('memberDynamics'),
),
_btn(
count: userStat.following,
countStyle: style,
name: '关注',
labelStyle: labelStyle,
onTap: () => controller.push('follow'),
),
_btn(
count: userStat.follower,
countStyle: style,
name: '粉丝',
labelStyle: labelStyle,
onTap: () => controller.push('fan'),
),
],
),
],
);
});
}
Widget _btn({
required int? count,
required TextStyle countStyle,
required String name,
required TextStyle? labelStyle,
required VoidCallback onTap,
}) {
return Flexible(
child: InkWell(
onTap: onTap,
borderRadius: Style.mdRadius,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 80),
child: AspectRatio(
aspectRatio: 1,
child: Column(
spacing: 4,
mainAxisSize: .min,
mainAxisAlignment: .center,
children: [
Text(
count?.toString() ?? '-',
style: countStyle,
),
Text(
name,
style: labelStyle,
),
],
),
),
),
),
);
}
void _autoRefresh() => Future.delayed(
const Duration(milliseconds: 150),
() => controller.onRefresh(isManual: false),
);
Widget _buildFav(ThemeData theme, Color secondary) {
return Column(
children: [
Divider(
height: 20,
color: theme.dividerColor.withValues(alpha: 0.1),
),
ListTile(
onTap: () => Get.toNamed('/fav')?.whenComplete(_autoRefresh),
dense: true,
title: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '我的收藏 ',
style: TextStyle(
fontSize: theme.textTheme.titleMedium!.fontSize,
fontWeight: .bold,
),
),
if (controller.favFolderCount != null)
TextSpan(
text: "${controller.favFolderCount} ",
style: TextStyle(
fontSize: theme.textTheme.titleSmall!.fontSize,
color: secondary,
),
),
WidgetSpan(
child: Icon(
Icons.arrow_forward_ios,
size: 18,
color: secondary,
),
),
],
),
),
),
trailing: IconButton(
tooltip: '刷新',
onPressed: controller.onRefresh,
icon: const Icon(Icons.refresh, size: 20),
),
),
_buildFavBody(theme, secondary, controller.loadingState.value),
],
);
}
Widget _buildFavBody(
ThemeData theme,
Color secondary,
LoadingState loadingState,
) {
return switch (loadingState) {
Loading() => const SizedBox.shrink(),
Success(:final response) => Builder(
builder: (context) {
List<FavFolderInfo>? favFolderList = response.list;
if (favFolderList == null || favFolderList.isEmpty) {
return const SizedBox.shrink();
}
bool flag = (controller.favFolderCount ?? 0) > favFolderList.length;
return SizedBox(
height: 200,
child: ListView.separated(
controller: controller.scrollController,
padding: const .only(left: 20, top: 10, right: 20),
itemCount: response.list.length + (flag ? 1 : 0),
itemBuilder: (context, index) {
if (flag && index == favFolderList.length) {
return Padding(
padding: const .only(bottom: 35),
child: Center(
child: IconButton(
tooltip: '查看更多',
style: ButtonStyle(
padding: const WidgetStatePropertyAll(.zero),
backgroundColor: WidgetStatePropertyAll(
theme.colorScheme.secondaryContainer.withValues(
alpha: 0.5,
),
),
),
onPressed: () =>
Get.toNamed('/fav')?.whenComplete(_autoRefresh),
icon: Icon(
Icons.arrow_forward_ios,
size: 18,
color: secondary,
),
),
),
);
} else {
return FavFolderItem(
heroTag: Utils.generateRandomString(8),
item: response.list[index],
onPop: _autoRefresh,
);
}
},
scrollDirection: .horizontal,
separatorBuilder: (_, _) => const SizedBox(width: 14),
),
);
},
),
Error(:final errMsg) => SizedBox(
height: 160,
child: Center(
child: Text(
errMsg ?? '',
textAlign: .center,
),
),
),
};
}
}