mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-05-20 16:15:47 +08:00
@@ -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),
|
||||
|
||||
@@ -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, // 高亮项在视图中的位置
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user