From cc40aa953622a98af45a87eaf6e2346e463eaa53 Mon Sep 17 00:00:00 2001 From: mmjsmohit Date: Tue, 27 Feb 2024 15:27:37 +0530 Subject: [PATCH 1/2] Initial Websocket Implementation --- lib/consts.dart | 10 +- lib/models/models.dart | 2 + lib/models/request_model.dart | 28 +- lib/models/websocket/request_model.dart | 300 ++++++++++++++++++ lib/models/websocket/response_model.dart | 130 ++++++++ lib/providers/collection_providers.dart | 93 +++++- .../request_pane/request_body.dart | 35 +- .../request_pane/request_pane.dart | 107 ++++++- .../request_pane/websocket_message_body.dart | 66 ++++ .../details_card/response_pane.dart | 7 + .../home_page/editor_pane/url_card.dart | 108 ++++++- lib/services/services.dart | 1 + lib/services/websocket_service.dart | 58 ++++ lib/widgets/buttons.dart | 44 +++ lib/widgets/dropdowns.dart | 43 +++ lib/widgets/editor.dart | 11 +- lib/widgets/request_widgets.dart | 8 +- lib/widgets/response_widgets.dart | 35 +- pubspec.lock | 62 ++-- pubspec.yaml | 1 + 20 files changed, 1114 insertions(+), 35 deletions(-) create mode 100644 lib/models/websocket/request_model.dart create mode 100644 lib/models/websocket/response_model.dart create mode 100644 lib/screens/home_page/editor_pane/details_card/request_pane/websocket_message_body.dart create mode 100644 lib/services/websocket_service.dart diff --git a/lib/consts.dart b/lib/consts.dart index f8f9f97c5..9abac5533 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -241,11 +241,13 @@ final kColorHttpMethodDelete = Colors.red.shade800; enum RequestItemMenuOption { edit, delete, duplicate } +enum ProtocolType { http, websocket } + enum HTTPVerb { get, head, post, put, patch, delete } enum FormDataType { text, file } -const kSupportedUriSchemes = ["https", "http"]; +const kSupportedUriSchemes = ["https", "http", "ws", "wss"]; const kDefaultUriScheme = "https"; const kMethodsWithBody = [ HTTPVerb.post, @@ -254,7 +256,9 @@ const kMethodsWithBody = [ HTTPVerb.delete, ]; +const kDefaultProtocolType = ProtocolType.http; const kDefaultHttpMethod = HTTPVerb.get; + const kDefaultContentType = ContentType.json; enum CodegenLanguage { @@ -497,6 +501,10 @@ const kLabelPlusNew = "+ New"; const kLabelSend = "Send"; const kLabelSending = "Sending.."; const kLabelBusy = "Busy"; +const kLabelConnect = "Connect"; +const kLabelConnecting = "Connecting.."; +const kLabelDisconnect = "Disconnect"; + const kLabelCopy = "Copy"; const kLabelSave = "Save"; const kLabelDownload = "Download"; diff --git a/lib/models/models.dart b/lib/models/models.dart index 66d6f6ce6..a852f86f7 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -3,3 +3,5 @@ export 'request_model.dart'; export 'response_model.dart'; export 'settings_model.dart'; export 'form_data_model.dart'; +export 'websocket/request_model.dart'; +export 'websocket/response_model.dart'; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 9154e4281..e523761bb 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -15,6 +15,7 @@ class RequestModel { const RequestModel({ required this.id, this.method = HTTPVerb.get, + this.protocol = ProtocolType.http, this.url = "", this.name = "", this.description = "", @@ -28,10 +29,12 @@ class RequestModel { this.requestFormDataList, this.responseStatus, this.message, + this.websocketMessageBody, this.responseModel, }); final String id; + final ProtocolType protocol; final HTTPVerb method; final String url; final String name; @@ -46,6 +49,7 @@ class RequestModel { final List? requestFormDataList; final int? responseStatus; final String? message; + final String? websocketMessageBody; final ResponseModel? responseModel; List? get enabledRequestHeaders => @@ -72,6 +76,7 @@ class RequestModel { }) { return RequestModel( id: id, + protocol: protocol, method: method, url: url, name: "$name (copy)", @@ -91,6 +96,7 @@ class RequestModel { RequestModel copyWith({ String? id, + ProtocolType? protocol, HTTPVerb? method, String? url, String? name, @@ -105,6 +111,7 @@ class RequestModel { List? requestFormDataList, int? responseStatus, String? message, + String? websocketMessageBody, ResponseModel? responseModel, }) { var headers = requestHeaders ?? this.requestHeaders; @@ -113,6 +120,7 @@ class RequestModel { var enabledParams = isParamEnabledList ?? this.isParamEnabledList; return RequestModel( id: id ?? this.id, + protocol: protocol ?? this.protocol, method: method ?? this.method, url: url ?? this.url, name: name ?? this.name, @@ -128,16 +136,23 @@ class RequestModel { requestFormDataList: requestFormDataList ?? this.requestFormDataList, responseStatus: responseStatus ?? this.responseStatus, message: message ?? this.message, + websocketMessageBody: websocketMessageBody ?? this.websocketMessageBody, responseModel: responseModel ?? this.responseModel, ); } factory RequestModel.fromJson(Map data) { + ProtocolType protocol; HTTPVerb method; ContentType requestBodyContentType; ResponseModel? responseModel; final id = data["id"] as String; + try { + protocol = ProtocolType.values.byName(data["protocol"] as String); + } catch (e) { + protocol = kDefaultProtocolType; + } try { method = HTTPVerb.values.byName(data["method"] as String); } catch (e) { @@ -160,6 +175,7 @@ class RequestModel { final requestFormDataList = data["requestFormDataList"]; final responseStatus = data["responseStatus"] as int?; final message = data["message"] as String?; + final websocketMessageBody = data["websocketMessageBody"] as String?; final responseModelJson = data["responseModel"]; if (responseModelJson != null) { @@ -171,6 +187,7 @@ class RequestModel { return RequestModel( id: id, + protocol: protocol, method: method, url: url, name: name ?? "", @@ -191,6 +208,7 @@ class RequestModel { : null, responseStatus: responseStatus, message: message, + websocketMessageBody: websocketMessageBody, responseModel: responseModel, ); } @@ -198,6 +216,7 @@ class RequestModel { Map toJson({bool includeResponse = true}) { return { "id": id, + "protocol": protocol.name, "method": method.name, "url": url, "name": name, @@ -211,6 +230,7 @@ class RequestModel { "requestFormDataList": rowsToFormDataMapList(requestFormDataList), "responseStatus": includeResponse ? responseStatus : null, "message": includeResponse ? message : null, + "websocketMessageBody": includeResponse ? websocketMessageBody : null, "responseModel": includeResponse ? responseModel?.toJson() : null, }; } @@ -219,6 +239,7 @@ class RequestModel { String toString() { return [ "Request Id: $id", + "Request Protocol: ${protocol.name}", "Request Method: ${method.name}", "Request URL: $url", "Request Name: $name", @@ -233,7 +254,8 @@ class RequestModel { "Request FormData: ${requestFormDataList.toString()}", "Response Status: $responseStatus", "Response Message: $message", - "Response: ${responseModel.toString()}" + "Websocket Message Body: $websocketMessageBody" + "Response: ${responseModel.toString()}" ].join("\n"); } @@ -242,6 +264,7 @@ class RequestModel { return other is RequestModel && other.runtimeType == runtimeType && other.id == id && + other.protocol == protocol && other.method == method && other.url == url && other.name == name && @@ -256,6 +279,7 @@ class RequestModel { other.requestFormDataList == requestFormDataList && other.responseStatus == responseStatus && other.message == message && + other.websocketMessageBody == websocketMessageBody && other.responseModel == responseModel; } @@ -264,6 +288,7 @@ class RequestModel { return Object.hash( runtimeType, id, + protocol, method, url, name, @@ -278,6 +303,7 @@ class RequestModel { requestFormDataList, responseStatus, message, + websocketMessageBody, responseModel, ); } diff --git a/lib/models/websocket/request_model.dart b/lib/models/websocket/request_model.dart new file mode 100644 index 000000000..aec002699 --- /dev/null +++ b/lib/models/websocket/request_model.dart @@ -0,0 +1,300 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:apidash/utils/utils.dart' + show + mapListToFormDataModelRows, + rowsToFormDataMapList, + mapToRows, + rowsToMap, + getEnabledRows; +import 'package:apidash/consts.dart'; +import '../models.dart'; + +@immutable +class WebsocketRequestModel { + const WebsocketRequestModel({ + required this.id, + this.method = HTTPVerb.get, + this.protocol = ProtocolType.http, + this.url = "", + this.name = "", + this.description = "", + this.requestTabIndex = 0, + this.requestHeaders, + this.requestParams, + this.isHeaderEnabledList, + this.isParamEnabledList, + this.requestBodyContentType = ContentType.json, + this.requestBody, + this.requestFormDataList, + this.responseStatus, + this.message, + this.responseModel, + }); + + final String id; + final ProtocolType protocol; + final HTTPVerb method; + final String url; + final String name; + final String description; + final int requestTabIndex; + final List? requestHeaders; + final List? requestParams; + final List? isHeaderEnabledList; + final List? isParamEnabledList; + final ContentType requestBodyContentType; + final String? requestBody; + final List? requestFormDataList; + final int? responseStatus; + final String? message; + final ResponseModel? responseModel; + + List? get enabledRequestHeaders => + getEnabledRows(requestHeaders, isHeaderEnabledList); + List? get enabledRequestParams => + getEnabledRows(requestParams, isParamEnabledList); + + Map get enabledHeadersMap => + rowsToMap(enabledRequestHeaders) ?? {}; + Map get enabledParamsMap => + rowsToMap(enabledRequestParams) ?? {}; + Map get headersMap => rowsToMap(requestHeaders) ?? {}; + Map get paramsMap => rowsToMap(requestParams) ?? {}; + + List> get formDataMapList => + rowsToFormDataMapList(requestFormDataList) ?? []; + bool get isFormDataRequest => requestBodyContentType == ContentType.formdata; + + bool get hasContentTypeHeader => enabledHeadersMap.keys + .any((k) => k.toLowerCase() == HttpHeaders.contentTypeHeader); + + WebsocketRequestModel duplicate({ + required String id, + }) { + return WebsocketRequestModel( + id: id, + protocol: protocol, + method: method, + url: url, + name: "$name (copy)", + description: description, + requestHeaders: requestHeaders != null ? [...requestHeaders!] : null, + requestParams: requestParams != null ? [...requestParams!] : null, + isHeaderEnabledList: + isHeaderEnabledList != null ? [...isHeaderEnabledList!] : null, + isParamEnabledList: + isParamEnabledList != null ? [...isParamEnabledList!] : null, + requestBodyContentType: requestBodyContentType, + requestBody: requestBody, + requestFormDataList: + requestFormDataList != null ? [...requestFormDataList!] : null, + ); + } + + WebsocketRequestModel copyWith({ + String? id, + ProtocolType? protocol, + HTTPVerb? method, + String? url, + String? name, + String? description, + int? requestTabIndex, + List? requestHeaders, + List? requestParams, + List? isHeaderEnabledList, + List? isParamEnabledList, + ContentType? requestBodyContentType, + String? requestBody, + List? requestFormDataList, + int? responseStatus, + String? message, + ResponseModel? responseModel, + }) { + var headers = requestHeaders ?? this.requestHeaders; + var params = requestParams ?? this.requestParams; + var enabledHeaders = isHeaderEnabledList ?? this.isHeaderEnabledList; + var enabledParams = isParamEnabledList ?? this.isParamEnabledList; + return WebsocketRequestModel( + id: id ?? this.id, + protocol: protocol ?? this.protocol, + method: method ?? this.method, + url: url ?? this.url, + name: name ?? this.name, + description: description ?? this.description, + requestTabIndex: requestTabIndex ?? this.requestTabIndex, + requestHeaders: headers != null ? [...headers] : null, + requestParams: params != null ? [...params] : null, + isHeaderEnabledList: enabledHeaders != null ? [...enabledHeaders] : null, + isParamEnabledList: enabledParams != null ? [...enabledParams] : null, + requestBodyContentType: + requestBodyContentType ?? this.requestBodyContentType, + requestBody: requestBody ?? this.requestBody, + requestFormDataList: requestFormDataList ?? this.requestFormDataList, + responseStatus: responseStatus ?? this.responseStatus, + message: message ?? this.message, + responseModel: responseModel ?? this.responseModel, + ); + } + + factory WebsocketRequestModel.fromJson(Map data) { + ProtocolType protocol; + HTTPVerb method; + ContentType requestBodyContentType; + ResponseModel? responseModel; + + final id = data["id"] as String; + try { + protocol = ProtocolType.values.byName(data["protocol"] as String); + } catch (e) { + protocol = kDefaultProtocolType; + } + try { + method = HTTPVerb.values.byName(data["method"] as String); + } catch (e) { + method = kDefaultHttpMethod; + } + final url = data["url"] as String; + final name = data["name"] as String?; + final description = data["description"] as String?; + final requestHeaders = data["requestHeaders"]; + final requestParams = data["requestParams"]; + final isHeaderEnabledList = data["isHeaderEnabledList"] as List?; + final isParamEnabledList = data["isParamEnabledList"] as List?; + try { + requestBodyContentType = + ContentType.values.byName(data["requestBodyContentType"] as String); + } catch (e) { + requestBodyContentType = kDefaultContentType; + } + final requestBody = data["requestBody"] as String?; + final requestFormDataList = data["requestFormDataList"]; + final responseStatus = data["responseStatus"] as int?; + final message = data["message"] as String?; + final responseModelJson = data["responseModel"]; + + if (responseModelJson != null) { + responseModel = + ResponseModel.fromJson(Map.from(responseModelJson)); + } else { + responseModel = null; + } + + return WebsocketRequestModel( + id: id, + protocol: protocol, + method: method, + url: url, + name: name ?? "", + description: description ?? "", + requestTabIndex: 0, + requestHeaders: requestHeaders != null + ? mapToRows(Map.from(requestHeaders)) + : null, + requestParams: requestParams != null + ? mapToRows(Map.from(requestParams)) + : null, + isHeaderEnabledList: isHeaderEnabledList, + isParamEnabledList: isParamEnabledList, + requestBodyContentType: requestBodyContentType, + requestBody: requestBody, + requestFormDataList: requestFormDataList != null + ? mapListToFormDataModelRows(List.from(requestFormDataList)) + : null, + responseStatus: responseStatus, + message: message, + responseModel: responseModel, + ); + } + + Map toJson({bool includeResponse = true}) { + return { + "id": id, + "protocol": protocol.name, + "method": method.name, + "url": url, + "name": name, + "description": description, + "requestHeaders": rowsToMap(requestHeaders), + "requestParams": rowsToMap(requestParams), + "isHeaderEnabledList": isHeaderEnabledList, + "isParamEnabledList": isParamEnabledList, + "requestBodyContentType": requestBodyContentType.name, + "requestBody": requestBody, + "requestFormDataList": rowsToFormDataMapList(requestFormDataList), + "responseStatus": includeResponse ? responseStatus : null, + "message": includeResponse ? message : null, + "responseModel": includeResponse ? responseModel?.toJson() : null, + }; + } + + @override + String toString() { + return [ + "Request Id: $id", + "Request Protocol: ${protocol.name}", + "Request Method: ${method.name}", + "Request URL: $url", + "Request Name: $name", + "Request Description: $description", + "Request Tab Index: ${requestTabIndex.toString()}", + "Request Headers: ${requestHeaders.toString()}", + "Enabled Headers: ${isHeaderEnabledList.toString()}", + "Request Params: ${requestParams.toString()}", + "Enabled Params: ${isParamEnabledList.toString()}", + "Request Body Content Type: ${requestBodyContentType.toString()}", + "Request Body: ${requestBody.toString()}", + "Request FormData: ${requestFormDataList.toString()}", + "Response Status: $responseStatus", + "Response Message: $message", + "Response: ${responseModel.toString()}" + ].join("\n"); + } + + @override + bool operator ==(Object other) { + return other is WebsocketRequestModel && + other.runtimeType == runtimeType && + other.id == id && + other.protocol == protocol && + other.method == method && + other.url == url && + other.name == name && + other.description == description && + other.requestTabIndex == requestTabIndex && + listEquals(other.requestHeaders, requestHeaders) && + listEquals(other.requestParams, requestParams) && + listEquals(other.isHeaderEnabledList, isHeaderEnabledList) && + listEquals(other.isParamEnabledList, isParamEnabledList) && + other.requestBodyContentType == requestBodyContentType && + other.requestBody == requestBody && + other.requestFormDataList == requestFormDataList && + other.responseStatus == responseStatus && + other.message == message && + other.responseModel == responseModel; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + id, + protocol, + method, + url, + name, + description, + requestTabIndex, + requestHeaders, + requestParams, + isHeaderEnabledList, + isParamEnabledList, + requestBodyContentType, + requestBody, + requestFormDataList, + responseStatus, + message, + responseModel, + ); + } +} diff --git a/lib/models/websocket/response_model.dart b/lib/models/websocket/response_model.dart new file mode 100644 index 000000000..0c7bdfc93 --- /dev/null +++ b/lib/models/websocket/response_model.dart @@ -0,0 +1,130 @@ +import 'dart:io'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:collection/collection.dart' show mergeMaps; +import 'package:http/http.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +@immutable +class WebsocketResponseModel { + const WebsocketResponseModel({ + this.statusCode, + this.headers, + this.requestHeaders, + this.body, + this.formattedBody, + this.bodyBytes, + this.time, + }); + + final int? statusCode; + final Map? headers; + final Map? requestHeaders; + final String? body; + final String? formattedBody; + final Uint8List? bodyBytes; + final Duration? time; + + String? get contentType => getContentTypeFromHeaders(headers); + MediaType? get mediaType => getMediaTypeFromHeaders(headers); + + WebsocketResponseModel fromResponse({ + required Response response, + Duration? time, + }) { + final responseHeaders = mergeMaps( + {HttpHeaders.contentLengthHeader: response.contentLength.toString()}, + response.headers); + MediaType? mediaType = getMediaTypeFromHeaders(responseHeaders); + final body = (mediaType?.subtype == kSubTypeJson) + ? utf8.decode(response.bodyBytes) + : response.body; + return WebsocketResponseModel( + statusCode: response.statusCode, + headers: responseHeaders, + requestHeaders: response.request?.headers, + body: body, + formattedBody: formatBody(body, mediaType), + bodyBytes: response.bodyBytes, + time: time, + ); + } + + factory WebsocketResponseModel.fromJson(Map data) { + Duration? timeElapsed; + final statusCode = data["statusCode"] as int?; + final headers = data["headers"] != null + ? Map.from(data["headers"]) + : null; + MediaType? mediaType = getMediaTypeFromHeaders(headers); + final requestHeaders = data["requestHeaders"] != null + ? Map.from(data["requestHeaders"]) + : null; + final body = data["body"] as String?; + final bodyBytes = data["bodyBytes"] as Uint8List?; + final time = data["time"] as int?; + if (time != null) { + timeElapsed = Duration(microseconds: time); + } + return WebsocketResponseModel( + statusCode: statusCode, + headers: headers, + requestHeaders: requestHeaders, + body: body, + formattedBody: formatBody(body, mediaType), + bodyBytes: bodyBytes, + time: timeElapsed, + ); + } + + Map toJson() { + return { + "statusCode": statusCode, + "headers": headers, + "requestHeaders": requestHeaders, + "body": body, + "bodyBytes": bodyBytes, + "time": time?.inMicroseconds, + }; + } + + @override + String toString() { + return [ + "Response Status: $statusCode", + "Response Time: $time", + "Response Headers: $headers", + "Response Request Headers: $requestHeaders", + "Response Body: $body", + ].join("\n"); + } + + @override + bool operator ==(Object other) { + return other is WebsocketResponseModel && + other.runtimeType == runtimeType && + other.statusCode == statusCode && + mapEquals(other.headers, headers) && + mapEquals(other.requestHeaders, requestHeaders) && + other.body == body && + other.formattedBody == formattedBody && + other.bodyBytes == bodyBytes && + other.time == time; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + statusCode, + headers, + requestHeaders, + body, + formattedBody, + bodyBytes, + time, + ); + } +} diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 0432b0e40..4cf4b50f3 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -2,13 +2,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'settings_providers.dart'; import 'ui_providers.dart'; import '../models/models.dart'; -import '../services/services.dart' show hiveHandler, HiveHandler, request; +import '../services/services.dart' + show hiveHandler, HiveHandler, request, WebSocketService; import '../utils/utils.dart' show uuid, collectionToHAR; import '../consts.dart'; import 'package:http/http.dart' as http; final selectedIdStateProvider = StateProvider((ref) => null); +final websocketStateProvider = StateProvider((ref) => 'disconnected'); +final websocketHistoryStateProvider = + StateProvider>>((ref) => []); final selectedRequestModelProvider = StateProvider((ref) { final selectedId = ref.watch(selectedIdStateProvider); final collection = ref.watch(collectionStateNotifierProvider); @@ -47,6 +51,8 @@ class CollectionStateNotifier final HiveHandler hiveHandler; final baseResponseModel = const ResponseModel(); + final websocketService = WebSocketService(); + bool hasId(String id) => state?.keys.contains(id) ?? false; RequestModel? getRequestModel(String id) { @@ -117,6 +123,7 @@ class CollectionStateNotifier void update( String id, { + ProtocolType? protocol, HTTPVerb? method, String? url, String? name, @@ -131,10 +138,12 @@ class CollectionStateNotifier List? requestFormDataList, int? responseStatus, String? message, + String? websocketMessageBody, ResponseModel? responseModel, }) { final newModel = state![id]!.copyWith( method: method, + protocol: protocol, url: url, name: name, description: description, @@ -148,8 +157,8 @@ class CollectionStateNotifier requestFormDataList: requestFormDataList, responseStatus: responseStatus, message: message, + websocketMessageBody: websocketMessageBody, responseModel: responseModel); - //print(newModel); var map = {...state!}; map[id] = newModel; state = map; @@ -191,6 +200,86 @@ class CollectionStateNotifier state = map; } + Future initiateWebSocketConnection(String id) async { + // ref.read(sentRequestIdStateProvider.notifier).state = id; + ref.read(codePaneVisibleStateProvider.notifier).state = false; + //Delete the previous history + ref.read(websocketHistoryStateProvider.notifier).state = []; + RequestModel requestModel = state![id]!; + (String?, Duration?) responseRec = + await websocketService.connect(requestModel.url, (receivedMessage) { + ref.read(websocketHistoryStateProvider.notifier).state = [ + { + "direction": 'receive', + "message": receivedMessage, + }, + ...ref.read(websocketHistoryStateProvider), + ]; + }); + ref.read(websocketStateProvider.notifier).state = 'connected'; + late final RequestModel newRequestModel; + newRequestModel = requestModel.copyWith( + responseStatus: 101, + message: "Connected", + requestHeaders: List.empty(), + responseModel: ResponseModel( + statusCode: 101, + time: responseRec.$2, + body: responseRec.$1, + ), + ); + + var map = {...state!}; + map[id] = newRequestModel; + state = map; + } + + Future sendWebSocketMessage(String id, String message) async { + ref.read(codePaneVisibleStateProvider.notifier).state = false; + RequestModel requestModel = state![id]!; + String? message = requestModel.websocketMessageBody; + + // Connect to the WebSocket server if not already connected + if (!websocketService.isInitialized()) { + await websocketService.connect(requestModel.url, (receivedMessage) { + ref.read(websocketHistoryStateProvider.notifier).state = [ + { + "direction": 'receive', + "message": receivedMessage, + }, + ...ref.read(websocketHistoryStateProvider), + ]; + }); + } + + // Send the message + websocketService.send(message!); + + // Add the sent message to the history + ref.read(websocketHistoryStateProvider.notifier).state = [ + { + "direction": 'send', + "message": message, + }, + ...ref.read(websocketHistoryStateProvider), + ]; + } + + Future closeWebSocketConnection(String id) async { + ref.read(websocketStateProvider.notifier).state = 'disconnected'; + ref.read(codePaneVisibleStateProvider.notifier).state = false; + websocketService.disconnect(); + ref.read(websocketHistoryStateProvider.notifier).state = [ + { + "direction": 'info', + "message": 'Connection was closed successfully.', + }, + ...ref.read(websocketHistoryStateProvider), + ]; + var map = {...state!}; + state = map; + } + Future clearData() async { ref.read(clearDataStateProvider.notifier).state = true; ref.read(selectedIdStateProvider.notifier).state = null; diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart index f667f6b5a..539701bb3 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_body.dart @@ -46,7 +46,7 @@ class EditRequestBody extends ConsumerWidget { initialValue: requestModel?.requestBody, onChanged: (String value) { ref - .read(collectionStateNotifierProvider.notifier) + .watch(collectionStateNotifierProvider.notifier) .update(selectedId, requestBody: value); }, ), @@ -56,12 +56,16 @@ class EditRequestBody extends ConsumerWidget { initialValue: requestModel?.requestBody, onChanged: (String value) { ref - .read(collectionStateNotifierProvider.notifier) + .watch(collectionStateNotifierProvider.notifier) .update(selectedId, requestBody: value); }, ), }, - ) + ), + ref.read(selectedRequestModelProvider)?.protocol == + ProtocolType.websocket + ? const SendWebsocketMessageButton() + : const SizedBox() ], ), ); @@ -88,3 +92,28 @@ class DropdownButtonBodyContentType extends ConsumerWidget { ); } } + +class SendWebsocketMessageButton extends ConsumerWidget { + const SendWebsocketMessageButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final sentRequestId = ref.watch(sentRequestIdStateProvider); + final message = ref + .watch(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!) + ?.websocketMessageBody; + return SendRequestButton( + selectedId: selectedId, + sentRequestId: sentRequestId, + onTap: () { + ref + .read(collectionStateNotifierProvider.notifier) + .sendWebSocketMessage(selectedId, message!); + }, + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart index a09c46091..f3faf1354 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart @@ -1,3 +1,5 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/websocket_message_body.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -12,6 +14,8 @@ class EditRequestPane extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); final codePaneVisible = ref.watch(codePaneVisibleStateProvider); final tabIndex = ref.watch( selectedRequestModelProvider.select((value) => value?.requestTabIndex)); @@ -22,11 +26,58 @@ class EditRequestPane extends ConsumerWidget { .select((value) => value?.paramsMap.length)); final bodyLength = ref.watch(selectedRequestModelProvider .select((value) => value?.requestBody?.length)); + final messageLength = ref.watch( + selectedRequestModelProvider.select((value) => value?.message?.length)); + if (protocol == ProtocolType.websocket) { + return WebSocketRequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + tabIndex: tabIndex, + paramLength: paramLength, + headerLength: headerLength, + messageLength: messageLength, + ref: ref); + } else { + return HTTPRequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + tabIndex: tabIndex, + paramLength: paramLength, + headerLength: headerLength, + bodyLength: bodyLength, + ref: ref); + } + } +} + +class HTTPRequestPane extends StatelessWidget { + const HTTPRequestPane({ + super.key, + required this.selectedId, + required this.codePaneVisible, + required this.tabIndex, + required this.paramLength, + required this.headerLength, + required this.bodyLength, + required this.ref, + }); + + final String? selectedId; + final bool codePaneVisible; + final int? tabIndex; + final int? paramLength; + final int? headerLength; + final int? bodyLength; + final WidgetRef ref; + + @override + Widget build(BuildContext context) { return RequestPane( selectedId: selectedId, codePaneVisible: codePaneVisible, tabIndex: tabIndex, + tabs: const ['URL Params', 'Headers', 'Body'], onPressedCodeButton: () { ref.read(codePaneVisibleStateProvider.notifier).state = !codePaneVisible; @@ -37,9 +88,9 @@ class EditRequestPane extends ConsumerWidget { .update(selectedId!, requestTabIndex: index); }, showIndicators: [ - paramLength != null && paramLength > 0, - headerLength != null && headerLength > 0, - bodyLength != null && bodyLength > 0, + paramLength != null && paramLength! > 0, + headerLength != null && headerLength! > 0, + bodyLength != null && bodyLength! > 0, ], children: const [ EditRequestURLParams(), @@ -49,3 +100,53 @@ class EditRequestPane extends ConsumerWidget { ); } } + +class WebSocketRequestPane extends StatelessWidget { + const WebSocketRequestPane({ + super.key, + required this.selectedId, + required this.codePaneVisible, + required this.tabIndex, + required this.messageLength, + required this.headerLength, + required this.paramLength, + required this.ref, + }); + + final String? selectedId; + final bool codePaneVisible; + final int? tabIndex; + final int? paramLength; + final int? headerLength; + final int? messageLength; + final WidgetRef ref; + + @override + Widget build(BuildContext context) { + return RequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + tabs: const ['Message', 'URL Params', 'Headers'], + tabIndex: tabIndex, + onPressedCodeButton: () { + ref.read(codePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + onTapTabBar: (index) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId!, requestTabIndex: index); + }, + showIndicators: [ + messageLength != null && messageLength! > 0, + paramLength != null && paramLength! > 0, + headerLength != null && headerLength! > 0, + ], + children: const [ + EditWebsocketMessage(), + EditRequestURLParams(), + EditRequestHeaders(), + ], + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/websocket_message_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/websocket_message_body.dart new file mode 100644 index 000000000..5463f1d06 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/websocket_message_body.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class EditWebsocketMessage extends ConsumerWidget { + const EditWebsocketMessage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + ), + margin: kPt5o10, + child: Column( + children: [ + Expanded( + child: TextFieldEditor( + key: Key("$selectedId-body"), + fieldKey: "$selectedId-body-editor", + initialValue: requestModel?.websocketMessageBody, + onChanged: (String value) { + ref + .watch(collectionStateNotifierProvider.notifier) + .update(selectedId, websocketMessageBody: value); + }, + ), + ), + const SendWebsocketMessageButton() + ], + ), + ); + } +} + +class SendWebsocketMessageButton extends ConsumerWidget { + const SendWebsocketMessageButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final sentRequestId = ref.watch(sentRequestIdStateProvider); + final message = ref + .watch(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!) + ?.websocketMessageBody; + return SendRequestButton( + selectedId: selectedId, + sentRequestId: sentRequestId, + onTap: () { + ref + .read(collectionStateNotifierProvider.notifier) + .sendWebSocketMessage(selectedId, message!); + }, + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index e7fc9f095..52c1859af 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -39,6 +39,8 @@ class ResponseDetails extends ConsumerWidget { .watch(selectedRequestModelProvider.select((value) => value?.message)); final responseModel = ref.watch( selectedRequestModelProvider.select((value) => value?.responseModel)); + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); return Column( children: [ ResponsePaneHeader( @@ -76,6 +78,11 @@ class ResponseBodyTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedRequestModel = ref.watch(selectedRequestModelProvider); + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); + if (protocol == ProtocolType.websocket) { + return const WebsocketResponseView(); + } return ResponseBody( selectedRequestModel: selectedRequestModel, ); diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 5fa4b404d..07600dedb 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -4,9 +4,25 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -class EditorPaneRequestURLCard extends StatelessWidget { +class EditorPaneRequestURLCard extends ConsumerWidget { const EditorPaneRequestURLCard({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); + if (protocol == ProtocolType.websocket) { + return const WebSocketCard(); + } + return const HTTPCard(); + } +} + +class HTTPCard extends StatelessWidget { + const HTTPCard({ + super.key, + }); + @override Widget build(BuildContext context) { return Card( @@ -24,6 +40,8 @@ class EditorPaneRequestURLCard extends StatelessWidget { ), child: Row( children: [ + DropdownButtonProtocol(), + kHSpacer20, DropdownButtonHTTPMethod(), kHSpacer20, Expanded( @@ -41,6 +59,66 @@ class EditorPaneRequestURLCard extends StatelessWidget { } } +class WebSocketCard extends StatelessWidget { + const WebSocketCard({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceVariant, + ), + borderRadius: kBorderRadius12, + ), + child: const Padding( + padding: EdgeInsets.symmetric( + vertical: 5, + horizontal: 20, + ), + child: Row( + children: [ + DropdownButtonProtocol(), + kHSpacer20, + Expanded( + child: URLTextField(), + ), + kHSpacer20, + SizedBox( + height: 36, + child: ConnectWebSocketButton(), + ), + ], + ), + ), + ); + } +} + +class DropdownButtonProtocol extends ConsumerWidget { + const DropdownButtonProtocol({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); + return DropdownButtonProtocolType( + protocol: protocol, + onChanged: (ProtocolType? value) { + final selectedId = ref.read(selectedRequestModelProvider)!.id; + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId, protocol: value); + }, + ); + } +} + class DropdownButtonHTTPMethod extends ConsumerWidget { const DropdownButtonHTTPMethod({ super.key, @@ -105,3 +183,31 @@ class SendButton extends ConsumerWidget { ); } } + +class ConnectWebSocketButton extends ConsumerWidget { + const ConnectWebSocketButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final websocketStatus = ref.watch(websocketStateProvider); + final sentRequestId = ref.watch(sentRequestIdStateProvider); + return ConnectButton( + selectedId: selectedId, + sentRequestId: sentRequestId, + websocketStatus: websocketStatus!, + onConnect: () { + ref + .read(collectionStateNotifierProvider.notifier) + .initiateWebSocketConnection(selectedId!); + }, + onDisconnect: () { + ref + .read(collectionStateNotifierProvider.notifier) + .closeWebSocketConnection(selectedId!); + }, + ); + } +} diff --git a/lib/services/services.dart b/lib/services/services.dart index a7cf03fd7..703762faf 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -1,3 +1,4 @@ export 'http_service.dart'; export 'hive_services.dart'; export 'window_services.dart'; +export 'websocket_service.dart'; diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart new file mode 100644 index 000000000..a494dda5b --- /dev/null +++ b/lib/services/websocket_service.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class WebSocketService { + final StreamController _messageController = + StreamController.broadcast(); + final StreamController _errorController = + StreamController.broadcast(); + + WebSocketChannel? _channel; + + Stream get messages => _messageController.stream; + Stream get errors => _errorController.stream; + Future<(String, Duration)> connect( + String url, Function(String) onMessage) async { + if (!isInitialized()) { + _channel = WebSocketChannel.connect(Uri.parse(url)); + } + Stopwatch stopwatch = Stopwatch()..start(); + await _channel!.ready; + // Start listening to the channel right after it's connected + _channel!.stream.listen((event) { + _messageController.add(event); + onMessage(event); + }, onError: (error) { + _errorController.add(error); + }); + stopwatch.stop(); + return ("Websocket connection established!", stopwatch.elapsed); + } + + void send(String message) { + if (_channel != null) { + _channel!.sink.add(message); + } + } + + void receive() { + if (_channel != null) { + _channel!.stream.listen((event) { + _messageController.add(event); + }, onError: (error) { + _errorController.add(error); + }); + } + } + + void disconnect() { + if (_channel != null) { + _channel!.sink.close(); + _channel = null; + } + } + + bool isInitialized() { + return _channel != null; + } +} diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index 2d63ede79..40f03e297 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:riverpod/src/state_provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; @@ -82,6 +83,49 @@ class SendRequestButton extends StatelessWidget { } } +class ConnectButton extends StatelessWidget { + const ConnectButton({ + super.key, + required this.selectedId, + required this.sentRequestId, + required this.websocketStatus, + required this.onConnect, + required this.onDisconnect, + }); + + final String? selectedId; + final String? sentRequestId; + final String websocketStatus; + final void Function() onConnect; + final void Function() onDisconnect; + + @override + Widget build(BuildContext context) { + bool disable = sentRequestId != null; + bool isBusy = selectedId == sentRequestId; + + return FilledButton( + onPressed: websocketStatus == 'connected' ? onDisconnect : onConnect, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (disable) + Text( + (isBusy ? kLabelSending : kLabelBusy), + style: kTextStyleButton, + ) + else ...[ + Text( + websocketStatus == 'connected' ? kLabelDisconnect : kLabelConnect, + style: kTextStyleButton, + ), + ] + ], + ), + ); + } +} + class SaveInDownloadsButton extends StatelessWidget { const SaveInDownloadsButton({ super.key, diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index 69008a6b7..bbc56fb91 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -2,6 +2,49 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; +class DropdownButtonProtocolType extends StatelessWidget { + const DropdownButtonProtocolType({ + super.key, + this.protocol, + this.onChanged, + }); + + final ProtocolType? protocol; + final void Function(ProtocolType? value)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + focusColor: surfaceColor, + value: protocol, + icon: const Icon(Icons.unfold_more_rounded), + elevation: 4, + underline: Container( + height: 0, + ), + borderRadius: kBorderRadius12, + onChanged: onChanged, + items: ProtocolType.values + .map>((ProtocolType value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + value.name.toUpperCase(), + style: kCodeStyle.copyWith( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ); + }).toList(), + ); + } +} + class DropdownButtonHttpMethod extends StatelessWidget { const DropdownButtonHttpMethod({ super.key, diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 109835286..b8c203c7f 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -19,7 +19,7 @@ class TextFieldEditor extends StatefulWidget { } class _TextFieldEditorState extends State { - final TextEditingController controller = TextEditingController(); + TextEditingController controller = TextEditingController(); late final FocusNode editorFocusNode; void insertTab() { @@ -43,6 +43,15 @@ class _TextFieldEditorState extends State { void initState() { super.initState(); editorFocusNode = FocusNode(debugLabel: "Editor Focus Node"); + + // Initialize the TextEditingController with the initial value + controller = TextEditingController(); + + // Add a listener to the TextEditingController + controller.addListener(() { + // Call the onChanged callback whenever the text changes + widget.onChanged?.call(controller.text); + }); } @override diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index ffac504ec..a01e53645 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -7,6 +7,7 @@ class RequestPane extends StatefulWidget { super.key, required this.selectedId, required this.codePaneVisible, + required this.tabs, this.tabIndex, this.onPressedCodeButton, this.onTapTabBar, @@ -16,6 +17,7 @@ class RequestPane extends StatefulWidget { final String? selectedId; final bool codePaneVisible; + final List? tabs; final int? tabIndex; final void Function()? onPressedCodeButton; final void Function(int)? onTapTabBar; @@ -82,15 +84,15 @@ class _RequestPaneState extends State onTap: widget.onTapTabBar, tabs: [ TabLabel( - text: 'URL Params', + text: widget.tabs![0], showIndicator: widget.showIndicators[0], ), TabLabel( - text: 'Headers', + text: widget.tabs![1], showIndicator: widget.showIndicators[1], ), TabLabel( - text: 'Body', + text: widget.tabs![2], showIndicator: widget.showIndicators[2], ), ], diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index 539a2a341..9f1d67241 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -1,5 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; import 'package:http_parser/http_parser.dart'; import 'package:lottie/lottie.dart'; import 'package:apidash/utils/utils.dart'; @@ -315,7 +317,7 @@ class ResponseBody extends StatelessWidget { ); } - var mediaType = responseModel.mediaType; + MediaType? mediaType = responseModel.mediaType; if (mediaType == null) { return ErrorMessage( message: @@ -486,3 +488,34 @@ class _BodySuccessState extends State { ); } } + +class WebsocketResponseView extends ConsumerWidget { + const WebsocketResponseView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final data = ref.watch(websocketHistoryStateProvider); + return ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) { + return ListTile( + leading: data[index]['direction'] == 'receive' + ? const Icon( + Icons.arrow_downward, + color: Colors.green, + ) + : data[index]['direction'] == 'send' + ? const Icon( + Icons.arrow_upward, + color: Colors.blue, + ) + : const Icon( + Icons.info, + color: Colors.white, + ), + title: Text(data[index]['message'] ?? ''), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 2f3f7f55f..402b16f53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -500,10 +500,10 @@ packages: dependency: "direct main" description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -625,6 +625,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -661,26 +685,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -757,10 +781,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1178,10 +1202,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -1250,18 +1274,18 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" webkit_inspection_protocol: dependency: transitive description: @@ -1320,5 +1344,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4196d007c..2f59eab8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: code_builder: ^4.9.0 dart_style: ^2.3.4 json_text_field: ^1.1.0 + web_socket_channel: ^2.4.4 dev_dependencies: flutter_test: From 48b7f2b5bae18918ad347d7744e10463fa2de2c8 Mon Sep 17 00:00:00 2001 From: mmjsmohit Date: Tue, 27 Feb 2024 15:47:50 +0530 Subject: [PATCH 2/2] Fix tests --- test/models/request_model_test.dart | 5 ++++- test/widgets/request_widgets_test.dart | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/test/models/request_model_test.dart b/test/models/request_model_test.dart index 626727de8..23fdbb9c2 100644 --- a/test/models/request_model_test.dart +++ b/test/models/request_model_test.dart @@ -98,6 +98,7 @@ void main() { Map requestModelAsJson = { "id": '1', + "protocol": "http", "method": 'post', "url": 'api.foss42.com/case/lower', "name": 'foss42 api', @@ -116,6 +117,7 @@ void main() { 'requestFormDataList': null, 'responseStatus': null, 'message': null, + 'websocketMessageBody': null, 'responseModel': null }; test('Testing copyWith', () { @@ -137,6 +139,7 @@ void main() { final requestModeDupString = [ "Request Id: 1", + 'Request Protocol: http', "Request Method: post", "Request URL: api.foss42.com/case/lower", "Request Name: foss42 api", @@ -151,7 +154,7 @@ void main() { 'Request FormData: null', "Response Status: null", "Response Message: null", - "Response: null" + 'Websocket Message Body: nullResponse: null', ].join("\n"); test('Testing toString', () { expect(requestModelDup.toString(), requestModeDupString); diff --git a/test/widgets/request_widgets_test.dart b/test/widgets/request_widgets_test.dart index b50a033f3..2879b3f43 100644 --- a/test/widgets/request_widgets_test.dart +++ b/test/widgets/request_widgets_test.dart @@ -11,6 +11,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: RequestPane( + tabs: ['URL Params', 'Headers', 'Body'], selectedId: '1', codePaneVisible: true, children: const [Text('abc'), Text('xyz'), Text('mno')], @@ -41,6 +42,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: RequestPane( + tabs: const ['URL Params', 'Headers', 'Body'], selectedId: '1', codePaneVisible: true, onPressedCodeButton: () {}, @@ -72,6 +74,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: RequestPane( + tabs: const ['URL Params', 'Headers', 'Body'], selectedId: '1', codePaneVisible: false, onPressedCodeButton: () {}, @@ -104,6 +107,7 @@ void main() { theme: kThemeDataLight, home: Scaffold( body: RequestPane( + tabs: const ['URL Params', 'Headers', 'Body'], selectedId: '1', codePaneVisible: false, onPressedCodeButton: () {},