mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
1099 lines
47 KiB
Dart
1099 lines
47 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart';
|
|
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
|
import 'package:PiliPlus/utils/app_scheme.dart';
|
|
import 'package:PiliPlus/utils/extension.dart';
|
|
import 'package:PiliPlus/utils/id_utils.dart';
|
|
import 'package:expandable/expandable.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
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:PiliPlus/common/constants.dart';
|
|
import 'package:PiliPlus/pages/mine/controller.dart';
|
|
import 'package:PiliPlus/pages/video/detail/index.dart';
|
|
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
|
|
import 'package:PiliPlus/common/widgets/stat/danmu.dart';
|
|
import 'package:PiliPlus/common/widgets/stat/view.dart';
|
|
import 'package:PiliPlus/models/video_detail_res.dart';
|
|
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
|
|
import 'package:PiliPlus/utils/feed_back.dart';
|
|
import 'package:PiliPlus/utils/storage.dart';
|
|
import 'package:PiliPlus/utils/utils.dart';
|
|
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
|
|
|
|
import 'widgets/action_item.dart';
|
|
import 'widgets/action_row_item.dart';
|
|
import 'widgets/page.dart';
|
|
import 'widgets/season.dart';
|
|
|
|
class VideoIntroPanel extends StatefulWidget {
|
|
const VideoIntroPanel({
|
|
super.key,
|
|
required this.heroTag,
|
|
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();
|
|
}
|
|
|
|
class _VideoIntroPanelState extends State<VideoIntroPanel>
|
|
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
|
late VideoIntroController videoIntroController;
|
|
|
|
// 添加页面缓存
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
videoIntroController = Get.put(VideoIntroController(), tag: widget.heroTag)
|
|
..heroTag = widget.heroTag;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Obx(
|
|
() => videoIntroController.videoDetail.value.title == null
|
|
? VideoInfo(
|
|
loadingStatus: true,
|
|
videoIntroController: videoIntroController,
|
|
heroTag: widget.heroTag,
|
|
showAiBottomSheet: widget.showAiBottomSheet,
|
|
showIntroDetail: () => widget.showIntroDetail(
|
|
videoIntroController.videoDetail.value,
|
|
videoIntroController.videoTags,
|
|
),
|
|
showEpisodes: widget.showEpisodes,
|
|
onShowMemberPage: widget.onShowMemberPage,
|
|
)
|
|
: VideoInfo(
|
|
key: ValueKey(widget.heroTag),
|
|
loadingStatus: false,
|
|
videoIntroController: videoIntroController,
|
|
heroTag: widget.heroTag,
|
|
showAiBottomSheet: widget.showAiBottomSheet,
|
|
showIntroDetail: () => widget.showIntroDetail(
|
|
videoIntroController.videoDetail.value,
|
|
videoIntroController.videoTags,
|
|
),
|
|
showEpisodes: widget.showEpisodes,
|
|
onShowMemberPage: widget.onShowMemberPage,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class VideoInfo extends StatefulWidget {
|
|
final bool loadingStatus;
|
|
final String heroTag;
|
|
final Function showAiBottomSheet;
|
|
final Function showIntroDetail;
|
|
final Function showEpisodes;
|
|
final ValueChanged onShowMemberPage;
|
|
final VideoIntroController videoIntroController;
|
|
|
|
const VideoInfo({
|
|
super.key,
|
|
this.loadingStatus = false,
|
|
required this.heroTag,
|
|
required this.showAiBottomSheet,
|
|
required this.showIntroDetail,
|
|
required this.showEpisodes,
|
|
required this.onShowMemberPage,
|
|
required this.videoIntroController,
|
|
});
|
|
|
|
@override
|
|
State<VideoInfo> createState() => _VideoInfoState();
|
|
}
|
|
|
|
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
|
|
late final VideoDetailController videoDetailCtr;
|
|
|
|
Map get videoItem => videoIntroController.videoItem;
|
|
VideoIntroController get videoIntroController => widget.videoIntroController;
|
|
VideoDetailData get videoDetail =>
|
|
widget.videoIntroController.videoDetail.value;
|
|
|
|
late final _coinKey = GlobalKey<ActionItemState>();
|
|
late final _favKey = GlobalKey<ActionItemState>();
|
|
|
|
bool isProcessing = false;
|
|
|
|
late final _horizontalMemberPage = GStorage.horizontalMemberPage;
|
|
|
|
Widget _buildVideoTitle([bool isExpand = false]) => videoDetailCtr
|
|
.enableSponsorBlock
|
|
? Obx(
|
|
() => Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
if (videoDetailCtr.videoLabel.value.isNotEmpty) ...[
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 4,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.shield_outlined,
|
|
size: 16,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSecondaryContainer,
|
|
),
|
|
Icon(
|
|
Icons.play_arrow_rounded,
|
|
size: 12,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSecondaryContainer,
|
|
),
|
|
],
|
|
),
|
|
Text(
|
|
videoDetailCtr.videoLabel.value,
|
|
textScaler: TextScaler.linear(1),
|
|
strutStyle: StrutStyle(leading: 0, height: 1),
|
|
style: TextStyle(
|
|
height: 1,
|
|
fontSize: 13,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.onSecondaryContainer,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
TextSpan(text: ' '),
|
|
],
|
|
TextSpan(
|
|
text: '${videoDetail.title ?? videoItem['title'] ?? ''}'),
|
|
],
|
|
),
|
|
maxLines: isExpand ? null : 2,
|
|
overflow: isExpand ? null : TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 16),
|
|
),
|
|
)
|
|
: Text(
|
|
'${videoDetail.title ?? videoItem['title'] ?? ''}',
|
|
maxLines: isExpand ? null : 2,
|
|
overflow: isExpand ? null : TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 16),
|
|
);
|
|
|
|
void handleState(Future Function() action) async {
|
|
if (isProcessing.not) {
|
|
isProcessing = true;
|
|
await action();
|
|
isProcessing = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
videoDetailCtr = Get.find<VideoDetailController>(tag: widget.heroTag);
|
|
|
|
if (videoIntroController.expandableCtr == null) {
|
|
bool alwaysExapndIntroPanel = GStorage.alwaysExapndIntroPanel;
|
|
|
|
videoIntroController.expandableCtr = ExpandableController(
|
|
initialExpanded: alwaysExapndIntroPanel,
|
|
);
|
|
|
|
if (alwaysExapndIntroPanel.not && GStorage.exapndIntroPanelH) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (context.orientation == Orientation.landscape &&
|
|
videoIntroController.expandableCtr?.expanded == false) {
|
|
videoIntroController.expandableCtr?.toggle();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 收藏
|
|
showFavBottomSheet({type = 'tap'}) {
|
|
if (videoIntroController.userInfo == null) {
|
|
SmartDialog.showToast('账号未登录');
|
|
return;
|
|
}
|
|
// 快速收藏 &
|
|
// 点按 收藏至默认文件夹
|
|
// 长按选择文件夹
|
|
if (videoIntroController.enableQuickFav) {
|
|
if (type == 'tap') {
|
|
videoIntroController.actionFavVideo(type: 'default');
|
|
} else {
|
|
Utils.showFavBottomSheet(context: context, ctr: videoIntroController);
|
|
}
|
|
} else if (type != 'longPress') {
|
|
Utils.showFavBottomSheet(context: context, ctr: videoIntroController);
|
|
}
|
|
}
|
|
|
|
// 视频介绍
|
|
showIntroDetail() {
|
|
if (widget.loadingStatus) {
|
|
return;
|
|
}
|
|
feedBack();
|
|
// widget.showIntroDetail();
|
|
videoIntroController.expandableCtr?.toggle();
|
|
}
|
|
|
|
// 用户主页
|
|
onPushMember() {
|
|
feedBack();
|
|
int? mid = !widget.loadingStatus
|
|
? videoDetail.owner?.mid
|
|
: videoItem['owner']?.mid;
|
|
if (mid != null) {
|
|
if (context.orientation == Orientation.landscape &&
|
|
_horizontalMemberPage) {
|
|
widget.onShowMemberPage(mid);
|
|
} else {
|
|
// memberHeroTag = Utils.makeHeroTag(mid);
|
|
// String face = !loadingStatus
|
|
// ? videoDetail.owner!.face
|
|
// : videoItem['owner'].face;
|
|
Get.toNamed(
|
|
'/member?mid=$mid',
|
|
// arguments: {
|
|
// 'face': face,
|
|
// 'heroTag': memberHeroTag,
|
|
// },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final ThemeData t = Theme.of(context);
|
|
return SliverLayoutBuilder(
|
|
builder: (BuildContext context, SliverConstraints constraints) {
|
|
bool isHorizontal = constraints.crossAxisExtent >
|
|
constraints.viewportMainAxisExtent * 1.25;
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(
|
|
left: StyleString.safeSpace,
|
|
right: StyleString.safeSpace,
|
|
top: 10,
|
|
),
|
|
sliver: SliverToBoxAdapter(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: showIntroDetail,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {},
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: videoItem['staff'] == null
|
|
? GestureDetector(
|
|
onTap: onPushMember,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Obx(
|
|
() => Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
NetworkImgLayer(
|
|
type: 'avatar',
|
|
src: videoIntroController.userStat
|
|
.value['card']?['face'] ??
|
|
'',
|
|
width: 35,
|
|
height: 35,
|
|
fadeInDuration: Duration.zero,
|
|
fadeOutDuration: Duration.zero,
|
|
),
|
|
if ((videoIntroController.userStat
|
|
.value['card']
|
|
?['official_verify']
|
|
?['type'] ??
|
|
-1) !=
|
|
-1)
|
|
Positioned(
|
|
right: -2,
|
|
bottom: -2,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.surface,
|
|
),
|
|
child: Icon(
|
|
Icons.offline_bolt,
|
|
color: videoIntroController
|
|
.userStat
|
|
.value['card']
|
|
?[
|
|
'official_verify']
|
|
?['type'] ==
|
|
0
|
|
? Colors.yellow
|
|
: Colors
|
|
.lightBlueAccent,
|
|
size: 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Obx(
|
|
() => Text(
|
|
videoIntroController.userStat
|
|
.value['card']
|
|
?['name'] ??
|
|
"",
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: (videoIntroController
|
|
.userStat
|
|
.value['card']?['vip']
|
|
?[
|
|
'status'] ??
|
|
-1) >
|
|
0 &&
|
|
(videoIntroController
|
|
.userStat
|
|
.value['card']?['vip']
|
|
?[
|
|
'type'] ??
|
|
-1) ==
|
|
2
|
|
? context.vipColor
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 0),
|
|
Obx(
|
|
() => Text(
|
|
'${Utils.numFormat(videoIntroController.userStat.value['follower'])}粉丝 ${videoIntroController.userStat.value['archive_count'] != null ? '${Utils.numFormat(videoIntroController.userStat.value['archive_count'])}视频' : ''}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: t.colorScheme.outline,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
followButton(context, t),
|
|
],
|
|
),
|
|
)
|
|
: SelfSizedHorizontalList(
|
|
gapSize: 25,
|
|
itemCount: videoItem['staff'].length,
|
|
childBuilder: (index) => GestureDetector(
|
|
onTap: () {
|
|
int? ownerMid = !widget.loadingStatus
|
|
? 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: Row(
|
|
children: [
|
|
NetworkImgLayer(
|
|
type: 'avatar',
|
|
src: videoItem['staff'][index].face,
|
|
width: 35,
|
|
height: 35,
|
|
fadeInDuration: Duration.zero,
|
|
fadeOutDuration: Duration.zero,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
videoItem['staff'][index].name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: videoItem['staff'][index]
|
|
.vip
|
|
.status >
|
|
0 &&
|
|
videoItem['staff']
|
|
[index]
|
|
.vip
|
|
.type ==
|
|
2
|
|
? context.vipColor
|
|
: null,
|
|
),
|
|
),
|
|
Text(
|
|
videoItem['staff'][index].title,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.outline,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (isHorizontal) ...[
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: actionGrid(context, videoIntroController)),
|
|
]
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ExpandablePanel(
|
|
controller: videoIntroController.expandableCtr,
|
|
collapsed: GestureDetector(
|
|
onLongPress: () {
|
|
feedBack();
|
|
Utils.copyText(
|
|
'${videoDetail.title ?? videoItem['title'] ?? ''}');
|
|
},
|
|
child: _buildVideoTitle(),
|
|
),
|
|
expanded: GestureDetector(
|
|
onLongPress: () {
|
|
feedBack();
|
|
Utils.copyText(
|
|
'${videoDetail.title ?? videoItem['title'] ?? ''}');
|
|
},
|
|
child: _buildVideoTitle(true),
|
|
),
|
|
theme: const ExpandableThemeData(
|
|
animationDuration: Duration(milliseconds: 300),
|
|
scrollAnimationDuration: Duration(milliseconds: 300),
|
|
crossFadePoint: 0,
|
|
fadeCurve: Curves.ease,
|
|
sizeCurve: Curves.linear,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Stack(
|
|
children: [
|
|
Row(
|
|
children: <Widget>[
|
|
statView(
|
|
context: context,
|
|
theme: 'gray',
|
|
view: !widget.loadingStatus
|
|
? videoDetail.stat?.view ?? '-'
|
|
: videoItem['stat']?.view ?? '-',
|
|
size: 'medium',
|
|
),
|
|
const SizedBox(width: 10),
|
|
statDanMu(
|
|
context: context,
|
|
theme: 'gray',
|
|
danmu: !widget.loadingStatus
|
|
? videoDetail.stat?.danmu ?? '-'
|
|
: videoItem['stat']?.danmu ?? '-',
|
|
size: 'medium',
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
Utils.dateFormat(
|
|
!widget.loadingStatus
|
|
? videoDetail.pubdate
|
|
: videoItem['pubdate'],
|
|
formatType: 'detail'),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: t.colorScheme.outline,
|
|
),
|
|
),
|
|
if (MineController.anonymity.value) ...<Widget>[
|
|
const SizedBox(width: 10),
|
|
Icon(
|
|
MdiIcons.incognito,
|
|
size: 15,
|
|
color: t.colorScheme.outline,
|
|
semanticLabel: '无痕',
|
|
),
|
|
],
|
|
const SizedBox(width: 10),
|
|
if (videoIntroController.isShowOnlineTotal)
|
|
Obx(
|
|
() => Text(
|
|
'${videoIntroController.total.value}人在看',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: t.colorScheme.outline,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (videoIntroController.enableAi)
|
|
Positioned(
|
|
right: 10,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Center(
|
|
child: Semantics(
|
|
label: 'AI总结',
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
final res =
|
|
await videoIntroController.aiConclusion();
|
|
if (res['status']) {
|
|
widget.showAiBottomSheet();
|
|
}
|
|
},
|
|
child: Image.asset('assets/images/ai.png',
|
|
height: 22),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
if (videoIntroController
|
|
.videoDetail.value.argueMsg?.isNotEmpty ==
|
|
true &&
|
|
videoIntroController.showArgueMsg) ...[
|
|
const SizedBox(height: 2),
|
|
Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Icon(
|
|
size: 13,
|
|
Icons.error_outline,
|
|
color: Theme.of(context).colorScheme.outline,
|
|
),
|
|
),
|
|
WidgetSpan(child: SizedBox(width: 2)),
|
|
TextSpan(
|
|
text:
|
|
'${videoIntroController.videoDetail.value.argueMsg}',
|
|
)
|
|
],
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Theme.of(context).colorScheme.outline,
|
|
),
|
|
),
|
|
],
|
|
ExpandablePanel(
|
|
controller: videoIntroController.expandableCtr,
|
|
collapsed: const SizedBox.shrink(),
|
|
expanded: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
GestureDetector(
|
|
onTap: () {
|
|
Utils.copyText(
|
|
'${videoIntroController.videoDetail.value.bvid}');
|
|
},
|
|
child: Text(
|
|
videoIntroController.videoDetail.value.bvid ?? '',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
if (videoIntroController
|
|
.videoDetail.value.descV2.isNullOrEmpty.not) ...[
|
|
const SizedBox(height: 8),
|
|
SelectableText.rich(
|
|
style: const TextStyle(
|
|
height: 1.4,
|
|
// fontSize: 13,
|
|
),
|
|
TextSpan(
|
|
children: [
|
|
buildContent(context,
|
|
videoIntroController.videoDetail.value),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
if (videoIntroController.videoTags is List &&
|
|
videoIntroController.videoTags.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: (videoIntroController.videoTags as List)
|
|
.map(
|
|
(item) => SearchText(
|
|
fontSize: 13,
|
|
text: item['tag_name'],
|
|
onTap: (_) => Get.toNamed('/searchResult',
|
|
parameters: {
|
|
'keyword': item['tag_name']
|
|
}),
|
|
onLongPress: (_) =>
|
|
Utils.copyText(item['tag_name']),
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
theme: const ExpandableThemeData(
|
|
animationDuration: Duration(milliseconds: 300),
|
|
scrollAnimationDuration: Duration(milliseconds: 300),
|
|
crossFadePoint: 0,
|
|
fadeCurve: Curves.ease,
|
|
sizeCurve: Curves.linear,
|
|
),
|
|
),
|
|
Obx(
|
|
() =>
|
|
videoIntroController.queryVideoIntroData.value["status"]
|
|
? const SizedBox.shrink()
|
|
: Center(
|
|
child: TextButton.icon(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: () {
|
|
videoIntroController.queryVideoIntroData
|
|
.value["status"] = true;
|
|
videoIntroController.queryVideoIntro();
|
|
if (videoDetailCtr.videoUrl.isNullOrEmpty &&
|
|
videoDetailCtr.isQuerying.not) {
|
|
videoDetailCtr.queryVideoUrl();
|
|
}
|
|
},
|
|
label: const Text("点此重新加载"),
|
|
),
|
|
),
|
|
),
|
|
// 点赞收藏转发 布局样式1
|
|
// SingleChildScrollView(
|
|
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
|
// scrollDirection: Axis.horizontal,
|
|
// child: actionRow(
|
|
// context,
|
|
// videoIntroController,
|
|
// videoDetailCtr,
|
|
// ),
|
|
// ),
|
|
// 点赞收藏转发 布局样式2
|
|
if (!isHorizontal) ...[
|
|
const SizedBox(height: 8),
|
|
actionGrid(context, videoIntroController),
|
|
],
|
|
// 合集
|
|
if (!widget.loadingStatus &&
|
|
videoDetail.ugcSeason != null &&
|
|
(context.orientation != Orientation.landscape ||
|
|
(context.orientation == Orientation.landscape &&
|
|
videoDetailCtr.horizontalSeasonPanel.not)))
|
|
SeasonPanel(
|
|
heroTag: widget.heroTag,
|
|
ugcSeason: videoDetail.ugcSeason!,
|
|
changeFuc: videoIntroController.changeSeasonOrbangu,
|
|
showEpisodes: widget.showEpisodes,
|
|
pages: videoDetail.pages,
|
|
videoIntroController: videoIntroController,
|
|
),
|
|
if (!widget.loadingStatus &&
|
|
videoDetail.pages != null &&
|
|
videoDetail.pages!.length > 1 &&
|
|
(context.orientation != Orientation.landscape ||
|
|
(context.orientation == Orientation.landscape &&
|
|
videoDetailCtr.horizontalSeasonPanel.not))) ...[
|
|
PagesPanel(
|
|
heroTag: widget.heroTag,
|
|
videoIntroController: videoIntroController,
|
|
bvid: videoIntroController.bvid,
|
|
changeFuc: videoIntroController.changeSeasonOrbangu,
|
|
showEpisodes: widget.showEpisodes,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Obx followButton(BuildContext context, ThemeData t) {
|
|
return Obx(
|
|
() {
|
|
int attr = videoIntroController.followStatus['attribute'] ?? 0;
|
|
return TextButton(
|
|
onPressed: () => videoIntroController.actionRelationMod(context),
|
|
style: TextButton.styleFrom(
|
|
visualDensity: const VisualDensity(vertical: -3),
|
|
foregroundColor: attr != 0
|
|
? t.colorScheme.outline
|
|
: t.colorScheme.onSecondaryContainer,
|
|
backgroundColor: attr != 0
|
|
? t.colorScheme.onInverseSurface
|
|
: t.colorScheme.secondaryContainer,
|
|
),
|
|
child: Text(
|
|
attr == 128
|
|
? '已拉黑'
|
|
: attr != 0
|
|
? '已关注'
|
|
: '关注',
|
|
style: TextStyle(fontSize: t.textTheme.labelMedium!.fontSize),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget actionGrid(BuildContext context, videoIntroController) {
|
|
return LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(top: 1),
|
|
height: 48,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: <Widget>[
|
|
Obx(
|
|
() => ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.thumbsUp),
|
|
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
|
|
onTap: () => handleState(videoIntroController.actionLikeVideo),
|
|
onLongPress: () =>
|
|
handleState(videoIntroController.actionOneThree),
|
|
selectStatus: videoIntroController.hasLike.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
semanticsLabel: '点赞',
|
|
text: !widget.loadingStatus
|
|
? Utils.numFormat(videoDetail.stat!.like!)
|
|
: '-',
|
|
needAnim: true,
|
|
hasOneThree: videoIntroController.hasLike.value &&
|
|
videoIntroController.hasCoin.value &&
|
|
videoIntroController.hasFav.value,
|
|
callBack: (start) {
|
|
if (start) {
|
|
_coinKey.currentState?.controller?.forward();
|
|
_favKey.currentState?.controller?.forward();
|
|
} else {
|
|
_coinKey.currentState?.controller?.reverse();
|
|
_favKey.currentState?.controller?.reverse();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
Obx(
|
|
() => ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.thumbsDown),
|
|
selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown),
|
|
onTap: () =>
|
|
handleState(videoIntroController.actionDislikeVideo),
|
|
selectStatus: videoIntroController.hasDislike.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
semanticsLabel: '点踩',
|
|
text: "点踩"),
|
|
),
|
|
// ActionItem(
|
|
// icon: const Icon(FontAwesomeIcons.clock),
|
|
// onTap: () => videoIntroController.actionShareVideo(),
|
|
// selectStatus: false,
|
|
// loadingStatus: loadingStatus,
|
|
// text: '稍后再看'),
|
|
Obx(
|
|
() => ActionItem(
|
|
key: _coinKey,
|
|
icon: const Icon(FontAwesomeIcons.b),
|
|
selectIcon: const Icon(FontAwesomeIcons.b),
|
|
onTap: () => handleState(videoIntroController.actionCoinVideo),
|
|
selectStatus: videoIntroController.hasCoin.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
semanticsLabel: '投币',
|
|
text: !widget.loadingStatus
|
|
? Utils.numFormat(videoDetail.stat!.coin!)
|
|
: '-',
|
|
needAnim: true,
|
|
),
|
|
),
|
|
Obx(
|
|
() => ActionItem(
|
|
key: _favKey,
|
|
icon: const Icon(FontAwesomeIcons.star),
|
|
selectIcon: const Icon(FontAwesomeIcons.solidStar),
|
|
onTap: () => showFavBottomSheet(),
|
|
onLongPress: () => showFavBottomSheet(type: 'longPress'),
|
|
selectStatus: videoIntroController.hasFav.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
semanticsLabel: '收藏',
|
|
text: !widget.loadingStatus
|
|
? Utils.numFormat(videoDetail.stat!.favorite!)
|
|
: '-',
|
|
needAnim: true,
|
|
),
|
|
),
|
|
ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.comment),
|
|
onTap: () => videoDetailCtr.tabCtr
|
|
.animateTo(videoDetailCtr.tabCtr.index == 1 ? 0 : 1),
|
|
selectStatus: false,
|
|
loadingStatus: widget.loadingStatus,
|
|
semanticsLabel: '评论',
|
|
text: !widget.loadingStatus
|
|
? Utils.numFormat(videoDetail.stat!.reply!)
|
|
: '评论'),
|
|
ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.shareFromSquare),
|
|
onTap: () => videoIntroController.actionShareVideo(),
|
|
selectStatus: false,
|
|
loadingStatus: widget.loadingStatus,
|
|
semanticsLabel: '分享',
|
|
text: !widget.loadingStatus
|
|
? Utils.numFormat(videoDetail.stat!.share!)
|
|
: '分享'),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
|
|
return Row(children: <Widget>[
|
|
Obx(
|
|
() => ActionRowItem(
|
|
icon: const Icon(FontAwesomeIcons.thumbsUp),
|
|
onTap: () => handleState(videoIntroController.actionLikeVideo),
|
|
selectStatus: videoIntroController.hasLike.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
text:
|
|
!widget.loadingStatus ? videoDetail.stat!.like!.toString() : '-',
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Obx(
|
|
() => ActionRowItem(
|
|
icon: const Icon(FontAwesomeIcons.b),
|
|
onTap: () => handleState(videoIntroController.actionCoinVideo),
|
|
selectStatus: videoIntroController.hasCoin.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
text:
|
|
!widget.loadingStatus ? videoDetail.stat!.coin!.toString() : '-',
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Obx(
|
|
() => ActionRowItem(
|
|
icon: const Icon(FontAwesomeIcons.heart),
|
|
onTap: () => showFavBottomSheet(),
|
|
onLongPress: () => showFavBottomSheet(type: 'longPress'),
|
|
selectStatus: videoIntroController.hasFav.value,
|
|
loadingStatus: widget.loadingStatus,
|
|
text: !widget.loadingStatus
|
|
? videoDetail.stat!.favorite!.toString()
|
|
: '-',
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ActionRowItem(
|
|
icon: const Icon(FontAwesomeIcons.comment),
|
|
onTap: () {
|
|
videoDetailCtr.tabCtr.animateTo(1);
|
|
},
|
|
selectStatus: false,
|
|
loadingStatus: widget.loadingStatus,
|
|
text: !widget.loadingStatus ? videoDetail.stat!.reply!.toString() : '-',
|
|
),
|
|
const SizedBox(width: 8),
|
|
ActionRowItem(
|
|
icon: const Icon(FontAwesomeIcons.share),
|
|
onTap: () => videoIntroController.actionShareVideo(),
|
|
selectStatus: false,
|
|
loadingStatus: widget.loadingStatus,
|
|
// text: !loadingStatus
|
|
// ? videoDetail.stat!.share!.toString()
|
|
// : '-',
|
|
text: '转发'),
|
|
]);
|
|
}
|
|
|
|
InlineSpan buildContent(BuildContext context, VideoDetailData content) {
|
|
final List descV2 = content.descV2!;
|
|
// type
|
|
// 1 普通文本
|
|
// 2 @用户
|
|
final List<TextSpan> spanChildren = List.generate(descV2.length, (index) {
|
|
final currentDesc = descV2[index];
|
|
switch (currentDesc.type) {
|
|
case 1:
|
|
final List<InlineSpan> spanChildren = <InlineSpan>[];
|
|
final RegExp urlRegExp = RegExp(
|
|
'${Constants.urlPattern}|av\\d+|bv\\S+\\b',
|
|
caseSensitive: false,
|
|
);
|
|
|
|
(currentDesc.rawText as String).splitMapJoin(
|
|
urlRegExp,
|
|
onMatch: (Match match) {
|
|
String matchStr = match[0]!;
|
|
if (RegExp(r'^av\d+$', caseSensitive: false).hasMatch(matchStr)) {
|
|
try {
|
|
// validate
|
|
int aid = int.parse(matchStr.substring(2));
|
|
IdUtils.av2bv(aid);
|
|
spanChildren.add(
|
|
TextSpan(
|
|
text: matchStr,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.primary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () {
|
|
PiliScheme.videoPush(aid, null);
|
|
},
|
|
),
|
|
);
|
|
} catch (e) {
|
|
spanChildren.add(TextSpan(text: matchStr));
|
|
}
|
|
} else if (RegExp(r'^bv\S+\b$', caseSensitive: false)
|
|
.hasMatch(matchStr)) {
|
|
try {
|
|
// validate
|
|
IdUtils.bv2av(matchStr);
|
|
spanChildren.add(
|
|
TextSpan(
|
|
text: matchStr,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.primary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () {
|
|
PiliScheme.videoPush(null, matchStr);
|
|
},
|
|
),
|
|
);
|
|
} catch (e) {
|
|
spanChildren.add(TextSpan(text: matchStr));
|
|
}
|
|
} else {
|
|
spanChildren.add(
|
|
TextSpan(
|
|
text: matchStr,
|
|
style:
|
|
TextStyle(color: Theme.of(context).colorScheme.primary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () {
|
|
try {
|
|
Utils.handleWebview(matchStr);
|
|
} catch (err) {
|
|
SmartDialog.showToast(err.toString());
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
return '';
|
|
},
|
|
onNonMatch: (String nonMatchStr) {
|
|
spanChildren.add(TextSpan(text: nonMatchStr));
|
|
return '';
|
|
},
|
|
);
|
|
return TextSpan(children: spanChildren);
|
|
case 2:
|
|
final Color colorSchemePrimary =
|
|
Theme.of(context).colorScheme.primary;
|
|
final String heroTag = Utils.makeHeroTag(currentDesc.bizId);
|
|
return TextSpan(
|
|
text: '@${currentDesc.rawText}',
|
|
style: TextStyle(color: colorSchemePrimary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () {
|
|
Get.toNamed(
|
|
'/member?mid=${currentDesc.bizId}',
|
|
arguments: {'face': '', 'heroTag': heroTag},
|
|
);
|
|
},
|
|
);
|
|
default:
|
|
return const TextSpan();
|
|
}
|
|
});
|
|
return TextSpan(children: spanChildren);
|
|
}
|
|
}
|