Files
PiliPlus/lib/pages/video/introduction/ugc/view.dart
bggRGjQaUbCoE d17dbe139e opt uplist item
end align player duration

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-07-26 11:46:26 +08:00

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