Merge branch 'add-feature-header-suggestions' of https://github.com/DenserMeerkat/apidash into add-feature-header-suggestions

This commit is contained in:
DenserMeerkat
2023-10-20 18:32:03 +05:30
9 changed files with 525 additions and 15 deletions

View File

@ -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<String, Map<String, List<ResponseBodyView>>>
kResponseBodyViewOptions = {
kTypeApplication: {
kSubTypeDefaultViewOptions: kNoRawBodyViewOptions,
kSubTypeJson: kCodeRawBodyViewOptions,
kSubTypeJson: kPreviewRawBodyViewOptions,
kSubTypeOctetStream: kNoBodyViewOptions,
kSubTypePdf: kPreviewBodyViewOptions,
kSubTypeSql: kCodeRawBodyViewOptions,

View File

@ -0,0 +1,403 @@
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";
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 String 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(jsonDecode(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) => 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,
),
),
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<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: Text(
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};
}
}

View File

@ -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<Previewer> {
@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,

View File

@ -453,11 +453,17 @@ class _BodySuccessState extends State<BodySuccess> {
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),
),
),
),
),

View File

@ -97,3 +97,25 @@ class _CellFieldState extends State<CellField> {
);
}
}
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..',
),
);
}
}

View File

@ -19,3 +19,4 @@ export 'snackbars.dart';
export 'markdown.dart';
export 'uint8_audio_player.dart';
export 'tabs.dart';
export 'json_previewer.dart';

View File

@ -520,6 +520,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:
@ -656,6 +664,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:
@ -816,6 +832,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:
@ -864,6 +888,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:

View File

@ -42,6 +42,9 @@ dependencies:
printing: ^5.11.0
package_info_plus: ^4.1.0
flutter_typeahead: ^4.8.0
provider: ^6.0.5
json_data_explorer: ^0.1.0
scrollable_positioned_list: ^0.2.3
dev_dependencies:
flutter_test:

View File

@ -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: "",
),
),
),
);