From 7d8172493633fed1aff306779a9d8568485476a7 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 14:36:42 +0530 Subject: [PATCH 01/10] feat: Add JSON Viewer & search --- lib/consts.dart | 6 +- lib/widgets/json_previewer.dart | 254 ++++++++++++++++++++++++++++++ lib/widgets/previewer.dart | 9 +- lib/widgets/response_widgets.dart | 16 +- lib/widgets/widgets.dart | 1 + pubspec.lock | 32 ++++ pubspec.yaml | 3 + 7 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 lib/widgets/json_previewer.dart diff --git a/lib/consts.dart b/lib/consts.dart index 004f8a71..f9fc453d 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -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>> kResponseBodyViewOptions = { kTypeApplication: { kSubTypeDefaultViewOptions: kNoRawBodyViewOptions, - kSubTypeJson: kCodeRawBodyViewOptions, + kSubTypeJson: kPreviewRawBodyViewOptions, kSubTypeOctetStream: kNoBodyViewOptions, kSubTypePdf: kPreviewBodyViewOptions, kSubTypeSql: kCodeRawBodyViewOptions, diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart new file mode 100644 index 00000000..0da46a74 --- /dev/null +++ b/lib/widgets/json_previewer.dart @@ -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 createState() => _JsonPreviewerState(); +} + +class _JsonPreviewerState extends State { + 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( + 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(); + } +} diff --git a/lib/widgets/previewer.dart b/lib/widgets/previewer.dart index 3e48dc6d..fbc2b0ff 100644 --- a/lib/widgets/previewer.dart +++ b/lib/widgets/previewer.dart @@ -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 { @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, diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index 5bcd9c7d..da905749 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -453,11 +453,17 @@ class _BodySuccessState extends State { 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), + ), ), ), ), diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index efdb1e25..ab5a48fe 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -18,3 +18,4 @@ export 'snackbars.dart'; export 'markdown.dart'; export 'uint8_audio_player.dart'; export 'tabs.dart'; +export 'json_previewer.dart'; diff --git a/pubspec.lock b/pubspec.lock index c78b1211..95a80d29 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 61cec3fb..e5b9f6ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: From 707d21ebbbfc3a807433623ae24c5567c7354294 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 16:39:56 +0530 Subject: [PATCH 02/10] Update color scheme of JSON previewer --- lib/widgets/json_previewer.dart | 233 +++++++++++++++++++++----------- 1 file changed, 155 insertions(+), 78 deletions(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 0da46a74..dbc122f5 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -4,6 +4,124 @@ import 'package:json_data_explorer/json_data_explorer.dart'; import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import '../consts.dart'; + +class JsonPreviewerColor { + const JsonPreviewerColor._(); + + static const Color lightRootInfoBox = Color(0x80E1E1E1); + static const Color lightRootKeyText = Colors.black; + static const Color lightPropertyKeyText = Colors.black; + static const Color lightKeySearchHighlightText = Colors.black; + static const Color lightKeySearchHighlightBackground = Color(0xFFFFEDAD); + static const Color lightFocusedKeySearchHighlightText = Colors.black; + static const Color lightFocusedKeySearchHighlightBackground = + Color(0xFFF29D0B); + static const Color lightValueText = Color(0xffc41a16); + static const Color lightValueSearchHighlightText = Color(0xffc41a16); + static const Color lightValueSearchHighlightBackground = Color(0xFFFFEDAD); + static const Color lightFocusedValueSearchHighlightText = Colors.black; + static const Color lightFocusedValueSearchHighlightBackground = + Color(0xFFF29D0B); + static const Color lightIndentationLineColor = + Color.fromARGB(255, 213, 213, 213); + static const Color lightHighlightColor = Color(0xFFF1F1F1); + +// Dark colors + static const Color darkRootInfoBox = Color.fromARGB(255, 83, 13, 19); + static const Color darkRootKeyText = Color(0xffd6deeb); + static const Color darkPropertyKeyText = Color(0xffd6deeb); + static const Color darkKeySearchHighlightText = Color(0xffd6deeb); + static const Color darkKeySearchHighlightBackground = Color(0xff9b703f); + static const Color darkFocusedKeySearchHighlightText = Color(0xffd6deeb); + static const Color darkFocusedKeySearchHighlightBackground = + Color(0xffc41a16); + static const Color darkValueText = Color(0xffecc48d); + static const Color darkValueSearchHighlightText = Color(0xffecc48d); + static const Color darkValueSearchHighlightBackground = Color(0xff9b703f); + static const Color darkFocusedValueSearchHighlightText = Color(0xffd6deeb); + static const Color darkFocusedValueSearchHighlightBackground = + Color(0xffc41a16); + static const Color darkIndentationLineColor = + Color.fromARGB(255, 119, 119, 119); + static const Color darkHighlightColor = Color.fromARGB(255, 55, 55, 55); +} + +final dataExplorerThemeLight = DataExplorerTheme( + rootKeyTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightRootKeyText, + fontWeight: FontWeight.bold, + ), + propertyKeyTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightPropertyKeyText, + fontWeight: FontWeight.bold, + ), + keySearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightKeySearchHighlightText, + backgroundColor: JsonPreviewerColor.lightKeySearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + focusedKeySearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightFocusedKeySearchHighlightText, + backgroundColor: + JsonPreviewerColor.lightFocusedKeySearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + valueTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightValueText, + ), + valueSearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightValueSearchHighlightText, + backgroundColor: JsonPreviewerColor.lightValueSearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + focusedValueSearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.lightFocusedValueSearchHighlightText, + backgroundColor: + JsonPreviewerColor.lightFocusedValueSearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + indentationLineColor: JsonPreviewerColor.lightIndentationLineColor, + highlightColor: JsonPreviewerColor.lightHighlightColor, +); + +final dataExplorerThemeDark = DataExplorerTheme( + rootKeyTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkRootKeyText, + fontWeight: FontWeight.bold, + ), + propertyKeyTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkPropertyKeyText, + fontWeight: FontWeight.bold, + ), + keySearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkKeySearchHighlightText, + backgroundColor: JsonPreviewerColor.darkKeySearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + focusedKeySearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkFocusedKeySearchHighlightText, + backgroundColor: JsonPreviewerColor.darkFocusedKeySearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + valueTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkValueText, + ), + valueSearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkValueSearchHighlightText, + backgroundColor: JsonPreviewerColor.darkValueSearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + focusedValueSearchHighlightTextStyle: kCodeStyle.copyWith( + color: JsonPreviewerColor.darkFocusedValueSearchHighlightText, + backgroundColor: + JsonPreviewerColor.darkFocusedValueSearchHighlightBackground, + fontWeight: FontWeight.bold, + ), + indentationLineColor: JsonPreviewerColor.darkIndentationLineColor, + highlightColor: JsonPreviewerColor.darkHighlightColor, +); + class JsonPreviewer extends StatefulWidget { const JsonPreviewer({ super.key, @@ -97,31 +215,8 @@ class _JsonPreviewerState extends State { 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), - ), - ), - ), - ), + rootInformationBuilder: (context, node) => + rootInfoBox(context, node), /// Build an animated collapse/expand indicator. Implicitly /// animates the indicator when @@ -153,61 +248,22 @@ class _JsonPreviewerState extends State { /// 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, - ); - }, + // 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), - ), + theme: (Theme.of(context).brightness == Brightness.light) + ? dataExplorerThemeLight + : dataExplorerThemeDark, ), ), ], @@ -216,6 +272,27 @@ class _JsonPreviewerState extends State { ); } + DecoratedBox rootInfoBox(BuildContext context, NodeViewModelState node) { + return DecoratedBox( + decoration: BoxDecoration( + color: (Theme.of(context).brightness == Brightness.light) + ? JsonPreviewerColor.lightRootInfoBox + : JsonPreviewerColor.darkRootInfoBox, + borderRadius: const BorderRadius.all(Radius.circular(2)), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + child: Text( + node.isClass ? '{${node.childrenCount}}' : '[${node.childrenCount}]', + style: kCodeStyle, + ), + ), + ); + } + String _searchFocusText() => '${store.focusedSearchResultIndex + 1} of ${store.searchResults.length}'; From 79d5951a6456386a601677598a6c59403d02fd12 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 21:27:55 +0530 Subject: [PATCH 03/10] Update num & bool color in JSON previewer --- lib/widgets/json_previewer.dart | 78 +++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index dbc122f5..37b652c0 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -3,7 +3,7 @@ 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'; - +import 'package:url_launcher/url_launcher_string.dart'; import '../consts.dart'; class JsonPreviewerColor { @@ -19,6 +19,8 @@ class JsonPreviewerColor { Color(0xFFF29D0B); static const Color lightValueText = Color(0xffc41a16); static const Color lightValueSearchHighlightText = Color(0xffc41a16); + static const Color lightValueNum = Color(0xff3F6E74); + static const Color lightValueBool = Color(0xff1c00cf); static const Color lightValueSearchHighlightBackground = Color(0xFFFFEDAD); static const Color lightFocusedValueSearchHighlightText = Colors.black; static const Color lightFocusedValueSearchHighlightBackground = @@ -38,6 +40,8 @@ class JsonPreviewerColor { Color(0xffc41a16); static const Color darkValueText = Color(0xffecc48d); static const Color darkValueSearchHighlightText = Color(0xffecc48d); + static const Color darkValueNum = Color(0xffaddb67); + static const Color darkValueBool = Color(0xff82aaff); static const Color darkValueSearchHighlightBackground = Color(0xff9b703f); static const Color darkFocusedValueSearchHighlightText = Color(0xffd6deeb); static const Color darkFocusedValueSearchHighlightBackground = @@ -217,20 +221,11 @@ class _JsonPreviewerState extends State { itemSpacing: 4, rootInformationBuilder: (context, node) => rootInfoBox(context, node), - - /// 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, @@ -242,25 +237,9 @@ class _JsonPreviewerState extends State { 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 + valueStyleBuilder: (value, style) => + valueStyleOverride(context, value, style), theme: (Theme.of(context).brightness == Brightness.light) ? dataExplorerThemeLight : dataExplorerThemeDark, @@ -272,6 +251,45 @@ class _JsonPreviewerState extends State { ); } + PropertyOverrides valueStyleOverride( + BuildContext context, + dynamic value, + TextStyle style, + ) { + TextStyle newStyle = style; + bool isUrl = false; + if (value.runtimeType.toString() == "num" || + value.runtimeType.toString() == "double" || + value.runtimeType.toString() == "int") { + newStyle = style.copyWith( + color: (Theme.of(context).brightness == Brightness.light) + ? JsonPreviewerColor.lightValueNum + : JsonPreviewerColor.darkValueNum, + ); + } else if (value.runtimeType.toString() == "bool") { + newStyle = style.copyWith( + color: (Theme.of(context).brightness == Brightness.light) + ? JsonPreviewerColor.lightValueBool + : JsonPreviewerColor.darkValueBool, + ); + } else { + isUrl = _valueIsUrl(value); + if (isUrl) { + newStyle = style.copyWith( + decoration: TextDecoration.underline, + decorationColor: (Theme.of(context).brightness == Brightness.light) + ? JsonPreviewerColor.lightValueText + : JsonPreviewerColor.darkValueText, + ); + } + } + + return PropertyOverrides( + style: newStyle, + onTap: isUrl ? () => _launchUrl(value as String) : null, + ); + } + DecoratedBox rootInfoBox(BuildContext context, NodeViewModelState node) { return DecoratedBox( decoration: BoxDecoration( @@ -323,6 +341,10 @@ class _JsonPreviewerState extends State { return false; } + Future _launchUrl(String url) { + return launchUrlString(url); + } + @override void dispose() { searchController.dispose(); From fbefcfd536f97651f9045ef69b63093143f35df1 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 21:29:17 +0530 Subject: [PATCH 04/10] Update previewer_test.dart --- test/widgets/previewer_test.dart | 48 ++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/test/widgets/previewer_test.dart b/test/widgets/previewer_test.dart index 3ca571ef..c98a9070 100644 --- a/test/widgets/previewer_test.dart +++ b/test/widgets/previewer_test.dart @@ -13,7 +13,12 @@ void main() { MaterialApp( title: 'Previewer', home: Scaffold( - body: Previewer(type: 'application', subtype: 'pdf', bytes: bytes1), + body: Previewer( + type: 'application', + subtype: 'pdf', + bytes: bytes1, + body: "", + ), ), ), ); @@ -30,7 +35,12 @@ void main() { MaterialApp( title: 'Previewer', home: Scaffold( - body: Previewer(type: 'audio', subtype: 'mpeg', bytes: bytes1), + body: Previewer( + type: 'audio', + subtype: 'mpeg', + bytes: bytes1, + body: "", + ), ), ), ); @@ -43,7 +53,12 @@ void main() { MaterialApp( title: 'Previewer', home: Scaffold( - body: Previewer(type: 'video', subtype: 'H264', bytes: bytes1), + body: Previewer( + type: 'video', + subtype: 'H264', + bytes: bytes1, + body: "", + ), ), ), ); @@ -58,7 +73,12 @@ void main() { MaterialApp( title: 'Previewer', home: Scaffold( - body: Previewer(type: 'model', subtype: 'step+xml', bytes: bytes1), + body: Previewer( + type: 'model', + subtype: 'step+xml', + bytes: bytes1, + body: "", + ), ), ), ); @@ -74,8 +94,12 @@ void main() { MaterialApp( title: 'Previewer', home: Scaffold( - body: - Previewer(type: 'image', subtype: 'jpeg', bytes: kBodyBytesJpeg), + body: Previewer( + type: 'image', + subtype: 'jpeg', + bytes: kBodyBytesJpeg, + body: "", + ), ), ), ); @@ -113,7 +137,11 @@ void main() { title: 'Previewer', home: Scaffold( body: Previewer( - type: 'image', subtype: 'jpeg', bytes: bytesJpegCorrupt), + type: 'image', + subtype: 'jpeg', + bytes: bytesJpegCorrupt, + body: "", + ), ), ), ); @@ -130,7 +158,11 @@ void main() { title: 'Previewer', home: Scaffold( body: Previewer( - type: 'audio', subtype: 'mpeg', bytes: bytesAudioCorrupt), + type: 'audio', + subtype: 'mpeg', + bytes: bytesAudioCorrupt, + body: "", + ), ), ), ); From 4f2c35c8d8bfbcc14d278e88ae6cbe3e5ee87ca2 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 21:34:04 +0530 Subject: [PATCH 05/10] Remove rootNameFormatter --- lib/widgets/json_previewer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 37b652c0..029ed2e6 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -237,7 +237,6 @@ class _JsonPreviewerState extends State { onPressed: () => _printNode(node), ) : const SizedBox(), - rootNameFormatter: (dynamic name) => '$name', valueStyleBuilder: (value, style) => valueStyleOverride(context, value, style), theme: (Theme.of(context).brightness == Brightness.light) From 52d4bc0721ca10104f18ab3667306f4f988db608 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 23:48:14 +0530 Subject: [PATCH 06/10] Add copy functionality --- lib/widgets/json_previewer.dart | 65 +++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 029ed2e6..6ce3b5e9 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -1,10 +1,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:json_data_explorer/json_data_explorer.dart'; import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../consts.dart'; +import "snackbars.dart"; class JsonPreviewerColor { const JsonPreviewerColor._(); @@ -151,6 +153,7 @@ class _JsonPreviewerState extends State { @override Widget build(BuildContext context) { + var sm = ScaffoldMessenger.of(context); return ChangeNotifierProvider.value( value: store, child: Consumer( @@ -162,9 +165,6 @@ class _JsonPreviewerState extends State { 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', @@ -227,14 +227,22 @@ class _JsonPreviewerState extends State { child: const Icon(Icons.arrow_drop_down), ), trailingBuilder: (context, node) => node.isFocused - ? IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(maxHeight: 18), - icon: const Icon( - Icons.copy, - size: 18, + ? Padding( + padding: const EdgeInsets.only(right: 12), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(maxHeight: 18), + icon: const Icon( + Icons.copy, + size: 18, + ), + onPressed: () async { + await Clipboard.setData(ClipboardData( + text: kEncoder.convert(toJson(node)))); + sm.hideCurrentSnackBar(); + sm.showSnackBar(getSnackBar("Copied")); + }, ), - onPressed: () => _printNode(node), ) : const SizedBox(), valueStyleBuilder: (value, style) => @@ -313,15 +321,6 @@ class _JsonPreviewerState extends State { 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) { @@ -350,3 +349,31 @@ class _JsonPreviewerState extends State { super.dispose(); } } + +dynamic toJson( + NodeViewModelState node, +) { + dynamic res; + if (node.isRoot) { + if (node.isClass) { + res = {}; + for (var i in node.children) { + res.addAll(toJson(i)); + } + } + if (node.isArray) { + res = []; + for (var i in node.children) { + res.add(toJson(i)); + } + } + } else { + res = node.value; + } + + if (node.parent != null && node.parent!.isArray) { + return res; + } else { + return {node.key: res}; + } +} From 213db2f13c107cff15955ce03d69f72bfcba3d9d Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sun, 15 Oct 2023 23:53:15 +0530 Subject: [PATCH 07/10] Update json_previewer.dart --- lib/widgets/json_previewer.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 6ce3b5e9..1fecf8b8 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -237,10 +237,16 @@ class _JsonPreviewerState extends State { size: 18, ), onPressed: () async { - await Clipboard.setData(ClipboardData( - text: kEncoder.convert(toJson(node)))); + String msg; + try { + await Clipboard.setData(ClipboardData( + text: kEncoder.convert(toJson(node)))); + msg = "Copied"; + } catch (e) { + msg = "An error occurred"; + } sm.hideCurrentSnackBar(); - sm.showSnackBar(getSnackBar("Copied")); + sm.showSnackBar(getSnackBar(msg)); }, ), ) From 1357feb8f0fe1e2ecb0bd92a6199026a74159a00 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 16 Oct 2023 00:08:55 +0530 Subject: [PATCH 08/10] Change layout and add copy --- lib/widgets/json_previewer.dart | 128 ++++++++++++++++---------------- 1 file changed, 66 insertions(+), 62 deletions(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 1fecf8b8..2baab3fb 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -160,6 +160,60 @@ class _JsonPreviewerState extends State { builder: (context, state, child) => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + await _copy(kEncoder.convert(jsonDecode(widget.code)), sm); + }, + child: const Text('Copy'), + ), + TextButton( + onPressed: state.areAllExpanded() ? null : state.expandAll, + child: const Text('Expand All'), + ), + TextButton( + onPressed: state.areAllCollapsed() ? null : state.collapseAll, + child: const Text('Collapse All'), + ), + ], + ), + Expanded( + child: JsonDataExplorer( + nodes: state.displayNodes, + itemScrollController: itemScrollController, + itemSpacing: 4, + rootInformationBuilder: (context, node) => + rootInfoBox(context, node), + collapsableToggleBuilder: (context, node) => AnimatedRotation( + turns: node.isCollapsed ? -0.25 : 0, + duration: const Duration(milliseconds: 300), + child: const Icon(Icons.arrow_drop_down), + ), + trailingBuilder: (context, node) => node.isFocused + ? Padding( + padding: const EdgeInsets.only(right: 12), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(maxHeight: 18), + icon: const Icon( + Icons.copy, + size: 18, + ), + onPressed: () async { + await _copy(kEncoder.convert(toJson(node)), sm); + }, + ), + ) + : const SizedBox(), + valueStyleBuilder: (value, style) => + valueStyleOverride(context, value, style), + theme: (Theme.of(context).brightness == Brightness.light) + ? dataExplorerThemeLight + : dataExplorerThemeDark, + ), + ), Row( children: [ Expanded( @@ -196,74 +250,24 @@ class _JsonPreviewerState extends State { 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, - rootInformationBuilder: (context, node) => - rootInfoBox(context, node), - collapsableToggleBuilder: (context, node) => AnimatedRotation( - turns: node.isCollapsed ? -0.25 : 0, - duration: const Duration(milliseconds: 300), - child: const Icon(Icons.arrow_drop_down), - ), - trailingBuilder: (context, node) => node.isFocused - ? Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(maxHeight: 18), - icon: const Icon( - Icons.copy, - size: 18, - ), - onPressed: () async { - String msg; - try { - await Clipboard.setData(ClipboardData( - text: kEncoder.convert(toJson(node)))); - msg = "Copied"; - } catch (e) { - msg = "An error occurred"; - } - sm.hideCurrentSnackBar(); - sm.showSnackBar(getSnackBar(msg)); - }, - ), - ) - : const SizedBox(), - valueStyleBuilder: (value, style) => - valueStyleOverride(context, value, style), - theme: (Theme.of(context).brightness == Brightness.light) - ? dataExplorerThemeLight - : dataExplorerThemeDark, - ), - ), ], ), ), ); } + Future _copy(String text, ScaffoldMessengerState sm) async { + String msg; + try { + await Clipboard.setData(ClipboardData(text: text)); + msg = "Copied"; + } catch (e) { + msg = "An error occurred"; + } + sm.hideCurrentSnackBar(); + sm.showSnackBar(getSnackBar(msg)); + } + PropertyOverrides valueStyleOverride( BuildContext context, dynamic value, From 692dd9b0ab87a4ff94b928262e0b8647acc2274d Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 16 Oct 2023 01:53:10 +0530 Subject: [PATCH 09/10] JsonSearchField --- lib/widgets/textfields.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/widgets/textfields.dart b/lib/widgets/textfields.dart index ed0e2cd1..ff05ca0f 100644 --- a/lib/widgets/textfields.dart +++ b/lib/widgets/textfields.dart @@ -97,3 +97,25 @@ class _CellFieldState extends State { ); } } + +class JsonSearchField extends StatelessWidget { + const JsonSearchField({super.key, this.onChanged, this.controller}); + + final void Function(String)? onChanged; + final TextEditingController? controller; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + onChanged: onChanged, + style: kCodeStyle, + cursorHeight: 18, + decoration: const InputDecoration( + isDense: true, + border: InputBorder.none, + hintText: 'Search', + ), + ); + } +} From 505f7e172cb0713de251821e7db8b22400e4670e Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Mon, 16 Oct 2023 01:59:48 +0530 Subject: [PATCH 10/10] Update search field --- lib/widgets/json_previewer.dart | 76 +++++++++++++++++++-------------- lib/widgets/textfields.dart | 2 +- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 2baab3fb..316387f9 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -7,6 +7,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../consts.dart'; import "snackbars.dart"; +import 'textfields.dart'; class JsonPreviewerColor { const JsonPreviewerColor._(); @@ -214,41 +215,54 @@ class _JsonPreviewerState extends State { : dataExplorerThemeDark, ), ), - Row( - children: [ - Expanded( - child: TextField( - controller: searchController, - onChanged: (term) => state.search(term), - decoration: const InputDecoration( - hintText: 'Search', + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + border: Border.all( + color: Theme.of(context).colorScheme.surfaceVariant), + borderRadius: kBorderRadius8, + ), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: Icon( + Icons.search, + size: 18, ), ), - ), - 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), + Expanded( + child: JsonSearchField( + controller: searchController, + onChanged: (term) => state.search(term), + ), ), - if (state.searchResults.isNotEmpty) - IconButton( - onPressed: () { - store.focusNextSearchResult(); - _scrollToSearchMatch(); - }, - icon: const Icon(Icons.arrow_drop_down), + const SizedBox( + width: 8, ), - ], - ), - const SizedBox( - height: 16.0, + if (state.searchResults.isNotEmpty) + Text(_searchFocusText(), + style: Theme.of(context).textTheme.bodySmall), + if (state.searchResults.isNotEmpty) + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + store.focusPreviousSearchResult(); + _scrollToSearchMatch(); + }, + icon: const Icon(Icons.arrow_drop_up), + ), + if (state.searchResults.isNotEmpty) + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + store.focusNextSearchResult(); + _scrollToSearchMatch(); + }, + icon: const Icon(Icons.arrow_drop_down), + ), + ], + ), ), ], ), diff --git a/lib/widgets/textfields.dart b/lib/widgets/textfields.dart index ff05ca0f..ff7b7313 100644 --- a/lib/widgets/textfields.dart +++ b/lib/widgets/textfields.dart @@ -114,7 +114,7 @@ class JsonSearchField extends StatelessWidget { decoration: const InputDecoration( isDense: true, border: InputBorder.none, - hintText: 'Search', + hintText: 'Search..', ), ); }