import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../theme/app_theme.dart'; class JsonTreeView extends StatelessWidget { final dynamic data; final int depth; final bool initiallyExpanded; const JsonTreeView({ super.key, required this.data, this.depth = 0, this.initiallyExpanded = false, }); @override Widget build(BuildContext context) { if (data is Map) { return _MapNode( map: data as Map, depth: depth, initiallyExpanded: initiallyExpanded || depth == 0, ); } else if (data is List) { return _ListNode( list: data as List, depth: depth, initiallyExpanded: initiallyExpanded || depth == 0, ); } else { return _ValueNode(value: data, depth: depth); } } } class _MapNode extends StatefulWidget { final Map map; final int depth; final bool initiallyExpanded; const _MapNode({ required this.map, required this.depth, this.initiallyExpanded = false, }); @override State<_MapNode> createState() => _MapNodeState(); } class _MapNodeState extends State<_MapNode> { late bool _expanded; @override void initState() { super.initState(); _expanded = widget.initiallyExpanded; } @override Widget build(BuildContext context) { if (widget.map.isEmpty) { return Text('{}', style: _valueStyle); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: () => setState(() => _expanded = !_expanded), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _expanded ? Icons.expand_more : Icons.chevron_right, size: 14, color: AppColors.textMuted, ), const SizedBox(width: 4), Text( '{${widget.map.length} keys}', style: _hintStyle, ), if (widget.depth == 0) ...[ const SizedBox(width: 8), _CopyButton(data: widget.map), ], ], ), ), if (_expanded) Padding( padding: const EdgeInsets.only(left: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: widget.map.entries.map((e) { return Padding( padding: const EdgeInsets.symmetric(vertical: 1), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${e.key}: ', style: _keyStyle, ), Expanded( child: JsonTreeView( data: e.value, depth: widget.depth + 1, ), ), ], ), ); }).toList(), ), ), ], ); } } class _ListNode extends StatefulWidget { final List list; final int depth; final bool initiallyExpanded; const _ListNode({ required this.list, required this.depth, this.initiallyExpanded = false, }); @override State<_ListNode> createState() => _ListNodeState(); } class _ListNodeState extends State<_ListNode> { late bool _expanded; @override void initState() { super.initState(); _expanded = widget.initiallyExpanded; } @override Widget build(BuildContext context) { if (widget.list.isEmpty) { return Text('[]', style: _valueStyle); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: () => setState(() => _expanded = !_expanded), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _expanded ? Icons.expand_more : Icons.chevron_right, size: 14, color: AppColors.textMuted, ), const SizedBox(width: 4), Text( '[${widget.list.length} items]', style: _hintStyle, ), ], ), ), if (_expanded) Padding( padding: const EdgeInsets.only(left: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: widget.list.asMap().entries.map((e) { return Padding( padding: const EdgeInsets.symmetric(vertical: 1), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${e.key}: ', style: _indexStyle, ), Expanded( child: JsonTreeView( data: e.value, depth: widget.depth + 1, ), ), ], ), ); }).toList(), ), ), ], ); } } class _ValueNode extends StatelessWidget { final dynamic value; final int depth; const _ValueNode({required this.value, required this.depth}); @override Widget build(BuildContext context) { if (value == null) { return Text('null', style: _nullStyle); } if (value is bool) { return Text('$value', style: _boolStyle); } if (value is num) { return Text('$value', style: _numberStyle); } final str = value.toString(); if (str.length > 200) { return _LongStringNode(text: str); } return SelectableText('"$str"', style: _stringStyle); } } class _LongStringNode extends StatefulWidget { final String text; const _LongStringNode({required this.text}); @override State<_LongStringNode> createState() => _LongStringNodeState(); } class _LongStringNodeState extends State<_LongStringNode> { bool _expanded = false; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( onTap: () => setState(() => _expanded = !_expanded), child: Text( _expanded ? '"${widget.text}"' : '"${widget.text.substring(0, 200)}..." (${widget.text.length} chars)', style: _stringStyle, ), ), ], ); } } class _CopyButton extends StatelessWidget { final dynamic data; const _CopyButton({required this.data}); @override Widget build(BuildContext context) { return InkWell( onTap: () { final json = const JsonEncoder.withIndent(' ').convert(data); Clipboard.setData(ClipboardData(text: json)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Copied to clipboard'), duration: Duration(seconds: 1), ), ); }, child: const Icon( Icons.copy, size: 14, color: AppColors.textMuted, ), ); } } const _keyStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.user, ); const _indexStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.textMuted, ); const _valueStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.textSecondary, ); const _stringStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: Color(0xFF10B981), ); const _numberStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: Color(0xFFF59E0B), ); const _boolStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: Color(0xFFA855F7), ); const _nullStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.textMuted, fontStyle: FontStyle.italic, ); const _hintStyle = TextStyle( fontFamily: 'JetBrains Mono', fontSize: 12, color: AppColors.textMuted, );