diff --git a/lib/common/widgets/dynamic_sliver_appbar.dart b/lib/common/widgets/dynamic_sliver_appbar.dart index 3aca8790..18173d95 100644 --- a/lib/common/widgets/dynamic_sliver_appbar.dart +++ b/lib/common/widgets/dynamic_sliver_appbar.dart @@ -95,6 +95,7 @@ class _DynamicSliverAppBarState extends State { // As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used // to calculate dynamically the size for the sliver app bar double _height = 0; + Orientation? _orientation; @override void initState() { @@ -105,6 +106,7 @@ class _DynamicSliverAppBarState extends State { @override void didUpdateWidget(covariant DynamicSliverAppBar oldWidget) { super.didUpdateWidget(oldWidget); + _updateHeight(); } @@ -124,6 +126,11 @@ class _DynamicSliverAppBarState extends State { @override Widget build(BuildContext context) { //Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height + Orientation orientation = MediaQuery.orientationOf(context); + if (_orientation != orientation) { + _orientation = orientation; + _height = 0; + } if (_height == 0) { return SliverToBoxAdapter( child: Stack( diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index 8b19c141..beb8874f 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -50,7 +50,8 @@ class VideoCardH extends StatelessWidget { excludeSemantics: true, customSemanticsActions: { for (var item in actions) - CustomSemanticsAction(label: item.title): item.onTap!, + CustomSemanticsAction( + label: item.title.isEmpty ? 'label' : item.title): item.onTap!, }, child: InkWell( borderRadius: BorderRadius.circular(12), diff --git a/lib/common/widgets/video_popup_menu.dart b/lib/common/widgets/video_popup_menu.dart index d1e62fb3..c0756e8d 100644 --- a/lib/common/widgets/video_popup_menu.dart +++ b/lib/common/widgets/video_popup_menu.dart @@ -24,19 +24,20 @@ class VideoCustomActions { late List actions; VideoCustomActions(this.videoItem, this.context) { actions = [ - VideoCustomAction( - videoItem.bvid, - 'copy', - Stack( - children: [ - Icon(MdiIcons.identifier, size: 16), - Icon(MdiIcons.circleOutline, size: 16), - ], + if ((videoItem.bvid as String?)?.isNotEmpty == true) + VideoCustomAction( + videoItem.bvid, + 'copy', + Stack( + children: [ + Icon(MdiIcons.identifier, size: 16), + Icon(MdiIcons.circleOutline, size: 16), + ], + ), + () { + Utils.copyText(videoItem.bvid); + }, ), - () { - Utils.copyText(videoItem.bvid); - }, - ), VideoCustomAction( '稍后再看', 'pause', diff --git a/lib/pages/member/new/member_page.dart b/lib/pages/member/new/member_page.dart index d7961317..15e32111 100644 --- a/lib/pages/member/new/member_page.dart +++ b/lib/pages/member/new/member_page.dart @@ -40,7 +40,7 @@ class _MemberPageNewState extends State ); _userController.scrollController.addListener(() { _userController.scrollRatio.value = - min(1.0, _userController.scrollController.offset.round() / 150); + min(1.0, _userController.scrollController.offset.round() / 120); }); } @@ -53,71 +53,73 @@ class _MemberPageNewState extends State () => _userController.loadingState.value is Success ? LayoutBuilder( builder: (_, constraints) { - if (constraints.maxHeight > constraints.maxWidth) { - return ExtendedNestedScrollView( - controller: _userController.scrollController, - onlyOneScrollInBody: true, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: ExtendedNestedScrollView - .sliverOverlapAbsorberHandleFor(context), - sliver: _buildAppBar(), - ), - ]; - }, - body: _userController.tab2?.isNotEmpty == true - ? LayoutBuilder( - builder: (context, _) { - return Padding( - padding: EdgeInsets.only( - top: ExtendedNestedScrollView - .sliverOverlapAbsorberHandleFor( - context) - .layoutExtent ?? - 0, - ), - child: _buildBody, - ); - }, - ) - : Center(child: const Text('EMPTY')), - ); - } else { - return Row( - children: [ - Expanded( - child: CustomScrollView( - slivers: [ - _buildAppBar(false), - ], + // if (constraints.maxHeight > constraints.maxWidth) { + return ExtendedNestedScrollView( + controller: _userController.scrollController, + onlyOneScrollInBody: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: ExtendedNestedScrollView + .sliverOverlapAbsorberHandleFor(context), + sliver: _buildAppBar( + isV: constraints.maxHeight > constraints.maxWidth, ), ), - Expanded( - child: SafeArea( - top: false, - left: false, - bottom: false, - child: Column( - children: [ - SizedBox(height: _userController.top), - if ((_userController.tab2?.length ?? -1) > 1) - _buildTab, - Expanded( - child: - _userController.tab2?.isNotEmpty == true - ? _buildBody - : Center( - child: const Text('EMPTY'), - ), + ]; + }, + body: _userController.tab2?.isNotEmpty == true + ? LayoutBuilder( + builder: (context, _) { + return Padding( + padding: EdgeInsets.only( + top: ExtendedNestedScrollView + .sliverOverlapAbsorberHandleFor( + context) + .layoutExtent ?? + 0, ), - ], - ), - ), - ), - ], - ); - } + child: _buildBody, + ); + }, + ) + : Center(child: const Text('EMPTY')), + ); + // } else { + // return Row( + // children: [ + // Expanded( + // child: CustomScrollView( + // slivers: [ + // _buildAppBar(false), + // ], + // ), + // ), + // Expanded( + // child: SafeArea( + // top: false, + // left: false, + // bottom: false, + // child: Column( + // children: [ + // SizedBox(height: _userController.top), + // if ((_userController.tab2?.length ?? -1) > 1) + // _buildTab, + // Expanded( + // child: + // _userController.tab2?.isNotEmpty == true + // ? _buildBody + // : Center( + // child: const Text('EMPTY'), + // ), + // ), + // ], + // ), + // ), + // ), + // ], + // ); + // } }, ) : Center( @@ -132,37 +134,44 @@ class _MemberPageNewState extends State tabs: _userController.tabs, ); - Widget get _buildBody => TabBarView( - controller: _userController.tabController, - children: _userController.tab2!.map((item) { - return switch (item.param!) { - 'home' => MemberHome(heroTag: _heroTag), - // 'dynamic' => MemberDynamic(mid: _mid ?? -1), - 'dynamic' => MemberDynamicsPage(mid: _mid), - 'contribute' => Obx( - () => MemberContribute( + Widget get _buildBody => Padding( + padding: EdgeInsets.only( + left: MediaQuery.paddingOf(context).left, + right: MediaQuery.paddingOf(context).right, + ), + child: TabBarView( + controller: _userController.tabController, + children: _userController.tab2!.map((item) { + return switch (item.param!) { + 'home' => MemberHome(heroTag: _heroTag), + // 'dynamic' => MemberDynamic(mid: _mid ?? -1), + 'dynamic' => MemberDynamicsPage(mid: _mid), + 'contribute' => Obx( + () => MemberContribute( + heroTag: _heroTag, + initialIndex: _userController.contributeInitialIndex.value, + mid: _mid ?? -1, + ), + ), + 'bangumi' => MemberBangumi( heroTag: _heroTag, - initialIndex: _userController.contributeInitialIndex.value, mid: _mid ?? -1, ), - ), - 'bangumi' => MemberBangumi( - heroTag: _heroTag, - mid: _mid ?? -1, - ), - 'favorite' => MemberFavorite( - heroTag: _heroTag, - mid: _mid ?? -1, - ), - _ => Center(child: Text(item.title ?? '')), - }; - }).toList(), + 'favorite' => MemberFavorite( + heroTag: _heroTag, + mid: _mid ?? -1, + ), + _ => Center(child: Text(item.title ?? '')), + }; + }).toList(), + ), ); - Widget _buildAppBar([bool needTab = true]) => MediaQuery.removePadding( + Widget _buildAppBar({bool needTab = true, bool isV = true}) => + MediaQuery.removePadding( context: context, removeTop: true, - removeRight: true, + // removeRight: true, child: DynamicSliverAppBar( leading: Padding( padding: EdgeInsets.only(top: _userController.top ?? 0), @@ -178,7 +187,8 @@ class _MemberPageNewState extends State pinned: true, backgroundColor: Theme.of(context).colorScheme.surface, scrolledUnderElevation: 0, - flexibleSpace: _buildUserInfo(_userController.loadingState.value), + flexibleSpace: + _buildUserInfo(_userController.loadingState.value, isV), bottom: needTab && (_userController.tab2?.length ?? -1) > 1 ? PreferredSize( preferredSize: Size.fromHeight(48), @@ -291,7 +301,7 @@ class _MemberPageNewState extends State ); } - Widget _buildUserInfo(LoadingState userState) { + Widget _buildUserInfo(LoadingState userState, [bool isV = true]) { switch (userState) { case Empty(): return _errorWidget('EMPTY'); @@ -303,6 +313,7 @@ class _MemberPageNewState extends State padding: EdgeInsets.only( bottom: (_userController.tab2?.length ?? 0) > 1 ? 48 : 0), child: UserInfoCard( + isV: isV, isOwner: _userController.mid == _userController.ownerMid, relation: _userController.relation.value, isFollow: _userController.isFollow.value, diff --git a/lib/pages/member/new/widget/user_info_card.dart b/lib/pages/member/new/widget/user_info_card.dart index a2a2fd03..ea610569 100644 --- a/lib/pages/member/new/widget/user_info_card.dart +++ b/lib/pages/member/new/widget/user_info_card.dart @@ -12,6 +12,7 @@ import 'package:get/get.dart'; class UserInfoCard extends StatelessWidget { const UserInfoCard({ super.key, + required this.isV, required this.isOwner, required this.card, required this.images, @@ -20,6 +21,7 @@ class UserInfoCard extends StatelessWidget { required this.onFollow, }); + final bool isV; final bool isOwner; final int relation; final bool isFollow; @@ -29,245 +31,79 @@ class UserInfoCard extends StatelessWidget { @override Widget build(BuildContext context) { + return isV ? _buildV(context) : _buildH(context); + } + + Widget _countWidget({ + required String title, + required int count, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + Utils.numFormat(count), + style: TextStyle( + fontSize: 14, + ), + ), + Text( + title, + style: TextStyle( + height: 1, + fontSize: 11, + color: Theme.of(Get.context!).colorScheme.outline, + ), + ), + ], + ), + ); + } + + _buildHeader(BuildContext context) { bool darken = GStorage.brightness == Brightness.dark; String? imgUrl = darken ? (images.nightImgurl?.isEmpty == true ? images.imgUrl?.http2https : images.nightImgurl?.http2https) : images.imgUrl?.http2https; - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () { - showDialog( - useSafeArea: false, - context: context, - builder: (context) { - return ImagePreview( - initialPage: 0, - imgList: [imgUrl ?? ''], - ); - }, - ); - }, - child: CachedNetworkImage( - imageUrl: imgUrl ?? '', - width: double.infinity, - height: 135, - imageBuilder: (context, imageProvider) => Container( - decoration: BoxDecoration( - image: DecorationImage( - image: imageProvider, - fit: BoxFit.cover, - colorFilter: ColorFilter.mode( - darken - ? const Color(0x8D000000) - : const Color(0x5DFFFFFF), - darken ? BlendMode.darken : BlendMode.lighten, - ), - ), - ), - ), - ), - ), - const SizedBox(width: double.infinity, height: 85) - ], - ), - Positioned( - top: 110, - left: 20, - child: GestureDetector( - onTap: () { - showDialog( - useSafeArea: false, - context: context, - builder: (context) { - return ImagePreview( - initialPage: 0, - imgList: [card.face ?? ''], - ); - }, - ); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - width: 2.5, - color: Theme.of(context).colorScheme.surface, - ), - shape: BoxShape.circle, - ), - child: NetworkImgLayer( - src: card.face, - type: 'avatar', - width: 80, - height: 80, - ), - ), + return GestureDetector( + onTap: () { + showDialog( + useSafeArea: false, + context: context, + builder: (context) { + return ImagePreview( + initialPage: 0, + imgList: [imgUrl ?? ''], + ); + }, + ); + }, + child: CachedNetworkImage( + imageUrl: imgUrl ?? '', + width: double.infinity, + height: 135, + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + colorFilter: ColorFilter.mode( + darken ? const Color(0x8D000000) : const Color(0x5DFFFFFF), + darken ? BlendMode.darken : BlendMode.lighten, ), ), - if (card.officialVerify?.icon?.isNotEmpty == true || - (card.vip?.vipStatus ?? -1) > 0) - Positioned( - top: 170, - left: 80, - child: Container( - padding: const EdgeInsets.all(0.01), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surface, - ), - child: card.officialVerify?.icon?.isNotEmpty == true - ? NetworkImgLayer( - src: card.officialVerify?.icon, - radius: null, - width: 24, - height: 24, - quality: 100, - ) - : Image.asset( - 'assets/images/big-vip.png', - width: 24, - height: 24, - ), - ), - ), - Positioned( - top: 140, - right: 20, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate( - 5, - (index) => index % 2 == 0 - ? Padding( - padding: - const EdgeInsets.symmetric(horizontal: 20), - child: _countWidget( - title: ['粉丝', '关注', '获赞'][index ~/ 2], - count: index == 0 - ? card.fans - : index == 2 - ? card.attention - : card.likes?.likeNum ?? 0, - onTap: () { - if (index == 0) { - Get.toNamed( - '/fan?mid=${card.mid}&name=${card.name}'); - } else if (index == 2) { - Get.toNamed( - '/follow?mid=${card.mid}&name=${card.name}'); - } - }, - ), - ) - : SizedBox( - height: 15, - width: 1, - child: VerticalDivider(), - ), - ), - ), - const SizedBox(height: 5), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isOwner) - IconButton.outlined( - onPressed: () { - if (GStorage.userInfo.get('userInfoCache') != - null) { - Get.toNamed( - '/whisperDetail', - parameters: { - 'talkerId': card.mid ?? '', - 'name': card.name ?? '', - 'face': card.face ?? '', - 'mid': card.mid ?? '', - }, - ); - } - }, - icon: const Icon(Icons.mail_outline, size: 21), - style: IconButton.styleFrom( - side: BorderSide( - width: 1.0, - color: Theme.of(context) - .colorScheme - .outline - .withOpacity(0.5), - ), - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - ), - const SizedBox(width: 10), - FilledButton.tonal( - onPressed: onFollow, - style: FilledButton.styleFrom( - backgroundColor: relation == -1 || isFollow - ? Theme.of(context).colorScheme.onInverseSurface - : null, - padding: const EdgeInsets.symmetric(horizontal: 50), - visualDensity: const VisualDensity( - horizontal: -2, - vertical: -2, - ), - ), - child: Text.rich( - style: TextStyle( - color: relation == -1 || isFollow - ? Theme.of(context).colorScheme.outline - : null, - ), - TextSpan( - children: [ - if (isFollow) - WidgetSpan( - alignment: PlaceholderAlignment.top, - child: Icon( - Icons.sort, - size: 16, - color: - Theme.of(context).colorScheme.outline, - ), - ), - TextSpan( - text: isOwner - ? '编辑资料' - : relation == -1 - ? '移除黑名单' - : relation == 2 - ? ' 特别关注' - : isFollow - ? ' 已关注' - : '关注', - ) - ], - ), - ), - ), - ], - ), - ], - ), - ), - ], + ), ), + ), + ); + } + + _buildLeft(BuildContext context) => [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Wrap( @@ -434,37 +270,255 @@ class UserInfoCard extends StatelessWidget { // .toList(), // ), // ), - const SizedBox(height: 5), - ], - ); - } + ]; - Widget _countWidget({ - required String title, - required int count, - required VoidCallback onTap, - }) { - return GestureDetector( - onTap: onTap, - child: Column( + _buildRight(BuildContext context) => Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - Utils.numFormat(count), - style: TextStyle( - fontSize: 14, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + 5, + (index) => index % 2 == 0 + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _countWidget( + title: ['粉丝', '关注', '获赞'][index ~/ 2], + count: index == 0 + ? card.fans + : index == 2 + ? card.attention + : card.likes?.likeNum ?? 0, + onTap: () { + if (index == 0) { + Get.toNamed( + '/fan?mid=${card.mid}&name=${card.name}'); + } else if (index == 2) { + Get.toNamed( + '/follow?mid=${card.mid}&name=${card.name}'); + } + }, + ), + ) + : SizedBox( + height: 15, + width: 1, + child: VerticalDivider(), + ), ), ), - Text( - title, - style: TextStyle( - height: 1, - fontSize: 11, - color: Theme.of(Get.context!).colorScheme.outline, - ), + const SizedBox(height: 5), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isOwner) + IconButton.outlined( + onPressed: () { + if (GStorage.userInfo.get('userInfoCache') != null) { + Get.toNamed( + '/whisperDetail', + parameters: { + 'talkerId': card.mid ?? '', + 'name': card.name ?? '', + 'face': card.face ?? '', + 'mid': card.mid ?? '', + }, + ); + } + }, + icon: const Icon(Icons.mail_outline, size: 21), + style: IconButton.styleFrom( + side: BorderSide( + width: 1.0, + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.5), + ), + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + ), + const SizedBox(width: 10), + FilledButton.tonal( + onPressed: onFollow, + style: FilledButton.styleFrom( + backgroundColor: relation == -1 || isFollow + ? Theme.of(context).colorScheme.onInverseSurface + : null, + padding: const EdgeInsets.symmetric(horizontal: 50), + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + ), + child: Text.rich( + style: TextStyle( + color: relation == -1 || isFollow + ? Theme.of(context).colorScheme.outline + : null, + ), + TextSpan( + children: [ + if (isFollow) + WidgetSpan( + alignment: PlaceholderAlignment.top, + child: Icon( + Icons.sort, + size: 16, + color: Theme.of(context).colorScheme.outline, + ), + ), + TextSpan( + text: isOwner + ? '编辑资料' + : relation == -1 + ? '移除黑名单' + : relation == 2 + ? ' 特别关注' + : isFollow + ? ' 已关注' + : '关注', + ) + ], + ), + ), + ), + ], ), ], - ), - ); - } + ); + + _buildBadge(BuildContext context) => Container( + padding: const EdgeInsets.all(0.01), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.surface, + ), + child: card.officialVerify?.icon?.isNotEmpty == true + ? NetworkImgLayer( + src: card.officialVerify?.icon, + radius: null, + width: 24, + height: 24, + quality: 100, + ) + : Image.asset( + 'assets/images/big-vip.png', + width: 24, + height: 24, + ), + ); + + _buildAvatar(BuildContext context) => GestureDetector( + onTap: () { + showDialog( + useSafeArea: false, + context: context, + builder: (context) { + return ImagePreview( + initialPage: 0, + imgList: [card.face ?? ''], + ); + }, + ); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 2.5, + color: Theme.of(context).colorScheme.surface, + ), + shape: BoxShape.circle, + ), + child: NetworkImgLayer( + src: card.face, + type: 'avatar', + width: 80, + height: 80, + ), + ), + ); + + _buildV(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildHeader(context), + const SizedBox(width: double.infinity, height: 85) + ], + ), + Positioned( + top: 110, + left: 20, + child: _buildAvatar(context), + ), + if (card.officialVerify?.icon?.isNotEmpty == true || + (card.vip?.vipStatus ?? -1) > 0) + Positioned( + top: 170, + left: 80, + child: _buildBadge(context), + ), + Positioned( + top: 140, + right: 20, + child: _buildRight(context), + ), + ], + ), + ..._buildLeft(context), + const SizedBox(height: 5), + ], + ); + + _buildH(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + Row( + children: [ + SizedBox(width: MediaQuery.paddingOf(context).left), + const SizedBox(width: 20), + Stack( + children: [ + _buildAvatar(context), + if (card.officialVerify?.icon?.isNotEmpty == true || + (card.vip?.vipStatus ?? -1) > 0) + Positioned( + right: 0, + bottom: 0, + child: _buildBadge(context), + ), + ], + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + ..._buildLeft(context), + const SizedBox(height: 5), + ], + ), + ), + Expanded(child: _buildRight(context)), + SizedBox(width: MediaQuery.paddingOf(context).right), + ], + ), + ], + ); }