Add Search shortcut (CTRL + F) (#554)(#507)(#110)

This commit is contained in:
wanghongenpin
2025-08-25 05:41:25 +08:00
parent bde6c4e0ce
commit 4711767e0c
5 changed files with 85 additions and 54 deletions

View File

@@ -48,7 +48,7 @@ class _JsonTextState extends State<JsonText> {
SearchTextController? searchController;
@override
initState() {
void initState() {
super.initState();
searchController = widget.searchController;
}
@@ -57,7 +57,6 @@ class _JsonTextState extends State<JsonText> {
void dispose() {
trackingScrollController?.dispose();
trackingScrollController = null;
logger.d('JsonText dispose');
super.dispose();
}
@@ -74,13 +73,6 @@ class _JsonTextState extends State<JsonText> {
);
}
double getAvailableHeight(BuildContext context) {
// 获取当前组件可用高度屏幕高度减去系统padding和AppBar高度等
final mediaQuery = MediaQuery.of(context);
final appBar = Scaffold.of(context).appBarMaxHeight ?? 0;
return mediaQuery.size.height - mediaQuery.padding.top - appBar;
}
Widget jsonTextWidget(BuildContext context) {
var jsonParser = JsonParser(widget.json, widget.colorTheme, widget.indent, searchController);
var textList = jsonParser.getJsonTree();
@@ -109,8 +101,6 @@ class _JsonTextState extends State<JsonText> {
final key = jsonParser.matchKeys[currentIndex];
final context = key.currentContext;
if (context != null) {
logger.d('scrollToMatch: currentIndex=$currentIndex, key=$key');
Scrollable.ensureVisible(
context,
duration: const Duration(milliseconds: 300),

View File

@@ -30,7 +30,7 @@ class HighlightTextWidget extends StatelessWidget {
);
}
List<TextSpan> _highlightMatches(BuildContext context) {
List<InlineSpan> _highlightMatches(BuildContext context) {
if (!searchController.shouldSearch()) {
return [TextSpan(text: text)];
}
@@ -43,24 +43,27 @@ class HighlightTextWidget extends StatelessWidget {
caseSensitive: searchController.value.isCaseSensitive,
);
final spans = <TextSpan>[];
final spans = <InlineSpan>[];
int start = 0;
var allMatches = regex.allMatches(text).toList();
final currentIndex = searchController.currentMatchIndex.value;
ColorScheme colorScheme = ColorScheme.of(context);
List<int> matchOffsets = [];
List<GlobalKey> matchKeys = [];
for (int i = 0; i < allMatches.length; i++) {
final match = allMatches[i];
if (match.start > start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
matchOffsets.add(match.start);
spans.add(TextSpan(
text: text.substring(match.start, match.end),
style: TextStyle(
backgroundColor: i == currentIndex ? colorScheme.primary : colorScheme.inversePrimary,
),
));
// 为每个高亮项分配一个 GlobalKey
final key = GlobalKey();
matchKeys.add(key);
spans.add(WidgetSpan(
child: Container(
key: key,
color: i == currentIndex ? colorScheme.primary : colorScheme.inversePrimary,
child: Text(text.substring(match.start, match.end)),
)));
start = match.end;
}
if (start < text.length) {
@@ -69,25 +72,28 @@ class HighlightTextWidget extends StatelessWidget {
WidgetsBinding.instance.addPostFrameCallback((_) {
searchController.updateMatchCount(allMatches.length);
if (scrollController != null && allMatches.isNotEmpty && currentIndex < matchOffsets.length) {
_scrollToMatch(context, matchOffsets[currentIndex]);
}
_scrollToMatch(context, matchKeys);
matchKeys.clear();
});
return spans;
}
void _scrollToMatch(BuildContext context, int charOffset) {
if (scrollController == null) return;
final textStyle = DefaultTextStyle.of(context).style;
final span = TextSpan(text: text.substring(0, charOffset), style: textStyle);
final tp = TextPainter(
text: span,
textDirection: TextDirection.ltr,
maxLines: null,
);
tp.layout(maxWidth: scrollController!.position.viewportDimension);
final offset = tp.height;
scrollController!.animateTo(offset, duration: const Duration(milliseconds: 300), curve: Curves.ease);
void _scrollToMatch(BuildContext context, List<GlobalKey> matchKeys) {
if (matchKeys.isNotEmpty) {
final currentIndex = searchController.currentMatchIndex.value;
if (currentIndex >= 0 && currentIndex < matchKeys.length) {
final key = matchKeys[currentIndex];
final context = key.currentContext;
if (context != null) {
Scrollable.ensureVisible(
context,
duration: const Duration(milliseconds: 300),
alignment: 0.5, // 高亮项在视图中的位置
);
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:proxypin/network/util/logger.dart';
import 'package:proxypin/ui/component/search/search_field.dart';
class SearchTextController extends ValueNotifier<SearchSettings> {
@@ -76,10 +77,11 @@ class SearchTextController extends ValueNotifier<SearchSettings> {
@override
void dispose() {
logger.d('Disposing SearchTextController');
removeSearchOverlay();
patternController.dispose();
totalMatchCount.close();
currentMatchIndex.close();
removeSearchOverlay();
super.dispose();
}
@@ -100,8 +102,8 @@ class SearchTextController extends ValueNotifier<SearchSettings> {
}
OverlayEntry _buildSearchOverlay(BuildContext context, {double? top, double? right}) {
overlayTop ??= top;
overlayRight ??= right;
overlayTop = top ?? overlayTop;
overlayRight = right ?? overlayRight;
return OverlayEntry(
builder: (context) {
return Positioned(

View File

@@ -70,6 +70,7 @@ class HttpBodyWidget extends StatefulWidget {
class HttpBodyState extends State<HttpBodyWidget> {
var bodyKey = GlobalKey<_BodyState>();
int tabIndex = 0;
final searchIconKey = GlobalKey();
final SearchTextController searchController = SearchTextController();
AppLocalizations get localizations => AppLocalizations.of(context)!;
@@ -142,12 +143,39 @@ class HttpBodyState extends State<HttpBodyWidget> {
searchController: searchController)) //body
];
var tabController = DefaultTabController(
initialIndex: tabIndex,
length: tabs.list.length,
child: widget.inNewWindow
? ListView(children: list)
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: list));
var tabController = FocusableActionDetector(
shortcuts: {
LogicalKeySet(
Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):
ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.escape): DismissIntent(),
},
actions: {
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (intent) {
if (searchController.isSearchOverlayVisible) {
hideSearchOverlay();
} else {
RenderBox renderBox = searchIconKey.currentContext?.findRenderObject() as RenderBox;
Offset position = renderBox.localToGlobal(Offset.zero); // 获取搜索图标的位置
searchController.showSearchOverlay(context, top: position.dy + renderBox.size.height + 50, right: 10);
}
return null;
},
),
DismissIntent: CallbackAction<DismissIntent>(
onInvoke: (intent) {
hideSearchOverlay();
return null;
},
),
},
child: DefaultTabController(
initialIndex: tabIndex,
length: tabs.list.length,
child: widget.inNewWindow
? ListView(children: list)
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: list)));
//在新窗口打开
if (widget.inNewWindow) {
@@ -180,13 +208,16 @@ class HttpBodyState extends State<HttpBodyWidget> {
Text('$type Body', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(width: 18),
InkWell(
key: searchIconKey,
child: Icon(Icons.search, size: 20),
// tooltip: localizations.search,
onTapDown: (TapDownDetails details) {
onTap: () {
if (searchController.isSearchOverlayVisible) {
searchController.removeSearchOverlay();
} else {
searchController.showSearchOverlay(context, top: details.globalPosition.dy + 50, right: 10);
RenderBox renderBox = searchIconKey.currentContext?.findRenderObject() as RenderBox;
Offset position = renderBox.localToGlobal(Offset.zero); // 获取搜索图标的位置
searchController.showSearchOverlay(context, top: position.dy + renderBox.size.height + 50, right: 10);
}
},
),

View File

@@ -232,10 +232,10 @@ class NetworkTabState extends State<NetworkTabController> with SingleTickerProvi
path = Uri.decodeFull(path);
} catch (_) {}
return ListView(
return SingleChildScrollView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: [RowWidget("Path", path), ...message(widget.request.get(), "Request", scrollController)]);
child:
Column(children: [RowWidget("Path", path), ...message(widget.request.get(), "Request", scrollController)]));
}
Widget response() {
@@ -244,10 +244,12 @@ class NetworkTabState extends State<NetworkTabController> with SingleTickerProvi
}
var scrollController = ScrollController();
return ListView(controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), children: [
RowWidget("StatusCode", widget.response.get()?.status.toString()),
...message(widget.response.get(), "Response", scrollController)
]);
return SingleChildScrollView(
controller: scrollController,
child: Column(children: [
RowWidget("StatusCode", widget.response.get()?.status.toString()),
...message(widget.response.get(), "Response", scrollController)
]));
}
List<Widget> message(HttpMessage? message, String type, ScrollController scrollController) {