import 'dart:async'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/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 createState() => _MediaPageState(); } class _MediaPageState extends CommonPageState with AutomaticKeepAliveClientMixin { final MineController controller = Get.putOrFind(MineController.new); late final MainController _mainController = Get.find(); @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), controller: controller.scrollController, 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: StyleString.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), ], 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/images/big-vip.png', height: 19, cacheHeight: 19.cacheSize(context), semanticLabel: "大会员", ), ), ], ) : ClipOval( child: Image.asset( width: 55, height: 55, cacheHeight: 55.cacheSize(context), 'assets/images/noface.jpeg', 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(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: StyleString.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, ), ], ), ), ), ), ); } 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( () => Future.delayed( const Duration(milliseconds: 150), controller.onRefresh, ), ), 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? 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( 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( () => Future.delayed( const Duration(milliseconds: 150), controller.onRefresh, ), ), icon: Icon( Icons.arrow_forward_ios, size: 18, color: secondary, ), ), ), ); } else { return FavFolderItem( heroTag: Utils.generateRandomString(8), item: response.list[index], onPop: () => Future.delayed( const Duration(milliseconds: 150), controller.onRefresh, ), ); } }, scrollDirection: .horizontal, separatorBuilder: (_, _) => const SizedBox(width: 14), ), ); }, ), Error(:final errMsg) => SizedBox( height: 160, child: Center( child: Text( errMsg ?? '', textAlign: .center, ), ), ), }; } }