From 177493fe38cc4f484d29622ca17af86b1f8a1c32 Mon Sep 17 00:00:00 2001 From: dom Date: Thu, 16 Jul 2026 20:25:21 +0800 Subject: [PATCH] opt dyn/reply text menu Signed-off-by: dom --- .../widgets/context_menu/dyn_menu_helper.dart | 114 +++++++++++ .../context_menu/reply_menu_helper.dart | 119 ++++++++++++ .../flutter/text_field/editable_text.dart | 8 +- lib/common/widgets/image/image_save.dart | 3 + .../widgets/loading_widget/http_error.dart | 4 +- lib/common/widgets/scroll_physics.dart | 4 +- lib/common/widgets/selectable_text.dart | 40 ---- lib/pages/audio/view.dart | 6 +- .../dynamics/widgets/additional_panel.dart | 3 + lib/pages/dynamics/widgets/content_panel.dart | 40 ++-- .../dynamics/widgets/rich_node_panel.dart | 2 +- lib/pages/dynamics_topic/view.dart | 3 + .../live_room/superchat/superchat_card.dart | 32 ++-- lib/pages/member/widget/user_info_card.dart | 1 + lib/pages/music/view.dart | 3 + lib/pages/pgc_review/child/view.dart | 3 + lib/pages/setting/pages/logs.dart | 8 +- lib/pages/video/ai_conclusion/view.dart | 9 +- .../pgc/widgets/intro_detail.dart | 7 +- lib/pages/video/introduction/ugc/view.dart | 181 ++++++++++-------- .../video/reply/widgets/reply_item_grpc.dart | 76 +------- lib/pages/video/view.dart | 8 +- lib/pages/webview/view.dart | 9 +- .../whisper_detail/widget/chat_item.dart | 12 +- lib/scripts/editable_text.patch | 45 +++++ lib/scripts/patch.ps1 | 5 +- lib/scripts/selectable_region.patch | 13 +- lib/utils/extension/editable_text_ext.dart | 38 ++++ lib/utils/extension/iterable_ext.dart | 8 + lib/utils/reply_utils.dart | 7 +- lib/utils/request_utils.dart | 7 +- 31 files changed, 550 insertions(+), 268 deletions(-) create mode 100644 lib/common/widgets/context_menu/dyn_menu_helper.dart create mode 100644 lib/common/widgets/context_menu/reply_menu_helper.dart delete mode 100644 lib/common/widgets/selectable_text.dart create mode 100644 lib/scripts/editable_text.patch create mode 100644 lib/utils/extension/editable_text_ext.dart diff --git a/lib/common/widgets/context_menu/dyn_menu_helper.dart b/lib/common/widgets/context_menu/dyn_menu_helper.dart new file mode 100644 index 000000000..16db73bbc --- /dev/null +++ b/lib/common/widgets/context_menu/dyn_menu_helper.dart @@ -0,0 +1,114 @@ +part of 'package:PiliPlus/pages/dynamics/widgets/content_panel.dart'; + +Widget dynTextMenuBuilder( + EditableTextState state, + String text, + ModuleDynamicModel? moduleDynamic, +) { + final buttonItems = state.contextMenuButtonItems + ..insertOrAdd( + 3, + ContextMenuButtonItem( + label: '文本', + onPressed: () { + state.hideToolbar(); + _showTextDialog(text); + }, + ), + ) + ..insertOrAdd( + 4, + ContextMenuButtonItem( + label: '表情', + onPressed: () { + state + ..hideToolbar() + ..clearSelectionIfNeeded(); + _showEmoteDialog(moduleDynamic); + }, + ), + ); + return AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: buttonItems, + anchors: state.contextMenuAnchors, + ); +} + +void _showEmoteDialog(ModuleDynamicModel? moduleDynamic) { + if (moduleDynamic == null) return; + final richTextNodes = + moduleDynamic.desc?.richTextNodes ?? + moduleDynamic.major?.opus?.summary?.richTextNodes; + if (richTextNodes == null || richTextNodes.isEmpty) return; + Map? emotes; + for (final e in richTextNodes) { + if (e.type == 'RICH_TEXT_NODE_TYPE_EMOJI') { + emotes ??= {}; + if (!emotes.containsKey(e.origText)) { + emotes[e.origText!] = e.emoji!; + } + } + } + if (emotes == null || emotes.isEmpty) return; + showDialog( + context: Get.context!, + builder: (context) => Dialog( + child: Padding( + padding: const .symmetric(horizontal: 20, vertical: 16), + child: SelectableText.rich( + TextSpan( + children: emotes!.entries.mapIndexed( + (i, e) { + final emoji = e.value; + final size = emoji.size * 25.0; + return TextSpan( + children: [ + if (i != 0) const TextSpan(text: '\n\n'), + WidgetSpan( + child: NetworkImgLayer( + src: emoji.url, + type: .emote, + width: size, + height: size, + ), + ), + TextSpan(text: '\n${e.key}\n${emoji.url}'), + ], + ); + }, + ).toList(), + ), + contextMenuBuilder: openUrlMenuBuilder, + style: const TextStyle(fontSize: 15, height: 1.7), + scrollPhysics: const ClampingScrollPhysicsExt(), + ), + ), + ), + ); +} + +void _showTextDialog(String text) { + showDialog( + context: Get.context!, + builder: (context) => Dialog( + child: Padding( + padding: const .symmetric(horizontal: 20, vertical: 16), + child: SelectableText( + text, + contextMenuBuilder: openUrlMenuBuilder, + style: const TextStyle(fontSize: 15, height: 1.7), + scrollPhysics: const ClampingScrollPhysicsExt(), + ), + ), + ), + ); +} + +Widget openUrlMenuBuilder(_, EditableTextState state) { + final buttonItems = state.contextMenuButtonItems; + state.addLaunchMenuIfNeeded(buttonItems, index: 3); + return AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: buttonItems, + anchors: state.contextMenuAnchors, + ); +} diff --git a/lib/common/widgets/context_menu/reply_menu_helper.dart b/lib/common/widgets/context_menu/reply_menu_helper.dart new file mode 100644 index 000000000..bf0b89e64 --- /dev/null +++ b/lib/common/widgets/context_menu/reply_menu_helper.dart @@ -0,0 +1,119 @@ +part of 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; + +void showReplyCopyDialog( + BuildContext context, + String message, + Map emotes, +) { + bool showEmote = false; + showDialog( + context: context, + builder: (context) => Dialog( + constraints: const BoxConstraints.tightFor(width: 380), + child: Padding( + padding: const .symmetric(horizontal: 20, vertical: 16), + child: SelectableText.rich( + scrollPhysics: const ClampingScrollPhysicsExt(), + showEmote + ? TextSpan( + children: emotes.entries.mapIndexed( + (i, e) { + final emote = e.value; + final size = emote.size.toInt() * 25.0; + return TextSpan( + children: [ + if (i != 0) const TextSpan(text: '\n\n'), + WidgetSpan( + child: NetworkImgLayer( + src: emote.url, + type: .emote, + width: size, + height: size, + ), + ), + TextSpan(text: '\n${e.key}\n${emote.url}'), + ], + ); + }, + ).toList(), + ) + : TextSpan(text: message), + contextMenuBuilder: (_, state) { + final buttonItems = state.contextMenuButtonItems; + final selection = state.textEditingValue.selection; + if (emotes.isNotEmpty) { + buttonItems.insertOrAdd( + 3, + ContextMenuButtonItem( + label: showEmote ? '文本' : '表情', + onPressed: () { + state + ..widget.focusNode.unfocus() + ..hideToolbar(); + showEmote = !showEmote; + (context as Element).markNeedsBuild(); + }, + ), + ); + if (showEmote) { + state.addLaunchMenuIfNeeded(buttonItems, index: 4); + } + } + if (!selection.isCollapsed) { + buttonItems.add( + ContextMenuButtonItem( + onPressed: () { + Navigator.of(context).pop(); + final value = state.textEditingValue; + String text = RegExp.escape( + selection.textInside(value.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: buttonItems, + anchors: state.contextMenuAnchors, + ); + }, + style: const TextStyle(fontSize: 15, height: 1.7), + ), + ), + ), + ); +} diff --git a/lib/common/widgets/flutter/text_field/editable_text.dart b/lib/common/widgets/flutter/text_field/editable_text.dart index 62aa509c1..531cce324 100644 --- a/lib/common/widgets/flutter/text_field/editable_text.dart +++ b/lib/common/widgets/flutter/text_field/editable_text.dart @@ -5111,9 +5111,11 @@ class EditableTextState extends State ) { // Compare the current TextEditingValue with the pre-format new // TextEditingValue value, in case the formatter would reject the change. - final shouldShowCaret = widget.readOnly - ? _value.selection != value.selection - : _value != value; + final shouldShowCaret = + cause != .drag && + (widget.readOnly + ? _value.selection != value.selection + : _value != value); if (shouldShowCaret) { scheduleShowCaretOnScreen(withAnimation: true); } diff --git a/lib/common/widgets/image/image_save.dart b/lib/common/widgets/image/image_save.dart index 3a232d45b..106e72163 100644 --- a/lib/common/widgets/image/image_save.dart +++ b/lib/common/widgets/image/image_save.dart @@ -1,6 +1,8 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; @@ -73,6 +75,7 @@ void imageSaveDialog({ child: SelectableText( title, style: theme.textTheme.titleSmall, + scrollPhysics: const NeverSelectableScrollPhysics(), ), ) else diff --git a/lib/common/widgets/loading_widget/http_error.dart b/lib/common/widgets/loading_widget/http_error.dart index 67be7fda8..dfcf62497 100644 --- a/lib/common/widgets/loading_widget/http_error.dart +++ b/lib/common/widgets/loading_widget/http_error.dart @@ -1,4 +1,6 @@ import 'package:PiliPlus/common/assets.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -35,7 +37,7 @@ class HttpError extends StatelessWidget { errMsg ?? '没有数据', textAlign: TextAlign.center, style: theme.textTheme.titleSmall, - scrollPhysics: const NeverScrollableScrollPhysics(), + scrollPhysics: const NeverSelectableScrollPhysics(), ), ), if (onReload != null) diff --git a/lib/common/widgets/scroll_physics.dart b/lib/common/widgets/scroll_physics.dart index e241fa8d1..26358a102 100644 --- a/lib/common/widgets/scroll_physics.dart +++ b/lib/common/widgets/scroll_physics.dart @@ -13,6 +13,8 @@ Widget tabBarView({ children: children, ); +final _springDescription = _customSpringDescription(); + SpringDescription _customSpringDescription() { final List springDescription = Pref.springDescription; return SpringDescription( @@ -34,8 +36,6 @@ class CustomTabBarViewScrollPhysics extends ScrollPhysics { return CustomTabBarViewScrollPhysics(parent: buildParent(ancestor)); } - static final _springDescription = _customSpringDescription(); - @override SpringDescription get spring => _springDescription; } diff --git a/lib/common/widgets/selectable_text.dart b/lib/common/widgets/selectable_text.dart deleted file mode 100644 index 235abda4f..000000000 --- a/lib/common/widgets/selectable_text.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart'; - -Widget selectableText( - String text, { - TextStyle? style, -}) { - if (PlatformUtils.isDesktop) { - return SelectionArea( - child: Text( - style: style, - text, - ), - ); - } - return SelectableText( - style: style, - text, - scrollPhysics: const NeverScrollableScrollPhysics(), - ); -} - -Widget selectableRichText( - TextSpan textSpan, { - TextStyle? style, -}) { - if (PlatformUtils.isDesktop) { - return SelectionArea( - child: Text.rich( - style: style, - textSpan, - ), - ); - } - return SelectableText.rich( - style: style, - textSpan, - scrollPhysics: const NeverScrollableScrollPhysics(), - ); -} diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index 87981a6e8..296444267 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -9,6 +9,8 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -939,7 +941,7 @@ class _AudioPageState extends State { SelectableText( audioItem.arc.title, style: const TextStyle(height: 1.7, fontSize: 16), - scrollPhysics: const NeverScrollableScrollPhysics(), + scrollPhysics: const NeverSelectableScrollPhysics(), ), const SizedBox(height: 12), if (audioItem.owner.hasName()) ...[ @@ -1004,7 +1006,7 @@ class _AudioPageState extends State { const SizedBox(height: 10), SelectableText( audioItem.arc.desc, - scrollPhysics: const NeverScrollableScrollPhysics(), + scrollPhysics: const NeverSelectableScrollPhysics(), ), ], ], diff --git a/lib/pages/dynamics/widgets/additional_panel.dart b/lib/pages/dynamics/widgets/additional_panel.dart index 711129483..6f6f7cd26 100644 --- a/lib/pages/dynamics/widgets/additional_panel.dart +++ b/lib/pages/dynamics/widgets/additional_panel.dart @@ -1,6 +1,8 @@ import 'package:PiliPlus/common/style.dart'; 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/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; @@ -698,6 +700,7 @@ additional panel error id: $idStr type: $type err: $e''', + scrollPhysics: const NeverSelectableScrollPhysics(), ), ); } diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index 2a7ea0547..7de980ff7 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -1,13 +1,21 @@ // 内容 import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/text/text.dart' as custom_text; +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/scroll_physics.dart' + show ClampingScrollPhysicsExt, NeverSelectableScrollPhysics; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart'; +import 'package:PiliPlus/utils/extension/editable_text_ext.dart'; +import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +part 'package:PiliPlus/common/widgets/context_menu/dyn_menu_helper.dart'; + Widget content( BuildContext context, { required int floor, @@ -77,7 +85,12 @@ Widget content( : const TextStyle(fontSize: 16), contextMenuBuilder: text == null || text.isEmpty ? null - : (_, state) => _contextMenuBuilder(state, text), + : (_, state) => dynTextMenuBuilder( + state, + text, + item.modules.moduleDynamic, + ), + scrollPhysics: const NeverSelectableScrollPhysics(), ) : custom_text.Text.rich( style: floor == 1 @@ -106,28 +119,3 @@ Widget content( ), ); } - -Widget _contextMenuBuilder(EditableTextState state, String text) { - return AdaptiveTextSelectionToolbar.buttonItems( - buttonItems: state.contextMenuButtonItems - ..add( - ContextMenuButtonItem(label: '文本', onPressed: () => _onCopyText(text)), - ), - anchors: state.contextMenuAnchors, - ); -} - -void _onCopyText(String text) { - showDialog( - context: Get.context!, - builder: (context) => Dialog( - child: Padding( - padding: const .symmetric(horizontal: 20, vertical: 16), - child: SelectableText( - text, - style: const TextStyle(fontSize: 15, height: 1.7), - ), - ), - ), - ); -} diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart index c79f7ce79..d74acaf6e 100644 --- a/lib/pages/dynamics/widgets/rich_node_panel.dart +++ b/lib/pages/dynamics/widgets/rich_node_panel.dart @@ -27,7 +27,7 @@ TextSpan? richNode( }) { try { late final style = TextStyle(color: theme.colorScheme.primary); - List spanChildren = []; + final List spanChildren = []; final moduleDynamic = item.modules.moduleDynamic; List? richTextNodes; diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index 36449ecbb..326b7853f 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -5,6 +5,8 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -261,6 +263,7 @@ class _DynTopicPageState extends State SelectableText( response.topicItem!.description!, style: TextStyle(color: colorScheme.onSurfaceVariant), + scrollPhysics: const NeverSelectableScrollPhysics(), ), const SizedBox(height: 10), Row( diff --git a/lib/pages/live_room/superchat/superchat_card.dart b/lib/pages/live_room/superchat/superchat_card.dart index b2e89d9ba..b3f8afdac 100644 --- a/lib/pages/live_room/superchat/superchat_card.dart +++ b/lib/pages/live_room/superchat/superchat_card.dart @@ -1,7 +1,8 @@ -import 'dart:async'; +import 'dart:async' show Timer; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; import 'package:PiliPlus/pages/member/widget/medal_widget.dart'; import 'package:PiliPlus/utils/color_utils.dart'; @@ -196,7 +197,7 @@ class _SuperChatCardState extends State { src: item.userInfo.face, width: 45, height: 45, - type: ImageType.avatar, + type: .avatar, ), Expanded( child: Column( @@ -234,19 +235,18 @@ class _SuperChatCardState extends State { borderRadius: const .vertical(bottom: .circular(8)), color: bottomColor, ), - padding: const EdgeInsets.all(8), - child: SelectionArea( - child: Text( - item.message, - style: TextStyle( - color: ColourUtils.parseColor(item.messageFontColor), - decoration: widget.persistentSC && item.deleted - ? .lineThrough - : null, - decorationThickness: 1.5, - decorationStyle: .double, - decorationColor: Colors.white, - ), + padding: const .all(8), + child: SelectableText( + item.message, + scrollPhysics: const NeverSelectableScrollPhysics(), + style: TextStyle( + color: ColourUtils.parseColor(item.messageFontColor), + decoration: widget.persistentSC && item.deleted + ? .lineThrough + : null, + decorationThickness: 1.5, + decorationStyle: .double, + decorationColor: Colors.white, ), ), ), diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 477793eb6..710b668f0 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -314,6 +314,7 @@ class UserInfoCard extends StatelessWidget { child: SelectableText( card.sign!.trim().replaceAll(RegExp(r'\n{2,}'), '\n'), style: const TextStyle(fontSize: 14), + scrollPhysics: const NeverSelectableScrollPhysics(), ), ); } diff --git a/lib/pages/music/view.dart b/lib/pages/music/view.dart index b2e204fb6..e42abf707 100644 --- a/lib/pages/music/view.dart +++ b/lib/pages/music/view.dart @@ -7,6 +7,8 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/music.dart'; @@ -561,6 +563,7 @@ class _MusicDetailPageState extends CommonDynPageState { if (!item.album.isNullOrEmpty) '专辑:${item.album}', if (!item.musicSource.isNullOrEmpty) '出处:${item.musicSource}', ].join('\n'), + scrollPhysics: const NeverSelectableScrollPhysics(), ), const Divider(), Row( diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index 36363ff86..6fad23ef9 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -6,6 +6,8 @@ import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -290,6 +292,7 @@ class _PgcReviewChildPageState extends State SelectableText( item.content!, style: const TextStyle(height: 1.75), + scrollPhysics: const NeverSelectableScrollPhysics(), ), Builder( builder: (context) { diff --git a/lib/pages/setting/pages/logs.dart b/lib/pages/setting/pages/logs.dart index 101ac3bad..b27744716 100644 --- a/lib/pages/setting/pages/logs.dart +++ b/lib/pages/setting/pages/logs.dart @@ -1,9 +1,11 @@ -import 'dart:async'; -import 'dart:convert'; +import 'dart:async' show Timer; +import 'dart:convert' show jsonDecode; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/services/logger.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -368,6 +370,7 @@ class _ReportCard extends StatelessWidget { fontFamily: 'Monospace', color: colorScheme.onSurfaceVariant, ), + scrollPhysics: const NeverSelectableScrollPhysics(), ), ), // stackTrace may be null or String("null") or blank @@ -406,6 +409,7 @@ class _ReportCard extends StatelessWidget { .toList(), ), style: const TextStyle(fontFamily: 'Monospace', fontSize: 13), + scrollPhysics: const NeverSelectableScrollPhysics(), ), ), ], diff --git a/lib/pages/video/ai_conclusion/view.dart b/lib/pages/video/ai_conclusion/view.dart index d91ce23b9..f60afd189 100644 --- a/lib/pages/video/ai_conclusion/view.dart +++ b/lib/pages/video/ai_conclusion/view.dart @@ -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(), ), ), ), diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index a3beecb1f..37bb04d29 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -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 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 style: theme.textTheme.titleMedium, ), const SizedBox(height: 4), - selectableText( + SelectableText( widget.item.evaluate!, style: textStyle, + scrollPhysics: const NeverSelectableScrollPhysics(), ), ], if (widget.item.actors?.isNotEmpty == true) ...[ diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index bd9c7e53b..d3ed3f4ef 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -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 { ), ), 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 { ); } - Widget buildTitle( + Widget _buildTitle( bool isLoading, bool isHorizontal, VideoDetailData videoDetail, @@ -250,29 +249,29 @@ class _UgcIntroPanelState extends State { 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 _infos(VideoDetailData videoDetail) => [ const SizedBox(height: 8, width: .infinity), @@ -285,9 +284,10 @@ class _UgcIntroPanelState extends State { ), 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 { 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 { caseSensitive: false, ); - TextSpan buildContent(List descV2) { + TextSpan buildDesc(List descV2) { + // type // 1 普通文本 // 2 @用户 final List spanChildren = descV2.map((currentDesc) { diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart index 1d5d332bd..8db5fa1a0 100644 --- a/lib/pages/video/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart @@ -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, - ); - } } diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 59fec3d20..33665db7d 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -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'; diff --git a/lib/pages/webview/view.dart b/lib/pages/webview/view.dart index 2e64eaa7b..12b0ddc56 100644 --- a/lib/pages/webview/view.dart +++ b/lib/pages/webview/view.dart @@ -1,5 +1,7 @@ -import 'dart:io'; +import 'dart:io' show Platform; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/webview_menu_type.dart'; @@ -268,7 +270,10 @@ class _WebviewPageState extends State { '下载文件: $suggestedFilename ?', style: const TextStyle(fontSize: 18), ), - content: SelectableText(request.url.toString()), + content: SelectableText( + request.url.toString(), + scrollPhysics: const NeverSelectableScrollPhysics(), + ), actions: [ TextButton( onPressed: Get.back, diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 70a070d81..432bd32b4 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -7,6 +7,8 @@ import 'package:PiliPlus/common/widgets/badge.dart'; 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_viewer/hero.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/grpc/bilibili/im/interfaces/v1.pb.dart' show EmotionInfo; import 'package:PiliPlus/grpc/bilibili/im/type.pb.dart' show Msg, MsgType; @@ -717,7 +719,10 @@ class ChatItem extends StatelessWidget { return ''; }, ); - return SelectableText.rich(TextSpan(children: children)); + return SelectableText.rich( + TextSpan(children: children), + scrollPhysics: const NeverSelectableScrollPhysics(), + ); } Widget msgTypeNotifyMsg_10(ThemeData theme, content) { @@ -759,7 +764,10 @@ class ChatItem extends StatelessWidget { ), Divider(color: theme.colorScheme.primary.withValues(alpha: 0.05)), if ((content['text'] as String?)?.isNotEmpty == true) - SelectableText(content['text']), + SelectableText( + content['text'], + scrollPhysics: const NeverSelectableScrollPhysics(), + ), if (modules != null && modules.isNotEmpty) ...[ const SizedBox(height: 4), ...modules.map( diff --git a/lib/scripts/editable_text.patch b/lib/scripts/editable_text.patch new file mode 100644 index 000000000..179925de4 --- /dev/null +++ b/lib/scripts/editable_text.patch @@ -0,0 +1,45 @@ +diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart +index b1ae878f2cc..8a7540350ef 100644 +--- a/packages/flutter/lib/src/widgets/editable_text.dart ++++ b/packages/flutter/lib/src/widgets/editable_text.dart +@@ -5046,7 +5046,9 @@ class EditableTextState extends State + void userUpdateTextEditingValue(TextEditingValue value, SelectionChangedCause? cause) { + // Compare the current TextEditingValue with the pre-format new + // TextEditingValue value, in case the formatter would reject the change. +- final shouldShowCaret = widget.readOnly ? _value.selection != value.selection : _value != value; ++ final shouldShowCaret = ++ cause != SelectionChangedCause.drag && ++ (widget.readOnly ? _value.selection != value.selection : _value != value); + if (shouldShowCaret) { + _scheduleShowCaretOnScreen(withAnimation: true); + } +@@ -5709,7 +5711,7 @@ class EditableTextState extends State + PasteTextIntent: _makeOverridable(_PasteSelectionAction(this)), + + TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction), +- EditableTextTapOutsideIntent: _makeOverridable(_EditableTextTapOutsideAction()), ++ EditableTextTapOutsideIntent: _makeOverridable(_EditableTextTapOutsideAction(this)), + EditableTextTapUpOutsideIntent: _makeOverridable(_EditableTextTapUpOutsideAction()), + }; + +@@ -6775,10 +6777,19 @@ class _WebClipboardStatusNotifier extends ClipboardStatusNotifier { + } + + class _EditableTextTapOutsideAction extends ContextAction { +- _EditableTextTapOutsideAction(); ++ _EditableTextTapOutsideAction(this.state); ++ ++ final EditableTextState state; + + @override + void invoke(EditableTextTapOutsideIntent intent, [BuildContext? context]) { ++ state.hideToolbar(); ++ final controller = state.widget.controller; ++ final selection = controller.value.selection; ++ if (!selection.isCollapsed) { ++ controller.selection = TextSelection.collapsed(offset: selection.end); ++ } ++ return; + // The focus dropping behavior is only present on desktop platforms. + switch (defaultTargetPlatform) { + case TargetPlatform.android: diff --git a/lib/scripts/patch.ps1 b/lib/scripts/patch.ps1 index 8a00e90aa..81a1088c5 100644 --- a/lib/scripts/patch.ps1 +++ b/lib/scripts/patch.ps1 @@ -35,6 +35,8 @@ $FABPatch = "lib/scripts/fab.patch" $SelectableRegionSelectionPatch = "lib/scripts/selectable_region.patch" +$EditableTextPatch = "lib/scripts/editable_text.patch" + # TODO: remove # https://github.com/flutter/flutter/pull/183261 $SelectableRegionPatch = "lib/scripts/null_safety_for_selectable_region.patch" @@ -66,7 +68,8 @@ $picks = @() $reverts = @() $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch, $ImageAnimPatch, $LayoutBuilderPatch, $NavigationDrawerPatch, - $PopupMenuPatch, $FABPatch, $SelectableRegionPatch, $SelectableRegionSelectionPatch) + $PopupMenuPatch, $FABPatch, $SelectableRegionPatch, $SelectableRegionSelectionPatch, + $EditableTextPatch) switch ($platform.ToLower()) { "android" { diff --git a/lib/scripts/selectable_region.patch b/lib/scripts/selectable_region.patch index 1204ae150..6653f48bf 100644 --- a/lib/scripts/selectable_region.patch +++ b/lib/scripts/selectable_region.patch @@ -1,8 +1,17 @@ diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart -index 59de8bae20b..07ac6981c78 100644 +index 59de8bae20b..a1b6ea5651e 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart -@@ -2822,7 +2822,10 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai +@@ -1957,6 +1957,8 @@ class SelectableRegionState extends State + // the region on non-web platforms. + if (kIsWeb) { + _focusNode.unfocus(); ++ } else { ++ clearSelection(); + } + }, + child: CompositedTransformTarget( +@@ -2822,7 +2824,10 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai return null; } final buffer = StringBuffer(); diff --git a/lib/utils/extension/editable_text_ext.dart b/lib/utils/extension/editable_text_ext.dart new file mode 100644 index 000000000..69b44c953 --- /dev/null +++ b/lib/utils/extension/editable_text_ext.dart @@ -0,0 +1,38 @@ +import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:flutter/material.dart'; + +extension EditableTextExt on EditableTextState { + void addLaunchMenuIfNeeded( + List buttonItems, { + required int index, + }) { + final selection = textEditingValue.selection; + if (!selection.isCollapsed) { + buttonItems.insertOrAdd( + index, + ContextMenuButtonItem( + label: '打开', + onPressed: () { + hideToolbar(); + clearSelection(selection); + PageUtils.launchURL( + selection.textInside(textEditingValue.text).trim(), + ); + }, + ), + ); + } + } + + void clearSelection(TextSelection currSelection) { + widget.controller.selection = .collapsed(offset: currSelection.end); + } + + void clearSelectionIfNeeded() { + final selection = textEditingValue.selection; + if (!selection.isCollapsed) { + clearSelection(selection); + } + } +} diff --git a/lib/utils/extension/iterable_ext.dart b/lib/utils/extension/iterable_ext.dart index 036fa1ea1..ad0804510 100644 --- a/lib/utils/extension/iterable_ext.dart +++ b/lib/utils/extension/iterable_ext.dart @@ -68,4 +68,12 @@ extension ListExt on List { T getOrFirst(int index) { return getOrNull(index) ?? first; } + + void insertOrAdd(int index, T element) { + if (length <= index) { + add(element); + } else { + insert(index, element); + } + } } diff --git a/lib/utils/reply_utils.dart b/lib/utils/reply_utils.dart index 25962ac56..94448656c 100644 --- a/lib/utils/reply_utils.dart +++ b/lib/utils/reply_utils.dart @@ -1,5 +1,7 @@ import 'dart:io' show Platform; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; @@ -137,7 +139,10 @@ abstract final class ReplyUtils { barrierDismissible: isManual, builder: (context) => AlertDialog( title: const Text('评论检查结果'), - content: SelectableText(message), + content: SelectableText( + message, + scrollPhysics: const NeverSelectableScrollPhysics(), + ), actions: actions.isEmpty ? null : actions, ), ); diff --git a/lib/utils/request_utils.dart b/lib/utils/request_utils.dart index 36a3dc3cd..18d643d47 100644 --- a/lib/utils/request_utils.dart +++ b/lib/utils/request_utils.dart @@ -1,9 +1,10 @@ -import 'dart:async'; -import 'dart:convert'; +import 'dart:convert' show jsonEncode; import 'dart:math'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show NeverSelectableScrollPhysics; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -363,6 +364,7 @@ abstract final class RequestUtils { title: const Text('动态检查结果'), content: SelectableText( '${isSuccess ? '无账号状态下找到了你的动态,动态正常!' : '你的动态被shadow ban(仅自己可见)!'}${dynText != null ? ' \n\n动态内容: $dynText' : ''}', + scrollPhysics: const NeverSelectableScrollPhysics(), ), actions: actions.isEmpty ? null : actions, ), @@ -580,6 +582,7 @@ abstract final class RequestUtils { builder: (context) => AlertDialog( title: SelectableText( show ? response.name! : response.rejectPage?.title ?? '', + scrollPhysics: const NeverSelectableScrollPhysics(), ), content: show ? null : Text(response.rejectPage?.text ?? ''), actions: [