diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index b3402cbf..9d37638b 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'package:PiliPalaX/common/constants.dart'; import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; @@ -42,9 +43,7 @@ class ListSheetContent extends StatefulWidget { class _ListSheetContentState extends State with TickerProviderStateMixin { late List itemScrollController = []; - late int currentIndex = - widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ?? - 0; + int? currentIndex; late List reverse; int get _index => widget.index ?? 0; @@ -60,11 +59,12 @@ class _ListSheetContentState extends State @override void didUpdateWidget(ListSheetContent oldWidget) { super.didUpdateWidget(oldWidget); - currentIndex = widget.episodes! - .indexWhere((dynamic e) => e.cid == widget.currentCid) ?? - 0; + currentIndex = _currentIndex; } + int get _currentIndex => + max(0, widget.episodes.indexWhere((e) => e.cid == widget.currentCid)); + @override void initState() { super.initState(); @@ -85,9 +85,7 @@ class _ListSheetContentState extends State reverse = _isList ? List.generate(widget.season.sections.length, (_) => false) : [false]; - WidgetsBinding.instance.addPostFrameCallback((_) { - itemScrollController[_index].jumpTo(index: currentIndex); - }); + currentIndex = _currentIndex; if (widget.bvid != null && widget.season != null) { _favStream ??= StreamController(); () async { @@ -98,6 +96,11 @@ class _ListSheetContentState extends State } }(); } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentIndex != null) { + itemScrollController[_index].jumpTo(index: currentIndex!); + } + }); } @override @@ -312,10 +315,12 @@ class _ListSheetContentState extends State _ctr?.animateTo(_index); await Future.delayed(const Duration(milliseconds: 225)); } - itemScrollController[_ctr?.index ?? 0].scrollTo( - index: currentIndex, - duration: const Duration(milliseconds: 200), - ); + if (currentIndex != null) { + itemScrollController[_ctr?.index ?? 0].scrollTo( + index: currentIndex!, + duration: const Duration(milliseconds: 200), + ); + } }, ), const Spacer(), diff --git a/lib/common/widgets/video_card_h_member_video.dart b/lib/common/widgets/video_card_h_member_video.dart index 1c69e801..5c028ac9 100644 --- a/lib/common/widgets/video_card_h_member_video.dart +++ b/lib/common/widgets/video_card_h_member_video.dart @@ -17,10 +17,14 @@ class VideoCardHMemberVideo extends StatelessWidget { required this.videoItem, this.longPress, this.longPressEnd, + this.onTap, + this.bvid, }); final Item videoItem; final Function()? longPress; final Function()? longPressEnd; + final VoidCallback? onTap; + final dynamic bvid; @override Widget build(BuildContext context) { @@ -32,6 +36,10 @@ class VideoCardHMemberVideo extends StatelessWidget { borderRadius: BorderRadius.circular(12), onLongPress: longPress, onTap: () async { + if (onTap != null) { + onTap!(); + return; + } try { Get.toNamed('/video?bvid=$bvid&cid=${videoItem.firstCid}', arguments: {'heroTag': heroTag}); @@ -115,10 +123,15 @@ class VideoCardHMemberVideo extends StatelessWidget { videoItem.title ?? '', textAlign: TextAlign.start, style: TextStyle( - fontWeight: FontWeight.w400, + fontWeight: videoItem.bvid == bvid + ? FontWeight.bold + : FontWeight.w400, fontSize: Theme.of(context).textTheme.bodyMedium!.fontSize, height: 1.42, letterSpacing: 0.3, + color: videoItem.bvid == bvid + ? Theme.of(context).colorScheme.primary + : null, ), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 57d17e22..08a532ba 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -1,7 +1,4 @@ -import 'dart:convert'; - -import 'package:PiliPalaX/http/constants.dart'; -import 'package:PiliPalaX/http/init.dart'; +import 'package:PiliPalaX/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -14,8 +11,6 @@ import 'package:PiliPalaX/models/member/coin.dart'; import 'package:PiliPalaX/models/member/info.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:share_plus/share_plus.dart'; -import 'package:html/dom.dart' as dom; -import 'package:html/parser.dart' as html_parser; import '../video/detail/introduction/widgets/group_panel.dart'; @@ -52,7 +47,7 @@ class MemberController extends GetxController { // 获取用户信息 Future> getInfo() async { - await getWwebid(); + wwebid = await Utils.getWwebid(mid); await getMemberStat(); await getMemberView(); var res = await MemberHttp.memberInfo(mid: mid, wwebid: wwebid); @@ -63,20 +58,6 @@ class MemberController extends GetxController { return res; } - Future getWwebid() async { - try { - dynamic response = - await Request().get('${HttpString.spaceBaseUrl}/$mid/dynamic'); - dom.Document document = html_parser.parse(response.data); - dom.Element? scriptElement = - document.querySelector('script#__RENDER_DATA__'); - wwebid = jsonDecode( - Uri.decodeComponent(scriptElement?.text ?? ''))['access_id']; - } catch (e) { - debugPrint('failed to get wwebid: $e'); - } - } - // 获取用户状态 Future> getMemberStat() async { var res = await MemberHttp.memberStat(mid: mid); diff --git a/lib/pages/member_search/controller.dart b/lib/pages/member_search/controller.dart index fc84a384..bd94fd0a 100644 --- a/lib/pages/member_search/controller.dart +++ b/lib/pages/member_search/controller.dart @@ -1,14 +1,9 @@ -import 'dart:convert'; - -import 'package:PiliPalaX/http/constants.dart'; -import 'package:PiliPalaX/http/init.dart'; import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/utils/extension.dart'; +import 'package:PiliPalaX/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/http/member.dart'; -import 'package:html/dom.dart' as dom; -import 'package:html/parser.dart' as html_parser; class MemberSearchController extends GetxController with GetSingleTickerProviderStateMixin { @@ -39,7 +34,9 @@ class MemberSearchController extends GetxController super.onInit(); mid = int.parse(Get.parameters['mid']!); uname.value = Get.parameters['uname']!; - getWwebid(); + Utils.getWwebid(mid).then((res) { + wwebid = res; + }); } // 清空搜索 @@ -105,20 +102,6 @@ class MemberSearchController extends GetxController } } - Future getWwebid() async { - try { - dynamic response = - await Request().get('${HttpString.spaceBaseUrl}/$mid/dynamic'); - dom.Document document = html_parser.parse(response.data); - dom.Element? scriptElement = - document.querySelector('script#__RENDER_DATA__'); - wwebid = jsonDecode( - Uri.decodeComponent(scriptElement?.text ?? ''))['access_id']; - } catch (e) { - debugPrint('failed to get wwebid: $e'); - } - } - // 搜索视频 Future searchArchives([bool isRefresh = true]) async { if (isRefresh.not && isEndArchive) return; diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index 568fa8a2..33be1b1e 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -274,6 +274,12 @@ class _ExtraSettingState extends State { setKey: SettingBoxKey.horizontalSeasonPanel, defaultVal: false, ), + SetSwitchItem( + title: '横屏播放页在侧栏打开UP主页', + leading: const Icon(Icons.account_circle_outlined), + setKey: SettingBoxKey.horizontalMemberPage, + defaultVal: false, + ), Obx( () => ListTile( enableFeedback: true, diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index cab58818..6e4dde93 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -10,7 +10,6 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; -import 'package:hive/hive.dart'; import 'package:PiliPalaX/common/constants.dart'; import 'package:PiliPalaX/pages/mine/controller.dart'; import 'package:PiliPalaX/pages/video/detail/index.dart'; @@ -37,11 +36,13 @@ class VideoIntroPanel extends StatefulWidget { required this.showAiBottomSheet, required this.showIntroDetail, required this.showEpisodes, + required this.onShowMemberPage, }); final String heroTag; final Function showAiBottomSheet; final Function showIntroDetail; final Function showEpisodes; + final ValueChanged onShowMemberPage; @override State createState() => _VideoIntroPanelState(); @@ -95,6 +96,7 @@ class _VideoIntroPanelState extends State videoIntroController.videoTags, ), showEpisodes: widget.showEpisodes, + onShowMemberPage: widget.onShowMemberPage, ) : VideoInfo( //key:herotag @@ -108,6 +110,7 @@ class _VideoIntroPanelState extends State videoIntroController.videoTags, ), showEpisodes: widget.showEpisodes, + onShowMemberPage: widget.onShowMemberPage, )); } } @@ -115,19 +118,21 @@ class _VideoIntroPanelState extends State class VideoInfo extends StatefulWidget { final bool loadingStatus; final VideoDetailData? videoDetail; - final String? heroTag; + final String heroTag; final Function showAiBottomSheet; final Function showIntroDetail; final Function showEpisodes; + final ValueChanged onShowMemberPage; const VideoInfo({ super.key, this.loadingStatus = false, this.videoDetail, - this.heroTag, + required this.heroTag, required this.showAiBottomSheet, required this.showIntroDetail, required this.showEpisodes, + required this.onShowMemberPage, }); @override @@ -135,19 +140,18 @@ class VideoInfo extends StatefulWidget { } class _VideoInfoState extends State with TickerProviderStateMixin { - // final String heroTag = Get.arguments['heroTag']; - late String heroTag; late final VideoIntroController videoIntroController; late final VideoDetailController videoDetailCtr; late final Map videoItem; - final Box setting = GStorage.setting; + late final _coinKey = GlobalKey(); + late final _favKey = GlobalKey(); - late final bool loadingStatus; // 加载状态 - - late String memberHeroTag; late bool enableAi; bool isProcessing = false; + + late final _horizontalMemberPage = GStorage.horizontalMemberPage; + void Function()? handleState(Future Function() action) { return isProcessing ? null @@ -158,19 +162,14 @@ class _VideoInfoState extends State with TickerProviderStateMixin { }; } - late final _coinKey = GlobalKey(); - late final _favKey = GlobalKey(); - @override void initState() { super.initState(); - heroTag = widget.heroTag!; - videoIntroController = Get.put(VideoIntroController(), tag: heroTag); - videoDetailCtr = Get.find(tag: heroTag); + videoIntroController = Get.put(VideoIntroController(), tag: widget.heroTag); + videoDetailCtr = Get.find(tag: widget.heroTag); videoItem = videoIntroController.videoItem!; - loadingStatus = widget.loadingStatus; - enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); + enableAi = GStorage.setting.get(SettingBoxKey.enableAi, defaultValue: true); if (videoIntroController.expandableCtr == null) { bool alwaysExapndIntroPanel = GStorage.alwaysExapndIntroPanel; @@ -224,7 +223,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { return; } final bool enableDragQuickFav = - setting.get(SettingBoxKey.enableQuickFav, defaultValue: false); + GStorage.setting.get(SettingBoxKey.enableQuickFav, defaultValue: false); // 快速收藏 & // 点按 收藏至默认文件夹 // 长按选择文件夹 @@ -241,7 +240,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 视频介绍 showIntroDetail() { - if (loadingStatus) { + if (widget.loadingStatus) { return; } feedBack(); @@ -252,16 +251,26 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 用户主页 onPushMember() { feedBack(); - int? mid = !loadingStatus + int? mid = !widget.loadingStatus ? widget.videoDetail?.owner?.mid : videoItem['owner']?.mid; if (mid != null) { - memberHeroTag = Utils.makeHeroTag(mid); - String face = !loadingStatus - ? widget.videoDetail!.owner!.face - : videoItem['owner'].face; - Get.toNamed('/member?mid=$mid', - arguments: {'face': face, 'heroTag': memberHeroTag}); + if (context.orientation == Orientation.landscape && + _horizontalMemberPage) { + widget.onShowMemberPage(mid); + } else { + // memberHeroTag = Utils.makeHeroTag(mid); + // String face = !loadingStatus + // ? widget.videoDetail!.owner!.face + // : videoItem['owner'].face; + Get.toNamed( + '/member?mid=$mid', + // arguments: { + // 'face': face, + // 'heroTag': memberHeroTag, + // }, + ); + } } } @@ -296,7 +305,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { children: [ NetworkImgLayer( type: 'avatar', - src: loadingStatus + src: widget.loadingStatus ? videoItem['owner']?.face ?? "" : widget.videoDetail!.owner!.face, width: 30, @@ -311,7 +320,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { CrossAxisAlignment.start, children: [ Text( - loadingStatus + widget.loadingStatus ? videoItem['owner']?.name ?? "" : widget.videoDetail!.owner!.name, maxLines: 1, @@ -347,13 +356,28 @@ class _VideoInfoState extends State with TickerProviderStateMixin { width: 80, alignment: Alignment.center, child: GestureDetector( - onTap: () => Get.toNamed( - '/member?mid=${videoItem['staff'][index].mid}', - arguments: { - 'face': videoItem['staff'][index].face, - heroTag: Utils.makeHeroTag( - videoItem['staff'][index].mid), - }), + onTap: () { + int? ownerMid = !widget.loadingStatus + ? widget.videoDetail?.owner?.mid + : videoItem['owner']?.mid; + if (videoItem['staff'][index].mid == + ownerMid && + context.orientation == + Orientation.landscape && + _horizontalMemberPage) { + widget.onShowMemberPage(ownerMid); + } else { + Get.toNamed( + '/member?mid=${videoItem['staff'][index].mid}', + // arguments: { + // 'face': + // videoItem['staff'][index].face, + // 'heroTag': Utils.makeHeroTag( + // videoItem['staff'][index].mid), + // }, + ); + } + }, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -460,7 +484,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { statView( context: context, theme: 'gray', - view: !loadingStatus + view: !widget.loadingStatus ? widget.videoDetail?.stat?.view ?? '-' : videoItem['stat']?.view ?? '-', size: 'medium', @@ -469,7 +493,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { statDanMu( context: context, theme: 'gray', - danmu: !loadingStatus + danmu: !widget.loadingStatus ? widget.videoDetail?.stat?.danmu ?? '-' : videoItem['stat']?.danmu ?? '-', size: 'medium', @@ -477,7 +501,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { const SizedBox(width: 10), Text( Utils.dateFormat( - !loadingStatus + !widget.loadingStatus ? widget.videoDetail?.pubdate : videoItem['pubdate'], formatType: 'detail'), @@ -635,14 +659,14 @@ class _VideoInfoState extends State with TickerProviderStateMixin { // 点赞收藏转发 布局样式2 if (!isHorizontal) actionGrid(context, videoIntroController), // 合集 - if (!loadingStatus && + if (!widget.loadingStatus && widget.videoDetail?.ugcSeason != null && (context.orientation != Orientation.landscape || (context.orientation == Orientation.landscape && videoDetailCtr.horizontalSeasonPanel.not))) Obx( () => SeasonPanel( - heroTag: heroTag, + heroTag: widget.heroTag, ugcSeason: widget.videoDetail!.ugcSeason!, cid: videoIntroController.lastPlayCid.value != 0 ? (widget.videoDetail!.pages?.isNotEmpty == true @@ -654,7 +678,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { pages: widget.videoDetail!.pages, ), ), - if (!loadingStatus && + if (!widget.loadingStatus && widget.videoDetail?.pages != null && widget.videoDetail!.pages!.length > 1 && (context.orientation != Orientation.landscape || @@ -662,7 +686,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { videoDetailCtr.horizontalSeasonPanel.not))) ...[ Obx( () => PagesPanel( - heroTag: heroTag, + heroTag: widget.heroTag, pages: widget.videoDetail!.pages!, cid: videoIntroController.lastPlayCid.value, bvid: videoIntroController.bvid, @@ -722,9 +746,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { onTap: handleState(videoIntroController.actionLikeVideo), onLongPress: handleState(videoIntroController.actionOneThree), selectStatus: videoIntroController.hasLike.value, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, semanticsLabel: '点赞', - text: !loadingStatus + text: !widget.loadingStatus ? Utils.numFormat(widget.videoDetail!.stat!.like!) : '-', needAnim: true, @@ -748,7 +772,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown), onTap: handleState(videoIntroController.actionDislikeVideo), selectStatus: videoIntroController.hasDislike.value, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, semanticsLabel: '点踩', text: "点踩"), ), @@ -765,9 +789,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { selectIcon: const Icon(FontAwesomeIcons.b), onTap: handleState(videoIntroController.actionCoinVideo), selectStatus: videoIntroController.hasCoin.value, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, semanticsLabel: '投币', - text: !loadingStatus + text: !widget.loadingStatus ? Utils.numFormat(widget.videoDetail!.stat!.coin!) : '-', needAnim: true, @@ -781,9 +805,9 @@ class _VideoInfoState extends State with TickerProviderStateMixin { onTap: () => showFavBottomSheet(), onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, semanticsLabel: '收藏', - text: !loadingStatus + text: !widget.loadingStatus ? Utils.numFormat(widget.videoDetail!.stat!.favorite!) : '-', needAnim: true, @@ -794,18 +818,18 @@ class _VideoInfoState extends State with TickerProviderStateMixin { onTap: () => videoDetailCtr.tabCtr .animateTo(videoDetailCtr.tabCtr.index == 1 ? 0 : 1), selectStatus: false, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, semanticsLabel: '评论', - text: !loadingStatus + text: !widget.loadingStatus ? Utils.numFormat(widget.videoDetail!.stat!.reply!) : '评论'), ActionItem( icon: const Icon(FontAwesomeIcons.shareFromSquare), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, semanticsLabel: '分享', - text: !loadingStatus + text: !widget.loadingStatus ? Utils.numFormat(widget.videoDetail!.stat!.share!) : '分享'), ], @@ -821,9 +845,10 @@ class _VideoInfoState extends State with TickerProviderStateMixin { icon: const Icon(FontAwesomeIcons.thumbsUp), onTap: handleState(videoIntroController.actionLikeVideo), selectStatus: videoIntroController.hasLike.value, - loadingStatus: loadingStatus, - text: - !loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-', + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.like!.toString() + : '-', ), ), const SizedBox(width: 8), @@ -832,9 +857,10 @@ class _VideoInfoState extends State with TickerProviderStateMixin { icon: const Icon(FontAwesomeIcons.b), onTap: handleState(videoIntroController.actionCoinVideo), selectStatus: videoIntroController.hasCoin.value, - loadingStatus: loadingStatus, - text: - !loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-', + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.coin!.toString() + : '-', ), ), const SizedBox(width: 8), @@ -844,8 +870,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { onTap: () => showFavBottomSheet(), onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, - loadingStatus: loadingStatus, - text: !loadingStatus + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus ? widget.videoDetail!.stat!.favorite!.toString() : '-', ), @@ -857,16 +883,17 @@ class _VideoInfoState extends State with TickerProviderStateMixin { videoDetailCtr.tabCtr.animateTo(1); }, selectStatus: false, - loadingStatus: loadingStatus, - text: - !loadingStatus ? widget.videoDetail!.stat!.reply!.toString() : '-', + loadingStatus: widget.loadingStatus, + text: !widget.loadingStatus + ? widget.videoDetail!.stat!.reply!.toString() + : '-', ), const SizedBox(width: 8), ActionRowItem( icon: const Icon(FontAwesomeIcons.share), onTap: () => videoIntroController.actionShareVideo(), selectStatus: false, - loadingStatus: loadingStatus, + loadingStatus: widget.loadingStatus, // text: !loadingStatus // ? widget.videoDetail!.stat!.share!.toString() // : '-', diff --git a/lib/pages/video/detail/member/controller.dart b/lib/pages/video/detail/member/controller.dart new file mode 100644 index 00000000..b8376178 --- /dev/null +++ b/lib/pages/video/detail/member/controller.dart @@ -0,0 +1,103 @@ +import 'package:PiliPalaX/http/loading_state.dart'; +import 'package:PiliPalaX/http/member.dart'; +import 'package:PiliPalaX/models/space_archive/data.dart'; +import 'package:PiliPalaX/pages/common/common_controller.dart'; +import 'package:PiliPalaX/pages/member/new/content/member_contribute/member_contribute.dart' + show ContributeType; +import 'package:PiliPalaX/utils/utils.dart'; +import 'package:get/get.dart'; + +class HorizontalMemberPageController extends CommonController { + HorizontalMemberPageController({this.mid}); + + dynamic mid; + dynamic name; + dynamic wwebid; + + Rx userState = LoadingState.loading().obs; + RxMap userStat = {}.obs; + + @override + void onInit() { + super.onInit(); + currentPage = 0; + getUserInfo(); + } + + Future getUserInfo() async { + wwebid ??= await Utils.getWwebid(mid); + dynamic res = await MemberHttp.memberInfo(mid: mid, wwebid: wwebid); + if (res['status']) { + name = res['data'].name; + userState.value = LoadingState.success(res['data']); + getMemberStat(); + queryData(); + } else { + userState.value = LoadingState.error(res['msg']); + } + } + + Future getMemberStat() async { + var res = await MemberHttp.memberStat(mid: mid); + if (res['status']) { + userStat.value = res['data']; + getMemberView(); + } + } + + Future getMemberView() async { + var res = await MemberHttp.memberView(mid: mid); + if (res['status']) { + userStat.addAll(res['data']); + } + } + + @override + bool customHandleResponse(Success response) { + Data data = response.response; + next = data.next; + aid = data.item?.lastOrNull?.param; + isEnd = data.hasNext == false; + if (currentPage == 0) { + count.value = data.count ?? -1; + } else if (loadingState.value is Success) { + data.item?.insertAll(0, (loadingState.value as Success).response); + } + loadingState.value = LoadingState.success(data.item); + return true; + } + + String? aid; + RxString order = 'pubdate'.obs; + RxString sort = 'desc'.obs; + RxInt count = (-1).obs; + int? next; + + @override + Future customGetData() => MemberHttp.spaceArchive( + type: ContributeType.video, + mid: mid, + aid: aid, + order: order.value, + sort: sort.value, + pn: null, + next: next, + seasonId: null, + seriesId: null, + ); + + @override + Future onRefresh() async { + aid = null; + next = null; + currentPage = 0; + isEnd = false; + await queryData(); + } + + queryBySort() { + order.value = order.value == 'pubdate' ? 'click' : 'pubdate'; + loadingState.value = LoadingState.loading(); + onRefresh(); + } +} diff --git a/lib/pages/video/detail/member/horizontal_member_page.dart b/lib/pages/video/detail/member/horizontal_member_page.dart new file mode 100644 index 00000000..a9ce8988 --- /dev/null +++ b/lib/pages/video/detail/member/horizontal_member_page.dart @@ -0,0 +1,401 @@ +import 'package:PiliPalaX/common/constants.dart'; +import 'package:PiliPalaX/common/widgets/icon_button.dart'; +import 'package:PiliPalaX/common/widgets/loading_widget.dart'; +import 'package:PiliPalaX/common/widgets/network_img_layer.dart'; +import 'package:PiliPalaX/common/widgets/video_card_h_member_video.dart'; +import 'package:PiliPalaX/http/loading_state.dart'; +import 'package:PiliPalaX/models/member/info.dart'; +import 'package:PiliPalaX/pages/video/detail/controller.dart'; +import 'package:PiliPalaX/pages/video/detail/introduction/controller.dart'; +import 'package:PiliPalaX/pages/video/detail/member/controller.dart'; +import 'package:PiliPalaX/pages/video/detail/reply/view.dart' + show MySliverPersistentHeaderDelegate; +import 'package:PiliPalaX/utils/extension.dart'; +import 'package:PiliPalaX/utils/grid.dart'; +import 'package:PiliPalaX/utils/id_utils.dart'; +import 'package:PiliPalaX/utils/storage.dart'; +import 'package:PiliPalaX/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +import '../../../../models/space_archive/item.dart'; + +class HorizontalMemberPage extends StatefulWidget { + const HorizontalMemberPage({ + super.key, + required this.mid, + required this.videoDetailController, + required this.videoIntroController, + }); + + final dynamic mid; + final VideoDetailController videoDetailController; + final VideoIntroController videoIntroController; + + @override + State createState() => _HorizontalMemberPageState(); +} + +class _HorizontalMemberPageState extends State { + late final HorizontalMemberPageController _controller; + int? _ownerMid; + dynamic _bvid; + late final String _tag; + + @override + void initState() { + super.initState(); + _tag = Utils.makeHeroTag(widget.mid); + _controller = Get.put( + HorizontalMemberPageController(mid: widget.mid), + tag: _tag, + ); + _bvid = widget.videoDetailController.bvid; + _ownerMid = GStorage.userInfo.get('userInfoCache')?.mid; + } + + @override + void dispose() { + Get.delete(tag: _tag); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + toolbarHeight: 36, + actions: [ + iconButton( + context: context, + onPressed: Get.back, + tooltip: '关闭', + icon: Icons.clear, + size: 28, + ), + const SizedBox(width: 16), + ], + ), + body: Obx( + () => _buildUserPage(_controller.userState.value), + ), + ); + } + + Widget _buildUserPage(LoadingState userState) { + return switch (userState) { + Loading() => loadingWidget, + Success() => Column( + children: [ + _buildUserInfo(userState.response), + const SizedBox(height: 5), + Expanded( + child: Obx(() => _buildVideoList(_controller.loadingState.value)), + ) + ], + ), + Error() => errorWidget( + errMsg: userState.errMsg, + callback: () { + _controller.userState.value = LoadingState.loading(); + _controller.getUserInfo(); + }, + ), + LoadingState() => throw UnimplementedError(), + }; + } + + Widget get _buildSliverHeader { + return SliverPersistentHeader( + pinned: false, + floating: true, + delegate: MySliverPersistentHeaderDelegate( + child: Container( + height: 40, + padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), + color: Theme.of(context).colorScheme.surface, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx( + () => Text( + _controller.count.value != -1 + ? '共${_controller.count.value}视频' + : '', + style: const TextStyle(fontSize: 13), + ), + ), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: _controller.queryBySort, + icon: Icon( + Icons.sort, + size: 16, + color: Theme.of(context).colorScheme.secondary, + ), + label: Obx( + () => Text( + _controller.order.value == 'pubdate' ? '最新发布' : '最多播放', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ), + ) + ], + ), + ), + ), + ); + } + + Widget _buildVideoList(LoadingState loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => CustomScrollView( + slivers: [ + _buildSliverHeader, + SliverPadding( + // 单列布局 EdgeInsets.zero + padding: EdgeInsets.fromLTRB( + StyleString.safeSpace, + StyleString.safeSpace - 5, + StyleString.safeSpace, + MediaQuery.of(context).padding.bottom + 10, + ), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.safeSpace, + crossAxisSpacing: StyleString.safeSpace, + maxCrossAxisExtent: Grid.maxRowWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.4, + mainAxisExtent: 0, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response.length - 1) { + _controller.onLoadMore(); + } + return VideoCardHMemberVideo( + videoItem: loadingState.response[index], + bvid: _bvid, + onTap: () { + final Item videoItem = loadingState.response[index]; + widget.videoIntroController.changeSeasonOrbangu( + null, + videoItem.bvid, + videoItem.firstCid, + IdUtils.bv2av(videoItem.bvid!), + videoItem.cover, + ); + _bvid = videoItem.bvid; + setState(() {}); + }, + ); + }, + childCount: loadingState.response.length, + ), + ), + ), + ], + ), + Error() => errorWidget( + errMsg: loadingState.errMsg, + callback: _controller.onReload, + ), + LoadingState() => throw UnimplementedError(), + }; + } + + Widget _buildUserInfo(MemberInfoModel memberInfoModel) { + return Row( + children: [ + const SizedBox(width: 16), + _buildAvatar(memberInfoModel.face), + const SizedBox(width: 10), + Expanded(child: _buildInfo(memberInfoModel)), + const SizedBox(width: 16), + ], + ); + } + + _buildInfo(MemberInfoModel memberInfoModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + Utils.copyText(memberInfoModel.name ?? ''); + }, + child: Text( + memberInfoModel.name ?? '', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: (memberInfoModel.vip?.status ?? -1) > 0 && + memberInfoModel.vip?.type == 2 + ? context.vipColor + : null, + ), + ), + ), + const SizedBox(width: 8), + Image.asset( + 'assets/images/lv/lv${memberInfoModel.level}.png', + height: 11, + ), + ], + ), + const SizedBox(height: 2), + Obx( + () => Row( + children: List.generate(5, (index) { + if (index % 2 == 0) { + return _buildChildInfo( + title: const ['粉丝', '关注', '获赞'][index ~/ 2], + num: index == 0 + ? _controller.userStat['follower'] != null + ? Utils.numFormat(_controller.userStat['follower']) + : '' + : index == 2 + ? _controller.userStat['following'] ?? '' + : _controller.userStat['likes'] != null + ? Utils.numFormat(_controller.userStat['likes']) + : '', + onTap: () { + if (index == 0) { + Get.toNamed( + '/fan?mid=${widget.mid}&name=${_controller.name}'); + } else if (index == 2) { + Get.toNamed( + '/follow?mid=${widget.mid}&name=${_controller.name}'); + } + }, + ); + } else { + return SizedBox( + height: 10, + width: 20, + child: VerticalDivider( + width: 1, + color: Theme.of(context).colorScheme.outline, + ), + ); + } + }), + ), + ), + const SizedBox(height: 2), + Row( + children: [ + Expanded( + child: FilledButton.tonal( + style: FilledButton.styleFrom( + backgroundColor: memberInfoModel.isFollowed == true + ? Theme.of(context).colorScheme.onInverseSurface + : null, + foregroundColor: memberInfoModel.isFollowed == true + ? Theme.of(context).colorScheme.outline + : null, + padding: const EdgeInsets.all(0), + visualDensity: const VisualDensity( + vertical: -2, + ), + ), + onPressed: () { + if (widget.mid == _ownerMid) { + Get.toNamed('/editProfile'); + } else { + if (_ownerMid == null) { + SmartDialog.showToast('账号未登录'); + return; + } + Utils.actionRelationMod( + context: context, + mid: widget.mid, + isFollow: memberInfoModel.isFollowed ?? false, + callback: (attribute) { + _controller.userState.value = LoadingState.success( + memberInfoModel..isFollowed = attribute != 0); + }, + ); + } + }, + child: Text( + widget.mid == _ownerMid + ? '编辑资料' + : memberInfoModel.isFollowed == true + ? '已关注' + : '关注', + maxLines: 1, + style: TextStyle(fontSize: 14), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.all(0), + visualDensity: const VisualDensity( + vertical: -2, + ), + ), + onPressed: () { + Get.toNamed('/member?mid=${widget.mid}'); + }, + child: Text( + '查看主页', + maxLines: 1, + style: TextStyle(fontSize: 14), + ), + ), + ), + ], + ), + ], + ); + + Widget _buildChildInfo({ + required String title, + required dynamic num, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Text( + '$num$title', + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.outline, + ), + ), + ); + } + + _buildAvatar(face) => Hero( + tag: face, + child: GestureDetector( + onTap: () { + widget.videoDetailController.onViewImage(); + context.imageView( + imgList: [face], + onDismissed: widget.videoDetailController.onDismissed, + ); + }, + child: NetworkImgLayer( + src: face, + type: 'avatar', + width: 70, + height: 70, + ), + ), + ); +} diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 65c6375b..c90175d1 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -15,6 +15,7 @@ import 'package:PiliPalaX/pages/video/detail/introduction/widgets/intro_detail.d as video; import 'package:PiliPalaX/pages/video/detail/introduction/widgets/page.dart'; import 'package:PiliPalaX/pages/video/detail/introduction/widgets/season.dart'; +import 'package:PiliPalaX/pages/video/detail/member/horizontal_member_page.dart'; import 'package:PiliPalaX/pages/video/detail/reply_reply/view.dart'; import 'package:PiliPalaX/pages/video/detail/widgets/ai_detail.dart'; import 'package:PiliPalaX/utils/extension.dart'; @@ -1365,6 +1366,7 @@ class _VideoDetailPageState extends State showAiBottomSheet: showAiBottomSheet, showIntroDetail: showIntroDetail, showEpisodes: showEpisodes, + onShowMemberPage: onShowMemberPage, ), if (needRelated && videoDetailController.showRelatedVideo) ...[ SliverToBoxAdapter( @@ -1796,4 +1798,17 @@ class _VideoDetailPageState extends State verticalScreenForTwoSeconds(); } } + + void onShowMemberPage(mid) { + videoDetailController.childKey.currentState?.showBottomSheet( + (context) { + return HorizontalMemberPage( + mid: mid, + videoDetailController: videoDetailController, + videoIntroController: videoIntroController, + ); + }, + enableDrag: true, + ); + } } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 55a8c0bd..ef45c1a4 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -133,6 +133,9 @@ class GStorage { static bool get horizontalSeasonPanel => setting.get(SettingBoxKey.horizontalSeasonPanel, defaultValue: false); + static bool get horizontalMemberPage => + setting.get(SettingBoxKey.horizontalMemberPage, defaultValue: false); + static List get dynamicDetailRatio => setting.get(SettingBoxKey.dynamicDetailRatio, defaultValue: [60.0, 40.0]); @@ -332,6 +335,7 @@ class SettingBoxKey { alwaysExapndIntroPanel = 'alwaysExapndIntroPanel', exapndIntroPanelH = 'exapndIntroPanelH', horizontalSeasonPanel = 'horizontalSeasonPanel', + horizontalMemberPage = 'horizontalMemberPage', // Sponsor Block enableSponsorBlock = 'enableSponsorBlock', diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 2b661e7a..eed1504b 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -27,10 +27,27 @@ import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_cookie_manager/webview_cookie_manager.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart' as web; +import 'package:html/dom.dart' as dom; +import 'package:html/parser.dart' as html_parser; class Utils { static final Random random = Random(); + static Future getWwebid(mid) async { + try { + dynamic response = + await Request().get('${HttpString.spaceBaseUrl}/$mid/dynamic'); + dom.Document document = html_parser.parse(response.data); + dom.Element? scriptElement = + document.querySelector('script#__RENDER_DATA__'); + return jsonDecode( + Uri.decodeComponent(scriptElement?.text ?? ''))['access_id']; + } catch (e) { + debugPrint('failed to get wwebid: $e'); + return null; + } + } + static Future afterLoginByApp( Map token_info, cookie_info) async { try {