mirror of
https://github.com/foss42/apidash.git
synced 2025-08-05 21:10:33 +08:00
feat: Add JSON Viewer & search
This commit is contained in:
@ -312,6 +312,10 @@ const kCodeRawBodyViewOptions = [ResponseBodyView.code, ResponseBodyView.raw];
|
||||
const kPreviewBodyViewOptions = [
|
||||
ResponseBodyView.preview,
|
||||
];
|
||||
const kPreviewRawBodyViewOptions = [
|
||||
ResponseBodyView.preview,
|
||||
ResponseBodyView.raw
|
||||
];
|
||||
const kPreviewCodeRawBodyViewOptions = [
|
||||
ResponseBodyView.preview,
|
||||
ResponseBodyView.code,
|
||||
@ -322,7 +326,7 @@ const Map<String, Map<String, List<ResponseBodyView>>>
|
||||
kResponseBodyViewOptions = {
|
||||
kTypeApplication: {
|
||||
kSubTypeDefaultViewOptions: kNoRawBodyViewOptions,
|
||||
kSubTypeJson: kCodeRawBodyViewOptions,
|
||||
kSubTypeJson: kPreviewRawBodyViewOptions,
|
||||
kSubTypeOctetStream: kNoBodyViewOptions,
|
||||
kSubTypePdf: kPreviewBodyViewOptions,
|
||||
kSubTypeSql: kCodeRawBodyViewOptions,
|
||||
|
254
lib/widgets/json_previewer.dart
Normal file
254
lib/widgets/json_previewer.dart
Normal file
@ -0,0 +1,254 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_data_explorer/json_data_explorer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
class JsonPreviewer extends StatefulWidget {
|
||||
const JsonPreviewer({
|
||||
super.key,
|
||||
required this.code,
|
||||
});
|
||||
final String code;
|
||||
|
||||
@override
|
||||
State<JsonPreviewer> createState() => _JsonPreviewerState();
|
||||
}
|
||||
|
||||
class _JsonPreviewerState extends State<JsonPreviewer> {
|
||||
final searchController = TextEditingController();
|
||||
final itemScrollController = ItemScrollController();
|
||||
final DataExplorerStore store = DataExplorerStore();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
store.buildNodes(jsonDecode(widget.code), areAllCollapsed: true);
|
||||
store.expandAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: store,
|
||||
child: Consumer<DataExplorerStore>(
|
||||
builder: (context, state, child) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
|
||||
/// Delegates the search to [DataExplorerStore] when
|
||||
/// the text field changes.
|
||||
onChanged: (term) => state.search(term),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Search',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
if (state.searchResults.isNotEmpty) Text(_searchFocusText()),
|
||||
if (state.searchResults.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
store.focusPreviousSearchResult();
|
||||
_scrollToSearchMatch();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_drop_up),
|
||||
),
|
||||
if (state.searchResults.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
store.focusNextSearchResult();
|
||||
_scrollToSearchMatch();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: state.areAllExpanded() ? null : state.expandAll,
|
||||
child: const Text('Expand All'),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: state.areAllCollapsed() ? null : state.collapseAll,
|
||||
child: const Text('Collapse All'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
child: JsonDataExplorer(
|
||||
nodes: state.displayNodes,
|
||||
itemScrollController: itemScrollController,
|
||||
itemSpacing: 4,
|
||||
|
||||
/// Builds a widget after each root node displaying the
|
||||
/// number of children nodes that it has. Displays `{x}`
|
||||
/// if it is a class or `[x]` in case of arrays.
|
||||
rootInformationBuilder: (context, node) => DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0x80E1E1E1),
|
||||
borderRadius: BorderRadius.all(Radius.circular(2)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 2,
|
||||
),
|
||||
child: Text(
|
||||
node.isClass
|
||||
? '{${node.childrenCount}}'
|
||||
: '[${node.childrenCount}]',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: const Color(0xFF6F6F6F),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// Build an animated collapse/expand indicator. Implicitly
|
||||
/// animates the indicator when
|
||||
/// [NodeViewModelState.isCollapsed] changes.
|
||||
collapsableToggleBuilder: (context, node) => AnimatedRotation(
|
||||
turns: node.isCollapsed ? -0.25 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: const Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
|
||||
/// Builds a trailing widget that copies the node key: value
|
||||
///
|
||||
/// Uses [NodeViewModelState.isFocused] to display the
|
||||
/// widget only in focused widgets.
|
||||
trailingBuilder: (context, node) => node.isFocused
|
||||
? IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(maxHeight: 18),
|
||||
icon: const Icon(
|
||||
Icons.copy,
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () => _printNode(node),
|
||||
)
|
||||
: const SizedBox(),
|
||||
|
||||
/// Creates a custom format for classes and array names.
|
||||
rootNameFormatter: (dynamic name) => '$name',
|
||||
|
||||
/// Dynamically changes the property value style and
|
||||
/// interaction when an URL is detected.
|
||||
valueStyleBuilder: (dynamic value, style) {
|
||||
final isUrl = _valueIsUrl(value);
|
||||
return PropertyOverrides(
|
||||
style: isUrl
|
||||
? style.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
)
|
||||
: style,
|
||||
//onTap: isUrl ? () => _launchUrl(value as String) : null,
|
||||
);
|
||||
},
|
||||
|
||||
/// Theme definitions of the json data explorer
|
||||
theme: DataExplorerTheme(
|
||||
rootKeyTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
propertyKeyTextStyle: TextStyle(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
keySearchHighlightTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
backgroundColor: const Color(0xFFFFEDAD),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
focusedKeySearchHighlightTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
backgroundColor: const Color(0xFFF29D0B),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
valueTextStyle: TextStyle(
|
||||
color: const Color(0xFFCA442C),
|
||||
fontSize: 16,
|
||||
),
|
||||
valueSearchHighlightTextStyle: TextStyle(
|
||||
color: const Color(0xFFCA442C),
|
||||
backgroundColor: const Color(0xFFFFEDAD),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
focusedValueSearchHighlightTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
backgroundColor: const Color(0xFFF29D0B),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
indentationLineColor: const Color(0xFFE1E1E1),
|
||||
highlightColor: const Color(0xFFF1F1F1),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _searchFocusText() =>
|
||||
'${store.focusedSearchResultIndex + 1} of ${store.searchResults.length}';
|
||||
|
||||
void _printNode(NodeViewModelState node) {
|
||||
if (node.isRoot) {
|
||||
final value = node.isClass ? 'class' : 'array';
|
||||
debugPrint('${node.key}: $value');
|
||||
return;
|
||||
}
|
||||
debugPrint('${node.key}: ${node.value}');
|
||||
}
|
||||
|
||||
void _scrollToSearchMatch() {
|
||||
final index = store.displayNodes.indexOf(store.focusedSearchResult.node);
|
||||
if (index != -1) {
|
||||
itemScrollController.scrollTo(
|
||||
index: index,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOutCubic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _valueIsUrl(dynamic value) {
|
||||
if (value is String) {
|
||||
return Uri.tryParse(value)?.hasAbsolutePath ?? false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -4,18 +4,20 @@ import 'error_message.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import 'uint8_audio_player.dart';
|
||||
|
||||
import 'json_previewer.dart';
|
||||
|
||||
class Previewer extends StatefulWidget {
|
||||
const Previewer({
|
||||
super.key,
|
||||
required this.bytes,
|
||||
required this.body,
|
||||
this.type,
|
||||
this.subtype,
|
||||
this.hasRaw = false,
|
||||
});
|
||||
|
||||
final Uint8List bytes;
|
||||
final String body;
|
||||
final String? type;
|
||||
final String? subtype;
|
||||
final bool hasRaw;
|
||||
@ -27,6 +29,11 @@ class Previewer extends StatefulWidget {
|
||||
class _PreviewerState extends State<Previewer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.type == kTypeApplication && widget.subtype == kSubTypeJson) {
|
||||
return JsonPreviewer(
|
||||
code: widget.body,
|
||||
);
|
||||
}
|
||||
if (widget.type == kTypeImage) {
|
||||
return Image.memory(
|
||||
widget.bytes,
|
||||
|
@ -453,11 +453,17 @@ class _BodySuccessState extends State<BodySuccess> {
|
||||
visible: currentSeg == ResponseBodyView.preview ||
|
||||
currentSeg == ResponseBodyView.none,
|
||||
child: Expanded(
|
||||
child: Previewer(
|
||||
bytes: widget.bytes,
|
||||
type: widget.mediaType.type,
|
||||
subtype: widget.mediaType.subtype,
|
||||
hasRaw: widget.options.contains(ResponseBodyView.raw),
|
||||
child: Container(
|
||||
width: double.maxFinite,
|
||||
padding: kP8,
|
||||
decoration: textContainerdecoration,
|
||||
child: Previewer(
|
||||
bytes: widget.bytes,
|
||||
body: widget.body,
|
||||
type: widget.mediaType.type,
|
||||
subtype: widget.mediaType.subtype,
|
||||
hasRaw: widget.options.contains(ResponseBodyView.raw),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -18,3 +18,4 @@ export 'snackbars.dart';
|
||||
export 'markdown.dart';
|
||||
export 'uint8_audio_player.dart';
|
||||
export 'tabs.dart';
|
||||
export 'json_previewer.dart';
|
||||
|
32
pubspec.lock
32
pubspec.lock
@ -464,6 +464,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
json_data_explorer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_data_explorer
|
||||
sha256: "303a00037b23963fd01be1b2dc509f14e9db2a40f852b0ce042d7635c22fd154"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -600,6 +608,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -752,6 +768,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.11.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -800,6 +824,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.8"
|
||||
scrollable_positioned_list:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: scrollable_positioned_list
|
||||
sha256: "9566352ab9ba05794ee6c8864f154afba5d36c5637d0e3e32c615ba4ceb92772"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -41,6 +41,9 @@ dependencies:
|
||||
json_annotation: ^4.8.1
|
||||
printing: ^5.11.0
|
||||
package_info_plus: ^4.1.0
|
||||
provider: ^6.0.5
|
||||
json_data_explorer: ^0.1.0
|
||||
scrollable_positioned_list: ^0.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Reference in New Issue
Block a user