mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
971 lines
36 KiB
Dart
971 lines
36 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:PiliPlus/common/constants.dart';
|
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
|
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
|
import 'package:PiliPlus/common/widgets/stat/stat.dart';
|
|
import 'package:PiliPlus/models/common/image_type.dart';
|
|
import 'package:PiliPlus/models/common/stat_type.dart';
|
|
import 'package:PiliPlus/models_new/video/video_detail/data.dart';
|
|
import 'package:PiliPlus/models_new/video/video_detail/staff.dart';
|
|
import 'package:PiliPlus/models_new/video/video_tag/data.dart';
|
|
import 'package:PiliPlus/pages/mine/controller.dart';
|
|
import 'package:PiliPlus/pages/search/widgets/search_text.dart';
|
|
import 'package:PiliPlus/pages/video/controller.dart';
|
|
import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart';
|
|
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart';
|
|
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/page.dart';
|
|
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/season.dart';
|
|
import 'package:PiliPlus/utils/app_scheme.dart';
|
|
import 'package:PiliPlus/utils/date_util.dart';
|
|
import 'package:PiliPlus/utils/extension.dart';
|
|
import 'package:PiliPlus/utils/feed_back.dart';
|
|
import 'package:PiliPlus/utils/id_utils.dart';
|
|
import 'package:PiliPlus/utils/num_util.dart';
|
|
import 'package:PiliPlus/utils/page_utils.dart';
|
|
import 'package:PiliPlus/utils/request_utils.dart';
|
|
import 'package:PiliPlus/utils/utils.dart';
|
|
import 'package:expandable/expandable.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart' show HapticFeedback;
|
|
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:material_design_icons_flutter/material_design_icons_flutter.dart';
|
|
|
|
class VideoIntroPanel extends StatefulWidget {
|
|
const VideoIntroPanel({
|
|
super.key,
|
|
required this.heroTag,
|
|
required this.showAiBottomSheet,
|
|
required this.showEpisodes,
|
|
required this.onShowMemberPage,
|
|
});
|
|
final String heroTag;
|
|
final Function showAiBottomSheet;
|
|
final Function showEpisodes;
|
|
final ValueChanged<int?> onShowMemberPage;
|
|
|
|
@override
|
|
State<VideoIntroPanel> createState() => _VideoIntroPanelState();
|
|
}
|
|
|
|
class _VideoIntroPanelState extends State<VideoIntroPanel>
|
|
with AutomaticKeepAliveClientMixin {
|
|
late VideoIntroController introController;
|
|
late final VideoDetailController videoDetailCtr =
|
|
Get.find<VideoDetailController>(tag: widget.heroTag);
|
|
|
|
late final _coinKey = GlobalKey<ActionItemState>();
|
|
late final _favKey = GlobalKey<ActionItemState>();
|
|
|
|
bool isProcessing = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
introController = Get.put(VideoIntroController(), tag: widget.heroTag)
|
|
..heroTag = widget.heroTag;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
final ThemeData theme = Theme.of(context);
|
|
const expandTheme = ExpandableThemeData(
|
|
animationDuration: Duration(milliseconds: 300),
|
|
scrollAnimationDuration: Duration(milliseconds: 300),
|
|
crossFadePoint: 0,
|
|
fadeCurve: Curves.ease,
|
|
sizeCurve: Curves.linear,
|
|
);
|
|
return SliverLayoutBuilder(
|
|
builder: (context, constraints) {
|
|
bool isHorizontal =
|
|
context.orientation == Orientation.landscape &&
|
|
constraints.crossAxisExtent >
|
|
constraints.viewportMainAxisExtent * 1.25;
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(
|
|
left: StyleString.safeSpace,
|
|
right: StyleString.safeSpace,
|
|
top: 10,
|
|
),
|
|
sliver: Obx(
|
|
() {
|
|
VideoDetailData videoDetail = introController.videoDetail.value;
|
|
bool isLoading = videoDetail.bvid == null;
|
|
int? mid = videoDetail.owner?.mid;
|
|
return SliverToBoxAdapter(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
if (isLoading) {
|
|
return;
|
|
}
|
|
feedBack();
|
|
introController.expandableCtr.toggle();
|
|
},
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {},
|
|
child: Row(
|
|
children: [
|
|
if (videoDetail.staff.isNullOrEmpty) ...[
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: _buildAvatar(
|
|
theme,
|
|
() {
|
|
if (mid != null) {
|
|
feedBack();
|
|
if (context.orientation ==
|
|
Orientation.landscape &&
|
|
introController
|
|
.horizontalMemberPage) {
|
|
widget.onShowMemberPage(mid);
|
|
} else {
|
|
Get.toNamed(
|
|
'/member?mid=$mid&from_view_aid=${videoDetailCtr.oid.value}',
|
|
);
|
|
}
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
followButton(context, theme),
|
|
] else
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: ReloadScrollPhysics(
|
|
controller: introController,
|
|
),
|
|
child: Row(
|
|
spacing: 25,
|
|
children: videoDetail.staff!
|
|
.map((e) => _buildStaff(theme, mid, e))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
if (isHorizontal) ...[
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: actionGrid(
|
|
context,
|
|
isLoading,
|
|
videoDetail,
|
|
introController,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
if (isLoading)
|
|
_buildVideoTitle(theme, videoDetail)
|
|
else
|
|
ExpandablePanel(
|
|
controller: introController.expandableCtr,
|
|
collapsed: GestureDetector(
|
|
onLongPress: () {
|
|
Feedback.forLongPress(context);
|
|
Utils.copyText(videoDetail.title ?? '');
|
|
},
|
|
child: _buildVideoTitle(theme, videoDetail),
|
|
),
|
|
expanded: GestureDetector(
|
|
onLongPress: () {
|
|
Feedback.forLongPress(context);
|
|
Utils.copyText(videoDetail.title ?? '');
|
|
},
|
|
child: _buildVideoTitle(
|
|
theme,
|
|
videoDetail,
|
|
isExpand: true,
|
|
),
|
|
),
|
|
theme: expandTheme,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
_buildInfo(theme, videoDetail),
|
|
if (introController.enableAi) _aiBtn,
|
|
],
|
|
),
|
|
if (videoDetail.argueInfo?.argueMsg?.isNotEmpty == true &&
|
|
introController.showArgueMsg) ...[
|
|
const SizedBox(height: 2),
|
|
Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Icon(
|
|
size: 13,
|
|
Icons.error_outline,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
),
|
|
const WidgetSpan(child: SizedBox(width: 2)),
|
|
TextSpan(
|
|
text: '${videoDetail.argueInfo!.argueMsg}',
|
|
),
|
|
],
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
),
|
|
],
|
|
ExpandablePanel(
|
|
controller: introController.expandableCtr,
|
|
collapsed: const SizedBox.shrink(),
|
|
expanded: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
GestureDetector(
|
|
onTap: () =>
|
|
Utils.copyText('${videoDetail.bvid}'),
|
|
child: Text(
|
|
videoDetail.bvid ?? '',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: theme.colorScheme.secondary,
|
|
),
|
|
),
|
|
),
|
|
if (videoDetail.descV2?.isNotEmpty == true) ...[
|
|
const SizedBox(height: 8),
|
|
SelectableText.rich(
|
|
style: const TextStyle(
|
|
height: 1.4,
|
|
),
|
|
TextSpan(
|
|
children: [
|
|
buildContent(theme, videoDetail),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
Obx(() {
|
|
final videoTags = introController.videoTags.value;
|
|
if (videoTags.isNullOrEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return _buildTags(videoTags!);
|
|
}),
|
|
],
|
|
),
|
|
theme: expandTheme,
|
|
),
|
|
Obx(
|
|
() => introController.status.value
|
|
? const SizedBox.shrink()
|
|
: Center(
|
|
child: TextButton.icon(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: () {
|
|
introController.status.value = true;
|
|
introController.queryVideoIntro();
|
|
if (videoDetailCtr.videoUrl.isNullOrEmpty &&
|
|
!videoDetailCtr.isQuerying) {
|
|
videoDetailCtr.queryVideoUrl();
|
|
}
|
|
},
|
|
label: const Text("点此重新加载"),
|
|
),
|
|
),
|
|
),
|
|
// 点赞收藏转发 布局样式2
|
|
if (!isHorizontal) ...[
|
|
const SizedBox(height: 8),
|
|
actionGrid(
|
|
context,
|
|
isLoading,
|
|
videoDetail,
|
|
introController,
|
|
),
|
|
],
|
|
// 合集
|
|
if (!isLoading &&
|
|
videoDetail.ugcSeason != null &&
|
|
(context.orientation != Orientation.landscape ||
|
|
(context.orientation == Orientation.landscape &&
|
|
!videoDetailCtr
|
|
.plPlayerController
|
|
.horizontalSeasonPanel)))
|
|
SeasonPanel(
|
|
heroTag: widget.heroTag,
|
|
changeFuc: introController.changeSeasonOrbangu,
|
|
showEpisodes: widget.showEpisodes,
|
|
videoIntroController: introController,
|
|
),
|
|
if (!isLoading &&
|
|
videoDetail.pages != null &&
|
|
videoDetail.pages!.length > 1 &&
|
|
(context.orientation != Orientation.landscape ||
|
|
(context.orientation == Orientation.landscape &&
|
|
!videoDetailCtr
|
|
.plPlayerController
|
|
.horizontalSeasonPanel))) ...[
|
|
PagesPanel(
|
|
heroTag: widget.heroTag,
|
|
videoIntroController: introController,
|
|
bvid: introController.bvid,
|
|
showEpisodes: widget.showEpisodes,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
WidgetSpan _labelWidget(String text, Color bgColor, Color textColor) {
|
|
return WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 4,
|
|
vertical: 3,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: bgColor,
|
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
child: Text(
|
|
text,
|
|
textScaler: TextScaler.noScaling,
|
|
strutStyle: const StrutStyle(
|
|
leading: 0,
|
|
height: 1,
|
|
fontSize: 12,
|
|
),
|
|
style: TextStyle(
|
|
height: 1,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildVideoTitle(
|
|
ThemeData theme,
|
|
VideoDetailData videoDetail, {
|
|
bool isExpand = false,
|
|
}) {
|
|
late final isDark = theme.brightness == Brightness.dark;
|
|
Widget child() {
|
|
final videoLabel = videoDetailCtr.videoLabel.value;
|
|
return Text.rich(
|
|
TextSpan(
|
|
children: [
|
|
if (videoLabel.isNotEmpty) ...[
|
|
WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 4,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.secondaryContainer,
|
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Stack(
|
|
clipBehavior: Clip.none,
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.shield_outlined,
|
|
size: 16,
|
|
color: theme.colorScheme.onSecondaryContainer,
|
|
),
|
|
Icon(
|
|
Icons.play_arrow_rounded,
|
|
size: 12,
|
|
color: theme.colorScheme.onSecondaryContainer,
|
|
),
|
|
],
|
|
),
|
|
Text(
|
|
videoLabel,
|
|
textScaler: TextScaler.noScaling,
|
|
strutStyle: const StrutStyle(
|
|
leading: 0,
|
|
height: 1,
|
|
fontSize: 13,
|
|
),
|
|
style: TextStyle(
|
|
height: 1,
|
|
fontSize: 13,
|
|
color: theme.colorScheme.onSecondaryContainer,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const TextSpan(text: ' '),
|
|
],
|
|
if (videoDetail.isUpowerExclusive == true) ...[
|
|
_labelWidget(
|
|
'充电专属',
|
|
isDark
|
|
? theme.colorScheme.error
|
|
: theme.colorScheme.errorContainer,
|
|
isDark
|
|
? theme.colorScheme.onError
|
|
: theme.colorScheme.onErrorContainer,
|
|
),
|
|
const TextSpan(text: ' '),
|
|
] else if (videoDetail.rights?.isSteinGate == 1) ...[
|
|
_labelWidget(
|
|
'互动视频',
|
|
theme.colorScheme.secondaryContainer,
|
|
theme.colorScheme.onSecondaryContainer,
|
|
),
|
|
const TextSpan(text: ' '),
|
|
],
|
|
TextSpan(text: videoDetail.title ?? ''),
|
|
],
|
|
),
|
|
maxLines: isExpand ? null : 2,
|
|
overflow: isExpand ? null : TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 16),
|
|
);
|
|
}
|
|
|
|
if (videoDetailCtr.plPlayerController.enableSponsorBlock) {
|
|
return Obx(child);
|
|
}
|
|
return child();
|
|
}
|
|
|
|
Future<void> handleState(FutureOr Function() action) async {
|
|
if (!isProcessing) {
|
|
isProcessing = true;
|
|
await action();
|
|
isProcessing = false;
|
|
}
|
|
}
|
|
|
|
Widget followButton(BuildContext context, ThemeData t) {
|
|
return Obx(
|
|
() {
|
|
int attr = introController.followStatus['attribute'] ?? 0;
|
|
return TextButton(
|
|
onPressed: () => introController.actionRelationMod(context),
|
|
style: TextButton.styleFrom(
|
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
visualDensity: const VisualDensity(vertical: -2.8),
|
|
foregroundColor: attr != 0
|
|
? t.colorScheme.outline
|
|
: t.colorScheme.onSecondaryContainer,
|
|
backgroundColor: attr != 0
|
|
? t.colorScheme.onInverseSurface
|
|
: t.colorScheme.secondaryContainer,
|
|
),
|
|
child: Text(
|
|
switch (attr) {
|
|
1 => '悄悄关注',
|
|
2 => '已关注',
|
|
4 || 6 => '已互关',
|
|
128 => '已拉黑',
|
|
-10 => '特别关注',
|
|
_ => ' 关注 ',
|
|
},
|
|
style: const TextStyle(fontSize: 13),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget actionGrid(
|
|
BuildContext context,
|
|
bool isLoading,
|
|
VideoDetailData videoDetail,
|
|
VideoIntroController videoIntroController,
|
|
) {
|
|
return SizedBox(
|
|
height: 48,
|
|
child: Row(
|
|
children: [
|
|
Obx(
|
|
() => ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.thumbsUp),
|
|
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
|
|
onTap: () => handleState(videoIntroController.actionLikeVideo),
|
|
onLongPress: () =>
|
|
handleState(videoIntroController.actionOneThree),
|
|
selectStatus: videoIntroController.hasLike.value,
|
|
semanticsLabel: '点赞',
|
|
text: !isLoading
|
|
? NumUtil.numFormat(videoDetail.stat!.like)
|
|
: null,
|
|
needAnim: true,
|
|
hasTriple:
|
|
videoIntroController.hasLike.value &&
|
|
videoIntroController.hasCoin &&
|
|
videoIntroController.hasFav.value,
|
|
callBack: (start) {
|
|
if (start) {
|
|
HapticFeedback.lightImpact();
|
|
_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,
|
|
semanticsLabel: '点踩',
|
|
text: "点踩",
|
|
),
|
|
),
|
|
Obx(
|
|
() => ActionItem(
|
|
key: _coinKey,
|
|
icon: const Icon(FontAwesomeIcons.b),
|
|
selectIcon: const Icon(FontAwesomeIcons.b),
|
|
onTap: () => handleState(videoIntroController.actionCoinVideo),
|
|
selectStatus: videoIntroController.hasCoin,
|
|
semanticsLabel: '投币',
|
|
text: !isLoading
|
|
? NumUtil.numFormat(videoDetail.stat!.coin)
|
|
: null,
|
|
needAnim: true,
|
|
),
|
|
),
|
|
Obx(
|
|
() => ActionItem(
|
|
key: _favKey,
|
|
icon: const Icon(FontAwesomeIcons.star),
|
|
selectIcon: const Icon(FontAwesomeIcons.solidStar),
|
|
onTap: () => videoIntroController.showFavBottomSheet(context),
|
|
onLongPress: () => videoIntroController.showFavBottomSheet(
|
|
context,
|
|
type: 'longPress',
|
|
),
|
|
selectStatus: videoIntroController.hasFav.value,
|
|
semanticsLabel: '收藏',
|
|
text: !isLoading
|
|
? NumUtil.numFormat(videoDetail.stat!.favorite)
|
|
: null,
|
|
needAnim: true,
|
|
),
|
|
),
|
|
Obx(
|
|
() => ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.clock),
|
|
selectIcon: const Icon(FontAwesomeIcons.solidClock),
|
|
onTap: () => handleState(videoIntroController.viewLater),
|
|
selectStatus: videoIntroController.hasLater.value,
|
|
semanticsLabel: '再看',
|
|
text: '再看',
|
|
),
|
|
),
|
|
ActionItem(
|
|
icon: const Icon(FontAwesomeIcons.shareFromSquare),
|
|
onTap: () => videoIntroController.actionShareVideo(context),
|
|
selectStatus: false,
|
|
semanticsLabel: '分享',
|
|
text: !isLoading
|
|
? NumUtil.numFormat(videoDetail.stat!.share!)
|
|
: null,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
static final RegExp urlRegExp = RegExp(
|
|
'${Constants.urlRegex.pattern}|av\\d+|bv[a-z\\d]{10}',
|
|
caseSensitive: false,
|
|
);
|
|
|
|
InlineSpan buildContent(ThemeData theme, VideoDetailData content) {
|
|
if (content.descV2.isNullOrEmpty) {
|
|
return const TextSpan();
|
|
}
|
|
// type
|
|
// 1 普通文本
|
|
// 2 @用户
|
|
final List<TextSpan> spanChildren = content.descV2!.map((currentDesc) {
|
|
switch (currentDesc.type) {
|
|
case 1:
|
|
final List<InlineSpan> spanChildren = <InlineSpan>[];
|
|
(currentDesc.rawText as String).splitMapJoin(
|
|
urlRegExp,
|
|
onMatch: (Match match) {
|
|
String matchStr = match[0]!;
|
|
if (matchStr.toLowerCase().startsWith('http')) {
|
|
spanChildren.add(
|
|
TextSpan(
|
|
text: matchStr,
|
|
style: TextStyle(color: theme.colorScheme.primary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () {
|
|
try {
|
|
PageUtils.handleWebview(matchStr);
|
|
} catch (err) {
|
|
SmartDialog.showToast(err.toString());
|
|
}
|
|
},
|
|
),
|
|
);
|
|
} else if (matchStr.startsWith('av')) {
|
|
try {
|
|
int aid = int.parse(matchStr.substring(2));
|
|
IdUtils.av2bv(aid);
|
|
spanChildren.add(
|
|
TextSpan(
|
|
text: matchStr,
|
|
style: TextStyle(color: theme.colorScheme.primary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () => PiliScheme.videoPush(aid, null),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
spanChildren.add(TextSpan(text: matchStr));
|
|
}
|
|
} else {
|
|
try {
|
|
IdUtils.bv2av(matchStr);
|
|
spanChildren.add(
|
|
TextSpan(
|
|
text: matchStr,
|
|
style: TextStyle(color: theme.colorScheme.primary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () => PiliScheme.videoPush(null, matchStr),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
spanChildren.add(TextSpan(text: matchStr));
|
|
}
|
|
}
|
|
return '';
|
|
},
|
|
onNonMatch: (String nonMatchStr) {
|
|
spanChildren.add(TextSpan(text: nonMatchStr));
|
|
return '';
|
|
},
|
|
);
|
|
return TextSpan(children: spanChildren);
|
|
case 2:
|
|
final Color colorSchemePrimary = theme.colorScheme.primary;
|
|
return TextSpan(
|
|
text: '@${currentDesc.rawText}',
|
|
style: TextStyle(color: colorSchemePrimary),
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () => Get.toNamed('/member?mid=${currentDesc.bizId}'),
|
|
);
|
|
default:
|
|
return const TextSpan();
|
|
}
|
|
}).toList();
|
|
return TextSpan(children: spanChildren);
|
|
}
|
|
|
|
Widget _buildStaff(ThemeData theme, int? ownerMid, Staff item) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () {
|
|
if (item.mid == ownerMid &&
|
|
context.orientation == Orientation.landscape &&
|
|
introController.horizontalMemberPage) {
|
|
widget.onShowMemberPage(ownerMid);
|
|
} else {
|
|
Get.toNamed(
|
|
'/member?mid=${item.mid}&from_view_aid=${videoDetailCtr.oid.value}',
|
|
);
|
|
}
|
|
},
|
|
child: Row(
|
|
children: [
|
|
Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
NetworkImgLayer(
|
|
type: ImageType.avatar,
|
|
src: item.face,
|
|
width: 35,
|
|
height: 35,
|
|
fadeInDuration: Duration.zero,
|
|
fadeOutDuration: Duration.zero,
|
|
),
|
|
if ((item.official?.type ?? -1) != -1)
|
|
Positioned(
|
|
right: -2,
|
|
bottom: -2,
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: theme.colorScheme.surface,
|
|
),
|
|
child: Icon(
|
|
Icons.offline_bolt,
|
|
color: item.official?.type == 0
|
|
? const Color(0xFFFFCC00)
|
|
: Colors.lightBlueAccent,
|
|
size: 14,
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 0,
|
|
right: -6,
|
|
child: Obx(
|
|
() =>
|
|
introController.staffRelations['status'] == true &&
|
|
introController.staffRelations['${item.mid}'] == null
|
|
? Material(
|
|
type: MaterialType.transparency,
|
|
shape: const CircleBorder(),
|
|
child: InkWell(
|
|
customBorder: const CircleBorder(),
|
|
onTap: () => RequestUtils.actionRelationMod(
|
|
context: context,
|
|
mid: item.mid,
|
|
isFollow: false,
|
|
callback: (val) {
|
|
introController.staffRelations['${item.mid}'] =
|
|
true;
|
|
},
|
|
),
|
|
child: Ink(
|
|
padding: const EdgeInsets.all(2),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.secondaryContainer,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
MdiIcons.plus,
|
|
size: 16,
|
|
color: theme.colorScheme.onSecondaryContainer,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: const SizedBox.shrink(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(width: 8),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.name!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: (item.vip?.status ?? 0) > 0 && item.vip?.type == 2
|
|
? context.vipColor
|
|
: null,
|
|
),
|
|
),
|
|
Text(
|
|
item.title!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAvatar(
|
|
ThemeData theme,
|
|
VoidCallback onPushMember,
|
|
) => GestureDetector(
|
|
onTap: onPushMember,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Obx(
|
|
() {
|
|
final userStat = introController.userStat.value;
|
|
final isVip = (userStat.card?.vip?.status ?? 0) > 0;
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
PendantAvatar(
|
|
avatar: userStat.card?.face,
|
|
size: 35,
|
|
badgeSize: 14,
|
|
isVip: isVip,
|
|
officialType: userStat.card?.officialVerify?.type,
|
|
),
|
|
const SizedBox(width: 10),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
userStat.card?.name ?? "",
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isVip && userStat.card?.vip?.type == 2
|
|
? context.vipColor
|
|
: null,
|
|
),
|
|
),
|
|
const SizedBox(height: 0),
|
|
Text(
|
|
'${NumUtil.numFormat(userStat.follower)}粉丝 ${'${NumUtil.numFormat(userStat.archiveCount)}视频'}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
Widget _buildInfo(ThemeData theme, VideoDetailData videoDetail) => Row(
|
|
spacing: 10,
|
|
children: [
|
|
StatWidget(
|
|
type: StatType.play,
|
|
value: videoDetail.stat?.view,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
StatWidget(
|
|
type: StatType.danmaku,
|
|
value: videoDetail.stat?.danmaku,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
Text(
|
|
DateUtil.format(videoDetail.pubdate),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
),
|
|
if (MineController.anonymity.value)
|
|
Icon(
|
|
MdiIcons.incognito,
|
|
size: 15,
|
|
color: theme.colorScheme.outline,
|
|
semanticLabel: '无痕',
|
|
),
|
|
if (introController.isShowOnlineTotal)
|
|
Obx(
|
|
() => Text(
|
|
'${introController.total.value}人在看',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: theme.colorScheme.outline,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget get _aiBtn => Positioned(
|
|
right: 10,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Center(
|
|
child: Semantics(
|
|
label: 'AI总结',
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
if (introController.aiConclusionResult == null) {
|
|
await introController.aiConclusion();
|
|
}
|
|
if (introController.aiConclusionResult == null) {
|
|
return;
|
|
}
|
|
if (introController.aiConclusionResult!.summary?.isNotEmpty ==
|
|
true ||
|
|
introController.aiConclusionResult!.outline?.isNotEmpty ==
|
|
true) {
|
|
widget.showAiBottomSheet();
|
|
} else {
|
|
SmartDialog.showToast("当前视频不支持AI视频总结");
|
|
}
|
|
},
|
|
child: Image.asset('assets/images/ai.png', height: 22),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _buildTags(List<VideoTagItem> tags) {
|
|
return GestureDetector(
|
|
onTap: () {},
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.only(top: 8),
|
|
child: Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: tags
|
|
.map(
|
|
(item) => SearchText(
|
|
fontSize: 13,
|
|
text: item.tagName!,
|
|
onTap: (tagName) => Get.toNamed(
|
|
'/searchResult',
|
|
parameters: {'keyword': tagName},
|
|
),
|
|
onLongPress: Utils.copyText,
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|