Merge branch 'main' into fix-kotlin-codegen-import-issue

This commit is contained in:
Ankit Mahato
2024-03-09 21:17:09 +05:30
committed by GitHub
54 changed files with 875 additions and 381 deletions

View File

@ -378,6 +378,7 @@ const Map<String, Map<String, List<ResponseBodyView>>>
kSubTypeDefaultViewOptions: kRawBodyViewOptions,
kSubTypeCss: kCodeRawBodyViewOptions,
kSubTypeHtml: kCodeRawBodyViewOptions,
kSubTypeCsv: kPreviewRawBodyViewOptions,
kSubTypeJavascript: kCodeRawBodyViewOptions,
kSubTypeMarkdown: kCodeRawBodyViewOptions,
kSubTypeTextXml: kCodeRawBodyViewOptions,
@ -495,7 +496,11 @@ const kAudioError =
const kRaiseIssue =
"\nPlease raise an issue in API Dash GitHub repo so that we can resolve it.";
const kHintTextUrlCard = "Enter API endpoint like api.foss42.com/country/codes";
const kCsvError =
"There seems to be an issue rendering this CSV. Please raise an issue in API Dash GitHub repo so that we can resolve it.";
const kHintTextUrlCard =
"Enter API endpoint like api.apidash.dev/country/codes";
const kLabelPlusNew = "+ New";
const kLabelSend = "Send";
const kLabelSending = "Sending..";

View File

@ -29,6 +29,7 @@ class RequestModel {
this.responseStatus,
this.message,
this.responseModel,
this.isWorking = false,
});
final String id;
@ -47,6 +48,7 @@ class RequestModel {
final int? responseStatus;
final String? message;
final ResponseModel? responseModel;
final bool isWorking;
List<NameValueModel>? get enabledRequestHeaders =>
getEnabledRows(requestHeaders, isHeaderEnabledList);
@ -106,6 +108,7 @@ class RequestModel {
int? responseStatus,
String? message,
ResponseModel? responseModel,
bool? isWorking,
}) {
var headers = requestHeaders ?? this.requestHeaders;
var params = requestParams ?? this.requestParams;
@ -129,6 +132,7 @@ class RequestModel {
responseStatus: responseStatus ?? this.responseStatus,
message: message ?? this.message,
responseModel: responseModel ?? this.responseModel,
isWorking: isWorking ?? this.isWorking,
);
}

View File

@ -156,11 +156,20 @@ class CollectionStateNotifier
}
Future<void> sendRequest(String id) async {
ref.read(sentRequestIdStateProvider.notifier).state = id;
ref.read(codePaneVisibleStateProvider.notifier).state = false;
final defaultUriScheme =
ref.read(settingsProvider.select((value) => value.defaultUriScheme));
final defaultUriScheme = ref.read(
settingsProvider.select(
(value) => value.defaultUriScheme,
),
);
RequestModel requestModel = state![id]!;
// set current model's isWorking to true and update state
var map = {...state!};
map[id] = requestModel.copyWith(isWorking: true);
state = map;
(http.Response?, Duration?, String?)? responseRec = await request(
requestModel,
defaultUriScheme: defaultUriScheme,
@ -172,6 +181,7 @@ class CollectionStateNotifier
newRequestModel = requestModel.copyWith(
responseStatus: -1,
message: responseRec.$3,
isWorking: false,
);
} else {
final responseModel = baseResponseModel.fromResponse(
@ -183,10 +193,12 @@ class CollectionStateNotifier
responseStatus: statusCode,
message: kResponseCodeReasons[statusCode],
responseModel: responseModel,
isWorking: false,
);
}
ref.read(sentRequestIdStateProvider.notifier).state = null;
var map = {...state!};
// update state with response data
map = {...state!};
map[id] = newRequestModel;
state = map;
}

View File

