feat: custom horizontal member page

Closes #51

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2024-12-25 21:22:44 +08:00
parent 513a3d2175
commit eee7eda1a2
11 changed files with 676 additions and 121 deletions

View File

@@ -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<ListSheetContent>
with TickerProviderStateMixin {
late List<ItemScrollController> itemScrollController = [];
late int currentIndex =
widget.episodes!.indexWhere((dynamic e) => e.cid == widget.currentCid) ??
0;
int? currentIndex;
late List<bool> reverse;
int get _index => widget.index ?? 0;
@@ -60,11 +59,12 @@ class _ListSheetContentState extends State<ListSheetContent>
@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<ListSheetContent>
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<int>();
() async {
@@ -98,6 +96,11 @@ class _ListSheetContentState extends State<ListSheetContent>
}
}();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (currentIndex != null) {
itemScrollController[_index].jumpTo(index: currentIndex!);
}
});
}
@override
@@ -312,10 +315,12 @@ class _ListSheetContentState extends State<ListSheetContent>
_ctr?.animateTo(_index);
await Future.delayed(const Duration(milliseconds: 225));
}
if (currentIndex != null) {
itemScrollController[_ctr?.index ?? 0].scrollTo(
index: currentIndex,
index: currentIndex!,
duration: const Duration(milliseconds: 200),
);
}
},
),
const Spacer(),

View File

@@ -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,

View File

@@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> getMemberStat() async {
var res = await MemberHttp.memberStat(mid: mid);

View File

@@ -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;

View File

@@ -274,6 +274,12 @@ class _ExtraSettingState extends State<ExtraSetting> {
setKey: SettingBoxKey.horizontalSeasonPanel,
defaultVal: false,
),
SetSwitchItem(
title: '横屏播放页在侧栏打开UP主页',
leading: const Icon(Icons.account_circle_outlined),
setKey: SettingBoxKey.horizontalMemberPage,
defaultVal: false,
),
Obx(
() => ListTile(
enableFeedback: true,

View File

@@ -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<VideoIntroPanel> createState() => _VideoIntroPanelState();
@@ -95,6 +96,7 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
videoIntroController.videoTags,
),
showEpisodes: widget.showEpisodes,
onShowMemberPage: widget.onShowMemberPage,
)
: VideoInfo(
//key:herotag
@@ -108,6 +110,7 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
videoIntroController.videoTags,
),
showEpisodes: widget.showEpisodes,
onShowMemberPage: widget.onShowMemberPage,
));
}
}
@@ -115,19 +118,21 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
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<VideoInfo> with TickerProviderStateMixin {
// final String heroTag = Get.arguments['heroTag'];
late String heroTag;
late final VideoIntroController videoIntroController;
late final VideoDetailController videoDetailCtr;
late final Map<dynamic, dynamic> videoItem;
final Box<dynamic> setting = GStorage.setting;
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
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<VideoInfo> with TickerProviderStateMixin {
};
}
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
@override
void initState() {
super.initState();
heroTag = widget.heroTag!;
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: widget.heroTag);
videoDetailCtr = Get.find<VideoDetailController>(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<VideoInfo> 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<VideoInfo> with TickerProviderStateMixin {
// 视频介绍
showIntroDetail() {
if (loadingStatus) {
if (widget.loadingStatus) {
return;
}
feedBack();
@@ -252,16 +251,26 @@ class _VideoInfoState extends State<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> with TickerProviderStateMixin {
width: 80,
alignment: Alignment.center,
child: GestureDetector(
onTap: () => Get.toNamed(
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),
}),
// 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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<VideoInfo> 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()
// : '-',

View File

@@ -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<LoadingState> 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<LoadingState> 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();
}
}

View File

@@ -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<HorizontalMemberPage> createState() => _HorizontalMemberPageState();
}
class _HorizontalMemberPageState extends State<HorizontalMemberPage> {
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<HorizontalMemberPageController>(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,
),
),
);
}

View File

@@ -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<VideoDetailPage>
showAiBottomSheet: showAiBottomSheet,
showIntroDetail: showIntroDetail,
showEpisodes: showEpisodes,
onShowMemberPage: onShowMemberPage,
),
if (needRelated && videoDetailController.showRelatedVideo) ...[
SliverToBoxAdapter(
@@ -1796,4 +1798,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
verticalScreenForTwoSeconds();
}
}
void onShowMemberPage(mid) {
videoDetailController.childKey.currentState?.showBottomSheet(
(context) {
return HorizontalMemberPage(
mid: mid,
videoDetailController: videoDetailController,
videoIntroController: videoIntroController,
);
},
enableDrag: true,
);
}
}

View File

@@ -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<double> 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',

View File

@@ -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<dynamic> 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<String, dynamic> token_info, cookie_info) async {
try {