Files
apidash/lib/widgets/json_previewer.dart
Ashita Prasad a3536b021b refactor
2024-10-22 07:06:06 +05:30

482 lines
18 KiB
Dart

import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/apidash_design_system.dart';
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 'field_json_search.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<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(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<DataExplorerStore>(
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: constraints.minWidth > kMinWindowSize.width
? [
TextButton(
onPressed: () async {
await _copy(
kJsonEncoder.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,
),
),
]
: [
IconButton(
tooltip: "Copy",
color: Theme.of(context).colorScheme.primary,
visualDensity: VisualDensity.compact,
onPressed: () async {
await _copy(
kJsonEncoder.convert(widget.code), sm);
},
icon: const Icon(
Icons.copy,
size: 16,
),
),
IconButton(
tooltip: "Expand All",
color: Theme.of(context).colorScheme.primary,
visualDensity: VisualDensity.compact,
onPressed: state.areAllExpanded()
? null
: state.expandAll,
icon: const Icon(
Icons.unfold_more,
size: 16,
),
),
IconButton(
tooltip: "Collapse All",
color: Theme.of(context).colorScheme.primary,
visualDensity: VisualDensity.compact,
onPressed: state.areAllCollapsed()
? null
: state.collapseAll,
icon: const Icon(
Icons.unfold_less,
size: 16,
),
),
]),
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(
kJsonEncoder.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.surface,
border: Border.all(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest),
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<void> _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};
}
}