diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e032ed..7ff0d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.1.0 - 22-09-2025 + +### Added +- New `expansionDepth` parameter to control the depth of expansion when `initiallyExpanded` is true + + ## 2.0.0 - 09-12-2024 ### Updated diff --git a/README.md b/README.md index 2ed26d6..4df478b 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ final jsonData = { JsonInspector( jsonData: jsonData, initiallyExpanded: true, // Optional: expand all nodes initially + expansionDepth: 2, // Optional: control expansion depth (default: -1 for all levels) ) ``` @@ -55,6 +56,7 @@ JsonInspector( JsonInspector( jsonData: jsonData, initiallyExpanded: false, + expansionDepth: 1, // Expand only first level when initiallyExpanded is true keyStyle: TextStyle( color: Colors.purple, fontWeight: FontWeight.bold, @@ -70,6 +72,11 @@ JsonInspector( ### 1. Interactive Expansion - Click on arrows to expand/collapse nodes - Use `initiallyExpanded` parameter to control initial state +- Use `expansionDepth` parameter to control how deep the initial expansion goes: + - `-1` (default): Expand all levels when `initiallyExpanded` is true + - `1`: Expand only the first level + - `2`: Expand up to the second level + - And so on... ### 2. Selection and Copying - Click anywhere on a line to select it @@ -121,6 +128,7 @@ class MyApp extends StatelessWidget { body: JsonInspector( jsonData: jsonData, initiallyExpanded: true, + expansionDepth: 2, // Expand up to 2 levels deep ), ), ); diff --git a/example/lib/main.dart b/example/lib/main.dart index 1472d05..25dcde4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -82,6 +82,15 @@ class _MyHomePageState extends State { }; bool _expandAll = true; + int _expansionDepth = -1; + + final List _depthOptions = [-1, 1, 2, 3]; + final Map _depthLabels = { + -1: 'All levels', + 1: '1 level', + 2: '2 levels', + 3: '3 levels', + }; @override Widget build(BuildContext context) { @@ -89,6 +98,32 @@ class _MyHomePageState extends State { appBar: AppBar( title: const Text('JSON Inspector Example'), actions: [ + PopupMenuButton( + icon: const Icon(Icons.settings), + tooltip: 'Expansion Settings', + onSelected: (int depth) { + setState(() { + _expansionDepth = depth; + _expandAll = true; // Enable expansion when depth is selected + }); + }, + itemBuilder: (BuildContext context) => + _depthOptions.map((int depth) { + return PopupMenuItem( + value: depth, + child: Row( + children: [ + Icon( + _expansionDepth == depth ? Icons.check : Icons.layers, + size: 20, + ), + const SizedBox(width: 8), + Text('Expand ${_depthLabels[depth]}'), + ], + ), + ); + }).toList(), + ), IconButton( icon: Icon(_expandAll ? Icons.unfold_less : Icons.unfold_more), onPressed: () { @@ -112,13 +147,26 @@ class _MyHomePageState extends State { style: Theme.of(context).textTheme.headlineSmall, ), ), - const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - '• Click arrows to expand/collapse\n' - '• Click anywhere to select\n' - '• Long press to copy values', - style: TextStyle(color: Colors.grey), + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '• Click arrows to expand/collapse\n' + '• Click anywhere to select\n' + '• Long press to copy values', + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 8), + Text( + 'Current expansion: ${_expandAll ? _depthLabels[_expansionDepth] : "Collapsed"}', + style: TextStyle( + color: Colors.blue[700], + fontWeight: FontWeight.w500, + ), + ), + ], ), ), Expanded( @@ -126,6 +174,7 @@ class _MyHomePageState extends State { child: JsonInspector( jsonData: complexJson, initiallyExpanded: _expandAll, + expansionDepth: _expansionDepth, keyStyle: const TextStyle( color: Colors.purple, fontWeight: FontWeight.w500, diff --git a/example/pubspec.lock b/example/pubspec.lock index a23e04d..6834ba6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,26 +21,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -81,31 +81,31 @@ packages: path: ".." relative: true source: path - version: "1.0.2" + version: "2.1.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -118,10 +118,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -134,18 +134,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" sky_engine: dependency: transitive description: flutter @@ -163,18 +163,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: @@ -195,18 +195,18 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.6" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -216,5 +216,5 @@ packages: source: hosted version: "14.3.0" sdks: - dart: ">=3.6.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/src/json_inspector_base.dart b/lib/src/json_inspector_base.dart index e76aa1a..6944033 100644 --- a/lib/src/json_inspector_base.dart +++ b/lib/src/json_inspector_base.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'dart:convert'; /// A widget for displaying and interacting with JSON data. /// Provides features like expanding/collapsing nodes, selecting values, and copying to the clipboard. @@ -10,6 +11,10 @@ class JsonInspector extends StatefulWidget { /// Whether the JSON nodes should be initially expanded. final bool initiallyExpanded; + /// The depth level to expand when initiallyExpanded is true. + /// -1 means expand all levels, 1 means expand only first level, etc. + final int expansionDepth; + /// Custom text style for JSON keys. final TextStyle? keyStyle; @@ -23,6 +28,7 @@ class JsonInspector extends StatefulWidget { super.key, required this.jsonData, this.initiallyExpanded = false, + this.expansionDepth = -1, this.keyStyle, this.valueStyle, }); @@ -40,81 +46,91 @@ class _JsonInspectorState extends State { super.initState(); _expandedState = {}; _selectedState = {}; - _initializeStates(widget.jsonData, ''); + _initializeStates(widget.jsonData, '', 0); } @override void didUpdateWidget(covariant JsonInspector oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.jsonData != widget.jsonData || - oldWidget.initiallyExpanded != widget.initiallyExpanded) { + oldWidget.initiallyExpanded != widget.initiallyExpanded || + oldWidget.expansionDepth != widget.expansionDepth) { _expandedState = {}; _selectedState = {}; - _initializeStates(widget.jsonData, ''); + _initializeStates(widget.jsonData, '', 0); } } - void _initializeStates(dynamic data, String path) { + void _initializeStates(dynamic data, String path, int currentDepth) { if (data is Map) { - _expandedState[path] = widget.initiallyExpanded; + bool shouldExpand = + widget.initiallyExpanded && (widget.expansionDepth == -1 || currentDepth < widget.expansionDepth); + _expandedState[path] = shouldExpand; _selectedState[path] = false; data.forEach((key, value) { - _initializeStates(value, '$path/$key'); + _initializeStates(value, '$path/$key', currentDepth + 1); }); } else if (data is List) { - _expandedState[path] = widget.initiallyExpanded; + bool shouldExpand = + widget.initiallyExpanded && (widget.expansionDepth == -1 || currentDepth < widget.expansionDepth); + _expandedState[path] = shouldExpand; _selectedState[path] = false; for (var i = 0; i < data.length; i++) { - _initializeStates(data[i], '$path/$i'); + _initializeStates(data[i], '$path/$i', currentDepth + 1); } } } Widget _buildJsonTree(dynamic data, String path, String key) { if (data == null) { - return _buildLeafNode('null', path, key); + return _buildLeafNode(null, 'null', path, key); } else if (data is num || data is bool) { - return _buildLeafNode(data.toString(), path, key); + return _buildLeafNode(data, data.toString(), path, key); } else if (data is String) { - return _buildLeafNode('"$data"', path, key); + return _buildLeafNode(data, '"$data"', path, key); } else if (data is List) { return _buildListNode(data, path, key); } else if (data is Map) { return _buildMapNode(data, path, key); } - return _buildLeafNode(data.toString(), path, key); + return _buildLeafNode(data, data.toString(), path, key); } - Widget _buildLeafNode(String value, String path, String key) { + Widget _buildLeafNode(dynamic originalValue, String displayValue, String path, String key) { return InkWell( onTap: () { setState(() { _selectedState[path] = !(_selectedState[path] ?? false); }); }, - onLongPress: () => _copyToClipboard(value), + onLongPress: () => _copyToClipboard(_toJsonString(originalValue)), child: Container( - color: _selectedState[path] ?? false - ? Colors.blue.withValues(alpha: 25.5) - : null, + color: _selectedState[path] ?? false ? Colors.blue.withValues(alpha: 25.5) : null, padding: const EdgeInsets.symmetric(vertical: 2), child: Row( children: [ - const SizedBox( - width: 24), // Space for consistency with expanded items - Text(key, - style: - widget.keyStyle ?? const TextStyle(color: Colors.purple)), + const SizedBox(width: 24), // Space for consistency with expanded items + Text(key, style: widget.keyStyle ?? const TextStyle(color: Colors.purple)), const Text(': '), - Flexible( + Expanded( child: Text( - value, + displayValue, overflow: TextOverflow.ellipsis, - style: widget.valueStyle ?? - TextStyle( - color: value == 'null' ? Colors.grey : Colors.green), + style: widget.valueStyle ?? TextStyle(color: originalValue == null ? Colors.grey : Colors.green), + ), + ), + Tooltip( + message: 'Copy value', + child: IconButton( + icon: const Icon(Icons.copy, size: 18), + visualDensity: VisualDensity.compact, + onPressed: () { + _copyToClipboard(_toJsonString(originalValue)); + _showCopiedSnackBar('Copied value'); + }, ), ), + _buildOverflowMenu(path: path, keyLabel: key, node: originalValue), ], ), ), @@ -132,17 +148,28 @@ class _JsonInspectorState extends State { _expandedState[path] = !isExpanded; }); }, - onLongPress: () => _copyToClipboard(data.toString()), + onLongPress: () => _copyToClipboard(_toJsonString(data)), child: Row( children: [ Icon( isExpanded ? Icons.arrow_drop_down : Icons.arrow_right, size: 24, ), - Text(key, - style: - widget.keyStyle ?? const TextStyle(color: Colors.purple)), + Text(key, style: widget.keyStyle ?? const TextStyle(color: Colors.purple)), Text(': [${data.length} items]'), + const Spacer(), + Tooltip( + message: 'Copy JSON', + child: IconButton( + icon: const Icon(Icons.copy, size: 18), + visualDensity: VisualDensity.compact, + onPressed: () { + _copyToClipboard(_toJsonString(data)); + _showCopiedSnackBar('Copied array JSON'); + }, + ), + ), + _buildOverflowMenu(path: path, keyLabel: key, node: data), ], ), ), @@ -152,8 +179,7 @@ class _JsonInspectorState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (var i = 0; i < data.length; i++) - _buildJsonTree(data[i], '$path/$i', '[$i]'), + for (var i = 0; i < data.length; i++) _buildJsonTree(data[i], '$path/$i', '[$i]'), ], ), ), @@ -172,20 +198,31 @@ class _JsonInspectorState extends State { _expandedState[path] = !isExpanded; }); }, - onLongPress: () => _copyToClipboard(data.toString()), + onLongPress: () => _copyToClipboard(_toJsonString(data)), child: Row( children: [ Icon( isExpanded ? Icons.arrow_drop_down : Icons.arrow_right, size: 24, ), - Text(key, - style: - widget.keyStyle ?? const TextStyle(color: Colors.purple)), + Text(key, style: widget.keyStyle ?? const TextStyle(color: Colors.purple)), Text( ': {${data.length} keys}', overflow: TextOverflow.ellipsis, ), + const Spacer(), + Tooltip( + message: 'Copy JSON', + child: IconButton( + icon: const Icon(Icons.copy, size: 18), + visualDensity: VisualDensity.compact, + onPressed: () { + _copyToClipboard(_toJsonString(data)); + _showCopiedSnackBar('Copied object JSON'); + }, + ), + ), + _buildOverflowMenu(path: path, keyLabel: key, node: data), ], ), ), @@ -195,9 +232,7 @@ class _JsonInspectorState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - for (var entry in data.entries) - _buildJsonTree( - entry.value, '$path/${entry.key}', entry.key.toString()), + for (var entry in data.entries) _buildJsonTree(entry.value, '$path/${entry.key}', entry.key.toString()), ], ), ), @@ -205,10 +240,76 @@ class _JsonInspectorState extends State { ); } + String _toJsonString(dynamic data) { + try { + return jsonEncode(data); + } catch (_) { + // Fallback for non-encodable objects + return data?.toString() ?? 'null'; + } + } + Future _copyToClipboard(String text) async { await Clipboard.setData(ClipboardData(text: text)); } + void _showCopiedSnackBar(String message) { + final messenger = ScaffoldMessenger.maybeOf(context); + if (messenger == null) return; + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 1), + ), + ); + } + + Widget _buildOverflowMenu({ + required String path, + required String keyLabel, + required dynamic node, + }) { + return PopupMenuButton( + tooltip: 'More actions', + itemBuilder: (context) => const [ + PopupMenuItem( + value: 'copy_key', + child: Text('Copy key'), + ), + PopupMenuItem( + value: 'copy_key_path', + child: Text('Copy key path'), + ), + PopupMenuItem( + value: 'copy_json', + child: Text('Copy JSON'), + ), + ], + onSelected: (value) { + switch (value) { + case 'copy_key': + _copyToClipboard(keyLabel); + _showCopiedSnackBar('Copied key'); + break; + case 'copy_key_path': + final normalized = path.startsWith('/') ? path.substring(1) : path; + _copyToClipboard(normalized); + _showCopiedSnackBar('Copied key path'); + break; + case 'copy_json': + _copyToClipboard(_toJsonString(node)); + _showCopiedSnackBar('Copied JSON'); + break; + } + }, + child: const Padding( + padding: EdgeInsets.only(right: 4.0), + child: Icon(Icons.more_vert, size: 18), + ), + ); + } + @override Widget build(BuildContext context) { return SingleChildScrollView( diff --git a/pubspec.yaml b/pubspec.yaml index 87fe1c9..2f22331 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: json_inspector description: "An interactive Flutter JSON viewer with expand/collapse, value selection, and copy-to-clipboard features. Ideal for debugging, inspecting, and visualizing JSON data." -version: 2.0.0 +version: 2.1.0 homepage: "https://github.com/Neelansh-ns/json_inspector" repository: "https://github.com/Neelansh-ns/json_inspector" topics: