opt dyn/reply text menu

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-16 20:25:21 +08:00
parent f7cd1aa502
commit 177493fe38
31 changed files with 550 additions and 268 deletions

View File

@@ -1,5 +1,4 @@
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/selectable_text.dart';
import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart';
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
import 'package:PiliPlus/pages/video/controller.dart';
@@ -35,12 +34,10 @@ class AiConclusionPanel extends CommonSlidePage {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: selectableText(
child: SelectableText(
res.summary!,
style: const TextStyle(
fontSize: 15,
height: 1.5,
),
style: const TextStyle(fontSize: 15, height: 1.5),
scrollPhysics: const NeverScrollableScrollPhysics(),
),
),
),

View File

@@ -1,7 +1,6 @@
import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/selectable_text.dart';
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/models/common/stat_type.dart';
import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart';
@@ -127,9 +126,10 @@ class _IntroDetailState extends State<PgcIntroPanel>
bottom: MediaQuery.viewPaddingOf(context).bottom + 100,
),
children: [
selectableText(
SelectableText(
widget.item.title!,
style: const TextStyle(fontSize: 16),
scrollPhysics: const NeverSelectableScrollPhysics(),
),
const SizedBox(height: 4),
Row(
@@ -171,9 +171,10 @@ class _IntroDetailState extends State<PgcIntroPanel>
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
selectableText(
SelectableText(
widget.item.evaluate!,
style: textStyle,
scrollPhysics: const NeverSelectableScrollPhysics(),
),
],
if (widget.item.actors?.isNotEmpty == true) ...[

View File

@@ -8,8 +8,7 @@ import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.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'
show ReloadScrollPhysics;
import 'package:PiliPlus/common/widgets/selectable_text.dart';
show ReloadScrollPhysics, NeverSelectableScrollPhysics;
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/common/widgets/translucent_column.dart';
import 'package:PiliPlus/http/sponsor_block.dart';
@@ -122,7 +121,7 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
),
),
const SizedBox(height: 8),
buildTitle(isLoading, isHorizontal, videoDetail),
_buildTitle(isLoading, isHorizontal, videoDetail),
const SizedBox(height: 8),
Stack(
clipBehavior: .none,
@@ -242,7 +241,7 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
);
}
Widget buildTitle(
Widget _buildTitle(
bool isLoading,
bool isHorizontal,
VideoDetailData videoDetail,
@@ -250,29 +249,29 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
if (isLoading) {
return _buildVideoTitle(videoDetail);
} else if (isHorizontal && PlatformUtils.isDesktop) {
return SelectionArea(
child: _buildVideoTitle(videoDetail, isExpand: true),
);
return _buildVideoTitle(videoDetail, isSelectable: true);
}
return Obx(
() => ExpandablePanel(
collapsed: _expandableTitle(videoDetail),
expanded: _expandableTitle(videoDetail, isExpand: true),
collapsed: _gestureVideoTitle(videoDetail),
expanded: _gestureVideoTitle(videoDetail, isExpand: true),
expand: introController.expand.value,
),
);
}
Widget _expandableTitle(
Widget _gestureVideoTitle(
VideoDetailData videoDetail, {
bool isExpand = false,
}) => GestureDetector(
onLongPress: () {
Feedback.forLongPress(context);
Utils.copyText(videoDetail.title ?? '');
},
child: _buildVideoTitle(videoDetail, isExpand: isExpand),
);
}) {
return GestureDetector(
onLongPress: () {
Feedback.forLongPress(context);
Utils.copyText(videoDetail.title ?? '');
},
child: _buildVideoTitle(videoDetail, isExpand: isExpand),
);
}
List<Widget> _infos(VideoDetailData videoDetail) => [
const SizedBox(height: 8, width: .infinity),
@@ -285,9 +284,10 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
),
if (videoDetail.descV2 case final descV2? when descV2.isNotEmpty) ...[
const SizedBox(height: 8),
selectableRichText(
SelectableText.rich(
buildDesc(descV2),
style: const TextStyle(height: 1.4),
buildContent(descV2),
scrollPhysics: const NeverSelectableScrollPhysics(),
),
],
NoTranslucentArea(
@@ -332,79 +332,91 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
Widget _buildVideoTitle(
VideoDetailData videoDetail, {
bool isExpand = false,
bool isSelectable = false,
}) {
late final isDark = colorScheme.isDark;
Widget child() {
final videoLabel = videoDetailCtr.videoLabel.value;
return Text.rich(
TextSpan(
children: [
if (videoLabel.isNotEmpty) ...[
WidgetSpan(
alignment: .middle,
child: Container(
padding: const .symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.secondaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
child: Row(
mainAxisSize: .min,
children: [
Stack(
clipBehavior: .none,
alignment: .center,
children: [
Icon(
Icons.shield_outlined,
size: 16,
color: colorScheme.onSecondaryContainer,
),
Icon(
Icons.play_arrow_rounded,
size: 12,
color: colorScheme.onSecondaryContainer,
),
],
),
Text(
videoLabel,
textScaler: TextScaler.noScaling,
strutStyle: const StrutStyle(
leading: 0,
height: 1,
fontSize: 13,
),
style: TextStyle(
height: 1,
fontSize: 13,
final textSpan = TextSpan(
children: [
if (videoLabel.isNotEmpty) ...[
WidgetSpan(
alignment: .middle,
child: Container(
padding: const .symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: colorScheme.secondaryContainer,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
child: Row(
mainAxisSize: .min,
children: [
Stack(
clipBehavior: .none,
alignment: Alignment.center,
children: [
Icon(
Icons.shield_outlined,
size: 16,
color: colorScheme.onSecondaryContainer,
),
Icon(
Icons.play_arrow_rounded,
size: 12,
color: colorScheme.onSecondaryContainer,
),
],
),
Text(
videoLabel,
textScaler: TextScaler.noScaling,
strutStyle: const StrutStyle(
leading: 0,
height: 1,
fontSize: 13,
),
],
),
style: TextStyle(
height: 1,
fontSize: 13,
color: colorScheme.onSecondaryContainer,
),
),
],
),
),
const TextSpan(text: ' '),
],
if (videoDetail.isUpowerExclusive == true) ...[
_labelWidget(
'充电专属',
isDark ? colorScheme.error : colorScheme.errorContainer,
isDark ? colorScheme.onError : colorScheme.onErrorContainer,
),
const TextSpan(text: ' '),
] else if (videoDetail.rights?.isSteinGate == 1) ...[
_labelWidget(
'互动视频',
colorScheme.secondaryContainer,
colorScheme.onSecondaryContainer,
),
const TextSpan(text: ' '),
],
TextSpan(text: videoDetail.title ?? ''),
),
const TextSpan(text: ' '),
],
),
if (videoDetail.isUpowerExclusive == true) ...[
_labelWidget(
'充电专属',
colorScheme.isDark
? colorScheme.error
: colorScheme.errorContainer,
colorScheme.isDark
? colorScheme.onError
: colorScheme.onErrorContainer,
),
const TextSpan(text: ' '),
] else if (videoDetail.rights?.isSteinGate == 1) ...[
_labelWidget(
'互动视频',
colorScheme.secondaryContainer,
colorScheme.onSecondaryContainer,
),
const TextSpan(text: ' '),
],
TextSpan(text: videoDetail.title ?? ''),
],
);
if (isSelectable) {
return SelectableText.rich(
textSpan,
style: const TextStyle(fontSize: 16),
scrollPhysics: const NeverSelectableScrollPhysics(),
);
}
return Text.rich(
textSpan,
maxLines: isExpand ? null : 2,
overflow: isExpand ? null : .ellipsis,
style: const TextStyle(fontSize: 16),
@@ -543,7 +555,8 @@ class _UgcIntroPanelState extends State<UgcIntroPanel> {
caseSensitive: false,
);
TextSpan buildContent(List<DescV2> descV2) {
TextSpan buildDesc(List<DescV2> descV2) {
// type
// 1 普通文本
// 2 @用户
final List<TextSpan> spanChildren = descV2.map((currentDesc) {

View File

@@ -13,8 +13,10 @@ import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'
show ClampingScrollPhysicsExt;
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo, ReplyControl, Content, Url, ReplyControl_VoteOption;
show ReplyInfo, ReplyControl, Content, Url, ReplyControl_VoteOption, Emote;
import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/reply.dart';
@@ -34,6 +36,8 @@ import 'package:PiliPlus/utils/danmaku_utils.dart';
import 'package:PiliPlus/utils/date_utils.dart';
import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart';
import 'package:PiliPlus/utils/extension/editable_text_ext.dart';
import 'package:PiliPlus/utils/extension/iterable_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/feed_back.dart';
@@ -47,6 +51,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/url_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image_ce/cached_network_image.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
@@ -54,6 +59,8 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:protobuf/protobuf.dart';
part 'package:PiliPlus/common/widgets/context_menu/reply_menu_helper.dart';
class ReplyItemGrpc extends StatelessWidget {
const ReplyItemGrpc({
super.key,
@@ -1183,20 +1190,7 @@ class ReplyItemGrpc extends StatelessWidget {
ListTile(
onTap: () {
Get.back();
showDialog(
context: context,
builder: (context) => Dialog(
child: Padding(
padding: const .symmetric(horizontal: 20, vertical: 16),
child: SelectableText(
message,
style: const TextStyle(fontSize: 15, height: 1.7),
contextMenuBuilder: (_, editableTextState) =>
_filterMenuBuilder(context, editableTextState),
),
),
),
);
showReplyCopyDialog(context, message, item.content.emotes);
},
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
@@ -1225,56 +1219,4 @@ class ReplyItemGrpc extends StatelessWidget {
),
);
}
static Widget _filterMenuBuilder(
BuildContext context,
EditableTextState editableTextState,
) {
final items = editableTextState.contextMenuButtonItems;
if (!editableTextState.textEditingValue.selection.isCollapsed) {
items.add(
ContextMenuButtonItem(
onPressed: () {
Navigator.of(context).pop();
final select = editableTextState.textEditingValue;
String text = RegExp.escape(
select.selection.textInside(select.text),
);
if (ReplyGrpc.enableFilter) text = '|$text';
showConfirmDialog(
context: context,
title: const Text('是否确认评论过滤的变更:'),
content: Text.rich(
TextSpan(
text: ReplyGrpc.replyRegExp.pattern,
children: [
TextSpan(
text: text,
style: const TextStyle(
color: Colors.green,
fontWeight: .bold,
),
),
],
),
),
onConfirm: () {
final filter = ReplyGrpc.replyRegExp.pattern + text;
ReplyGrpc.replyRegExp = RegExp(filter, caseSensitive: true);
ReplyGrpc.enableFilter = true;
GStorage.setting.put(SettingBoxKey.banWordForReply, filter);
SmartDialog.showToast('已保存');
},
);
},
label: '加入过滤',
),
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: items,
anchors: editableTextState.contextMenuAnchors,
);
}
}

View File

@@ -1,7 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:io' show Platform;
import 'dart:math';
import 'dart:ui';
import 'package:PiliPlus/common/assets.dart';
import 'package:PiliPlus/common/style.dart';
@@ -10,7 +8,7 @@ import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/route_aware_mixin.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart' show tabBarView;
import 'package:PiliPlus/common/widgets/sliver/video_header.dart';
import 'package:PiliPlus/common/widgets/svg/play_icon.dart';
import 'package:PiliPlus/models/common/episode_panel_type.dart';
@@ -63,7 +61,7 @@ import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/theme_utils.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/foundation.dart' show kDebugMode, clampDouble;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';