opt text selection

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-25 13:00:02 +08:00
parent 56ca0cade4
commit 10b723fa64
11 changed files with 648 additions and 390 deletions

View File

@@ -54,33 +54,34 @@ void _showEmoteDialog(ModuleDynamicModel? moduleDynamic) {
builder: (context) => Dialog(
child: Padding(
padding: const .symmetric(horizontal: 20, vertical: 16),
child: SingleChildScrollView(
child: SelectionText.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'),
EmoteSpan(
rawText: Style.placeHolder,
child: NetworkImgLayer(
src: emoji.url,
type: .emote,
width: size,
height: size,
child: SelectionArea(
contextMenuBuilder: openUrlMenuBuilder,
child: SingleChildScrollView(
child: Text.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(),
TextSpan(text: '\n${e.key}\n${emoji.url}'),
],
);
},
).toList(),
),
style: const TextStyle(fontSize: 15, height: 1.7),
),
contextMenuBuilder: openUrlMenuBuilder,
style: const TextStyle(fontSize: 15, height: 1.7),
),
),
),
@@ -94,11 +95,13 @@ void _showTextDialog(String text) {
builder: (context) => Dialog(
child: Padding(
padding: const .symmetric(horizontal: 20, vertical: 16),
child: SingleChildScrollView(
child: SelectionText(
text,
contextMenuBuilder: openUrlMenuBuilder,
style: const TextStyle(fontSize: 15, height: 1.7),
child: SelectionArea(
contextMenuBuilder: openUrlMenuBuilder,
child: SingleChildScrollView(
child: Text(
text,
style: const TextStyle(fontSize: 15, height: 1.7),
),
),
),
),

View File

@@ -12,98 +12,99 @@ void showReplyCopyDialog(
constraints: const BoxConstraints.tightFor(width: 380),
child: Padding(
padding: const .symmetric(horizontal: 20, vertical: 16),
child: SingleChildScrollView(
child: SelectionText.rich(
showEmote
? TextSpan(
children: emotes.entries.mapIndexed(
(i, e) {
final emote = e.value;
final size = emote.size.toInt() * 25.0;
return TextSpan(
child: SelectionArea(
contextMenuBuilder: (_, state) {
final buttonItems = state.contextMenuButtonItems;
if (emotes.isNotEmpty) {
buttonItems.insertOrAdd(
3,
ContextMenuButtonItem(
label: showEmote ? '文本' : '表情',
onPressed: () {
state.hideAndClear();
showEmote = !showEmote;
(context as Element).markNeedsBuild();
},
),
);
state.addLaunchMenuIfNeeded(buttonItems, index: 4);
}
if (state.isUncollapsed) {
buttonItems.add(
ContextMenuButtonItem(
onPressed: () {
String text = RegExp.escape(state.selectedText!);
if (ReplyGrpc.enableFilter) text = '|$text';
showConfirmDialog(
context: context,
title: const Text('是否确认评论过滤的变更:'),
content: Text.rich(
TextSpan(
text: ReplyGrpc.replyRegExp.pattern,
children: [
if (i != 0) const TextSpan(text: '\n\n'),
EmoteSpan(
rawText: Style.placeHolder,
child: NetworkImgLayer(
src: emote.url,
type: .emote,
width: size,
height: size,
TextSpan(
text: text,
style: const TextStyle(
color: Colors.green,
fontWeight: .bold,
),
),
TextSpan(text: '\n${e.key}\n${emote.url}'),
],
),
),
onConfirm: () {
final filter = ReplyGrpc.replyRegExp.pattern + text;
ReplyGrpc.replyRegExp = RegExp(
filter,
caseSensitive: true,
);
ReplyGrpc.enableFilter = true;
GStorage.setting.put(
SettingBoxKey.banWordForReply,
filter,
);
SmartDialog.showToast('已保存');
},
).toList(),
)
: TextSpan(text: message),
contextMenuBuilder: (_, state) {
final buttonItems = state.contextMenuButtonItems;
if (emotes.isNotEmpty) {
buttonItems.insertOrAdd(
3,
ContextMenuButtonItem(
label: showEmote ? '文本' : '表情',
onPressed: () {
state.hideAndClear();
showEmote = !showEmote;
(context as Element).markNeedsBuild();
},
),
);
state.addLaunchMenuIfNeeded(buttonItems, index: 4);
}
if (state.isUncollapsed) {
buttonItems.add(
ContextMenuButtonItem(
onPressed: () {
String text = RegExp.escape(state.selectedText!);
if (ReplyGrpc.enableFilter) text = '|$text';
showConfirmDialog(
context: context,
title: const Text('是否确认评论过滤的变更:'),
content: Text.rich(
TextSpan(
text: ReplyGrpc.replyRegExp.pattern,
);
},
label: '加入过滤',
),
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: buttonItems,
anchors: state.contextMenuAnchors,
);
},
child: SingleChildScrollView(
child: Text.rich(
showEmote
? TextSpan(
children: emotes.entries.mapIndexed(
(i, e) {
final emote = e.value;
final size = emote.size.toInt() * 25.0;
return TextSpan(
children: [
TextSpan(
text: text,
style: const TextStyle(
color: Colors.green,
fontWeight: .bold,
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}'),
],
),
),
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),
).toList(),
)
: TextSpan(text: message),
style: const TextStyle(fontSize: 15, height: 1.7),
),
),
),
),

View File

@@ -0,0 +1,46 @@
import 'package:flutter/rendering.dart' show BoxHitTestResult, HitTestEntry;
import 'package:flutter/widgets.dart';
class SelectionTapRegionSurface extends TapRegionSurface {
const SelectionTapRegionSurface({
super.key,
required super.child,
required this.isScrolling,
});
final ValueGetter<bool> isScrolling;
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSelectionTapRegionSurface(isScrolling);
}
@override
void updateRenderObject(
BuildContext context,
RenderSelectionTapRegionSurface renderObject,
) {
renderObject.isScrolling = isScrolling;
}
}
class RenderSelectionTapRegionSurface extends RenderTapRegionSurface {
RenderSelectionTapRegionSurface(this.isScrolling);
ValueGetter<bool> isScrolling;
bool _isScrolling = false;
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
_isScrolling = isScrolling();
return super.hitTest(result, position: position);
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (_isScrolling) {
return;
}
super.handleEvent(event, entry);
}
}