replace SelectableText with SelectionArea

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-20 13:38:56 +08:00
parent 4ce32b8698
commit 0c73e01013
26 changed files with 406 additions and 300 deletions

View File

@@ -1,7 +1,7 @@
part of 'package:PiliPlus/pages/dynamics/widgets/content_panel.dart';
Widget dynTextMenuBuilder(
EditableTextState state,
SelectableRegionState state,
String text,
ModuleDynamicModel? moduleDynamic,
) {
@@ -23,7 +23,7 @@ Widget dynTextMenuBuilder(
onPressed: () {
state
..hideToolbar()
..clearSelectionIfNeeded();
..clearSelection();
_showEmoteDialog(moduleDynamic);
},
),
@@ -55,32 +55,33 @@ void _showEmoteDialog(ModuleDynamicModel? moduleDynamic) {
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,
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'),
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(),
),
contextMenuBuilder: openUrlMenuBuilder,
style: const TextStyle(fontSize: 15, height: 1.7),
),
contextMenuBuilder: openUrlMenuBuilder,
style: const TextStyle(fontSize: 15, height: 1.7),
scrollPhysics: const ClampingScrollPhysicsExt(),
),
),
),
@@ -93,18 +94,19 @@ void _showTextDialog(String text) {
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(),
child: SingleChildScrollView(
child: SelectionText(
text,
contextMenuBuilder: openUrlMenuBuilder,
style: const TextStyle(fontSize: 15, height: 1.7),
),
),
),
),
);
}
Widget openUrlMenuBuilder(_, EditableTextState state) {
Widget openUrlMenuBuilder(_, SelectableRegionState state) {
final buttonItems = state.contextMenuButtonItems;
state.addLaunchMenuIfNeeded(buttonItems, index: 3);
return AdaptiveTextSelectionToolbar.buttonItems(

View File

@@ -12,106 +12,108 @@ void showReplyCopyDialog(
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,
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(
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: '加入过滤',
),
).toList(),
)
: TextSpan(text: message),
contextMenuBuilder: (_, state) {
final buttonItems = state.contextMenuButtonItems;
if (emotes.isNotEmpty) {
buttonItems.insertOrAdd(
3,
ContextMenuButtonItem(
label: showEmote ? '文本' : '表情',
onPressed: () {
state
..hideToolbar()
..clearSelection();
showEmote = !showEmote;
(context as Element).markNeedsBuild();
},
),
);
if (showEmote) {
state.addLaunchMenuIfNeeded(buttonItems, index: 4);
}
}
try {
if (state.selectionEndpoints.first !=
state.selectionEndpoints[1]) {
buttonItems.add(
ContextMenuButtonItem(
onPressed: () {
final selectedText = (state as dynamic).selectable
?.getSelectedContent()
?.plainText;
String text = RegExp.escape(selectedText);
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: '加入过滤',
),
);
}
} catch (_) {}
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: buttonItems,
anchors: state.contextMenuAnchors,
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: buttonItems,
anchors: state.contextMenuAnchors,
);
},
style: const TextStyle(fontSize: 15, height: 1.7),
},
style: const TextStyle(fontSize: 15, height: 1.7),
),
),
),
),

View File

@@ -1,8 +1,7 @@
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/common/widgets/selection_text.dart';
import 'package:PiliPlus/http/user.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
@@ -72,10 +71,9 @@ void imageSaveDialog({
children: [
if (title != null)
Expanded(
child: SelectableText(
child: SelectionText(
title,
style: theme.textTheme.titleSmall,
scrollPhysics: const NeverSelectableScrollPhysics(),
),
)
else

View File

@@ -1,6 +1,5 @@
import 'package:PiliPlus/common/assets.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'
show NeverSelectableScrollPhysics;
import 'package:PiliPlus/common/widgets/selection_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -24,20 +23,19 @@ class HttpError extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final child = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: .min,
mainAxisAlignment: .center,
crossAxisAlignment: .center,
children: [
const SizedBox(height: 40),
SvgPicture.asset(Assets.error, height: 200),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5),
child: SelectableText(
padding: const .symmetric(horizontal: 16, vertical: 5),
child: SelectionText(
errMsg ?? '没有数据',
textAlign: TextAlign.center,
textAlign: .center,
style: theme.textTheme.titleSmall,
scrollPhysics: const NeverSelectableScrollPhysics(),
),
),
if (onReload != null)

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
class SelectionText extends StatelessWidget {
const SelectionText(
String this.data, {
super.key,
this.style,
this.textAlign,
this.contextMenuBuilder = _defaultContextMenuBuilder,
}) : textSpan = null;
const SelectionText.rich(
InlineSpan this.textSpan, {
super.key,
this.style,
this.textAlign,
this.contextMenuBuilder = _defaultContextMenuBuilder,
}) : data = null;
final String? data;
final InlineSpan? textSpan;
final TextStyle? style;
final TextAlign? textAlign;
final SelectableRegionContextMenuBuilder? contextMenuBuilder;
static Widget _defaultContextMenuBuilder(
BuildContext context,
SelectableRegionState selectableRegionState,
) {
return AdaptiveTextSelectionToolbar.selectableRegion(
selectableRegionState: selectableRegionState,
);
}
@override
Widget build(BuildContext context) {
return SelectionArea(
contextMenuBuilder: contextMenuBuilder,
child: Text.rich(
style: style,
textAlign: textAlign,
TextSpan(
text: data,
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
),
),
);
}
}