mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-15 04:23:17 +08:00
@@ -30,8 +30,9 @@ If ProxyPin is helpful to you, you are welcome to support us in the following wa
|
||||
|
||||
**Your support will be used for project maintenance, feature development, and user experience optimization. Thank you very much!**
|
||||
|
||||
## Downloads
|
||||
|
||||
Downloads: https://github.com/wanghongenpin/proxypin/releases
|
||||
Github Releases: https://github.com/wanghongenpin/proxypin/releases
|
||||
|
||||
iOS App Store:https://apps.apple.com/app/proxypin/id6450932949
|
||||
|
||||
|
||||
@@ -30,10 +30,11 @@
|
||||
|
||||
**您的支持将用于项目的维护、功能开发和用户体验优化,非常感谢!**
|
||||
|
||||
## 下载地址
|
||||
|
||||
国内下载地址: https://gitee.com/wanghongenpin/proxypin/releases
|
||||
国内下载: https://gitee.com/wanghongenpin/proxypin/releases
|
||||
|
||||
iOS AppStore下载地址: https://apps.apple.com/app/proxypin/id6450932949
|
||||
iOS App Store: https://apps.apple.com/app/proxypin/id6450932949
|
||||
|
||||
Android Google Play:https://play.google.com/store/apps/details?id=com.network.proxy
|
||||
|
||||
|
||||
@@ -17,71 +17,153 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:proxypin/network/util/logger.dart';
|
||||
import 'package:proxypin/ui/component/json/theme.dart';
|
||||
import 'package:proxypin/ui/component/search/search_controller.dart';
|
||||
|
||||
class JsonText extends StatelessWidget {
|
||||
import '../../../utils/platform.dart';
|
||||
|
||||
class JsonText extends StatefulWidget {
|
||||
final ColorTheme colorTheme;
|
||||
final dynamic json;
|
||||
final String indent;
|
||||
final ScrollController? scrollController;
|
||||
final SearchTextController? searchController;
|
||||
|
||||
const JsonText({super.key, required this.json, this.indent = ' ', required this.colorTheme, this.scrollController});
|
||||
const JsonText({
|
||||
super.key,
|
||||
required this.json,
|
||||
this.indent = ' ',
|
||||
required this.colorTheme,
|
||||
this.scrollController,
|
||||
this.searchController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<JsonText> createState() => _JsonTextState();
|
||||
}
|
||||
|
||||
class _JsonTextState extends State<JsonText> {
|
||||
ScrollController? trackingScrollController;
|
||||
SearchTextController? searchController;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
searchController = widget.searchController;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
trackingScrollController?.dispose();
|
||||
trackingScrollController = null;
|
||||
logger.d('JsonText dispose');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var jsnParser = JsnParser(json, colorTheme, indent);
|
||||
var textList = jsnParser.getJsonTree();
|
||||
if (searchController == null) {
|
||||
return jsonTextWidget(context);
|
||||
}
|
||||
return AnimatedBuilder(
|
||||
animation: searchController!,
|
||||
builder: (context, child) {
|
||||
return jsonTextWidget(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget widget;
|
||||
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();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
searchController?.updateMatchCount(jsonParser.searchMatchTotal);
|
||||
// 自动滚动到当前高亮项
|
||||
scrollToMatch(jsonParser);
|
||||
});
|
||||
if (textList.length < 1500) {
|
||||
widget = Column(crossAxisAlignment: CrossAxisAlignment.start, children: textList);
|
||||
return SelectableText.rich(TextSpan(children: textList), showCursor: true);
|
||||
} else {
|
||||
widget = SizedBox(
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height - 160,
|
||||
child: ListView.builder(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: trackingScroll(),
|
||||
cacheExtent: 1000,
|
||||
itemBuilder: (context, index) => textList[index],
|
||||
itemCount: textList.length));
|
||||
child: SingleChildScrollView(
|
||||
physics: Platforms.isDesktop() ? null : const BouncingScrollPhysics(),
|
||||
controller: Platforms.isDesktop() ? null : trackingScroll(),
|
||||
child: SelectableText.rich(TextSpan(children: textList), showCursor: true)));
|
||||
}
|
||||
}
|
||||
|
||||
void scrollToMatch(JsonParser jsonParser) {
|
||||
if (searchController != null && jsonParser.matchKeys.isNotEmpty) {
|
||||
final currentIndex = searchController!.currentMatchIndex.value;
|
||||
if (currentIndex >= 0 && currentIndex < jsonParser.matchKeys.length) {
|
||||
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),
|
||||
alignment: 0.5, // 高亮项在视图中的位置
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return SelectionArea(child: widget);
|
||||
}
|
||||
|
||||
///滚动条
|
||||
ScrollController trackingScroll() {
|
||||
if (trackingScrollController != null) {
|
||||
return trackingScrollController!;
|
||||
}
|
||||
|
||||
var trackingScroll = TrackingScrollController();
|
||||
ScrollController? scrollController = widget.scrollController;
|
||||
|
||||
double offset = 0;
|
||||
trackingScroll.addListener(() {
|
||||
if (trackingScroll.offset < -10 || (trackingScroll.offset < 30 && trackingScroll.offset < offset)) {
|
||||
if (scrollController != null && scrollController!.offset >= 0) {
|
||||
scrollController?.jumpTo(scrollController!.offset - max((offset - trackingScroll.offset), 15));
|
||||
if (scrollController != null && scrollController.offset >= 0) {
|
||||
scrollController.jumpTo(scrollController.offset - max((offset - trackingScroll.offset), 15));
|
||||
}
|
||||
}
|
||||
offset = trackingScroll.offset;
|
||||
});
|
||||
|
||||
if (Platform.isIOS) {
|
||||
scrollController?.addListener(() {
|
||||
if (scrollController!.offset >= scrollController!.position.maxScrollExtent) {
|
||||
scrollController?.jumpTo(scrollController!.position.maxScrollExtent);
|
||||
if (Platform.isIOS && scrollController != null) {
|
||||
scrollController.addListener(() {
|
||||
if (scrollController.offset >= scrollController.position.maxScrollExtent) {
|
||||
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
||||
trackingScroll
|
||||
.jumpTo(trackingScroll.offset + (scrollController!.offset - scrollController!.position.maxScrollExtent));
|
||||
.jumpTo(trackingScroll.offset + (scrollController.offset - scrollController.position.maxScrollExtent));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
trackingScrollController = trackingScroll;
|
||||
return trackingScroll;
|
||||
}
|
||||
}
|
||||
|
||||
class JsnParser {
|
||||
class JsonParser {
|
||||
final dynamic json;
|
||||
final ColorTheme colorTheme;
|
||||
final String indent;
|
||||
final SearchTextController? searchController;
|
||||
int searchMatchTotal = 0;
|
||||
final List<GlobalKey> matchKeys = [];
|
||||
|
||||
JsnParser(this.json, this.colorTheme, this.indent);
|
||||
JsonParser(this.json, this.colorTheme, this.indent, this.searchController);
|
||||
|
||||
int getLength() {
|
||||
if (json is Map) {
|
||||
@@ -93,24 +175,26 @@ class JsnParser {
|
||||
}
|
||||
}
|
||||
|
||||
List<Text> getJsonTree() {
|
||||
List<Text> textList = [];
|
||||
List<TextSpan> getJsonTree() {
|
||||
matchKeys.clear(); // 每次渲染前清空
|
||||
List<TextSpan> textList = [];
|
||||
if (json is Map) {
|
||||
textList.add(const Text('{'));
|
||||
textList.add(const TextSpan(text: '{ \n'));
|
||||
textList.addAll(getMapText(json, prefix: indent));
|
||||
} else if (json is List) {
|
||||
textList.add(const Text('['));
|
||||
textList.add(const TextSpan(text: '[ \n'));
|
||||
textList.addAll(getArrayText(json));
|
||||
} else {
|
||||
textList.add(Text(json == null ? '' : json.toString()));
|
||||
textList.add(TextSpan(text: json == null ? '' : json.toString()));
|
||||
textList.add(TextSpan(text: '\n'));
|
||||
}
|
||||
return textList;
|
||||
}
|
||||
|
||||
/// 获取Map json
|
||||
List<Text> getMapText(Map<String, dynamic> map, {String openPrefix = '', String prefix = '', String suffix = ''}) {
|
||||
var result = <Text>[];
|
||||
// result.add(Text('$openPrefix{'));
|
||||
List<TextSpan> getMapText(Map<String, dynamic> map,
|
||||
{String openPrefix = '', String prefix = '', String suffix = ''}) {
|
||||
var result = <TextSpan>[];
|
||||
|
||||
var entries = map.entries;
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
@@ -118,11 +202,12 @@ class JsnParser {
|
||||
String postfix = '${i == entries.length - 1 ? '' : ','} ';
|
||||
|
||||
var textSpan = TextSpan(text: prefix, children: [
|
||||
TextSpan(text: '"${entry.key}"', style: TextStyle(color: colorTheme.propertyKey)),
|
||||
..._highlightMatches('"${entry.key}"', textColor: colorTheme.propertyKey),
|
||||
const TextSpan(text: ': '),
|
||||
getBasicValue(entry.value, postfix),
|
||||
]);
|
||||
result.add(Text.rich(textSpan));
|
||||
result.add(textSpan);
|
||||
result.add(TextSpan(text: '\n'));
|
||||
|
||||
if (entry.value is Map<String, dynamic>) {
|
||||
result.addAll(getMapText(entry.value, openPrefix: prefix, prefix: '$prefix$indent', suffix: postfix));
|
||||
@@ -131,20 +216,21 @@ class JsnParser {
|
||||
}
|
||||
}
|
||||
|
||||
result.add(Text('$openPrefix}$suffix'));
|
||||
result.add(TextSpan(text: '$openPrefix}$suffix \n'));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 获取数组json
|
||||
List<Text> getArrayText(List<dynamic> list, {String openPrefix = '', String prefix = '', String suffix = ''}) {
|
||||
var result = <Text>[];
|
||||
// result.add(Text('$openPrefix['));
|
||||
List<TextSpan> getArrayText(List<dynamic> list, {String openPrefix = '', String prefix = '', String suffix = ''}) {
|
||||
var result = <TextSpan>[];
|
||||
result.add(TextSpan(text: '$openPrefix[ \n'));
|
||||
|
||||
for (int i = 0; i < list.length; i++) {
|
||||
var value = list[i];
|
||||
String postfix = i == list.length - 1 ? '' : ',';
|
||||
|
||||
result.add(Text.rich(getBasicValue(value, postfix, prefix: prefix)));
|
||||
result.add(getBasicValue(value, postfix, prefix: prefix));
|
||||
result.add(TextSpan(text: '\n'));
|
||||
|
||||
if (value is Map<String, dynamic>) {
|
||||
result.addAll(getMapText(value, openPrefix: '$openPrefix ', prefix: '$prefix$indent', suffix: postfix));
|
||||
@@ -153,42 +239,82 @@ class JsnParser {
|
||||
}
|
||||
}
|
||||
|
||||
result.add(Text('$openPrefix]$suffix'));
|
||||
result.add(TextSpan(text: '$openPrefix]$suffix \n'));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 获取基本类型值 复杂类型会忽略
|
||||
InlineSpan getBasicValue(dynamic value, String suffix, {String? prefix}) {
|
||||
TextSpan getBasicValue(dynamic value, String suffix, {String? prefix}) {
|
||||
if (value == null) {
|
||||
return TextSpan(
|
||||
text: prefix,
|
||||
children: [TextSpan(text: 'null', style: TextStyle(color: colorTheme.keyword)), TextSpan(text: suffix)]);
|
||||
children: [..._highlightMatches('null', textColor: colorTheme.keyword), TextSpan(text: suffix)]);
|
||||
}
|
||||
|
||||
if (value is String) {
|
||||
return TextSpan(
|
||||
text: prefix,
|
||||
children: [TextSpan(text: '"$value"', style: TextStyle(color: colorTheme.string)), TextSpan(text: suffix)]);
|
||||
children: [..._highlightMatches('"$value"', textColor: colorTheme.string), TextSpan(text: suffix)]);
|
||||
}
|
||||
|
||||
if (value is num) {
|
||||
return TextSpan(text: prefix, children: [
|
||||
TextSpan(text: value.toString(), style: TextStyle(color: colorTheme.number)),
|
||||
TextSpan(text: suffix)
|
||||
]);
|
||||
return TextSpan(
|
||||
text: prefix,
|
||||
children: [..._highlightMatches(value.toString(), textColor: colorTheme.number), TextSpan(text: suffix)]);
|
||||
}
|
||||
|
||||
if (value is bool) {
|
||||
return TextSpan(text: prefix, children: [
|
||||
TextSpan(text: value.toString(), style: TextStyle(color: colorTheme.keyword)),
|
||||
TextSpan(text: suffix)
|
||||
]);
|
||||
return TextSpan(
|
||||
text: prefix,
|
||||
children: [..._highlightMatches(value.toString(), textColor: colorTheme.keyword), TextSpan(text: suffix)]);
|
||||
}
|
||||
|
||||
if (value is List) {
|
||||
return TextSpan(text: "${prefix ?? ''}[");
|
||||
return TextSpan(children: _highlightMatches("${prefix ?? ''}["));
|
||||
}
|
||||
|
||||
return TextSpan(text: "${prefix ?? ''}{");
|
||||
return TextSpan(children: _highlightMatches("${prefix ?? ''}{"));
|
||||
}
|
||||
|
||||
List<InlineSpan> _highlightMatches(String text, {Color? textColor}) {
|
||||
if (searchController == null || searchController?.shouldSearch() == false) {
|
||||
return [TextSpan(text: text, style: TextStyle(color: textColor))];
|
||||
}
|
||||
|
||||
final pattern = searchController!.value.pattern;
|
||||
final regex = searchController!.value.isRegExp
|
||||
? RegExp(pattern, caseSensitive: searchController!.value.isCaseSensitive)
|
||||
: RegExp(RegExp.escape(pattern), caseSensitive: searchController!.value.isCaseSensitive);
|
||||
|
||||
final spans = <InlineSpan>[];
|
||||
int start = 0;
|
||||
var allMatches = regex.allMatches(text).toList();
|
||||
final currentIndex = searchController!.currentMatchIndex.value;
|
||||
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), style: TextStyle(color: textColor)));
|
||||
}
|
||||
// 为每个高亮项分配一个 GlobalKey
|
||||
final key = GlobalKey();
|
||||
matchKeys.add(key);
|
||||
spans.add(WidgetSpan(
|
||||
child: Container(
|
||||
key: key,
|
||||
color: searchMatchTotal == currentIndex ? colorTheme.searchMatchCurrentColor : colorTheme.searchMatchColor,
|
||||
child: Text(
|
||||
text.substring(match.start, match.end),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
),
|
||||
));
|
||||
start = match.end;
|
||||
searchMatchTotal += 1; // 统计总匹配数
|
||||
}
|
||||
|
||||
if (start < text.length) {
|
||||
spans.add(TextSpan(text: text.substring(start), style: TextStyle(color: textColor)));
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum ColorTheme {
|
||||
light(
|
||||
background: Color(0xffffffff),
|
||||
propertyKey: Color(0xff871094),
|
||||
colon: Colors.black,
|
||||
string: Color(0xff067d17),
|
||||
number: Color(0xff1750eb),
|
||||
keyword: Color(0xff0033b3)),
|
||||
dark(
|
||||
background: Color(0xff2b2b2b),
|
||||
propertyKey: Color(0xff9876aa),
|
||||
colon: Color(0xffcc7832),
|
||||
string: Color(0xff6a8759),
|
||||
number: Color(0xff6897bb),
|
||||
keyword: Color(0xffcc7832));
|
||||
class ColorTheme {
|
||||
static ColorTheme light(ColorScheme colorScheme) => ColorTheme(
|
||||
background: const Color(0xffffffff),
|
||||
propertyKey: const Color(0xff871094),
|
||||
colon: Colors.black,
|
||||
string: const Color(0xff067d17),
|
||||
number: const Color(0xff1750eb),
|
||||
keyword: const Color(0xff0033b3),
|
||||
searchMatchColor: colorScheme.inversePrimary,
|
||||
searchMatchCurrentColor: colorScheme.primary,
|
||||
);
|
||||
|
||||
static ColorTheme dark(ColorScheme colorScheme) => ColorTheme(
|
||||
background: const Color(0xff2b2b2b),
|
||||
propertyKey: const Color(0xff9876aa),
|
||||
colon: const Color(0xffcc7832),
|
||||
string: const Color(0xff6a8759),
|
||||
number: const Color(0xff6897bb),
|
||||
keyword: const Color(0xffcc7832),
|
||||
searchMatchColor: colorScheme.inversePrimary,
|
||||
searchMatchCurrentColor: colorScheme.primary,
|
||||
);
|
||||
|
||||
final Color background;
|
||||
final Color propertyKey;
|
||||
@@ -22,16 +29,23 @@ enum ColorTheme {
|
||||
final Color string;
|
||||
final Color number;
|
||||
final Color keyword;
|
||||
final Color? searchMatchColor;
|
||||
final Color? searchMatchCurrentColor;
|
||||
|
||||
const ColorTheme(
|
||||
{required this.background,
|
||||
required this.propertyKey,
|
||||
required this.colon,
|
||||
required this.string,
|
||||
required this.number,
|
||||
required this.keyword});
|
||||
const ColorTheme({
|
||||
required this.background,
|
||||
required this.propertyKey,
|
||||
required this.colon,
|
||||
required this.string,
|
||||
required this.number,
|
||||
required this.keyword,
|
||||
required this.searchMatchColor,
|
||||
required this.searchMatchCurrentColor,
|
||||
});
|
||||
|
||||
static ColorTheme of(Brightness brightness) {
|
||||
return brightness == Brightness.dark ? ColorTheme.dark : ColorTheme.light;
|
||||
static ColorTheme of(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final brightness = Theme.of(context).brightness;
|
||||
return brightness == Brightness.dark ? ColorTheme.dark(colorScheme) : ColorTheme.light(colorScheme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class HighlightTextWidget extends StatelessWidget {
|
||||
int start = 0;
|
||||
var allMatches = regex.allMatches(text).toList();
|
||||
final currentIndex = searchController.currentMatchIndex.value;
|
||||
ColorScheme colorScheme = ColorScheme.of(context);
|
||||
List<int> matchOffsets = [];
|
||||
for (int i = 0; i < allMatches.length; i++) {
|
||||
final match = allMatches[i];
|
||||
@@ -57,7 +58,7 @@ class HighlightTextWidget extends StatelessWidget {
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(match.start, match.end),
|
||||
style: TextStyle(
|
||||
backgroundColor: i == currentIndex ? Colors.orange : Colors.yellow,
|
||||
backgroundColor: i == currentIndex ? colorScheme.primary : colorScheme.inversePrimary,
|
||||
),
|
||||
));
|
||||
start = match.end;
|
||||
@@ -87,10 +88,6 @@ class HighlightTextWidget extends StatelessWidget {
|
||||
);
|
||||
tp.layout(maxWidth: scrollController!.position.viewportDimension);
|
||||
final offset = tp.height;
|
||||
scrollController!.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
scrollController!.animateTo(offset, duration: const Duration(milliseconds: 300), curve: Curves.ease);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ class HttpBodyState extends State<HttpBodyWidget> {
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
||||
searchController.dispose();
|
||||
widget.scrollController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -468,12 +469,13 @@ class _BodyState extends State<_Body> {
|
||||
return JsonText(
|
||||
json: jsonObject,
|
||||
indent: Platforms.isDesktop() ? ' ' : ' ',
|
||||
colorTheme: ColorTheme.of(Theme.of(context).brightness),
|
||||
colorTheme: ColorTheme.of(context),
|
||||
searchController: widget.searchController,
|
||||
scrollController: widget.scrollController);
|
||||
}
|
||||
|
||||
if (type == ViewType.json) {
|
||||
return JsonViewer(json.decode(body), colorTheme: ColorTheme.of(Theme.of(context).brightness));
|
||||
return JsonViewer(json.decode(body), colorTheme: ColorTheme.of(context));
|
||||
}
|
||||
|
||||
return HighlightTextWidget(
|
||||
|
||||
Reference in New Issue
Block a user