@ -3,7 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
final navRailIndexStateProvider = StateProvider<int>((ref) => 0);
final selectedIdEditStateProvider = StateProvider<String?>((ref) => null);
final sentRequestIdStateProvider = StateProvider<String?>((ref) => null);
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
final saveDataStateProvider = StateProvider<bool>((ref) => false);
final clearDataStateProvider = StateProvider<bool>((ref) => false);

View File

@ -9,13 +9,14 @@ class ResponsePane extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedIdStateProvider);
final sentRequestId = ref.watch(sentRequestIdStateProvider);
final isWorking = ref.watch(
selectedRequestModelProvider.select((value) => value?.isWorking)) ??
false;
final responseStatus = ref.watch(
selectedRequestModelProvider.select((value) => value?.responseStatus));
final message = ref
.watch(selectedRequestModelProvider.select((value) => value?.message));
if (sentRequestId != null && sentRequestId == selectedId) {
if (isWorking) {
return const SendingWidget();
}
if (responseStatus == null) {

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/collection_providers.dart';
import 'package:apidash/consts.dart';
import 'details_card/details_card.dart';
import 'url_card.dart';
@ -10,6 +12,7 @@ class RequestEditor extends StatelessWidget {
Widget build(BuildContext context) {
return const Column(
children: [
RequestEditorTopBar(),
EditorPaneRequestURLCard(),
kVSpacer10,
Expanded(
@ -19,3 +22,91 @@ class RequestEditor extends StatelessWidget {
);
}
}
class RequestEditorTopBar extends ConsumerWidget {
const RequestEditorTopBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final id = ref.watch(selectedIdStateProvider);
final name =
ref.watch(selectedRequestModelProvider.select((value) => value?.name));
return Padding(
padding: const EdgeInsets.only(
left: 12.0,
top: 4.0,
right: 8.0,
bottom: 4.0,
),
child: Row(
children: [
Expanded(
child: Text(
name ?? "",
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
const SizedBox(
width: 6,
),
SizedBox(
width: 90,
height: 24,
child: FilledButton.tonalIcon(
style: const ButtonStyle(
padding: MaterialStatePropertyAll(EdgeInsets.zero),
),
onPressed: () {
showDialog(
context: context,
builder: (context) {
final controller =
TextEditingController(text: name ?? "");
controller.selection = TextSelection(
baseOffset: 0, extentOffset: controller.text.length);
return AlertDialog(
title: const Text('Rename Request'),
content: TextField(
autofocus: true,
controller: controller,
decoration:
const InputDecoration(hintText: "Enter new name"),
),
actions: <Widget>[
OutlinedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('CANCEL')),
FilledButton(
onPressed: () {
final val = controller.text.trim();
ref
.read(collectionStateNotifierProvider
.notifier)
.update(id!, name: val);
Navigator.pop(context);
controller.dispose();
},
child: const Text('OK')),
],
);
});
},
icon: const Icon(
Icons.edit,
size: 12,
),
label: Text(
"Rename",
style: Theme.of(context).textTheme.bodySmall,
),
),
)
],
),
);
}
}

View File

@ -81,6 +81,11 @@ class URLTextField extends ConsumerWidget {
.read(collectionStateNotifierProvider.notifier)
.update(selectedId, url: value);
},
onFieldSubmitted: (value) {
ref
.read(collectionStateNotifierProvider.notifier)
.sendRequest(selectedId);
},
);
}
}
@ -93,10 +98,11 @@ class SendButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedIdStateProvider);
final sentRequestId = ref.watch(sentRequestIdStateProvider);
final isWorking = ref.watch(
selectedRequestModelProvider.select((value) => value?.isWorking));
return SendRequestButton(
selectedId: selectedId,
sentRequestId: sentRequestId,
isWorking: isWorking ?? false,
onTap: () {
ref
.read(collectionStateNotifierProvider.notifier)

View File

@ -65,7 +65,7 @@ class SettingsPage extends ConsumerWidget {
hoverColor: kColorTransparent,
title: const Text('Default URI Scheme'),
subtitle: Text(
'api.foss42.com${settings.defaultUriScheme}://api.foss42.com'),
'api.apidash.dev${settings.defaultUriScheme}://api.apidash.dev'),
trailing: DropdownMenu(
onSelected: (value) {
ref

View File

@ -43,7 +43,7 @@ String getShortPath(String path) {
var f = p.split(path);
if (f.length > 2) {
f = f.sublist(f.length - 2);
return ".../${p.joinAll(f)}";
return p.join("...", p.joinAll(f));
}
return path;
}

View File

@ -2,6 +2,7 @@ Map<String, String> headers = {
"Accept": "Specifies the media types that are acceptable for the response.",
"Accept-Encoding":
"Indicates the encoding methods the client can understand.",
"Accept-Charset": "Specifies the character sets that are acceptable.",
"Access-Control-Allow-Headers":
"Specifies a list of HTTP headers that can be used in an actual request after a preflight request including the Access-Control-Request-Headers header is made.",
"Access-Control-Allow-Methods":
@ -41,11 +42,16 @@ Map<String, String> headers = {
"Cross-Origin-Resource-Policy":
"Controls how cross-origin requests for resources are handled.",
"Date": "Indicates the date and time at which the message was sent.",
"Device-Memory":
"Indicates the approximate amount of device memory in gigabytes.",
"DNT":
"Informs websites whether the user's preference is to opt out of online tracking.",
"Expect": "Indicates certain expectations that need to be met by the server.",
"Expires":
"Contains the date/time after which the response is considered expired",
"Forwarded":
"Contains information from the client-facing side of proxy servers that is altered or lost when a proxy is involved in the path of the request.",
"From": "Contains an Internet email address for a human user who controls the requesting user agent.",
"Host": "Specifies the domain name of the server and the port number.",
"If-Match":
"Used for conditional requests, allows the server to respond based on certain conditions.",
@ -57,9 +63,15 @@ Map<String, String> headers = {
"Used in conjunction with the Range header to conditionally request a partial resource.",
"If-Unmodified-Since":
"Used for conditional requests, allows the server to respond based on certain conditions.",
"Keep-Alive":
"Used to allow the connection to be reused for further requests.",
"Location":
"Indicates the URL a client should redirect to for further interaction.",
"Max-Forwards":
"Indicates the remaining number of times a request can be forwarded by proxies.",
"Origin": "Specifies the origin of a cross-origin request.",
"Proxy-Authorization":
"Contains credentials for authenticating a client with a proxy server.",
"Range":
"Used to request only part of a resource, typically in the context of downloading large files.",
"Referer":
@ -68,10 +80,14 @@ Map<String, String> headers = {
"Specifies how much information the browser should include in the Referer header when navigating to other pages.",
"Retry-After":
"Informs the client how long it should wait before making another request after a server has responded with a rate-limiting status code.",
"Save-Data":
"Indicates the client's preference for reduced data usage.",
"Server": "Indicates the software used by the origin server.",
"Strict-Transport-Security":
"Instructs the browser to always use HTTPS for the given domain.",
"TE": "Specifies the transfer encodings that are acceptable to the client.",
"Upgrade-Insecure-Requests":
"Instructs the browser to prefer secure connections when available.",
"User-Agent":
"Identifies the client software and version making the request.",
"Via":

View File

@ -47,36 +47,37 @@ class CopyButton extends StatelessWidget {
class SendRequestButton extends StatelessWidget {
const SendRequestButton({
super.key,
required this.selectedId,
required this.sentRequestId,
required this.isWorking,
required this.onTap,
});
final String? selectedId;
final String? sentRequestId;
final bool isWorking;
final void Function() onTap;
@override
Widget build(BuildContext context) {
bool disable = sentRequestId != null;
return FilledButton(
onPressed: disable ? null : onTap,
onPressed: isWorking ? null : onTap,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
disable
? (selectedId == sentRequestId ? kLabelSending : kLabelBusy)
: kLabelSend,
style: kTextStyleButton,
),
if (!disable) kHSpacer10,
if (!disable)
const Icon(
size: 16,
Icons.send,
),
],
children: isWorking
? const [
Text(
kLabelSending,
style: kTextStyleButton,
),
]
: const [
Text(
kLabelSend,
style: kTextStyleButton,
),
kHSpacer10,
Icon(
size: 16,
Icons.send,
),
],
),
);
}

View File

@ -97,6 +97,9 @@ class SidebarRequestCard extends StatelessWidget {
onTapOutsideNameEditor?.call();
//FocusScope.of(context).unfocus();
},
onFieldSubmitted: (value) {
onTapOutsideNameEditor?.call();
},
onChanged: onChangedNameEditor,
decoration: const InputDecoration(
isCollapsed: true,

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:csv/csv.dart';
import 'error_message.dart';
import '../consts.dart';
class CsvPreviewer extends StatelessWidget {
const CsvPreviewer({super.key, required this.body});
final String body;
@override
Widget build(BuildContext context) {
try {
final List<List<dynamic>> csvData =
const CsvToListConverter().convert(body, eol: '\n');
return SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: csvData[0]
.map(
(item) => DataColumn(
label: Text(
item.toString(),
),
),
)
.toList(),
rows: csvData
.skip(1)
.map(
(csvrow) => DataRow(
cells: csvrow
.map(
(csvItem) => DataCell(
Text(
csvItem.toString(),
),
),
)
.toList(),
),
)
.toList(),
),
),
);
} catch (e) {
return const ErrorMessage(message: kCsvError);
}
}
}

View File

@ -46,32 +46,34 @@ class _FormDataFieldState extends State<FormDataField> {
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintStyle: kCodeStyle.copyWith(
color: colorScheme.outline.withOpacity(
hintStyle: kCodeStyle.copyWith(
color: colorScheme.outline.withOpacity(
kHintOpacity,
),
),
hintText: widget.hintText,
contentPadding: const EdgeInsets.only(bottom: 16),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.primary.withOpacity(
kHintOpacity,
),
),
hintText: widget.hintText,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.primary.withOpacity(
kHintOpacity,
),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.surfaceVariant,
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colorScheme.surfaceVariant,
),
),
suffixIcon: DropdownButtonFormData(
formDataType: widget.formDataType,
onChanged: (p0) {
if (widget.onFormDataTypeChanged != null) {
widget.onFormDataTypeChanged!(p0);
}
},
)),
),
suffixIcon: DropdownButtonFormData(
formDataType: widget.formDataType,
onChanged: (p0) {
if (widget.onFormDataTypeChanged != null) {
widget.onFormDataTypeChanged!(p0);
}
},
),
),
onChanged: widget.onChanged,
),
),

View File

@ -1,6 +1,6 @@
import 'package:apidash/consts.dart';
import 'package:apidash/utils/header_utils.dart';
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
class HeaderField extends StatefulWidget {
@ -41,6 +41,7 @@ class _HeaderFieldState extends State<HeaderField> {
@override
void didUpdateWidget(HeaderField oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialValue != widget.initialValue) {
controller.text = widget.initialValue ?? "";
controller.selection =
@ -54,8 +55,8 @@ class _HeaderFieldState extends State<HeaderField> {
return TypeAheadField(
key: Key(widget.keyId),
hideOnEmpty: true,
minCharsForSuggestions: 1,
onSuggestionSelected: (value) {
controller: controller,
onSelected: (value) {
setState(() {
controller.text = value;
});
@ -68,19 +69,17 @@ class _HeaderFieldState extends State<HeaderField> {
);
},
suggestionsCallback: headerSuggestionCallback,
suggestionsBoxDecoration: suggestionBoxDecorations(context),
textFieldConfiguration: TextFieldConfiguration(
decorationBuilder: (context, child) =>
suggestionBoxDecorations(context, child, colorScheme),
constraints: const BoxConstraints(maxHeight: 400),
builder: (context, controller, focusNode) => TextField(
onChanged: widget.onChanged,
controller: controller,
style: kCodeStyle.copyWith(
color: colorScheme.onSurface,
),
focusNode: focusNode,
style: kCodeStyle.copyWith(color: colorScheme.onSurface),
decoration: InputDecoration(
hintStyle: kCodeStyle.copyWith(
color: colorScheme.outline.withOpacity(
kHintOpacity,
),
),
color: colorScheme.outline.withOpacity(kHintOpacity)),
hintText: widget.hintText,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
@ -99,22 +98,26 @@ class _HeaderFieldState extends State<HeaderField> {
);
}
SuggestionsBoxDecoration suggestionBoxDecorations(BuildContext context) {
return SuggestionsBoxDecoration(
elevation: 4,
constraints: const BoxConstraints(maxHeight: 400),
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
width: 1.2,
Theme suggestionBoxDecorations(
BuildContext context, Widget child, ColorScheme colorScheme) {
return Theme(
data: ThemeData(colorScheme: colorScheme),
child: Material(
elevation: 4,
shape: RoundedRectangleBorder(
side: BorderSide(color: Theme.of(context).dividerColor, width: 1.2),
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(8)),
),
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(8)),
clipBehavior: Clip.hardEdge,
child: child,
),
clipBehavior: Clip.hardEdge,
);
}
Future<List<String>> headerSuggestionCallback(String pattern) async {
Future<List<String>?> headerSuggestionCallback(String pattern) async {
if (pattern.isEmpty) {
return null;
}
return getHeaderSuggestions(pattern);
}
}

View File

@ -154,6 +154,7 @@ class _JsonPreviewerState extends State<JsonPreviewer> {
@override
void didUpdateWidget(JsonPreviewer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.code != widget.code) {
store.buildNodes(widget.code, areAllCollapsed: true);
store.expandAll();

View File

@ -9,9 +9,12 @@ class CustomMarkdown extends StatelessWidget {
super.key,
required this.data,
this.padding = const EdgeInsets.all(16.0),
this.onTapLink,
});
final String data;
final EdgeInsets padding;
final void Function(String text, String? href, String title)? onTapLink;
@override
Widget build(BuildContext context) {
@ -25,9 +28,10 @@ class CustomMarkdown extends StatelessWidget {
data: data,
selectable: true,
extensionSet: md.ExtensionSet.gitHubFlavored,
onTapLink: (text, href, title) {
launchUrl(Uri.parse(href ?? ""));
},
onTapLink: onTapLink ??
(text, href, title) {
launchUrl(Uri.parse(href ?? ""));
},
builders: {
"inlineButton": InlineButton(),
},

View File

@ -7,6 +7,7 @@ import 'package:vector_graphics_compiler/vector_graphics_compiler.dart';
import 'error_message.dart';
import 'uint8_audio_player.dart';
import 'json_previewer.dart';
import 'csv_previewer.dart';
import '../consts.dart';
class Previewer extends StatefulWidget {
@ -81,6 +82,9 @@ class _PreviewerState extends State<Previewer> {
},
);
}
if (widget.type == kTypeText && widget.subtype == kSubTypeCsv) {
return CsvPreviewer(body: widget.body);
}
if (widget.type == kTypeVideo) {
// TODO: Video Player
}

View File

@ -7,11 +7,13 @@ class URLField extends StatelessWidget {
required this.selectedId,
this.initialValue,
this.onChanged,
this.onFieldSubmitted,
});
final String selectedId;
final String? initialValue;
final void Function(String)? onChanged;
final void Function(String)? onFieldSubmitted;
@override
Widget build(BuildContext context) {
@ -29,6 +31,7 @@ class URLField extends StatelessWidget {
border: InputBorder.none,
),
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
);
}
}
@ -95,7 +98,6 @@ class JsonSearchField extends StatelessWidget {
controller: controller,
onChanged: onChanged,
style: kCodeStyle,
cursorHeight: 18,
decoration: const InputDecoration(
isDense: true,
border: InputBorder.none,