diff --git a/assets/images/live.gif b/assets/images/live.gif new file mode 100644 index 000000000..ca0fb0964 Binary files /dev/null and b/assets/images/live.gif differ diff --git a/lib/http/api.dart b/lib/http/api.dart index 9ea957e3c..5707ca05b 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -199,6 +199,8 @@ class Api { // 用户信息 需要Wbi签名 // https://api.bilibili.com/x/space/wbi/acc/info?mid=503427686&token=&platform=web&web_location=1550101&w_rid=d709892496ce93e3d94d6d37c95bde91&wts=1689301482 - static const String memberInfo = - 'https://api.bilibili.com/x/space/wbi/acc/info'; + static const String memberInfo = '/x/space/wbi/acc/info'; + + // 用户名片信息 + static const String memberCardInfo = '/x/web-interface/card'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index 40b1c4c45..d9f33bcc9 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -19,10 +19,23 @@ class MemberHttp { } static Future memberStat({int? mid}) async { - var res = await Request().get(Api.userStat, data: {mid: mid}); + var res = await Request().get(Api.userStat, data: {'vmid': mid}); if (res.data['code'] == 0) { - print(res.data['data']); - // return {'status': true, 'data': FansDataModel.fromJson(res.data['data'])}; + return {'status': true, 'data': res.data['data']}; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + static Future memberCardInfo({int? mid}) async { + var res = await Request() + .get(Api.memberCardInfo, data: {'mid': mid, 'photo': true}); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; } else { return { 'status': false, diff --git a/lib/models/member/info.dart b/lib/models/member/info.dart index 9b7b94537..4b668c7d5 100644 --- a/lib/models/member/info.dart +++ b/lib/models/member/info.dart @@ -7,6 +7,9 @@ class MemberInfoModel { this.sign, this.level, this.isFollowed, + this.topPhoto, + this.official, + this.vip, }); int? mid; @@ -16,14 +19,73 @@ class MemberInfoModel { String? sign; int? level; bool? isFollowed; + String? topPhoto; + Map? official; + Vip? vip; + LiveRoom? liveRoom; MemberInfoModel.fromJson(Map json) { mid = json['mid']; name = json['name']; sex = json['sex']; face = json['face']; - sign = json['sign']; + sign = json['sign'] == '' ? '该用户还没有签名' : json['sign']; level = json['level']; isFollowed = json['is_followed']; + topPhoto = json['top_photo']; + official = json['official']; + vip = Vip.fromJson(json['vip']); + liveRoom = LiveRoom.fromJson(json['live_room']); + } +} + +class Vip { + Vip({ + this.type, + this.status, + this.dueDate, + this.label, + }); + + int? type; + int? status; + int? dueDate; + Map? label; + + Vip.fromJson(Map json) { + type = json['type']; + status = json['status']; + dueDate = json['due_date']; + label = json['label']; + } +} + +class LiveRoom { + LiveRoom({ + this.roomStatus, + this.liveStatus, + this.url, + this.title, + this.cover, + this.roomId, + this.roundStatus, + }); + + int? roomStatus; + int? liveStatus; + String? url; + String? title; + String? cover; + int? roomId; + int? roundStatus; + + LiveRoom.fromJson(Map json) { + roomStatus = json['room_status']; + liveStatus = json['live_status']; + url = json['url']; + title = json['title']; + cover = json['cover']; + roomId = json['room_id']; + roundStatus = json['round_status']; } } diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 08adad598..8aea087c8 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -193,7 +193,7 @@ class SearchUserItemModel { usign = json['usign']; fans = json['fans']; videos = json['videos']; - upic = json['upic']; + upic = 'https:' + json['upic']; faceNft = json['face_nft']; faceNftType = json['face_nft_type']; verifyInfo = json['verify_info']; diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index 8aa334870..a1b67a92e 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -1,20 +1,29 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/utils.dart'; Widget author(item, context) { + String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid); return Container( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), child: Row( children: [ GestureDetector( - onTap: () => - Get.toNamed('/member?mid=${item.modules.moduleAuthor.mid}'), - child: NetworkImgLayer( - width: 40, - height: 40, - type: 'avatar', - src: item.modules.moduleAuthor.face, + onTap: () => Get.toNamed( + '/member?mid=${item.modules.moduleAuthor.mid}', + arguments: { + 'face': item.modules.moduleAuthor.face, + 'heroTag': heroTag + }), + child: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 40, + height: 40, + type: 'avatar', + src: item.modules.moduleAuthor.face, + ), ), ), const SizedBox(width: 10), diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index d36f7b302..96ae30d52 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -174,12 +174,13 @@ class _UpPanelState extends State { Badge( smallSize: 8, label: data.type == 'live' ? const Text('Live') : null, - textColor: Theme.of(context).colorScheme.onPrimary, + textColor: Theme.of(context).colorScheme.onSecondaryContainer, alignment: AlignmentDirectional.bottomCenter, - padding: const EdgeInsets.only(left: 4, right: 4), + padding: const EdgeInsets.only(left: 6, right: 6), isLabelVisible: data.type == 'live' || (data.type == 'up' && (data.hasUpdate ?? false)), - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: + Theme.of(context).colorScheme.secondaryContainer, child: NetworkImgLayer( width: 49, height: 49, diff --git a/lib/pages/fan/widgets/fan_item.dart b/lib/pages/fan/widgets/fan_item.dart index 074a8d5eb..072b959ff 100644 --- a/lib/pages/fan/widgets/fan_item.dart +++ b/lib/pages/fan/widgets/fan_item.dart @@ -1,14 +1,21 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/utils.dart'; Widget fanItem({item}) { + String heroTag = Utils.makeHeroTag(item!.mid); return ListTile( - onTap: () {}, - leading: NetworkImgLayer( - width: 38, - height: 38, - type: 'avatar', - src: item.face, + onTap: () => Get.toNamed('/member?mid=${item.mid}', + arguments: {'face': item.face, 'heroTag': heroTag}), + leading: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 38, + height: 38, + type: 'avatar', + src: item.face, + ), ), title: Text(item.uname), subtitle: Text( diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index 37a7b761f..c1ac61a27 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -1,15 +1,21 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/utils.dart'; Widget followItem({item}) { + String heroTag = Utils.makeHeroTag(item!.mid); return ListTile( - onTap: () {}, - leading: NetworkImgLayer( - width: 38, - height: 38, - type: 'avatar', - src: item.face, + onTap: () => Get.toNamed('/member?mid=${item.mid}', + arguments: {'face': item.face, 'heroTag': heroTag}), + leading: Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 38, + height: 38, + type: 'avatar', + src: item.face, + ), ), title: Text(item.uname), subtitle: Text( diff --git a/lib/pages/liveRoom/controller.dart b/lib/pages/liveRoom/controller.dart index a4593ba2a..93bf10188 100644 --- a/lib/pages/liveRoom/controller.dart +++ b/lib/pages/liveRoom/controller.dart @@ -23,7 +23,7 @@ class LiveRoomController extends GetxController { super.onInit(); if (Get.arguments != null) { var args = Get.arguments['liveItem']; - heroTag = Get.arguments['heroTag']; + heroTag = Get.arguments['heroTag'] ?? ''; liveItem = args; roomId = liveItem.roomId!; if (args.pic != null && args.pic != '') { diff --git a/lib/pages/liveRoom/view.dart b/lib/pages/liveRoom/view.dart index dc0694acf..556803e54 100644 --- a/lib/pages/liveRoom/view.dart +++ b/lib/pages/liveRoom/view.dart @@ -89,20 +89,21 @@ class _LiveRoomPageState extends State { controller: _meeduPlayerController!, ), ), - Visibility( - visible: isShowCover, - child: Positioned( - top: 0, - left: 0, - right: 0, - child: NetworkImgLayer( - type: 'emote', - src: _liveRoomController.liveItem.cover, - width: Get.size.width, - height: videoHeight, + if (_liveRoomController.liveItem.cover != null) + Visibility( + visible: isShowCover, + child: Positioned( + top: 0, + left: 0, + right: 0, + child: NetworkImgLayer( + type: 'emote', + src: _liveRoomController.liveItem.cover, + width: Get.size.width, + height: videoHeight, + ), ), ), - ), ], )), if (_liveRoomController.liveItem.watchedShow != null) diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index dc662f6d7..6fb0f3369 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -1,18 +1,26 @@ import 'package:get/get.dart'; import 'package:pilipala/http/member.dart'; +import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/utils/wbi_sign.dart'; class MemberController extends GetxController { late int mid; + Rx memberInfo = MemberInfoModel().obs; + Map? userStat; + String? face; + String? heroTag; @override void onInit() { super.onInit(); mid = int.parse(Get.parameters['mid']!); - getInfo(); + face = Get.arguments['face']!; + heroTag = Get.arguments['heroTag']!; } - getInfo() async { + // 获取用户信息 + Future> getInfo() async { + await getMemberStat(); String params = await WbiSign().makSign({ 'mid': mid, 'token': '', @@ -22,7 +30,25 @@ class MemberController extends GetxController { params = '?$params'; var res = await MemberHttp.memberInfo(params: params); if (res['status']) { - print(res['data']); + memberInfo.value = res['data']; } + return res; + } + + // 获取用户状态 + Future> getMemberStat() async { + var res = await MemberHttp.memberStat(mid: mid); + if (res['status']) { + userStat = res['data']; + } + return res; + } + + Future getMemberCardInfo() async { + var res = await MemberHttp.memberCardInfo(mid: mid); + if (res['status']) { + print(userStat); + } + return res; } } diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 0cd02c55c..eed507a20 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -1,7 +1,10 @@ import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/models/user/stat.dart'; import 'package:pilipala/pages/member/index.dart'; +import 'package:pilipala/utils/utils.dart'; class MemberPage extends StatefulWidget { const MemberPage({super.key}); @@ -10,11 +13,379 @@ class MemberPage extends StatefulWidget { State createState() => _MemberPageState(); } -class _MemberPageState extends State { +class _MemberPageState extends State + with SingleTickerProviderStateMixin { final MemberController _memberController = Get.put(MemberController()); + final ScrollController _extendNestCtr = ScrollController(); + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } @override Widget build(BuildContext context) { - return Scaffold(); + return Scaffold( + primary: true, + body: ExtendedNestedScrollView( + controller: _extendNestCtr, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + pinned: false, + primary: true, + elevation: 0, + scrolledUnderElevation: 0, + forceElevated: innerBoxIsScrolled, + expandedHeight: 300, + actions: [ + IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), + const SizedBox(width: 4), + ], + flexibleSpace: FlexibleSpaceBar( + background: Stack( + children: [ + Positioned.fill( + bottom: 10, + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + image: NetworkImage(_memberController.face!), + alignment: Alignment.topCenter, + isAntiAlias: true, + ), + ), + foregroundDecoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context) + .colorScheme + .background + .withOpacity(0.44), + Theme.of(context).colorScheme.background, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.46], + ), + ), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + height: 20, + child: Container( + color: Theme.of(context).colorScheme.background, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 18, right: 18), + child: FutureBuilder( + future: _memberController.getInfo(), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done) { + Map data = snapshot.data!; + if (data['status']) { + return Obx( + () => Stack( + alignment: AlignmentDirectional.center, + children: [ + Column( + // mainAxisAlignment: + // MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + profile( + _memberController.memberInfo.value), + const SizedBox(height: 14), + Row( + children: [ + Text( + _memberController + .memberInfo.value.name!, + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith( + fontWeight: + FontWeight.bold), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${_memberController.memberInfo.value.level}.png', + height: 11, + ), + const SizedBox(width: 6), + if (_memberController.memberInfo + .value.vip!.status == + 1 && + _memberController.memberInfo + .value.vip!.label![ + 'img_label_uri_hans'] != + '') ...[ + Image.network( + _memberController.memberInfo + .value.vip!.label![ + 'img_label_uri_hans'], + height: 20, + ), + ] else if (_memberController + .memberInfo + .value + .vip! + .status == + 1 && + _memberController.memberInfo + .value.vip!.label![ + 'img_label_uri_hans_static'] != + '') ...[ + Image.network( + _memberController.memberInfo + .value.vip!.label![ + 'img_label_uri_hans_static'], + height: 20, + ), + ] + ], + ), + if (_memberController.memberInfo.value + .official!['title'] != + '') ...[ + const SizedBox(height: 6), + Row( + children: [ + Text( + _memberController + .memberInfo + .value + .official![ + 'role'] == + 1 + ? '个人认证:' + : '企业认证:', + ), + Text( + _memberController.memberInfo + .value.official!['title']!, + ), + ], + ), + ], + const SizedBox(height: 4), + if (_memberController + .memberInfo.value.sign != + '') + Text( + _memberController + .memberInfo.value.sign!, + textAlign: TextAlign.left, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ); + } else { + return SizedBox(); + } + } else { + // 骨架屏 + return profile(null, loadingStatus: true); + } + }, + ), + ) + ], + ), + ), + ), + ]; + }, + pinnedHeaderSliverHeightBuilder: () { + return MediaQuery.of(context).padding.top + kToolbarHeight; + }, + onlyOneScrollInBody: true, + body: Column( + children: [ + Container( + width: double.infinity, + height: 50, + child: TabBar(controller: _tabController, tabs: [ + Tab(text: '主页'), + Tab(text: '动态'), + Tab(text: '投稿'), + ]), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + Text('主页'), + Text('动态'), + Text('投稿'), + ], + )) + ], + ), + ), + ); + } + + Widget profile(memberInfo, {loadingStatus = false}) { + return Padding( + padding: EdgeInsets.only(top: 3 * MediaQuery.of(context).padding.top), + child: Row( + children: [ + Hero( + tag: _memberController.heroTag!, + child: Stack( + children: [ + NetworkImgLayer( + width: 90, + height: 90, + type: 'avatar', + src: !loadingStatus + ? memberInfo.face + : _memberController.face, + ), + Positioned( + bottom: 0, + left: 14, + child: Container( + padding: const EdgeInsets.fromLTRB(6, 2, 6, 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: + const BorderRadius.all(Radius.circular(10)), + ), + child: Row(children: [ + Image.asset( + 'assets/images/live.gif', + height: 10, + ), + Text( + ' 直播中', + style: TextStyle( + color: Colors.white, + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize), + ) + ]), + ), + ) + ], + )), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + !loadingStatus + ? _memberController.userStat!['following'] + .toString() + : '-', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + Text( + '关注', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize), + ) + ], + ), + Column( + children: [ + Text( + !loadingStatus + ? Utils.numFormat( + _memberController.userStat!['follower'], + ) + : '-', + style: + const TextStyle(fontWeight: FontWeight.bold)), + Text('粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize)) + ], + ), + Column( + children: [ + const Text('-', + style: TextStyle(fontWeight: FontWeight.bold)), + Text( + '获赞', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelMedium! + .fontSize), + ) + ], + ), + ], + ), + ), + const SizedBox(height: 10), + Row( + children: [ + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 42, right: 42), + foregroundColor: !loadingStatus && memberInfo.isFollowed + ? null + : Theme.of(context).colorScheme.onPrimary, + backgroundColor: !loadingStatus && memberInfo.isFollowed + ? Theme.of(context).colorScheme.onInverseSurface + : Theme.of(context).colorScheme.primary, // 设置按钮背景色 + ), + child: Text(!loadingStatus && memberInfo.isFollowed + ? '取关' + : '关注'), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 42, right: 42), + backgroundColor: + Theme.of(context).colorScheme.onInverseSurface, + ), + child: const Text('发消息'), + ) + ], + ) + ], + ), + ), + ], + ), + ); } } diff --git a/lib/pages/searchPanel/widgets/user_panel.dart b/lib/pages/searchPanel/widgets/user_panel.dart index fa81a98a1..471b8c982 100644 --- a/lib/pages/searchPanel/widgets/user_panel.dart +++ b/lib/pages/searchPanel/widgets/user_panel.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/utils.dart'; Widget searchUserPanel(BuildContext context, ctr, list) { TextStyle style = TextStyle( fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, color: Theme.of(context).colorScheme.outline); + return ListView.builder( controller: ctr!.scrollController, addAutomaticKeepAlives: false, @@ -12,17 +15,22 @@ Widget searchUserPanel(BuildContext context, ctr, list) { itemCount: list!.length, itemBuilder: (context, index) { var i = list![index]; + String heroTag = Utils.makeHeroTag(i!.mid); return InkWell( - onTap: () {}, + onTap: () => Get.toNamed('/member?mid=${i.mid}', + arguments: {'heroTag': heroTag, 'face': i.upic}), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), child: Row( children: [ - NetworkImgLayer( - width: 42, - height: 42, - src: i.upic, - type: 'avatar', + Hero( + tag: heroTag, + child: NetworkImgLayer( + width: 42, + height: 42, + src: i.upic, + type: 'avatar', + ), ), const SizedBox(width: 10), Column( diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index b11da53e7..1b7a2b6a1 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -360,62 +360,80 @@ class _VideoInfoState extends State with TickerProviderStateMixin { height: 26, color: Theme.of(context).dividerColor.withOpacity(0.1), ), - Row( - children: [ - NetworkImgLayer( - type: 'avatar', - src: !widget.loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face, - width: 38, - height: 38, - fadeInDuration: Duration.zero, - fadeOutDuration: Duration.zero, - ), - const SizedBox(width: 14), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(!widget.loadingStatus - ? widget.videoDetail!.owner!.name - : videoItem['owner'].name), - // const SizedBox(width: 10), - Text( - widget.loadingStatus - ? '- 粉丝' - : '${Utils.numFormat(videoIntroController.userStat['follower'])}粉丝', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize, - color: Theme.of(context).colorScheme.outline), - ), - ], - ), - const Spacer(), - AnimatedOpacity( - opacity: widget.loadingStatus ? 0 : 1, - duration: const Duration(milliseconds: 150), - child: SizedBox( - height: 36, - child: Obx( - () => videoIntroController.followStatus.isNotEmpty - ? ElevatedButton( - onPressed: () => videoIntroController - .actionRelationMod(), - child: Text(videoIntroController - .followStatus['attribute'] == - 0 - ? '关注' - : '已关注'), - ) - : const SizedBox(), + GestureDetector( + onTap: () { + int mid = !widget.loadingStatus + ? widget.videoDetail!.owner!.mid + : videoItem['owner'].mid; + String face = !widget.loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face; + Get.toNamed('/member?mid=$mid', arguments: { + 'face': face, + 'heroTag': (mid + 99).toString() + }); + }, + child: Row( + children: [ + Hero( + tag: videoItem['owner'].mid + 99, + child: NetworkImgLayer( + type: 'avatar', + src: !widget.loadingStatus + ? widget.videoDetail!.owner!.face + : videoItem['owner'].face, + width: 38, + height: 38, + fadeInDuration: Duration.zero, + fadeOutDuration: Duration.zero, ), ), - ), - ], + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(!widget.loadingStatus + ? widget.videoDetail!.owner!.name + : videoItem['owner'].name), + // const SizedBox(width: 10), + Text( + widget.loadingStatus + ? '- 粉丝' + : '${Utils.numFormat(videoIntroController.userStat['follower'])}粉丝', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: Theme.of(context).colorScheme.outline), + ), + ], + ), + const Spacer(), + AnimatedOpacity( + opacity: widget.loadingStatus ? 0 : 1, + duration: const Duration(milliseconds: 150), + child: SizedBox( + height: 36, + child: Obx( + () => videoIntroController.followStatus.isNotEmpty + ? ElevatedButton( + onPressed: () => videoIntroController + .actionRelationMod(), + child: Text(videoIntroController + .followStatus['attribute'] == + 0 + ? '关注' + : '已关注'), + ) + : const SizedBox(), + ), + ), + ), + ], + ), ), + Divider( height: 26, color: Theme.of(context).dividerColor.withOpacity(0.1), diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 16489ef28..1c038ad93 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -128,115 +128,112 @@ class _VideoReplyPanelState extends State _videoReplyController.currentPage = 0; return await _videoReplyController.queryReplyList(); }, - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Stack( - children: [ - CustomScrollView( - controller: _videoReplyController.scrollController, - key: const PageStorageKey('评论'), - slivers: [ - const SliverToBoxAdapter(child: SizedBox(height: 12)), - FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data as Map; - if (data['status']) { - // 请求成功 - return Obx( - () => SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == - _videoReplyController.replyList.length) { - return Container( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context) - .padding - .bottom), - height: - MediaQuery.of(context).padding.bottom + - 100, - child: Center( - child: Obx(() => Text( - _videoReplyController.noMore.value)), - ), - ); - } else { - return ReplyItem( - replyItem: _videoReplyController - .replyList[index], - showReplyRow: true, - replyLevel: replyLevel); - } - }, - childCount: - _videoReplyController.replyList.length + 1, - ), + child: Stack( + children: [ + CustomScrollView( + controller: _videoReplyController.scrollController, + key: const PageStorageKey('评论'), + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 12)), + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data as Map; + if (data['status']) { + // 请求成功 + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == + _videoReplyController.replyList.length) { + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context) + .padding + .bottom), + height: + MediaQuery.of(context).padding.bottom + + 100, + child: Center( + child: Obx(() => Text( + _videoReplyController.noMore.value)), + ), + ); + } else { + return ReplyItem( + replyItem: + _videoReplyController.replyList[index], + showReplyRow: true, + replyLevel: replyLevel); + } + }, + childCount: + _videoReplyController.replyList.length + 1, ), - ); - } else { - // 请求错误 - return HttpError( - errMsg: data['msg'], - fn: () => setState(() {}), - ); - } + ), + ); } else { - // 骨架屏 - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return const VideoReplySkeleton(); - }, childCount: 5), + // 请求错误 + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), ); } - }, - ) - ], - ), - Positioned( - bottom: MediaQuery.of(context).padding.bottom + 14, - right: 14, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 2), - // 评论内容为空/不足一屏 - // begin: const Offset(0, 0), - end: const Offset(0, 0), - ).animate(CurvedAnimation( - parent: fabAnimationCtr, - curve: Curves.easeInOut, - )), - child: FloatingActionButton( - heroTag: null, - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (builder) { - return VideoReplyNewDialog( - replyLevel: '0', - oid: IdUtils.bv2av(Get.parameters['bvid']!), - root: 0, - parent: 0, - ); - }, - ).then( - (value) => { - // 完成评论,数据添加 - if (value != null && value['data']) - {_videoReplyController.replyList.add(value['data'])} - }, + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoReplySkeleton(); + }, childCount: 5), ); - }, - tooltip: '发表评论', - child: const Icon(Icons.reply), - ), + } + }, + ) + ], + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 14, + right: 14, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 2), + // 评论内容为空/不足一屏 + // begin: const Offset(0, 0), + end: const Offset(0, 0), + ).animate(CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + )), + child: FloatingActionButton( + heroTag: null, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (builder) { + return VideoReplyNewDialog( + replyLevel: '0', + oid: IdUtils.bv2av(Get.parameters['bvid']!), + root: 0, + parent: 0, + ); + }, + ).then( + (value) => { + // 完成评论,数据添加 + if (value != null && value['data']) + {_videoReplyController.replyList.add(value['data'])} + }, + ); + }, + tooltip: '发表评论', + child: const Icon(Icons.reply), ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 2ea5f5c6f..390a24205 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -46,16 +46,19 @@ class ReplyItem extends StatelessWidget { ); } - Widget lfAvtar(context) { + Widget lfAvtar(context, heroTag) { return Container( margin: const EdgeInsets.only(top: 5), child: Stack( children: [ - NetworkImgLayer( - src: replyItem!.member!.avatar, - width: 34, - height: 34, - type: 'avatar', + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: replyItem!.member!.avatar, + width: 34, + height: 34, + type: 'avatar', + ), ), if (replyItem!.member!.officialVerify != null && replyItem!.member!.officialVerify!['type'] == 0) @@ -87,16 +90,18 @@ class ReplyItem extends StatelessWidget { } Widget content(context) { + String heroTag = Utils.makeHeroTag(replyItem!.mid); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 头像、昵称 GestureDetector( - // onTap: () => - // Get.toNamed('/member/${reply.userName}', parameters: { - // 'memberAvatar': reply.avatar, - // 'heroTag': reply.userName + heroTag, - // }), + onTap: () { + Get.toNamed('/member?mid=${replyItem!.mid}', arguments: { + 'face': replyItem!.member!.avatar!, + 'heroTag': heroTag + }); + }, child: SizedBox( width: double.infinity, child: Stack( @@ -105,7 +110,7 @@ class ReplyItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - lfAvtar(context), + lfAvtar(context, heroTag), const SizedBox(width: 12), Text( replyItem!.member!.uname!, @@ -367,9 +372,16 @@ class ReplyItemRow extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), recognizer: TapGestureRecognizer() - ..onTap = () => { - print('跳转至用户主页'), - }, + ..onTap = () { + String heroTag = + Utils.makeHeroTag(replies![i].member.mid); + Get.toNamed( + '/member?mid=${replies![i].member.mid}', + arguments: { + 'face': replies![i].member.avatar, + 'heroTag': heroTag + }); + }, ), if (replies![i].isUp) WidgetSpan( @@ -521,9 +533,13 @@ InlineSpan buildContent(BuildContext context, content) { color: Theme.of(context).colorScheme.primary, ), recognizer: TapGestureRecognizer() - ..onTap = () => { - print('跳转至用户主页'), - }, + ..onTap = () { + String heroTag = Utils.makeHeroTag(value); + Get.toNamed( + '/member?mid=$value', + arguments: {'face': '', 'heroTag': heroTag}, + ); + }, ), ); });