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 '../utils/ui_utils.dart'; import "snackbars.dart"; import 'textfields.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 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 = 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 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 = 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, required this.code, }); final dynamic 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(widget.code, areAllCollapsed: true); store.expandAll(); } @override void didUpdateWidget(JsonPreviewer oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.code != widget.code) { store.buildNodes(widget.code, areAllCollapsed: true); store.expandAll(); } } @override Widget build(BuildContext context) { var sm = ScaffoldMessenger.of(context); return ChangeNotifierProvider.value( value: store, child: Consumer( builder: (context, state, child) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var maxRootNodeWidth = getJsonPreviewerMaxRootNodeWidth(constraints.maxWidth); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () async { await _copy(kEncoder.convert(widget.code), sm); }, child: const Text( 'Copy', style: kTextStyleButtonSmall, ), ), TextButton( onPressed: state.areAllExpanded() ? null : state.expandAll, child: const Text( 'Expand All', style: kTextStyleButtonSmall, ), ), TextButton( onPressed: state.areAllCollapsed() ? null : state.collapseAll, child: const Text( 'Collapse All', style: kTextStyleButtonSmall, ), ), ], ), 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, maxRootNodeWidth: maxRootNodeWidth, ), ), 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, ), ), Expanded( child: JsonSearchField( controller: searchController, onChanged: (term) => state.search(term), ), ), const SizedBox( width: 8, ), 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), ), ], ), ), ], ); }, ); }, ), ); } 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, 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( 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: SelectableText( node.isClass ? '{${node.childrenCount}}' : '[${node.childrenCount}]', style: kCodeStyle, ), ), ); } String _searchFocusText() => '${store.focusedSearchResultIndex + 1} of ${store.searchResults.length}'; 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; } Future _launchUrl(String url) { return launchUrlString(url); } @override void dispose() { searchController.dispose(); 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}; } }