diff --git a/.gitignore b/.gitignore index 9268964a..4bdfd329 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ macos/ windows/ web/ ios/ +android/ .vscode/* icons/ coverage/* diff --git a/INSTALLATION.md b/INSTALLATION.md index 05740be7..5673a691 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -3,7 +3,11 @@ ## Windows Download the latest Windows Installer (64 bit) from [here](https://github.com/foss42/apidash/releases/latest) -To install it, simply double click on the installer and follow the step by step installation wizard. +To install it, simply double click on the installer. + +If prompted by Windows that **Windows prevented an unrecognized app from running**, click on **Run anyway**. + +Now, follow the step by step installation wizard. ## MacOS diff --git a/README.md b/README.md index 134abbba..f67c8f17 100644 --- a/README.md +++ b/README.md @@ -27,35 +27,41 @@ API Dash can be downloaded from the links below: .dmg Link Apple Silicon & Intel - Link + Link Windows .exe Link 64-bit - Link + Link - Linux + Linux .deb Link amd64 - Link + Link arm64 - Link + Link .rpm Link x86_64 - Link + Link aarch64 - Link + Link + + + PKGBUILD (Arch Linux) + Link + x86_64 + Link @@ -132,7 +138,7 @@ Here is the complete list of mimetypes that can be directly previewed in API Das | Image | `image/portable-anymap` | `.pnm` | | | Image | `image/png` | `.png` | | | Image | `image/sgi` | `.sgi` | | -| Image | `image/svg+xml` | `.svg` | Partial support. See issue https://github.com/foss42/apidash/issues/20 | +| Image | `image/svg+xml` | `.svg` | | | Image | `image/tiff` | `.tiff` | | | Image | `image/targa` | `.tga` | | | Image | `image/vnd.wap.wbmp` | `.wbmp` | | diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..7e7e7f67 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/lib/app.dart b/lib/app.dart index ca9e7be0..3f6b2a53 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; +import 'package:window_manager/window_manager.dart' hide WindowCaption; +import 'widgets/widgets.dart' show WindowCaption; import 'providers/providers.dart'; import 'screens/screens.dart'; -import 'consts.dart' show kFontFamily, kFontFamilyFallback, kColorSchemeSeed; +import 'consts.dart'; class App extends ConsumerStatefulWidget { const App({super.key}); @@ -38,7 +39,7 @@ class _AppState extends ConsumerState with WindowListener { @override Widget build(BuildContext context) { - return const DashApp(); + return const Dashboard(); } @override @@ -63,6 +64,7 @@ class _DashAppState extends ConsumerState { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( + visualDensity: VisualDensity.adaptivePlatformDensity, fontFamily: kFontFamily, fontFamilyFallback: kFontFamilyFallback, colorSchemeSeed: kColorSchemeSeed, @@ -77,7 +79,25 @@ class _DashAppState extends ConsumerState { brightness: Brightness.dark, ), themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light, - home: const Dashboard(), + home: kIsMobile + ? const MobileDashboard( + title: 'Requests', + scaffoldBody: CollectionPane(), + ) + : Stack( + children: [ + kIsLinux ? const Dashboard() : const App(), + if (kIsWindows) + SizedBox( + height: 29, + child: WindowCaption( + backgroundColor: Colors.transparent, + brightness: + isDarkMode ? Brightness.dark : Brightness.light, + ), + ), + ], + ), ); } } diff --git a/lib/codegen/codegen.dart b/lib/codegen/codegen.dart index 356854f6..1aec7144 100644 --- a/lib/codegen/codegen.dart +++ b/lib/codegen/codegen.dart @@ -1,6 +1,7 @@ import 'package:apidash/models/models.dart' show RequestModel; import 'package:apidash/consts.dart'; import 'dart/http.dart'; +import 'dart/dio.dart'; import 'kotlin/okhttp.dart'; import 'python/http_client.dart'; import 'python/requests.dart'; @@ -22,6 +23,8 @@ class Codegen { return HARCodeGen().getCode(requestModel, defaultUriScheme); case CodegenLanguage.dartHttp: return DartHttpCodeGen().getCode(requestModel, defaultUriScheme); + case CodegenLanguage.dartDio: + return DartDioCodeGen().getCode(requestModel, defaultUriScheme); case CodegenLanguage.jsAxios: return AxiosCodeGen().getCode(requestModel, defaultUriScheme); case CodegenLanguage.jsFetch: @@ -39,8 +42,6 @@ class Codegen { .getCode(requestModel, defaultUriScheme); case CodegenLanguage.pythonRequests: return PythonRequestsCodeGen().getCode(requestModel, defaultUriScheme); - default: - throw ArgumentError('Invalid codegenLanguage'); } } } diff --git a/lib/codegen/dart/dio.dart b/lib/codegen/dart/dio.dart new file mode 100644 index 00000000..c4a7ed8e --- /dev/null +++ b/lib/codegen/dart/dio.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; +import 'package:apidash/models/request_model.dart' show RequestModel; +import 'package:apidash/consts.dart'; +import 'shared.dart'; + +class DartDioCodeGen { + String? getCode( + RequestModel requestModel, + String defaultUriScheme, + ) { + try { + String url = requestModel.url; + if (!url.contains("://") && url.isNotEmpty) { + url = "$defaultUriScheme://$url"; + } + final next = generatedDartCode( + url: url, + method: requestModel.method, + queryParams: requestModel.enabledParamsMap, + headers: requestModel.enabledHeadersMap, + body: requestModel.requestBody, + contentType: requestModel.requestBodyContentType, + formData: requestModel.formDataMapList, + ); + return next; + } catch (e) { + return null; + } + } + + String generatedDartCode({ + required String url, + required HTTPVerb method, + required Map queryParams, + required Map headers, + required String? body, + required ContentType contentType, + required List> formData, + }) { + final sbf = StringBuffer(); + final emitter = DartEmitter(); + final dioImport = Directive.import('package:dio/dio.dart', as: 'dio'); + sbf.writeln(dioImport.accept(emitter)); + + Expression? queryParamExp; + if (queryParams.isNotEmpty) { + queryParamExp = declareFinal('queryParams').assign( + literalMap(queryParams.map((key, value) => MapEntry(key, value))), + ); + } + Expression? headerExp; + if (headers.isNotEmpty) { + headerExp = declareFinal('headers').assign( + literalMap(headers.map((key, value) => MapEntry(key, value))), + ); + } + final multiPartList = Code(''' + final List> formDataList = ${json.encode(formData)}; + for (var formField in formDataList) { + if (formField['type'] == 'file') { + formData.files.add(MapEntry( + formField['name'], + await MultipartFile.fromFile(formField['value'], filename: formField['value']), + )); + } else { + formData.fields.add(MapEntry(formField['name'], formField['value'])); + } + } + '''); + Expression? dataExp; + if ((kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false) || + contentType == ContentType.formdata)) { + final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); + switch (contentType) { + // dio dosen't need pass `content-type` header when body is json or plain text + case ContentType.json: + final convertImport = Directive.import('dart:convert', as: 'convert'); + sbf.writeln(convertImport.accept(emitter)); + dataExp = declareFinal('data') + .assign(refer('convert.json.decode').call([strContent])); + case ContentType.text: + dataExp = declareFinal('data').assign(strContent); + // when add new type of [ContentType], need update [dataExp]. + case ContentType.formdata: + dataExp = declareFinal('data').assign(refer('FormData()')); + } + } + final responseExp = declareFinal('response').assign(InvokeExpression.newOf( + refer('dio.Dio'), + [literalString(url)], + { + if (queryParamExp != null) 'queryParameters': refer('queryParams'), + if (headerExp != null) + 'options': refer('Options').newInstance( + [], + {'headers': refer('headers')}, + ), + if (dataExp != null) 'data': refer('data'), + }, + [], + method.name, + ).awaited); + + final mainFunction = Method((m) { + final content = declareTryCatch( + showStackStrace: true, + body: [ + if (queryParamExp != null) queryParamExp, + if (headerExp != null) headerExp, + if (dataExp != null) dataExp, + if ((contentType == ContentType.formdata && formData.isNotEmpty)) + multiPartList, + responseExp, + refer('print').call([refer('response').property('statusCode')]), + refer('print').call([refer('response').property('data')]), + ], + onError: { + 'DioException': [ + refer('print').call([ + refer('e').property('response').nullSafeProperty('statusCode'), + ]), + refer('print').call([ + refer('e').property('response').nullSafeProperty('data'), + ]), + refer('print').call([refer('s')]), + ], + null: [ + refer('print').call([refer('e')]), + refer('print').call([refer('s')]), + ], + }, + ); + m + ..name = 'main' + ..returns = refer('void') + ..modifier = MethodModifier.async + ..body = content; + }); + + sbf.writeln(mainFunction.accept(emitter)); + + return DartFormatter( + pageWidth: contentType == ContentType.formdata ? 70 : 160) + .format(sbf.toString()); + } +} diff --git a/lib/codegen/dart/http.dart b/lib/codegen/dart/http.dart index 2c0e016c..3e423743 100644 --- a/lib/codegen/dart/http.dart +++ b/lib/codegen/dart/http.dart @@ -1,147 +1,223 @@ -import 'dart:io'; import 'dart:convert'; -import 'package:jinja/jinja.dart' as jj; -import 'package:apidash/consts.dart'; -import 'package:apidash/utils/utils.dart' show padMultilineString; +import 'dart:io'; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; +import 'shared.dart'; class DartHttpCodeGen { - String kTemplateStart = """import 'package:http/http.dart' as http; - -void main() async { - var uri = Uri.parse('{{url}}'); - -"""; - - String kTemplateParams = """ - - var queryParams = {{params}}; -"""; - int kParamsPadding = 20; - - String kStringUrlParams = """ - - var urlQueryParams = Map.from(uri.queryParameters); - urlQueryParams.addAll(queryParams); - uri = uri.replace(queryParameters: urlQueryParams); -"""; - - String kStringNoUrlParams = """ - - uri = uri.replace(queryParameters: queryParams); -"""; - - String kTemplateBody = """ - - String body = r'''{{body}}'''; - -"""; - - String kTemplateHeaders = """ - - var headers = {{headers}}; - -"""; - int kHeadersPadding = 16; - - String kTemplateRequest = """ - - final response = await http.{{method}}(uri"""; - - String kStringRequestHeaders = """, - headers: headers"""; - - String kStringRequestBody = """, - body: body"""; - String kStringRequestEnd = r"""); - - int statusCode = response.statusCode; - if (statusCode >= 200 && statusCode < 300) { - print('Status Code: $statusCode'); - print('Response Body: ${response.body}'); - } - else{ - print('Error Status Code: $statusCode'); - print('Error Response Body: ${response.body}'); - } -} -"""; - String? getCode( RequestModel requestModel, String defaultUriScheme, ) { try { - String result = ""; - bool hasHeaders = false; - bool hasBody = false; - String url = requestModel.url; if (!url.contains("://") && url.isNotEmpty) { url = "$defaultUriScheme://$url"; } - var templateStartUrl = jj.Template(kTemplateStart); - result += templateStartUrl.render({"url": url}); - - var paramsList = requestModel.requestParams; - if (paramsList != null) { - var params = requestModel.paramsMap; - if (params.isNotEmpty) { - var templateParams = jj.Template(kTemplateParams); - var paramsString = kEncoder.convert(params); - paramsString = padMultilineString(paramsString, kParamsPadding); - result += templateParams.render({"params": paramsString}); - Uri uri = Uri.parse(url); - if (uri.hasQuery) { - result += kStringUrlParams; - } else { - result += kStringNoUrlParams; - } - } - } - - var method = requestModel.method; - var requestBody = requestModel.requestBody; - if (kMethodsWithBody.contains(method) && requestBody != null) { - var contentLength = utf8.encode(requestBody).length; - if (contentLength > 0) { - hasBody = true; - var templateBody = jj.Template(kTemplateBody); - result += templateBody.render({"body": requestBody}); - } - } - - var headersList = requestModel.requestHeaders; - if (headersList != null || hasBody) { - var headers = requestModel.headersMap; - if (headers.isNotEmpty || hasBody) { - hasHeaders = true; - if (hasBody) { - headers[HttpHeaders.contentTypeHeader] = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; - } - var headersString = kEncoder.convert(headers); - headersString = padMultilineString(headersString, kHeadersPadding); - var templateHeaders = jj.Template(kTemplateHeaders); - result += templateHeaders.render({"headers": headersString}); - } - } - - var templateRequest = jj.Template(kTemplateRequest); - result += templateRequest.render({"method": method.name}); - - if (hasHeaders) { - result += kStringRequestHeaders; - } - - if (hasBody) { - result += kStringRequestBody; - } - - result += kStringRequestEnd; - return result; + final next = generatedDartCode( + url: url, + method: requestModel.method, + queryParams: requestModel.enabledParamsMap, + headers: {...requestModel.enabledHeadersMap}, + contentType: requestModel.requestBodyContentType, + hasContentTypeHeader: requestModel.hasContentTypeHeader, + body: requestModel.requestBody, + formData: requestModel.formDataMapList, + ); + return next; } catch (e) { return null; } } + + String generatedDartCode({ + required String url, + required HTTPVerb method, + required Map queryParams, + required Map headers, + required ContentType contentType, + required String? body, + required bool hasContentTypeHeader, + required List> formData, + }) { + final uri = Uri.parse(url); + + final sbf = StringBuffer(); + final emitter = DartEmitter(); + final dioImport = Directive.import('package:http/http.dart', as: 'http'); + sbf.writeln(dioImport.accept(emitter)); + + final uriExp = + declareVar('uri').assign(refer('Uri.parse').call([literalString(url)])); + + Expression? dataExp; + if (kMethodsWithBody.contains(method) && (body?.isNotEmpty ?? false)) { + final strContent = CodeExpression(Code('r\'\'\'$body\'\'\'')); + dataExp = declareVar('body', type: refer('String')).assign(strContent); + if (!hasContentTypeHeader) { + headers.putIfAbsent( + HttpHeaders.contentTypeHeader, () => contentType.header); + } + } + + Expression? queryParamExp; + List? uriReassignExps; + // var urlQueryParams = Map.from(uri.queryParameters); + // urlQueryParams.addAll(queryParams); + // uri = uri.replace(queryParameters: urlQueryParams); + + if (queryParams.isNotEmpty) { + queryParamExp = declareVar('queryParams').assign( + literalMap(queryParams.map((key, value) => MapEntry(key, value))), + ); + + uriReassignExps = [ + if (uri.hasQuery) + declareVar('urlQueryParams').assign( + InvokeExpression.newOf( + refer('Map'), + [refer('uri.queryParameters')], + {}, + [], + 'from', + ), + ), + if (uri.hasQuery) + refer('urlQueryParams') + .property('addAll') + .call([refer('queryParams')], {}), + refer('uri').assign(refer('uri').property('replace').call([], { + 'queryParameters': + uri.hasQuery ? refer('urlQueryParams') : refer('queryParams'), + })) + ]; + } + + Expression? headerExp; + if (headers.isNotEmpty) { + headerExp = declareVar('headers').assign( + literalMap(headers.map((key, value) => MapEntry(key, value))), + ); + } + final responseExp = declareFinal('response').assign(InvokeExpression.newOf( + refer( + 'http.${method.name}', + ), + [refer('uri')], + { + if (headerExp != null) 'headers': refer('headers'), + if (dataExp != null) 'body': refer('body'), + }, + [], + ).awaited); + final multiPartRequest = + declareFinal('request').assign(InvokeExpression.newOf( + refer( + 'http.MultipartRequest', + ), + [refer(jsonEncode(method.name.toUpperCase())), refer('uri')], + )); + final multiPartFiles = declareFinal('formDataList').assign(refer( + jsonEncode(formData), + )); + + final addHeaders = refer('request.headers.addAll').call([refer('headers')]); + const multiPartList = Code(''' + for (Map formData in formDataList){ + if (formData['type'] == 'text') { + request.fields.addAll({formData['name']: formData['value']}); + } else { + request.files.add( + await http.MultipartFile.fromPath( + formData['name'], + formData['value'], + ), + ); + } + } +'''); + var multiPartRequestSend = + declareFinal('response').assign(refer('request.send()').awaited); + var multiPartResponseBody = declareFinal('responseBody') + .assign(refer('response.stream.bytesToString()').awaited); + final mainFunction = Method((m) { + final statusRef = refer('statusCode'); + m + ..name = 'main' + ..returns = refer('void') + ..modifier = MethodModifier.async + ..body = Block((b) { + b.statements.add(uriExp.statement); + if (queryParamExp != null) { + b.statements.add(const Code('\n')); + b.statements.add(queryParamExp.statement); + } + if (uriReassignExps != null) { + b.statements.addAll(uriReassignExps.map((e) => e.statement)); + } + if (dataExp != null) { + b.statements.add(const Code('\n')); + b.statements.add(dataExp.statement); + } + if (headerExp != null) { + b.statements.add(const Code('\n')); + b.statements.add(headerExp.statement); + } + b.statements.add(const Code('\n')); + if (contentType == ContentType.formdata) { + if (formData.isNotEmpty) { + b.statements.add(multiPartFiles.statement); + } + b.statements.add(multiPartRequest.statement); + if (formData.isNotEmpty) { + b.statements.add(multiPartList); + } + if (headerExp != null) { + b.statements.add(addHeaders.statement); + } + b.statements.add(multiPartRequestSend.statement); + b.statements.add(multiPartResponseBody.statement); + b.statements.add(declareVar('statusCode', type: refer('int')) + .assign(refer('response').property('statusCode')) + .statement); + b.statements.add(const Code('\n')); + } else { + b.statements.add(responseExp.statement); + b.statements.add(const Code('\n')); + b.statements.add(declareVar('statusCode', type: refer('int')) + .assign(refer('response').property('statusCode')) + .statement); + } + + b.statements.add(declareIfElse( + condition: statusRef + .greaterOrEqualTo(literalNum(200)) + .and(statusRef.lessThan(literalNum(300))), + body: [ + refer('print').call([literalString(r'Status Code: $statusCode')]), + refer('print').call([ + literalString( + 'Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}') + ]), + ], + elseBody: [ + refer('print') + .call([literalString(r'Error Status Code: $statusCode')]), + refer('print').call([ + literalString( + 'Error Response Body: ${contentType == ContentType.formdata ? ':\$responseBody' : '\${response.body}'}') + ]), + ], + )); + }); + }); + + sbf.writeln(mainFunction.accept(emitter)); + + return DartFormatter( + pageWidth: contentType == ContentType.formdata ? 70 : 160) + .format(sbf.toString()); + } } diff --git a/lib/codegen/dart/shared.dart b/lib/codegen/dart/shared.dart new file mode 100644 index 00000000..073112dd --- /dev/null +++ b/lib/codegen/dart/shared.dart @@ -0,0 +1,52 @@ +import 'package:code_builder/code_builder.dart'; + +Code _toStatement(Spec spec) { + if (spec is Expression) { + return spec.statement; + } else if (spec is Code) { + return spec; + } else { + throw UnimplementedError(); + } +} + +Block declareTryCatch({ + required List body, + required Map> onError, + bool showStackStrace = false, +}) { + return Block((b) { + b.statements.add(const Code('try')); + b.statements.add(const Code('{')); + b.statements.addAll(body.map(_toStatement).toList()); + final entries = onError.entries; + + for (var error in entries) { + b.statements.add(const Code('}')); + if (error.key != null) { + b.statements.add(Code('on ${error.key}')); + } + b.statements.add(Code(showStackStrace ? 'catch(e,s)' : 'catch(e)')); + + b.statements.add(const Code('{')); + b.statements.addAll(error.value.map(_toStatement).toList()); + if (entries.last.key == error.key) b.statements.add(const Code('}')); + } + }); +} + +Block declareIfElse({ + required Expression condition, + required List body, + required List elseBody, +}) { + return Block.of([ + const Code('if('), + condition.code, + const Code('){'), + ...body.map(_toStatement), + const Code('} else {'), + ...elseBody.map(_toStatement), + const Code('}'), + ]); +} diff --git a/lib/codegen/js/axios.dart b/lib/codegen/js/axios.dart index 21c99c8b..1aeee285 100644 --- a/lib/codegen/js/axios.dart +++ b/lib/codegen/js/axios.dart @@ -1,16 +1,18 @@ -import 'package:apidash/consts.dart'; +import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/utils/utils.dart' - show requestModelToHARJsonRequest, padMultilineString, stripUrlParams; + show padMultilineString, requestModelToHARJsonRequest, stripUrlParams; import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; class AxiosCodeGen { AxiosCodeGen({this.isNodeJs = false}); final bool isNodeJs; - String kStringImportNode = """import axios from 'axios'; + String kStringImportNode = """{% if isNodeJs %}import axios from 'axios'; +{% endif %}{% if isFormDataRequest and isNodeJs %}const fs = require('fs');{% endif %} """; String kTemplateStart = """let config = { @@ -46,13 +48,49 @@ axios(config) console.log(error); }); """; + String kMultiPartBodyTemplate = r''' + +async function buildFormData(fields) { + var formdata = new FormData(); + for (const field of fields) { + const name = field.name || ''; + const value = field.value || ''; + const type = field.type || 'text'; + + if (type === 'text') { + formdata.append(name, value); + } else if (type === 'file') { + formdata.append(name,{% if isNodeJs %} fs.createReadStream(value){% else %} fileInput.files[0],value{% endif %}); + } + } + return formdata; +} + + +'''; + var kGetFormDataTemplate = '''buildFormData({{fields_list}}); +'''; String? getCode( RequestModel requestModel, String defaultUriScheme, ) { try { - String result = isNodeJs ? kStringImportNode : ""; + jj.Template kNodejsImportTemplate = jj.Template(kStringImportNode); + String importsData = kNodejsImportTemplate.render({ + "isFormDataRequest": requestModel.isFormDataRequest, + "isNodeJs": isNodeJs, + }); + + String result = importsData; + if (requestModel.isFormDataRequest && + requestModel.formDataMapList.isNotEmpty) { + var templateMultiPartBody = jj.Template(kMultiPartBodyTemplate); + var renderedMultiPartBody = templateMultiPartBody.render({ + "isNodeJs": isNodeJs, + }); + result += renderedMultiPartBody; + } String url = requestModel.url; if (!url.contains("://") && url.isNotEmpty) { @@ -60,7 +98,7 @@ axios(config) } var rM = requestModel.copyWith(url: url); - var harJson = requestModelToHARJsonRequest(rM); + var harJson = requestModelToHARJsonRequest(rM, useEnabled: true); var templateStart = jj.Template(kTemplateStart); result += templateStart.render({ @@ -80,18 +118,31 @@ axios(config) } var headers = harJson["headers"]; - if (headers.isNotEmpty) { + if (headers.isNotEmpty || requestModel.isFormDataRequest) { var templateHeader = jj.Template(kTemplateHeader); var m = {}; for (var i in headers) { m[i["name"]] = i["value"]; } + if (requestModel.isFormDataRequest) { + m['Content-Type'] = 'multipart/form-data'; + } result += templateHeader .render({"headers": padMultilineString(kEncoder.convert(m), 2)}); } + var templateBody = jj.Template(kTemplateBody); + if (requestModel.isFormDataRequest && + requestModel.formDataMapList.isNotEmpty) { + var getFieldDataTemplate = jj.Template(kGetFormDataTemplate); + + result += templateBody.render({ + "body": getFieldDataTemplate.render({ + "fields_list": json.encode(requestModel.formDataMapList), + }) + }); + } if (harJson["postData"]?["text"] != null) { - var templateBody = jj.Template(kTemplateBody); result += templateBody .render({"body": kEncoder.convert(harJson["postData"]["text"])}); } diff --git a/lib/codegen/js/fetch.dart b/lib/codegen/js/fetch.dart index 2d57d290..4ef2e551 100644 --- a/lib/codegen/js/fetch.dart +++ b/lib/codegen/js/fetch.dart @@ -1,15 +1,18 @@ -import 'package:apidash/consts.dart'; +import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/utils/utils.dart' - show requestModelToHARJsonRequest, padMultilineString; + show padMultilineString, requestModelToHARJsonRequest; import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; class FetchCodeGen { FetchCodeGen({this.isNodeJs = false}); final bool isNodeJs; - String kStringImportNode = """import fetch from 'node-fetch'; + String kStringImportNode = """ +import fetch from 'node-fetch'; +{% if isFormDataRequest %}const fs = require('fs');{% endif %} """; @@ -28,6 +31,26 @@ let options = { {{body}} """; + String kMultiPartBodyTemplate = r''' +async function buildDataList(fields) { + var formdata = new FormData(); + for (const field of fields) { + const name = field.name || ''; + const value = field.value || ''; + const type = field.type || 'text'; + + if (type === 'text') { + formdata.append(name, value); + } else if (type === 'file') { + formdata.append(name,{% if isNodeJs %} fs.createReadStream(value){% else %} fileInput.files[0],value{% endif %}); + } + } + return formdata; +} + +const payload = buildDataList({{fields_list}}); + +'''; String kStringRequest = """ }; @@ -53,15 +76,26 @@ fetch(url, options) String defaultUriScheme, ) { try { - String result = isNodeJs ? kStringImportNode : ""; + jj.Template kNodejsImportTemplate = jj.Template(kStringImportNode); + String importsData = kNodejsImportTemplate.render({ + "isFormDataRequest": requestModel.isFormDataRequest, + }); + String result = isNodeJs ? importsData : ""; + if (requestModel.isFormDataRequest) { + var templateMultiPartBody = jj.Template(kMultiPartBodyTemplate); + result += templateMultiPartBody.render({ + "isNodeJs": isNodeJs, + "fields_list": json.encode(requestModel.formDataMapList), + }); + } String url = requestModel.url; if (!url.contains("://") && url.isNotEmpty) { url = "$defaultUriScheme://$url"; } var rM = requestModel.copyWith(url: url); - var harJson = requestModelToHARJsonRequest(rM); + var harJson = requestModelToHARJsonRequest(rM, useEnabled: true); var templateStart = jj.Template(kTemplateStart); result += templateStart.render({ @@ -70,21 +104,33 @@ fetch(url, options) }); var headers = harJson["headers"]; + if (headers.isNotEmpty) { var templateHeader = jj.Template(kTemplateHeader); var m = {}; + if (requestModel.isFormDataRequest) { + m["Content-Type"] = "multipart/form-data"; + } for (var i in headers) { m[i["name"]] = i["value"]; } - result += templateHeader - .render({"headers": padMultilineString(kEncoder.convert(m), 2)}); + result += templateHeader.render({ + "headers": padMultilineString(kEncoder.convert(m), 2), + }); } if (harJson["postData"]?["text"] != null) { var templateBody = jj.Template(kTemplateBody); - result += templateBody - .render({"body": kEncoder.convert(harJson["postData"]["text"])}); + result += templateBody.render({ + "body": kEncoder.convert(harJson["postData"]["text"]), + }); + } else if (requestModel.isFormDataRequest) { + var templateBody = jj.Template(kTemplateBody); + result += templateBody.render({ + "body": 'payload', + }); } + result += kStringRequest; return result; } catch (e) { diff --git a/lib/codegen/kotlin/okhttp.dart b/lib/codegen/kotlin/okhttp.dart index 09cd2ce9..07e5a92b 100644 --- a/lib/codegen/kotlin/okhttp.dart +++ b/lib/codegen/kotlin/okhttp.dart @@ -1,9 +1,9 @@ import 'dart:convert'; -import 'package:apidash/consts.dart'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/utils/utils.dart' show getValidRequestUri, stripUriParams; import '../../models/request_model.dart'; +import 'package:apidash/consts.dart'; class KotlinOkHttpCodeGen { final String kTemplateStart = """import okhttp3.OkHttpClient @@ -61,6 +61,13 @@ import okhttp3.MediaType.Companion.toMediaType"""; } """; +// Converting list of form data objects to kolin multi part data + String kFormDataBody = ''' + val body = MultipartBody.Builder().setType(MultipartBody.FORM){% for item in formDataList %}{% if item.type == 'file' %} + .addFormDataPart("{{item.name}}",null,File("{{item.value}}").asRequestBody("application/octet-stream".toMediaType())) + {% else %}.addFormDataPart("{{item.name}}","{{item.value}}") + {% endif %}{% endfor %}.build() +'''; String? getCode( RequestModel requestModel, @@ -68,7 +75,6 @@ import okhttp3.MediaType.Companion.toMediaType"""; ) { try { String result = ""; - bool hasHeaders = false; bool hasQuery = false; bool hasBody = false; @@ -77,7 +83,10 @@ import okhttp3.MediaType.Companion.toMediaType"""; url = "$defaultUriScheme://$url"; } - var rec = getValidRequestUri(url, requestModel.requestParams); + var rec = getValidRequestUri( + url, + requestModel.enabledRequestParams, + ); Uri? uri = rec.$1; if (uri != null) { @@ -99,12 +108,17 @@ import okhttp3.MediaType.Companion.toMediaType"""; var method = requestModel.method; var requestBody = requestModel.requestBody; - if (kMethodsWithBody.contains(method) && requestBody != null) { + if (requestModel.isFormDataRequest) { + var formDataTemplate = jj.Template(kFormDataBody); + + result += formDataTemplate.render({ + "formDataList": requestModel.formDataMapList, + }); + } else if (kMethodsWithBody.contains(method) && requestBody != null) { var contentLength = utf8.encode(requestBody).length; if (contentLength > 0) { hasBody = true; - String contentType = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; + String contentType = requestModel.requestBodyContentType.header; var templateBody = jj.Template(kTemplateRequestBody); result += templateBody .render({"contentType": contentType, "body": requestBody}); @@ -120,11 +134,10 @@ import okhttp3.MediaType.Companion.toMediaType"""; result = stringStart + result; result += kStringRequestStart; - var headersList = requestModel.requestHeaders; + var headersList = requestModel.enabledRequestHeaders; if (headersList != null) { - var headers = requestModel.headersMap; + var headers = requestModel.enabledHeadersMap; if (headers.isNotEmpty) { - hasHeaders = true; result += getHeaders(headers); } } @@ -132,7 +145,7 @@ import okhttp3.MediaType.Companion.toMediaType"""; var templateRequestEnd = jj.Template(kTemplateRequestEnd); result += templateRequestEnd.render({ "method": method.name.toLowerCase(), - "hasBody": hasBody ? "body" : "", + "hasBody": (hasBody || requestModel.isFormDataRequest) ? "body" : "", }); } return result; diff --git a/lib/codegen/others/curl.dart b/lib/codegen/others/curl.dart index 5405ee1a..4ae610de 100644 --- a/lib/codegen/others/curl.dart +++ b/lib/codegen/others/curl.dart @@ -10,6 +10,9 @@ class cURLCodeGen { String kTemplateHeader = """ \\ --header '{{name}}: {{value}}' """; + String kTemplateFormData = """ \\ + --form '{{name}}: {{value}}' +"""; String kTemplateBody = """ \\ --data '{{body}}' @@ -28,7 +31,7 @@ class cURLCodeGen { } var rM = requestModel.copyWith(url: url); - var harJson = requestModelToHARJsonRequest(rM); + var harJson = requestModelToHARJsonRequest(rM, useEnabled: true); var templateStart = jj.Template(kTemplateStart); result += templateStart.render({ @@ -48,6 +51,23 @@ class cURLCodeGen { .render({"name": item["name"], "value": item["value"]}); } } + if (harJson['formData'] != null) { + var formDataList = harJson['formData'] as List>; + for (var formData in formDataList) { + var templateFormData = jj.Template(kTemplateFormData); + if (formData['type'] != null && + formData['name'] != null && + formData['value'] != null && + formData['name']!.isNotEmpty && + formData['value']!.isNotEmpty) { + result += templateFormData.render({ + "name": formData["name"], + "value": + "${formData['type'] == 'file' ? '@' : ''}${formData["value"]}", + }); + } + } + } if (harJson["postData"]?["text"] != null) { var templateBody = jj.Template(kTemplateBody); diff --git a/lib/codegen/others/har.dart b/lib/codegen/others/har.dart index f540cf76..77d78f58 100644 --- a/lib/codegen/others/har.dart +++ b/lib/codegen/others/har.dart @@ -9,8 +9,10 @@ class HARCodeGen { ) { try { var harString = kEncoder.convert(requestModelToHARJsonRequest( - requestModel, - defaultUriScheme: defaultUriScheme)); + requestModel, + defaultUriScheme: defaultUriScheme, + useEnabled: true, + )); return harString; } catch (e) { return null; diff --git a/lib/codegen/python/http_client.dart b/lib/codegen/python/http_client.dart index 83d02676..4406effb 100644 --- a/lib/codegen/python/http_client.dart +++ b/lib/codegen/python/http_client.dart @@ -1,13 +1,16 @@ import 'dart:io'; import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; -import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart' - show getValidRequestUri, padMultilineString; + show getNewUuid, getValidRequestUri, padMultilineString; import 'package:apidash/models/models.dart' show RequestModel; +import 'package:apidash/consts.dart'; class PythonHttpClientCodeGen { final String kTemplateStart = """import http.client +{% if isFormDataRequest %}import mimetypes +from codecs import encode +{% endif %} """; String kTemplateParams = """ @@ -30,6 +33,8 @@ body = r'''{{body}}''' headers = {{headers}} """; + String kTemplateFormHeaderContentType = ''' +multipart/form-data; boundary={{boundary}}'''; int kHeadersPadding = 10; @@ -55,10 +60,38 @@ data = res.read() print(data.decode("utf-8")) """; + final String kStringFormDataBody = r''' + +def build_data_list(fields): + dataList = [] + for field in fields: + name = field.get('name', '') + value = field.get('value', '') + type_ = field.get('type', 'text') + dataList.append(encode('--{{boundary}}')) + if type_ == 'text': + dataList.append(encode(f'Content-Disposition: form-data; name="{name}"')) + dataList.append(encode('Content-Type: text/plain')) + dataList.append(encode('')) + dataList.append(encode(value)) + elif type_ == 'file': + dataList.append(encode(f'Content-Disposition: form-data; name="{name}"; filename="{value}"')) + dataList.append(encode(f'Content-Type: {mimetypes.guess_type(value)[0] or "application/octet-stream"}')) + dataList.append(encode('')) + dataList.append(open(value, 'rb').read()) + dataList.append(encode(f'--{{boundary}}--')) + dataList.append(encode('')) + return dataList + +dataList = build_data_list({{fields_list}}) +body = b'\r\n'.join(dataList) +'''; String? getCode( RequestModel requestModel, String defaultUriScheme, ) { + String uuid = getNewUuid(); + try { String result = ""; bool hasHeaders = false; @@ -70,8 +103,17 @@ print(data.decode("utf-8")) url = "$defaultUriScheme://$url"; } - result += kTemplateStart; - var rec = getValidRequestUri(url, requestModel.requestParams); + var templateStartUrl = jj.Template(kTemplateStart); + result += templateStartUrl.render( + { + "isFormDataRequest": requestModel.isFormDataRequest, + }, + ); + var rec = getValidRequestUri( + url, + requestModel.enabledRequestParams, + ); + Uri? uri = rec.$1; if (uri != null) { @@ -97,14 +139,22 @@ print(data.decode("utf-8")) } } - var headersList = requestModel.requestHeaders; + var headersList = requestModel.enabledRequestHeaders; if (headersList != null || hasBody) { - var headers = requestModel.headersMap; + var headers = requestModel.enabledHeadersMap; + if (requestModel.isFormDataRequest) { + var formHeaderTemplate = + jj.Template(kTemplateFormHeaderContentType); + headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({ + "boundary": uuid, + }); + } + if (headers.isNotEmpty || hasBody) { hasHeaders = true; - if (hasBody) { + if (hasBody && !requestModel.hasContentTypeHeader) { headers[HttpHeaders.contentTypeHeader] = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; + requestModel.requestBodyContentType.header; } var headersString = kEncoder.convert(headers); headersString = padMultilineString(headersString, kHeadersPadding); @@ -112,7 +162,15 @@ print(data.decode("utf-8")) result += templateHeaders.render({"headers": headersString}); } } - + if (requestModel.isFormDataRequest) { + var formDataBodyData = jj.Template(kStringFormDataBody); + result += formDataBodyData.render( + { + "fields_list": json.encode(requestModel.formDataMapList), + "boundary": uuid, + }, + ); + } var templateConnection = jj.Template(kTemplateConnection); result += templateConnection.render({ "isHttps": uri.scheme == "https" ? "S" : "", @@ -126,11 +184,11 @@ print(data.decode("utf-8")) "queryParamsStr": hasQuery ? " + queryParamsStr" : "", }); - if (hasBody) { + if (hasBody || requestModel.isFormDataRequest) { result += kStringRequestBody; } - if (hasHeaders) { + if (hasHeaders || requestModel.isFormDataRequest) { result += kStringRequestHeaders; } diff --git a/lib/codegen/python/requests.dart b/lib/codegen/python/requests.dart index af8b3fbe..3a5beeb6 100644 --- a/lib/codegen/python/requests.dart +++ b/lib/codegen/python/requests.dart @@ -3,12 +3,14 @@ import 'dart:convert'; import 'package:jinja/jinja.dart' as jj; import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart' - show getValidRequestUri, padMultilineString, stripUriParams; + show getNewUuid, getValidRequestUri, padMultilineString, stripUriParams; import 'package:apidash/models/models.dart' show RequestModel; class PythonRequestsCodeGen { final String kTemplateStart = """import requests - +{% if isFormDataRequest %}import mimetypes +from codecs import encode +{% endif %} url = '{{url}}' """; @@ -37,6 +39,8 @@ payload = {{body}} headers = {{headers}} """; + String kTemplateFormHeaderContentType = ''' +multipart/form-data; boundary={{boundary}}'''; int kHeadersPadding = 10; @@ -45,6 +49,34 @@ headers = {{headers}} response = requests.{{method}}(url """; + final String kStringFormDataBody = r''' + +def build_data_list(fields): + dataList = [] + for field in fields: + name = field.get('name', '') + value = field.get('value', '') + type_ = field.get('type', 'text') + + dataList.append(encode('--{{boundary}}')) + if type_ == 'text': + dataList.append(encode(f'Content-Disposition: form-data; name="{name}"')) + dataList.append(encode('Content-Type: text/plain')) + dataList.append(encode('')) + dataList.append(encode(value)) + elif type_ == 'file': + dataList.append(encode(f'Content-Disposition: form-data; name="{name}"; filename="{value}"')) + dataList.append(encode(f'Content-Type: {mimetypes.guess_type(value)[0] or "application/octet-stream"}')) + dataList.append(encode('')) + dataList.append(open(value, 'rb').read()) + dataList.append(encode('--{{boundary}}--')) + dataList.append(encode('')) + return dataList + +dataList = build_data_list({{fields_list}}) +payload = b'\r\n'.join(dataList) +'''; + String kStringRequestParams = """, params=params"""; String kStringRequestBody = """, data=payload"""; @@ -69,17 +101,24 @@ print('Response Body:', response.text) bool hasHeaders = false; bool hasBody = false; bool hasJsonBody = false; + String uuid = getNewUuid(); String url = requestModel.url; if (!url.contains("://") && url.isNotEmpty) { url = "$defaultUriScheme://$url"; } - var rec = getValidRequestUri(url, requestModel.requestParams); + var rec = getValidRequestUri( + url, + requestModel.enabledRequestParams, + ); Uri? uri = rec.$1; if (uri != null) { var templateStartUrl = jj.Template(kTemplateStart); - result += templateStartUrl.render({"url": stripUriParams(uri)}); + result += templateStartUrl.render({ + "url": stripUriParams(uri), + 'isFormDataRequest': requestModel.isFormDataRequest + }); if (uri.hasQuery) { var params = uri.queryParameters; @@ -109,14 +148,21 @@ print('Response Body:', response.text) } } - var headersList = requestModel.requestHeaders; + var headersList = requestModel.enabledRequestHeaders; if (headersList != null || hasBody) { - var headers = requestModel.headersMap; + var headers = requestModel.enabledHeadersMap; + if (requestModel.isFormDataRequest) { + var formHeaderTemplate = + jj.Template(kTemplateFormHeaderContentType); + headers[HttpHeaders.contentTypeHeader] = formHeaderTemplate.render({ + "boundary": uuid, + }); + } if (headers.isNotEmpty || hasBody) { hasHeaders = true; if (hasBody) { headers[HttpHeaders.contentTypeHeader] = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; + requestModel.requestBodyContentType.header; } var headersString = kEncoder.convert(headers); headersString = padMultilineString(headersString, kHeadersPadding); @@ -124,7 +170,15 @@ print('Response Body:', response.text) result += templateHeaders.render({"headers": headersString}); } } - + if (requestModel.isFormDataRequest) { + var formDataBodyData = jj.Template(kStringFormDataBody); + result += formDataBodyData.render( + { + "fields_list": json.encode(requestModel.formDataMapList), + "boundary": uuid, + }, + ); + } var templateRequest = jj.Template(kTemplateRequest); result += templateRequest.render({ "method": method.name.toLowerCase(), @@ -134,15 +188,15 @@ print('Response Body:', response.text) result += kStringRequestParams; } - if (hasBody) { + if (hasBody || requestModel.isFormDataRequest) { result += kStringRequestBody; } - if (hasJsonBody) { + if (hasJsonBody || requestModel.isFormDataRequest) { result += kStringRequestJson; } - if (hasHeaders) { + if (hasHeaders || requestModel.isFormDataRequest) { result += kStringRequestHeaders; } diff --git a/lib/common/utils.dart b/lib/common/utils.dart new file mode 100644 index 00000000..8eaf2b2c --- /dev/null +++ b/lib/common/utils.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/widgets/widgets.dart'; + +Future saveCollection( + Map data, ScaffoldMessengerState sm) async { + var message = ""; + try { + var pth = await getFileDownloadpath(null, "har"); + if (pth != null) { + await saveFile(pth, jsonMapToBytes(data)); + var sp = getShortPath(pth); + message = 'Saved to $sp'; + } + } catch (e) { + message = "An error occurred while exporting."; + } + sm.hideCurrentSnackBar(); + sm.showSnackBar(getSnackBar(message, small: false)); +} diff --git a/lib/consts.dart b/lib/consts.dart index 25d93569..f8f9f97c 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -16,6 +16,10 @@ final kIsApple = !kIsWeb && (Platform.isIOS || Platform.isMacOS); final kIsDesktop = !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux); +final kIsIOS = !kIsWeb && Platform.isIOS; +final kIsAndroid = !kIsWeb && Platform.isAndroid; +final kIsMobile = !kIsWeb && (Platform.isIOS || Platform.isAndroid); + final kColorTransparentState = MaterialStateProperty.all(Colors.transparent); const kColorTransparent = Colors.transparent; @@ -44,6 +48,11 @@ const kHintOpacity = 0.6; const kForegroundOpacity = 0.05; const kTextStyleButton = TextStyle(fontWeight: FontWeight.bold); +const kTextStyleButtonSmall = TextStyle(fontSize: 12); +const kFormDataButtonLabelTextStyle = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, +); const kBorderRadius8 = BorderRadius.all(Radius.circular(8)); final kBorderRadius10 = BorderRadius.circular(10); @@ -53,22 +62,36 @@ const kP1 = EdgeInsets.all(1); const kP5 = EdgeInsets.all(5); const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); +const kPs2 = EdgeInsets.only(left: 2); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kP10 = EdgeInsets.all(10); const kPt24o8 = EdgeInsets.only(top: 24, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 10.0); +const kPh20 = EdgeInsets.symmetric( + horizontal: 20, +); const kPh20t40 = EdgeInsets.only( left: 20, right: 20, top: 40, ); const kPh60 = EdgeInsets.symmetric(horizontal: 60); -const kP24CollectionPane = EdgeInsets.only(top: 24, left: 8.0, bottom: 8.0); -const kP8CollectionPane = EdgeInsets.only(top: 8.0, left: 8.0, bottom: 8.0); +const kP24CollectionPane = EdgeInsets.only( + top: 24, + left: 4.0, + //right: 4.0, + // bottom: 8.0, +); +const kP8CollectionPane = EdgeInsets.only( + top: 8.0, + left: 4.0, + //right: 4.0, + // bottom: 8.0, +); const kPr8CollectionPane = EdgeInsets.only(right: 8.0); - +const kpsV5 = EdgeInsets.symmetric(vertical: 2); const kHSpacer4 = SizedBox(width: 4); const kHSpacer5 = SizedBox(width: 5); const kHSpacer10 = SizedBox(width: 10); @@ -220,7 +243,7 @@ enum RequestItemMenuOption { edit, delete, duplicate } enum HTTPVerb { get, head, post, put, patch, delete } -enum ContentType { json, text } +enum FormDataType { text, file } const kSupportedUriSchemes = ["https", "http"]; const kDefaultUriScheme = "https"; @@ -238,6 +261,7 @@ enum CodegenLanguage { curl("cURL", "bash", "curl"), har("HAR", "json", "har"), dartHttp("Dart (http)", "dart", "dart"), + dartDio("Dart (dio)", "dart", "dart"), jsAxios("JavaScript (axios)", "javascript", "js"), jsFetch("JavaScript (fetch)", "javascript", "js"), nodejsAxios("node.js (axios)", "javascript", "js"), @@ -287,24 +311,25 @@ const kTypeVideo = 'video'; const kSubTypeDefaultViewOptions = 'all'; -const kContentTypeMap = { - ContentType.json: "$kTypeApplication/$kSubTypeJson", - ContentType.text: "$kTypeText/$kSubTypePlain", -}; +enum ContentType { + json("$kTypeApplication/$kSubTypeJson"), + text("$kTypeText/$kSubTypePlain"), + formdata("multipart/form-data"); -enum ResponseBodyView { preview, code, raw, none } + const ContentType(this.header); + final String header; +} -const kKeyIcon = "icon"; -const kKeyName = "name"; -const Map kResponseBodyViewIcons = { - ResponseBodyView.none: {kKeyName: "Preview", kKeyIcon: Icons.warning}, - ResponseBodyView.preview: { - kKeyName: "Preview", - kKeyIcon: Icons.visibility_rounded - }, - ResponseBodyView.code: {kKeyName: "Preview", kKeyIcon: Icons.code_rounded}, - ResponseBodyView.raw: {kKeyName: "Raw", kKeyIcon: Icons.text_snippet_rounded} -}; +enum ResponseBodyView { + preview("Preview", Icons.visibility_rounded), + code("Preview", Icons.code_rounded), + raw("Raw", Icons.text_snippet_rounded), + none("Preview", Icons.warning); + + const ResponseBodyView(this.label, this.icon); + final String label; + final IconData icon; +} const kNoBodyViewOptions = [ResponseBodyView.none]; const kNoRawBodyViewOptions = [ResponseBodyView.none, ResponseBodyView.raw]; @@ -338,7 +363,7 @@ const Map>> }, kTypeImage: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, - kSubTypeSvg: kCodeRawBodyViewOptions, + kSubTypeSvg: kPreviewRawBodyViewOptions, }, kTypeAudio: { kSubTypeDefaultViewOptions: kPreviewBodyViewOptions, @@ -455,6 +480,9 @@ const kUnexpectedRaiseIssue = const kImageError = "There seems to be an issue rendering this image. Please raise an issue in API Dash GitHub repo so that we can resolve it."; +const kSvgError = + "There seems to be an issue rendering this SVG image. Please raise an issue in API Dash GitHub repo so that we can resolve it."; + const kPdfError = "There seems to be an issue rendering this pdf. Please raise an issue in API Dash GitHub repo so that we can resolve it."; diff --git a/lib/main.dart b/lib/main.dart index 3f72d775..5ac878aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'services/services.dart'; -import 'consts.dart' show kIsLinux; +import 'consts.dart' show kIsLinux, kIsMacOS, kIsWindows; import 'app.dart'; void main() async { @@ -11,13 +11,14 @@ void main() async { await openBoxes(); if (kIsLinux) { await setupInitialWindow(); - } else { + } + if (kIsMacOS || kIsWindows) { var win = getInitialSize(); await setupWindow(sz: win.$1, off: win.$2); } runApp( - ProviderScope( - child: kIsLinux ? const DashApp() : const App(), + const ProviderScope( + child: DashApp(), ), ); } diff --git a/lib/models/form_data_model.dart b/lib/models/form_data_model.dart new file mode 100644 index 00000000..add20833 --- /dev/null +++ b/lib/models/form_data_model.dart @@ -0,0 +1,23 @@ +import 'package:apidash/consts.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'form_data_model.freezed.dart'; +part 'form_data_model.g.dart'; + +@freezed +class FormDataModel with _$FormDataModel { + const factory FormDataModel({ + required String name, + required String value, + required FormDataType type, + }) = _FormDataModel; + + factory FormDataModel.fromJson(Map json) => + _$FormDataModelFromJson(json); +} + +const kFormDataEmptyModel = FormDataModel( + name: "", + value: "", + type: FormDataType.text, +); diff --git a/lib/models/form_data_model.freezed.dart b/lib/models/form_data_model.freezed.dart new file mode 100644 index 00000000..0958bd4e --- /dev/null +++ b/lib/models/form_data_model.freezed.dart @@ -0,0 +1,187 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'form_data_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +FormDataModel _$FormDataModelFromJson(Map json) { + return _FormDataModel.fromJson(json); +} + +/// @nodoc +mixin _$FormDataModel { + String get name => throw _privateConstructorUsedError; + String get value => throw _privateConstructorUsedError; + FormDataType get type => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $FormDataModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FormDataModelCopyWith<$Res> { + factory $FormDataModelCopyWith( + FormDataModel value, $Res Function(FormDataModel) then) = + _$FormDataModelCopyWithImpl<$Res, FormDataModel>; + @useResult + $Res call({String name, String value, FormDataType type}); +} + +/// @nodoc +class _$FormDataModelCopyWithImpl<$Res, $Val extends FormDataModel> + implements $FormDataModelCopyWith<$Res> { + _$FormDataModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? value = null, + Object? type = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + value: null == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as FormDataType, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$FormDataModelImplCopyWith<$Res> + implements $FormDataModelCopyWith<$Res> { + factory _$$FormDataModelImplCopyWith( + _$FormDataModelImpl value, $Res Function(_$FormDataModelImpl) then) = + __$$FormDataModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, String value, FormDataType type}); +} + +/// @nodoc +class __$$FormDataModelImplCopyWithImpl<$Res> + extends _$FormDataModelCopyWithImpl<$Res, _$FormDataModelImpl> + implements _$$FormDataModelImplCopyWith<$Res> { + __$$FormDataModelImplCopyWithImpl( + _$FormDataModelImpl _value, $Res Function(_$FormDataModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? value = null, + Object? type = null, + }) { + return _then(_$FormDataModelImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + value: null == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as FormDataType, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$FormDataModelImpl implements _FormDataModel { + const _$FormDataModelImpl( + {required this.name, required this.value, required this.type}); + + factory _$FormDataModelImpl.fromJson(Map json) => + _$$FormDataModelImplFromJson(json); + + @override + final String name; + @override + final String value; + @override + final FormDataType type; + + @override + String toString() { + return 'FormDataModel(name: $name, value: $value, type: $type)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FormDataModelImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.value, value) || other.value == value) && + (identical(other.type, type) || other.type == type)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, name, value, type); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$FormDataModelImplCopyWith<_$FormDataModelImpl> get copyWith => + __$$FormDataModelImplCopyWithImpl<_$FormDataModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$FormDataModelImplToJson( + this, + ); + } +} + +abstract class _FormDataModel implements FormDataModel { + const factory _FormDataModel( + {required final String name, + required final String value, + required final FormDataType type}) = _$FormDataModelImpl; + + factory _FormDataModel.fromJson(Map json) = + _$FormDataModelImpl.fromJson; + + @override + String get name; + @override + String get value; + @override + FormDataType get type; + @override + @JsonKey(ignore: true) + _$$FormDataModelImplCopyWith<_$FormDataModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/form_data_model.g.dart b/lib/models/form_data_model.g.dart new file mode 100644 index 00000000..f539458f --- /dev/null +++ b/lib/models/form_data_model.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'form_data_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$FormDataModelImpl _$$FormDataModelImplFromJson(Map json) => + _$FormDataModelImpl( + name: json['name'] as String, + value: json['value'] as String, + type: $enumDecode(_$FormDataTypeEnumMap, json['type']), + ); + +Map _$$FormDataModelImplToJson(_$FormDataModelImpl instance) => + { + 'name': instance.name, + 'value': instance.value, + 'type': _$FormDataTypeEnumMap[instance.type]!, + }; + +const _$FormDataTypeEnumMap = { + FormDataType.text: 'text', + FormDataType.file: 'file', +}; diff --git a/lib/models/models.dart b/lib/models/models.dart index 3820e3c2..66d6f6ce 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -2,3 +2,4 @@ export 'name_value_model.dart'; export 'request_model.dart'; export 'response_model.dart'; export 'settings_model.dart'; +export 'form_data_model.dart'; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index a928dac1..9154e428 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,8 +1,14 @@ +import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:apidash/consts.dart'; -import 'package:apidash/utils/utils.dart' show mapToRows, rowsToMap; -import 'name_value_model.dart'; -import 'response_model.dart'; +import '../utils/utils.dart' + show + mapListToFormDataModelRows, + rowsToFormDataMapList, + mapToRows, + rowsToMap, + getEnabledRows; +import '../consts.dart'; +import 'models.dart'; @immutable class RequestModel { @@ -15,8 +21,11 @@ class RequestModel { 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, @@ -30,15 +39,34 @@ class RequestModel { 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); + RequestModel duplicate({ required String id, }) { @@ -50,8 +78,14 @@ class RequestModel { 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, ); } @@ -64,14 +98,19 @@ class RequestModel { 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 RequestModel( id: id ?? this.id, method: method ?? this.method, @@ -81,9 +120,12 @@ class RequestModel { 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, @@ -106,6 +148,8 @@ class RequestModel { 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); @@ -113,9 +157,11 @@ class RequestModel { 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)); @@ -136,8 +182,13 @@ class RequestModel { 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, @@ -153,8 +204,11 @@ class RequestModel { "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, @@ -171,9 +225,12 @@ class RequestModel { "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()}" @@ -192,8 +249,11 @@ class RequestModel { 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; @@ -211,8 +271,11 @@ class RequestModel { requestTabIndex, requestHeaders, requestParams, + isHeaderEnabledList, + isParamEnabledList, requestBodyContentType, requestBody, + requestFormDataList, responseStatus, message, responseModel, diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index eaeb4fa7..8ee87ee7 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -101,4 +101,32 @@ class SettingsModel { String toString() { return toJson().toString(); } + + @override + bool operator ==(Object other) { + return other is SettingsModel && + other.runtimeType == runtimeType && + other.isDark == isDark && + other.alwaysShowCollectionPaneScrollbar == + alwaysShowCollectionPaneScrollbar && + other.size == size && + other.offset == offset && + other.defaultUriScheme == defaultUriScheme && + other.defaultCodeGenLang == defaultCodeGenLang && + other.saveResponses == saveResponses; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + isDark, + alwaysShowCollectionPaneScrollbar, + size, + offset, + defaultUriScheme, + defaultCodeGenLang, + saveResponses, + ); + } } diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index e0273a34..415c9715 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -5,6 +5,7 @@ import '../models/models.dart'; import '../services/services.dart' show hiveHandler, HiveHandler, request; import '../utils/utils.dart' show uuid, collectionToHAR; import '../consts.dart'; +import 'package:http/http.dart' as http; final activeIdStateProvider = StateProvider((ref) => null); @@ -123,8 +124,11 @@ class CollectionStateNotifier int? requestTabIndex, List? requestHeaders, List? requestParams, + List? isHeaderEnabledList, + List? isParamEnabledList, ContentType? requestBodyContentType, String? requestBody, + List? requestFormDataList, int? responseStatus, String? message, ResponseModel? responseModel, @@ -137,8 +141,11 @@ class CollectionStateNotifier requestTabIndex: requestTabIndex, requestHeaders: requestHeaders, requestParams: requestParams, + isHeaderEnabledList: isHeaderEnabledList, + isParamEnabledList: isParamEnabledList, requestBodyContentType: requestBodyContentType, requestBody: requestBody, + requestFormDataList: requestFormDataList, responseStatus: responseStatus, message: message, responseModel: responseModel); @@ -153,10 +160,13 @@ class CollectionStateNotifier ref.read(codePaneVisibleStateProvider.notifier).state = false; final defaultUriScheme = ref.read(settingsProvider.select((value) => value.defaultUriScheme)); - RequestModel requestModel = state![id]!; - var responseRec = - await request(requestModel, defaultUriScheme: defaultUriScheme); + (http.Response?, Duration?, String?)? responseRec = await request( + requestModel, + defaultUriScheme: defaultUriScheme, + isMultiPartRequest: + requestModel.requestBodyContentType == ContentType.formdata, + ); late final RequestModel newRequestModel; if (responseRec.$1 == null) { newRequestModel = requestModel.copyWith( @@ -175,7 +185,6 @@ class CollectionStateNotifier responseModel: responseModel, ); } - //print(newRequestModel); ref.read(sentRequestIdStateProvider.notifier).state = null; var map = {...state!}; map[id] = newRequestModel; diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 19d2ca28..35189fd0 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -6,16 +6,11 @@ import 'home_page/home_page.dart'; import 'intro_page.dart'; import 'settings_page.dart'; -class Dashboard extends ConsumerStatefulWidget { +class Dashboard extends ConsumerWidget { const Dashboard({super.key}); @override - ConsumerState createState() => _DashboardState(); -} - -class _DashboardState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); return Scaffold( body: SafeArea( @@ -50,13 +45,13 @@ class _DashboardState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: bottomButton(context, railIdx, 1, Icons.help, - Icons.help_outline), + child: bottomButton(context, ref, railIdx, 1, + Icons.help, Icons.help_outline), ), Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: bottomButton(context, railIdx, 2, Icons.settings, - Icons.settings_outlined), + child: bottomButton(context, ref, railIdx, 2, + Icons.settings, Icons.settings_outlined), ), ], ), @@ -99,6 +94,7 @@ class _DashboardState extends ConsumerState { TextButton bottomButton( BuildContext context, + WidgetRef ref, int railIdx, int buttonIdx, IconData selectedIcon, 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 07c5bf53..3c8b74cb 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 @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import 'request_form_data.dart'; class EditRequestBody extends ConsumerStatefulWidget { const EditRequestBody({super.key}); @@ -18,6 +19,10 @@ class _EditRequestBodyState extends ConsumerState { final requestModel = ref .read(collectionStateNotifierProvider.notifier) .getRequestModel(activeId!); + ContentType? requestBodyStateWatcher = (ref + .watch(collectionStateNotifierProvider)![activeId] + ?.requestBodyContentType) ?? + ContentType.values.first; return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, @@ -38,16 +43,18 @@ class _EditRequestBodyState extends ConsumerState { ), ), Expanded( - child: TextFieldEditor( - key: Key("$activeId-body"), - fieldKey: "$activeId-body-editor", - initialValue: requestModel?.requestBody, - onChanged: (String value) { - ref - .read(collectionStateNotifierProvider.notifier) - .update(activeId, requestBody: value); - }, - ), + child: requestBodyStateWatcher == ContentType.formdata + ? const FormDataWidget() + : TextFieldEditor( + key: Key("$activeId-body"), + fieldKey: "$activeId-body-editor", + initialValue: requestModel?.requestBody, + onChanged: (String value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(activeId, requestBody: value); + }, + ), ) ], ), diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart new file mode 100644 index 00000000..bb4782d2 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart @@ -0,0 +1,223 @@ +import 'dart:math'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/form_data_model.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/widgets/form_data_field.dart'; +import 'package:apidash/widgets/textfields.dart'; +import 'package:davi/davi.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class FormDataWidget extends ConsumerStatefulWidget { + const FormDataWidget({super.key}); + @override + ConsumerState createState() => _FormDataBodyState(); +} + +class _FormDataBodyState extends ConsumerState { + late int seed; + final random = Random.secure(); + late List rows; + @override + void initState() { + super.initState(); + seed = random.nextInt(kRandMax); + } + + @override + Widget build(BuildContext context) { + final activeId = ref.watch(activeIdStateProvider); + var formRows = ref.read(activeRequestModelProvider)?.requestFormDataList; + rows = + formRows == null || formRows.isEmpty ? [kFormDataEmptyModel] : formRows; + + DaviModel daviModelRows = DaviModel( + rows: rows, + columns: [ + DaviColumn( + cellPadding: kpsV5, + name: 'Key', + grow: 4, + cellBuilder: (_, row) { + int idx = row.index; + return Theme( + data: Theme.of(context), + child: FormDataField( + keyId: "$activeId-$idx-form-v-$seed", + initialValue: rows[idx].name, + hintText: " Add Key", + onChanged: (value) { + rows[idx] = rows[idx].copyWith( + name: value, + ); + _onFieldChange(activeId!); + }, + colorScheme: Theme.of(context).colorScheme, + formDataType: rows[idx].type, + onFormDataTypeChanged: (value) { + rows[idx] = rows[idx].copyWith( + type: value ?? FormDataType.text, + ); + rows[idx] = rows[idx].copyWith(value: ""); + setState(() {}); + _onFieldChange(activeId!); + }, + ), + ); + }, + sortable: false, + ), + DaviColumn( + width: 40, + cellPadding: kpsV5, + cellAlignment: Alignment.center, + cellBuilder: (_, row) { + return Text( + "=", + style: kCodeStyle, + ); + }, + ), + DaviColumn( + name: 'Value', + grow: 4, + cellPadding: kpsV5, + cellBuilder: (_, row) { + int idx = row.index; + return rows[idx].type == FormDataType.file + ? Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Expanded( + child: Theme( + data: Theme.of(context), + child: ElevatedButton.icon( + icon: const Icon( + Icons.snippet_folder_rounded, + size: 20, + ), + style: ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + ), + onPressed: () async { + FilePickerResult? pickedResult = + await FilePicker.platform.pickFiles(); + if (pickedResult != null && + pickedResult.files.isNotEmpty && + pickedResult.files.first.path != null) { + rows[idx] = rows[idx].copyWith( + value: pickedResult.files.first.path!, + ); + setState(() {}); + _onFieldChange(activeId!); + } + }, + label: Text( + (rows[idx].type == FormDataType.file && + rows[idx].value.isNotEmpty) + ? rows[idx].value.toString() + : "Select File", + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: kFormDataButtonLabelTextStyle, + ), + ), + ), + ), + ], + ), + ) + : CellField( + keyId: "$activeId-$idx-form-v-$seed", + initialValue: rows[idx].value, + hintText: " Add Value", + onChanged: (value) { + rows[idx] = rows[idx].copyWith(value: value); + _onFieldChange(activeId!); + }, + colorScheme: Theme.of(context).colorScheme, + ); + }, + sortable: false, + ), + DaviColumn( + pinStatus: PinStatus.none, + width: 30, + cellBuilder: (_, row) { + return InkWell( + child: Theme.of(context).brightness == Brightness.dark + ? kIconRemoveDark + : kIconRemoveLight, + onTap: () { + seed = random.nextInt(kRandMax); + if (rows.length == 1) { + setState(() { + rows = [kFormDataEmptyModel]; + }); + } else { + rows.removeAt(row.index); + } + _onFieldChange(activeId!); + setState(() {}); + }, + ); + }, + ), + ], + ); + return Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: Column( + children: [ + Expanded( + child: DaviTheme( + data: kTableThemeData, + child: Davi(daviModelRows), + ), + ), + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: ElevatedButton.icon( + onPressed: () { + setState(() { + rows.add(kFormDataEmptyModel); + }); + _onFieldChange(activeId!); + }, + icon: const Icon(Icons.add), + label: const Text( + "Add Form Data", + style: kTextStyleButton, + ), + ), + ), + ), + ], + ); + } + + void _onFieldChange(String activeId) { + ref.read(collectionStateNotifierProvider.notifier).update( + activeId, + requestFormDataList: rows, + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart index 82138db5..76866ab9 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_headers.dart @@ -16,6 +16,7 @@ class EditRequestHeaders extends ConsumerStatefulWidget { class EditRequestHeadersState extends ConsumerState { late List rows; + late List isRowEnabledList; final random = Random.secure(); late int seed; @@ -26,9 +27,11 @@ class EditRequestHeadersState extends ConsumerState { } void _onFieldChange(String activeId) { - ref - .read(collectionStateNotifierProvider.notifier) - .update(activeId, requestHeaders: rows); + ref.read(collectionStateNotifierProvider.notifier).update( + activeId, + requestHeaders: rows, + isHeaderEnabledList: isRowEnabledList, + ); } @override @@ -42,12 +45,34 @@ class EditRequestHeadersState extends ConsumerState { kNameValueEmptyModel, ] : rH; + isRowEnabledList = + ref.read(activeRequestModelProvider)?.isHeaderEnabledList ?? + List.filled(rows.length, true, growable: true); DaviModel model = DaviModel( rows: rows, columns: [ + DaviColumn( + name: 'Checkbox', + width: 30, + cellBuilder: (_, row) { + int idx = row.index; + return CheckBox( + keyId: "$activeId-$idx-headers-c-$seed", + value: isRowEnabledList[idx], + onChanged: (value) { + setState(() { + isRowEnabledList[idx] = value!; + }); + _onFieldChange(activeId!); + }, + colorScheme: Theme.of(context).colorScheme, + ); + }, + ), DaviColumn( name: 'Header Name', + width: 70, grow: 1, cellBuilder: (_, row) { int idx = row.index; @@ -106,9 +131,11 @@ class EditRequestHeadersState extends ConsumerState { rows = [ kNameValueEmptyModel, ]; + isRowEnabledList = [true]; }); } else { rows.removeAt(row.index); + isRowEnabledList.removeAt(row.index); } _onFieldChange(activeId!); }, @@ -143,6 +170,7 @@ class EditRequestHeadersState extends ConsumerState { child: ElevatedButton.icon( onPressed: () { rows.add(kNameValueEmptyModel); + isRowEnabledList.add(true); _onFieldChange(activeId!); }, icon: const Icon(Icons.add), diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart index 9a8961c6..736bf619 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_params.dart @@ -17,6 +17,7 @@ class EditRequestURLParams extends ConsumerStatefulWidget { class EditRequestURLParamsState extends ConsumerState { late List rows; + late List isRowEnabledList; final random = Random.secure(); late int seed; @@ -27,9 +28,11 @@ class EditRequestURLParamsState extends ConsumerState { } void _onFieldChange(String activeId) { - ref - .read(collectionStateNotifierProvider.notifier) - .update(activeId, requestParams: rows); + ref.read(collectionStateNotifierProvider.notifier).update( + activeId, + requestParams: rows, + isParamEnabledList: isRowEnabledList, + ); } @override @@ -43,12 +46,35 @@ class EditRequestURLParamsState extends ConsumerState { kNameValueEmptyModel, ] : rP; + isRowEnabledList = + ref.read(activeRequestModelProvider)?.isParamEnabledList ?? + List.filled(rows.length, true, growable: true); DaviModel model = DaviModel( rows: rows, columns: [ + DaviColumn( + name: 'Checkbox', + width: 30, + cellBuilder: (_, row) { + int idx = row.index; + + return CheckBox( + keyId: "$activeId-$idx-params-c-$seed", + value: isRowEnabledList[idx], + onChanged: (value) { + setState(() { + isRowEnabledList[idx] = value!; + }); + _onFieldChange(activeId!); + }, + colorScheme: Theme.of(context).colorScheme, + ); + }, + ), DaviColumn( name: 'URL Parameter', + width: 70, grow: 1, cellBuilder: (_, row) { int idx = row.index; @@ -107,9 +133,11 @@ class EditRequestURLParamsState extends ConsumerState { rows = [ kNameValueEmptyModel, ]; + isRowEnabledList = [true]; }); } else { rows.removeAt(row.index); + isRowEnabledList.removeAt(row.index); } _onFieldChange(activeId!); }, @@ -144,6 +172,7 @@ class EditRequestURLParamsState extends ConsumerState { child: ElevatedButton.icon( onPressed: () { rows.add(kNameValueEmptyModel); + isRowEnabledList.add(true); _onFieldChange(activeId!); }, icon: const Icon(Icons.add), diff --git a/lib/screens/home_page/editor_pane/editor_pane.dart b/lib/screens/home_page/editor_pane/editor_pane.dart index 9a391279..ec7d87d0 100644 --- a/lib/screens/home_page/editor_pane/editor_pane.dart +++ b/lib/screens/home_page/editor_pane/editor_pane.dart @@ -27,7 +27,7 @@ class _RequestEditorPaneState extends ConsumerState { return const RequestEditorDefault(); } else { return Padding( - padding: kIsMacOS ? kPt24o8 : kP8, + padding: kIsMacOS || kIsWindows ? kPt24o8 : kP8, child: const Column( children: [ EditorPaneRequestURLCard(), diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 753fb34a..b7df1d66 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -4,20 +4,9 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -class EditorPaneRequestURLCard extends StatefulWidget { +class EditorPaneRequestURLCard extends StatelessWidget { const EditorPaneRequestURLCard({super.key}); - @override - State createState() => - _EditorPaneRequestURLCardState(); -} - -class _EditorPaneRequestURLCardState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return Card( @@ -52,25 +41,13 @@ class _EditorPaneRequestURLCardState extends State { } } -class DropdownButtonHTTPMethod extends ConsumerStatefulWidget { +class DropdownButtonHTTPMethod extends ConsumerWidget { const DropdownButtonHTTPMethod({ super.key, }); @override - ConsumerState createState() => - _DropdownButtonHTTPMethodState(); -} - -class _DropdownButtonHTTPMethodState - extends ConsumerState { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final method = ref.watch(activeRequestModelProvider.select((value) => value?.method)); return DropdownButtonHttpMethod( @@ -85,23 +62,13 @@ class _DropdownButtonHTTPMethodState } } -class URLTextField extends ConsumerStatefulWidget { +class URLTextField extends ConsumerWidget { const URLTextField({ super.key, }); @override - ConsumerState createState() => _URLTextFieldState(); -} - -class _URLTextFieldState extends ConsumerState { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final activeId = ref.watch(activeIdStateProvider); return URLField( activeId: activeId!, @@ -118,23 +85,13 @@ class _URLTextFieldState extends ConsumerState { } } -class SendButton extends ConsumerStatefulWidget { +class SendButton extends ConsumerWidget { const SendButton({ super.key, }); @override - ConsumerState createState() => _SendButtonState(); -} - -class _SendButtonState extends ConsumerState { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final activeId = ref.watch(activeIdStateProvider); final sentRequestId = ref.watch(sentRequestIdStateProvider); return SendRequestButton( diff --git a/lib/screens/home_page/home_page.dart b/lib/screens/home_page/home_page.dart index d8843f0d..a7338a08 100644 --- a/lib/screens/home_page/home_page.dart +++ b/lib/screens/home_page/home_page.dart @@ -3,14 +3,9 @@ import 'package:apidash/widgets/widgets.dart'; import 'editor_pane/editor_pane.dart'; import 'collection_pane.dart'; -class HomePage extends StatefulWidget { +class HomePage extends StatelessWidget { const HomePage({super.key}); - @override - HomePageState createState() => HomePageState(); -} - -class HomePageState extends State { @override Widget build(BuildContext context) { return const Column( diff --git a/lib/screens/intro_page.dart b/lib/screens/intro_page.dart index f7ca937b..83d32253 100644 --- a/lib/screens/intro_page.dart +++ b/lib/screens/intro_page.dart @@ -1,14 +1,9 @@ import 'package:flutter/material.dart'; import 'package:apidash/widgets/widgets.dart'; -class IntroPage extends StatefulWidget { +class IntroPage extends StatelessWidget { const IntroPage({super.key}); - @override - State createState() => _IntroPageState(); -} - -class _IntroPageState extends State { @override Widget build(BuildContext context) { return const IntroMessage(); diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart new file mode 100644 index 00000000..b6f25041 --- /dev/null +++ b/lib/screens/mobile/dashboard.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../intro_page.dart'; +import '../settings_page.dart'; +import '../home_page/collection_pane.dart'; + +class MobileDashboard extends ConsumerStatefulWidget { + const MobileDashboard( + {required this.scaffoldBody, required this.title, super.key}); + + final Widget scaffoldBody; + final String title; + + @override + ConsumerState createState() => _MobileDashboardState(); +} + +class _MobileDashboardState extends ConsumerState { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + drawer: Drawer( + child: ListView( + padding: EdgeInsets.zero, + children: [ + const SizedBox( + height: 70, + ), + ListTile( + title: const Text('Home'), + leading: const Icon(Icons.home_outlined), + onTap: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const MobileDashboard( + title: 'Home', + scaffoldBody: IntroPage(), + ), + ), + (Route route) => false); + }, + ), + ListTile( + title: const Text('Requests'), + leading: const Icon(Icons.auto_awesome_mosaic_outlined), + onTap: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const MobileDashboard( + title: 'Requests', + scaffoldBody: CollectionPane(), + ), + ), + (Route route) => false); + }, + ), + ListTile( + title: const Text('Settings'), + leading: const Icon(Icons.settings_outlined), + onTap: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const MobileDashboard( + title: 'Settings', + scaffoldBody: SettingsPage(), + ), + ), + (Route route) => false); + }, + ), + const Divider(), + ], + ), + ), + body: SafeArea( + child: widget.scaffoldBody, + ), + ); + } +} diff --git a/lib/screens/mobile/mobile.dart b/lib/screens/mobile/mobile.dart new file mode 100644 index 00000000..1b8c3333 --- /dev/null +++ b/lib/screens/mobile/mobile.dart @@ -0,0 +1 @@ +export 'dashboard.dart'; diff --git a/lib/screens/screens.dart b/lib/screens/screens.dart index c3c70a99..c647c90d 100644 --- a/lib/screens/screens.dart +++ b/lib/screens/screens.dart @@ -1 +1,3 @@ -export "dashboard.dart"; +export 'dashboard.dart'; +export 'mobile/mobile.dart'; +export 'home_page/collection_pane.dart'; diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 28e93658..c40c8dd2 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -2,34 +2,40 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; import '../widgets/widgets.dart'; -import '../utils/utils.dart'; -import 'package:apidash/consts.dart'; +import '../common/utils.dart'; +import '../consts.dart'; -class SettingsPage extends ConsumerStatefulWidget { +class SettingsPage extends ConsumerWidget { const SettingsPage({super.key}); @override - ConsumerState createState() => _SettingsPageState(); -} - -class _SettingsPageState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final settings = ref.watch(settingsProvider); final clearingData = ref.watch(clearDataStateProvider); var sm = ScaffoldMessenger.of(context); return Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - constraints: const BoxConstraints(maxWidth: 800), + Padding( + padding: kPh20t40, + child: kIsDesktop + ? Text("Settings", + style: Theme.of(context).textTheme.headlineLarge) + : const SizedBox.shrink(), + ), + kIsDesktop + ? const Padding( + padding: kPh20, + child: Divider( + height: 1, + ), + ) + : const SizedBox.shrink(), + Expanded( child: ListView( - padding: kPh20t40, shrinkWrap: true, + padding: kPh20, children: [ - Text("Settings", - style: Theme.of(context).textTheme.headlineLarge), - const Divider(), SwitchListTile( contentPadding: EdgeInsets.zero, hoverColor: kColorTransparent, @@ -112,26 +118,18 @@ class _SettingsPageState extends ConsumerState { title: const Text('Export Data'), subtitle: const Text( 'Export your collection to HAR (HTTP Archive format).\nVersion control this file or import in other API clients.'), - trailing: FilledButton( + trailing: FilledButton.icon( onPressed: () async { - var message = ""; - try { - var data = await ref - .read(collectionStateNotifierProvider.notifier) - .exportDataToHAR(); - var pth = await getFileDownloadpath(null, "har"); - if (pth != null) { - await saveFile(pth, jsonMapToBytes(data)); - var sp = getShortPath(pth); - message = 'Saved to $sp'; - } - } catch (e) { - message = "An error occurred while exporting."; - } - sm.hideCurrentSnackBar(); - sm.showSnackBar(getSnackBar(message, small: false)); + var data = await ref + .read(collectionStateNotifierProvider.notifier) + .exportDataToHAR(); + await saveCollection(data, sm); }, - child: const Text("Export Data"), + label: const Text("Export"), + icon: const Icon( + Icons.arrow_outward_rounded, + size: 20, + ), ), ), ListTile( @@ -139,7 +137,7 @@ class _SettingsPageState extends ConsumerState { hoverColor: kColorTransparent, title: const Text('Clear Data'), subtitle: const Text('Delete all requests data from the disk'), - trailing: FilledButton.tonal( + trailing: FilledButton.tonalIcon( style: FilledButton.styleFrom( backgroundColor: settings.isDark ? kColorDarkDanger @@ -183,7 +181,11 @@ class _SettingsPageState extends ConsumerState { ], ), ), - child: const Text("Clear Data"), + label: const Text("Clear"), + icon: const Icon( + Icons.delete_forever_rounded, + size: 20, + ), ), ), ], diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 3052024e..43ed769c 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; -const String kDataBox = "data"; +const String kDataBox = "apidash-data"; const String kKeyDataBoxIds = "ids"; -const String kSettingsBox = "settings"; +const String kSettingsBox = "apidash-settings"; Future openBoxes() async { await Hive.initFlutter(); diff --git a/lib/services/http_service.dart b/lib/services/http_service.dart index 8265a299..c5a7b4ce 100644 --- a/lib/services/http_service.dart +++ b/lib/services/http_service.dart @@ -9,15 +9,16 @@ import 'package:apidash/consts.dart'; Future<(http.Response?, Duration?, String?)> request( RequestModel requestModel, { String defaultUriScheme = kDefaultUriScheme, + bool isMultiPartRequest = false, }) async { (Uri?, String?) uriRec = getValidRequestUri( requestModel.url, - requestModel.requestParams, + requestModel.enabledRequestParams, defaultUriScheme: defaultUriScheme, ); if (uriRec.$1 != null) { Uri requestUrl = uriRec.$1!; - Map headers = requestModel.headersMap; + Map headers = requestModel.enabledHeadersMap; http.Response response; String? body; try { @@ -28,11 +29,38 @@ Future<(http.Response?, Duration?, String?)> request( if (contentLength > 0) { body = requestBody; headers[HttpHeaders.contentLengthHeader] = contentLength.toString(); - headers[HttpHeaders.contentTypeHeader] = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; + if (!requestModel.hasContentTypeHeader) { + headers[HttpHeaders.contentTypeHeader] = + requestModel.requestBodyContentType.header; + } } } Stopwatch stopwatch = Stopwatch()..start(); + if (isMultiPartRequest) { + var multiPartRequest = http.MultipartRequest( + requestModel.method.name.toUpperCase(), + requestUrl, + ); + multiPartRequest.headers.addAll(headers); + for (FormDataModel formData + in (requestModel.requestFormDataList ?? [])) { + if (formData.type == FormDataType.text) { + multiPartRequest.fields.addAll({formData.name: formData.value}); + } else { + multiPartRequest.files.add( + await http.MultipartFile.fromPath( + formData.name, + formData.value, + ), + ); + } + } + http.StreamedResponse multiPartResponse = await multiPartRequest.send(); + stopwatch.stop(); + http.Response convertedMultiPartResponse = + await convertStreamedResponse(multiPartResponse); + return (convertedMultiPartResponse, stopwatch.elapsed, null); + } switch (requestModel.method) { case HTTPVerb.get: response = await http.get(requestUrl, headers: headers); diff --git a/lib/services/window_services.dart b/lib/services/window_services.dart index 60b13f50..0b430e53 100644 --- a/lib/services/window_services.dart +++ b/lib/services/window_services.dart @@ -62,7 +62,7 @@ Future setupWindow({Size? sz, Offset? off, bool center = false}) async { minimumSize: kMinWindowSize, skipTaskbar: false, title: kWindowTitle, - titleBarStyle: kIsMacOS ? TitleBarStyle.hidden : null, + titleBarStyle: kIsMacOS || kIsWindows ? TitleBarStyle.hidden : null, ); if (off != null) { await windowManager.setPosition(off); diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index e12f2dcc..bc704af9 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'dart:convert'; import '../models/models.dart'; import '../consts.dart'; +import 'package:http/http.dart' as http; String humanizeDuration(Duration? duration) { if (duration == null) { @@ -89,6 +90,43 @@ List? mapToRows(Map? kvMap) { return finalRows; } +List>? rowsToFormDataMapList( + List? kvRows, +) { + if (kvRows == null) { + return null; + } + List> finalMap = kvRows + .map((FormDataModel formData) => { + "name": formData.name, + "value": formData.value, + "type": formData.type.name, + }) + .toList(); + return finalMap; +} + +List? mapListToFormDataModelRows(List? kvMap) { + if (kvMap == null) { + return null; + } + List finalRows = kvMap.map( + (formData) { + return FormDataModel( + name: formData["name"], + value: formData["value"], + type: getFormDataType(formData["type"]), + ); + }, + ).toList(); + return finalRows; +} + +FormDataType getFormDataType(String? type) { + return FormDataType.values.firstWhere((element) => element.name == type, + orElse: () => FormDataType.text); +} + Uint8List? stringToBytes(String? text) { if (text == null) { return null; @@ -109,3 +147,30 @@ Uint8List jsonMapToBytes(Map? map) { return bytes; } } + +Future convertStreamedResponse( + http.StreamedResponse streamedResponse, +) async { + Uint8List bodyBytes = await streamedResponse.stream.toBytes(); + + http.Response response = http.Response.bytes( + bodyBytes, + streamedResponse.statusCode, + headers: streamedResponse.headers, + persistentConnection: streamedResponse.persistentConnection, + reasonPhrase: streamedResponse.reasonPhrase, + request: streamedResponse.request, + ); + + return response; +} + +List? getEnabledRows( + List? rows, List? isRowEnabledList) { + if (rows == null || isRowEnabledList == null) { + return rows; + } + List finalRows = + rows.where((element) => isRowEnabledList[rows.indexOf(element)]).toList(); + return finalRows == [] ? null : finalRows; +} diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart index be18a209..e9594864 100644 --- a/lib/utils/file_utils.dart +++ b/lib/utils/file_utils.dart @@ -7,6 +7,10 @@ import 'package:path_provider/path_provider.dart'; const uuid = Uuid(); +String getNewUuid() { + return uuid.v1(); +} + String? getFileExtension(String? mimeType) { if (mimeType == null) { return null; diff --git a/lib/utils/har_utils.dart b/lib/utils/har_utils.dart index f0838807..38913c6e 100644 --- a/lib/utils/har_utils.dart +++ b/lib/utils/har_utils.dart @@ -74,13 +74,14 @@ Map requestModelToHARJsonRequest( RequestModel requestModel, { defaultUriScheme = kDefaultUriScheme, bool exportMode = false, + bool useEnabled = false, }) { Map json = {}; bool hasBody = false; var rec = getValidRequestUri( requestModel.url, - requestModel.requestParams, + useEnabled ? requestModel.enabledRequestParams : requestModel.requestParams, defaultUriScheme: defaultUriScheme, ); @@ -117,7 +118,7 @@ Map requestModelToHARJsonRequest( hasBody = true; json["postData"] = {}; json["postData"]["mimeType"] = - kContentTypeMap[requestModel.requestBodyContentType] ?? ""; + requestModel.requestBodyContentType.header; json["postData"]["text"] = requestBody; if (exportMode) { json["postData"]["comment"] = ""; @@ -125,14 +126,17 @@ Map requestModelToHARJsonRequest( } } - var headersList = requestModel.requestHeaders; + var headersList = useEnabled + ? requestModel.enabledRequestHeaders + : requestModel.requestHeaders; if (headersList != null || hasBody) { - var headers = requestModel.headersMap; + var headers = + useEnabled ? requestModel.enabledHeadersMap : requestModel.headersMap; if (headers.isNotEmpty || hasBody) { - if (hasBody) { + if (hasBody && !requestModel.hasContentTypeHeader) { var m = { "name": "Content-Type", - "value": kContentTypeMap[requestModel.requestBodyContentType] ?? "" + "value": requestModel.requestBodyContentType.header }; if (exportMode) { m["comment"] = ""; @@ -148,7 +152,9 @@ Map requestModelToHARJsonRequest( } } } - + if (requestModel.isFormDataRequest) { + json["formData"] = requestModel.formDataMapList; + } if (exportMode) { json["comment"] = ""; json["cookies"] = []; diff --git a/lib/utils/header_utils.dart b/lib/utils/header_utils.dart index 63a456ef..45a24fe9 100644 --- a/lib/utils/header_utils.dart +++ b/lib/utils/header_utils.dart @@ -2,19 +2,37 @@ Map headers = { "Accept": "Specifies the media types that are acceptable for the response.", "Accept-Encoding": "Indicates the encoding methods the client can understand.", + "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": + "Specifies a list of HTTP request methods allowed during CORS. ", + "Access-Control-Allow-Origin": + "Indicates whether the response can be shared with the requesting code from the given origin.", + "Access-Control-Max-Age": + "Indicates the maximum amount of time the results of a preflight request can be cached.", "Access-Control-Request-Headers": "Used in preflight requests during CORS to specify the headers that will be included in the actual request.", + "Access-Control-Request-Method": + "Used in preflight requests during CORS to indicate the HTTP method that will be used in the actual request.", + "Accept-Language": + "Specifies the preferred natural language and locale for the response.", "Authorization": "Contains credentials for authenticating the client with the server.", "Authorization Bearer Token": "Often used for token-based authentication.", "Cache-Control": "Provides directives for caching mechanisms in both requests and responses.", + "Connection": + "Informs whether the connection stays open or close after the current transaction finishes.", "Content-Disposition": "Specifies the presentation style (inline or attachment) of the response.", "Content-Encoding": "Indicates the encoding transformations that have been applied to the entity body of the response.", + "Content-Length": + "Indicates the size of the message body sent to the recipient in bytes.", "Content-Security-Policy": "Controls the sources from which content can be loaded on a web page to mitigate various types of attacks.", + "Content-Type": + "Indicates the original media type of the resource (prior to any content encoding applied for sending)", "Cookie": "Used to send previously stored cookies back to the server.", "Cross-Origin-Embedder-Policy": "Controls whether a document is allowed to be embedded in another document.", @@ -22,9 +40,12 @@ Map headers = { "Controls which documents are allowed to open a new window or access the current window.", "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.", "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", "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.", @@ -36,6 +57,8 @@ Map 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.", + "Location": + "Indicates the URL a client should redirect to for further interaction.", "Origin": "Specifies the origin of a cross-origin request.", "Range": "Used to request only part of a resource, typically in the context of downloading large files.", @@ -45,6 +68,7 @@ Map 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.", + "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.", @@ -53,10 +77,14 @@ Map headers = { "Via": "Indicates intermediate proxies or gateways through which the request or response has passed.", "X-Api-Key": "Used to authenticate requests to an API with an API key.", + "X-Content-Type-Options": + "Used to prevent browsers from MIME-sniffing a response.", "X-CSRF-Token": "Used for protection against Cross-Site Request Forgery (CSRF) attacks.", "X-Forwarded-For": "Identifies the client's original IP address when behind a proxy or load balancer.", + "X-Frame-Options": + "Controls whether a webpage can be displayed within an iframe or other embedded frame elements.", "X-Requested-With": "Indicates whether the request was made with JavaScript using XMLHttpRequest.", "X-XSS-Protection": diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index aeaf1b1a..3bf1fb98 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -59,3 +59,13 @@ Color getDarkModeColor(Color col) { kColorWhite, ); } + +double? getJsonPreviewerMaxRootNodeWidth(double w) { + if (w < 300) { + return 150; + } + if (w < 400) { + return 200; + } + return w - 150; +} diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index 10d0c59c..348b81ca 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -5,7 +5,7 @@ import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; import "snackbars.dart"; -class CopyButton extends StatefulWidget { +class CopyButton extends StatelessWidget { const CopyButton({ super.key, required this.toCopy, @@ -15,21 +15,16 @@ class CopyButton extends StatefulWidget { final String toCopy; final bool showLabel; - @override - State createState() => _CopyButtonState(); -} - -class _CopyButtonState extends State { @override Widget build(BuildContext context) { var sm = ScaffoldMessenger.of(context); return Tooltip( - message: widget.showLabel ? '' : kLabelCopy, + message: showLabel ? '' : kLabelCopy, child: SizedBox( - width: widget.showLabel ? null : kTextButtonMinWidth, + width: showLabel ? null : kTextButtonMinWidth, child: TextButton( onPressed: () async { - await Clipboard.setData(ClipboardData(text: widget.toCopy)); + await Clipboard.setData(ClipboardData(text: toCopy)); sm.hideCurrentSnackBar(); sm.showSnackBar(getSnackBar("Copied")); }, @@ -40,7 +35,7 @@ class _CopyButtonState extends State { Icons.content_copy, size: 20, ), - if (widget.showLabel) const Text(kLabelCopy) + if (showLabel) const Text(kLabelCopy) ], ), ), @@ -49,7 +44,7 @@ class _CopyButtonState extends State { } } -class SendRequestButton extends StatefulWidget { +class SendRequestButton extends StatelessWidget { const SendRequestButton({ super.key, required this.activeId, @@ -61,29 +56,17 @@ class SendRequestButton extends StatefulWidget { final String? sentRequestId; final void Function() onTap; - @override - State createState() => _SendRequestButtonState(); -} - -class _SendRequestButtonState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { - bool disable = widget.sentRequestId != null; + bool disable = sentRequestId != null; return FilledButton( - onPressed: disable ? null : widget.onTap, + onPressed: disable ? null : onTap, child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( disable - ? (widget.activeId == widget.sentRequestId - ? kLabelSending - : kLabelBusy) + ? (activeId == sentRequestId ? kLabelSending : kLabelBusy) : kLabelSend, style: kTextStyleButton, ), @@ -99,7 +82,7 @@ class _SendRequestButtonState extends State { } } -class SaveInDownloadsButton extends StatefulWidget { +class SaveInDownloadsButton extends StatelessWidget { const SaveInDownloadsButton({ super.key, this.content, @@ -115,29 +98,24 @@ class SaveInDownloadsButton extends StatefulWidget { final String? name; final bool showLabel; - @override - State createState() => _SaveInDownloadsButtonState(); -} - -class _SaveInDownloadsButtonState extends State { @override Widget build(BuildContext context) { var sm = ScaffoldMessenger.of(context); return Tooltip( - message: widget.showLabel ? '' : kLabelDownload, + message: showLabel ? '' : kLabelDownload, child: SizedBox( - width: widget.showLabel ? null : kTextButtonMinWidth, + width: showLabel ? null : kTextButtonMinWidth, child: TextButton( - onPressed: (widget.content != null) + onPressed: (content != null) ? () async { var message = ""; var path = await getFileDownloadpath( - widget.name, - widget.ext ?? getFileExtension(widget.mimeType), + name, + ext ?? getFileExtension(mimeType), ); if (path != null) { try { - await saveFile(path, widget.content!); + await saveFile(path, content!); var sp = getShortPath(path); message = 'Saved to $sp'; } catch (e) { @@ -157,7 +135,7 @@ class _SaveInDownloadsButtonState extends State { Icons.download, size: 20, ), - if (widget.showLabel) const Text(kLabelDownload) + if (showLabel) const Text(kLabelDownload) ], ), ), @@ -166,7 +144,7 @@ class _SaveInDownloadsButtonState extends State { } } -class RepoButton extends StatefulWidget { +class RepoButton extends StatelessWidget { const RepoButton({ super.key, this.text, @@ -176,15 +154,10 @@ class RepoButton extends StatefulWidget { final String? text; final IconData? icon; - @override - State createState() => _RepoButtonState(); -} - -class _RepoButtonState extends State { @override Widget build(BuildContext context) { - var label = widget.text ?? "GitHub"; - if (widget.icon == null) { + var label = text ?? "GitHub"; + if (icon == null) { return FilledButton( onPressed: () { launchUrl(Uri.parse(kGitUrl)); @@ -200,7 +173,7 @@ class _RepoButtonState extends State { launchUrl(Uri.parse(kGitUrl)); }, icon: Icon( - widget.icon, + icon, size: 20.0, ), label: Text( @@ -211,7 +184,7 @@ class _RepoButtonState extends State { } } -class DiscordButton extends StatefulWidget { +class DiscordButton extends StatelessWidget { const DiscordButton({ super.key, this.text, @@ -219,14 +192,9 @@ class DiscordButton extends StatefulWidget { final String? text; - @override - State createState() => _DiscordButtonState(); -} - -class _DiscordButtonState extends State { @override Widget build(BuildContext context) { - var label = widget.text ?? 'Discord Server'; + var label = text ?? 'Discord Server'; return FilledButton.icon( onPressed: () { launchUrl(Uri.parse(kDiscordUrl)); @@ -242,3 +210,27 @@ class _DiscordButtonState extends State { ); } } + +class SaveButton extends StatelessWidget { + const SaveButton({ + super.key, + this.onPressed, + }); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: onPressed, + icon: const Icon( + Icons.save, + size: 20, + ), + label: const Text( + kLabelSave, + style: kTextStyleButton, + ), + ); + } +} diff --git a/lib/widgets/cards.dart b/lib/widgets/cards.dart index 43b3cb0c..e99a71f8 100644 --- a/lib/widgets/cards.dart +++ b/lib/widgets/cards.dart @@ -129,15 +129,11 @@ class SidebarRequestCard extends StatelessWidget { } } -class RequestDetailsCard extends StatefulWidget { +class RequestDetailsCard extends StatelessWidget { const RequestDetailsCard({super.key, this.child}); final Widget? child; @override - State createState() => _RequestDetailsCardState(); -} - -class _RequestDetailsCardState extends State { @override Widget build(BuildContext context) { return Card( @@ -148,7 +144,7 @@ class _RequestDetailsCardState extends State { borderRadius: kBorderRadius12, ), elevation: 0, - child: widget.child, + child: child, ); } } diff --git a/lib/widgets/checkbox.dart b/lib/widgets/checkbox.dart new file mode 100644 index 00000000..22c3869f --- /dev/null +++ b/lib/widgets/checkbox.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class CheckBox extends StatelessWidget { + final String keyId; + final bool value; + final ValueChanged onChanged; + final ColorScheme? colorScheme; + const CheckBox({ + super.key, + required this.keyId, + required this.value, + required this.onChanged, + this.colorScheme, + }); + + @override + Widget build(BuildContext context) { + var colorScheme = this.colorScheme ?? Theme.of(context).colorScheme; + return Checkbox( + key: Key(keyId), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(3), + ), + side: BorderSide( + color: colorScheme.surfaceVariant, + width: 1.5, + ), + splashRadius: 0, + value: value, + onChanged: onChanged, + checkColor: colorScheme.onPrimary, + fillColor: MaterialStateProperty.resolveWith( + (Set states) { + if (states.contains(MaterialState.selected)) { + return colorScheme.primary; + } + return null; + }, + )); + } +} diff --git a/lib/widgets/codegen_previewer.dart b/lib/widgets/codegen_previewer.dart index 537458b7..298610c7 100644 --- a/lib/widgets/codegen_previewer.dart +++ b/lib/widgets/codegen_previewer.dart @@ -97,7 +97,7 @@ List generateSpans( return spans; } -class ViewCodePane extends StatefulWidget { +class ViewCodePane extends StatelessWidget { const ViewCodePane({ super.key, required this.code, @@ -109,11 +109,6 @@ class ViewCodePane extends StatefulWidget { final CodegenLanguage codegenLanguage; final Function(CodegenLanguage?) onChangedCodegenLanguage; - @override - State createState() => _ViewCodePaneState(); -} - -class _ViewCodePaneState extends State { @override Widget build(BuildContext context) { var codeTheme = Theme.of(context).brightness == Brightness.light @@ -145,17 +140,17 @@ class _ViewCodePaneState extends State { children: [ Expanded( child: DropdownButtonCodegenLanguage( - codegenLanguage: widget.codegenLanguage, - onChanged: widget.onChangedCodegenLanguage, + codegenLanguage: codegenLanguage, + onChanged: onChangedCodegenLanguage, ), ), CopyButton( - toCopy: widget.code, + toCopy: code, showLabel: showLabel, ), SaveInDownloadsButton( - content: stringToBytes(widget.code), - ext: widget.codegenLanguage.ext, + content: stringToBytes(code), + ext: codegenLanguage.ext, showLabel: showLabel, ) ], @@ -168,9 +163,9 @@ class _ViewCodePaneState extends State { padding: kP8, decoration: textContainerdecoration, child: CodeGenPreviewer( - code: widget.code, + code: code, theme: codeTheme, - language: widget.codegenLanguage.codeHighlightLang, + language: codegenLanguage.codeHighlightLang, textStyle: kCodeStyle, ), ), diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index e8290714..69008a6b 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; -class DropdownButtonHttpMethod extends StatefulWidget { +class DropdownButtonHttpMethod extends StatelessWidget { const DropdownButtonHttpMethod({ super.key, this.method, @@ -12,30 +12,19 @@ class DropdownButtonHttpMethod extends StatefulWidget { final HTTPVerb? method; final void Function(HTTPVerb? value)? onChanged; - @override - State createState() => - _DropdownButtonHttpMethodState(); -} - -class _DropdownButtonHttpMethodState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { final surfaceColor = Theme.of(context).colorScheme.surface; return DropdownButton( focusColor: surfaceColor, - value: widget.method, + value: method, icon: const Icon(Icons.unfold_more_rounded), elevation: 4, underline: Container( height: 0, ), borderRadius: kBorderRadius12, - onChanged: widget.onChanged, + onChanged: onChanged, items: HTTPVerb.values.map>((HTTPVerb value) { return DropdownMenuItem( value: value, @@ -58,7 +47,7 @@ class _DropdownButtonHttpMethodState extends State { } } -class DropdownButtonContentType extends StatefulWidget { +class DropdownButtonContentType extends StatelessWidget { const DropdownButtonContentType({ super.key, this.contentType, @@ -68,18 +57,12 @@ class DropdownButtonContentType extends StatefulWidget { final ContentType? contentType; final void Function(ContentType?)? onChanged; - @override - State createState() => - _DropdownButtonContentTypeState(); -} - -class _DropdownButtonContentTypeState extends State { @override Widget build(BuildContext context) { final surfaceColor = Theme.of(context).colorScheme.surface; return DropdownButton( focusColor: surfaceColor, - value: widget.contentType, + value: contentType, icon: const Icon( Icons.unfold_more_rounded, size: 16, @@ -91,7 +74,7 @@ class _DropdownButtonContentTypeState extends State { underline: Container( height: 0, ), - onChanged: widget.onChanged, + onChanged: onChanged, borderRadius: kBorderRadius12, items: ContentType.values .map>((ContentType value) { @@ -110,28 +93,72 @@ class _DropdownButtonContentTypeState extends State { } } -class DropdownButtonCodegenLanguage extends StatefulWidget { +class DropdownButtonFormData extends StatefulWidget { + const DropdownButtonFormData({ + super.key, + this.formDataType, + this.onChanged, + }); + + final FormDataType? formDataType; + final void Function(FormDataType?)? onChanged; + + @override + State createState() => _DropdownButtonFormData(); +} + +class _DropdownButtonFormData extends State { + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + dropdownColor: surfaceColor, + focusColor: surfaceColor, + value: widget.formDataType, + icon: const Icon( + Icons.unfold_more_rounded, + size: 16, + ), + elevation: 4, + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + underline: const IgnorePointer(), + onChanged: widget.onChanged, + borderRadius: kBorderRadius12, + items: FormDataType.values + .map>((FormDataType value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.name, + style: kTextStyleButton, + ), + ), + ); + }).toList(), + ); + } +} + +class DropdownButtonCodegenLanguage extends StatelessWidget { const DropdownButtonCodegenLanguage({ super.key, this.codegenLanguage, this.onChanged, }); - @override - State createState() => - _DropdownButtonCodegenLanguageState(); final CodegenLanguage? codegenLanguage; final void Function(CodegenLanguage?)? onChanged; -} -class _DropdownButtonCodegenLanguageState - extends State { @override Widget build(BuildContext context) { final surfaceColor = Theme.of(context).colorScheme.surface; return DropdownButton( focusColor: surfaceColor, - value: widget.codegenLanguage, + value: codegenLanguage, icon: const Icon( Icons.unfold_more_rounded, size: 16, @@ -143,7 +170,7 @@ class _DropdownButtonCodegenLanguageState underline: Container( height: 0, ), - onChanged: widget.onChanged, + onChanged: onChanged, borderRadius: kBorderRadius12, items: CodegenLanguage.values .map>((CodegenLanguage value) { diff --git a/lib/widgets/form_data_field.dart b/lib/widgets/form_data_field.dart new file mode 100644 index 00000000..0353d033 --- /dev/null +++ b/lib/widgets/form_data_field.dart @@ -0,0 +1,81 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +class FormDataField extends StatefulWidget { + const FormDataField({ + super.key, + required this.keyId, + this.initialValue, + this.hintText, + this.onChanged, + this.colorScheme, + this.formDataType, + this.onFormDataTypeChanged, + }); + + final String keyId; + final String? initialValue; + final String? hintText; + final void Function(String)? onChanged; + final ColorScheme? colorScheme; + final FormDataType? formDataType; + final void Function(FormDataType?)? onFormDataTypeChanged; + + @override + State createState() => _FormDataFieldState(); +} + +class _FormDataFieldState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + var colorScheme = widget.colorScheme ?? Theme.of(context).colorScheme; + return Row( + children: [ + Expanded( + flex: 1, + child: TextFormField( + initialValue: widget.initialValue, + key: Key(widget.keyId), + style: kCodeStyle.copyWith( + color: colorScheme.onSurface, + ), + decoration: InputDecoration( + hintStyle: kCodeStyle.copyWith( + color: colorScheme.outline.withOpacity( + kHintOpacity, + ), + ), + hintText: widget.hintText, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: colorScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: colorScheme.surfaceVariant, + ), + ), + suffixIcon: DropdownButtonFormData( + formDataType: widget.formDataType, + onChanged: (p0) { + if (widget.onFormDataTypeChanged != null) { + widget.onFormDataTypeChanged!(p0); + } + }, + )), + onChanged: widget.onChanged, + ), + ), + ], + ); + } +} diff --git a/lib/widgets/intro_message.dart b/lib/widgets/intro_message.dart index 3feaf712..970596d3 100644 --- a/lib/widgets/intro_message.dart +++ b/lib/widgets/intro_message.dart @@ -5,16 +5,11 @@ import '../consts.dart'; import 'markdown.dart'; import 'error_message.dart'; -class IntroMessage extends StatefulWidget { +class IntroMessage extends StatelessWidget { const IntroMessage({ super.key, }); - @override - State createState() => _IntroMessageState(); -} - -class _IntroMessageState extends State { @override Widget build(BuildContext context) { late String text; diff --git a/lib/widgets/json_previewer.dart b/lib/widgets/json_previewer.dart index 06d0067b..078a9721 100644 --- a/lib/widgets/json_previewer.dart +++ b/lib/widgets/json_previewer.dart @@ -5,6 +5,7 @@ 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 'textfields.dart'; @@ -151,120 +152,151 @@ class _JsonPreviewerState extends State { store.expandAll(); } + @override + void didUpdateWidget(JsonPreviewer 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( - builder: (context, state, child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () async { - await _copy(kEncoder.convert(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( + builder: (context, state, child) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + var maxRootNodeWidth = + getJsonPreviewerMaxRootNodeWidth(constraints.maxWidth); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: Icon( - Icons.search, - size: 18, - ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + await _copy(kEncoder.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, + ), + ), + ], ), Expanded( - child: JsonSearchField( - controller: searchController, - onChanged: (term) => state.search(term), + 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, + maxRootNodeWidth: maxRootNodeWidth, ), ), - const SizedBox( - width: 8, + 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), + ), + ], + ), ), - 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), - ), ], - ), - ), - ], - ), + ); + }, + ); + }, ), ); } @@ -333,7 +365,7 @@ class _JsonPreviewerState extends State { horizontal: 4, vertical: 2, ), - child: Text( + child: SelectableText( node.isClass ? '{${node.childrenCount}}' : '[${node.childrenCount}]', style: kCodeStyle, ), diff --git a/lib/widgets/markdown.dart b/lib/widgets/markdown.dart index a9dde1dc..c1467fba 100644 --- a/lib/widgets/markdown.dart +++ b/lib/widgets/markdown.dart @@ -4,7 +4,7 @@ import 'package:markdown/markdown.dart' as md; import 'package:url_launcher/url_launcher.dart'; import 'buttons.dart'; -class CustomMarkdown extends StatefulWidget { +class CustomMarkdown extends StatelessWidget { const CustomMarkdown({ super.key, required this.data, @@ -13,11 +13,6 @@ class CustomMarkdown extends StatefulWidget { final String data; final EdgeInsets padding; - @override - State createState() => _CustomMarkdownState(); -} - -class _CustomMarkdownState extends State { @override Widget build(BuildContext context) { final mdStyleSheet = MarkdownStyleSheet( @@ -25,9 +20,9 @@ class _CustomMarkdownState extends State { p: Theme.of(context).textTheme.titleMedium, ); return Markdown( - padding: widget.padding, + padding: padding, styleSheet: mdStyleSheet, - data: widget.data, + data: data, selectable: true, extensionSet: md.ExtensionSet.gitHubFlavored, onTapLink: (text, href, title) { diff --git a/lib/widgets/menus.dart b/lib/widgets/menus.dart index 02a5a15c..81958a02 100644 --- a/lib/widgets/menus.dart +++ b/lib/widgets/menus.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; -class RequestCardMenu extends StatefulWidget { +class RequestCardMenu extends StatelessWidget { const RequestCardMenu({ super.key, this.onSelected, @@ -9,18 +9,13 @@ class RequestCardMenu extends StatefulWidget { final Function(RequestItemMenuOption)? onSelected; - @override - State createState() => _RequestCardMenuState(); -} - -class _RequestCardMenuState extends State { @override Widget build(BuildContext context) { return PopupMenuButton( padding: EdgeInsets.zero, splashRadius: 14, iconSize: 14, - onSelected: widget.onSelected, + onSelected: onSelected, itemBuilder: (BuildContext context) => >[ const PopupMenuItem( diff --git a/lib/widgets/previewer.dart b/lib/widgets/previewer.dart index db7d9edb..9841ac5b 100644 --- a/lib/widgets/previewer.dart +++ b/lib/widgets/previewer.dart @@ -1,11 +1,13 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'error_message.dart'; -import 'package:apidash/consts.dart'; import 'package:printing/printing.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; +import 'error_message.dart'; import 'uint8_audio_player.dart'; import 'json_previewer.dart'; +import '../consts.dart'; class Previewer extends StatefulWidget { const Previewer({ @@ -40,6 +42,18 @@ class _PreviewerState extends State { // pass } } + if (widget.type == kTypeImage && widget.subtype == kSubTypeSvg) { + final String rawSvg = widget.body; + try { + parseWithoutOptimizers(rawSvg); + var svgImg = SvgPicture.string( + rawSvg, + ); + return svgImg; + } catch (e) { + return const ErrorMessage(message: kSvgError); + } + } if (widget.type == kTypeImage) { return Image.memory( widget.bytes, diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index bd57a867..8d4d4c9e 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -50,7 +50,7 @@ class SendingWidget extends StatelessWidget { } } -class ResponsePaneHeader extends StatefulWidget { +class ResponsePaneHeader extends StatelessWidget { const ResponsePaneHeader({ super.key, this.responseStatus, @@ -61,11 +61,7 @@ class ResponsePaneHeader extends StatefulWidget { final int? responseStatus; final String? message; final Duration? time; - @override - State createState() => _ResponsePaneHeaderState(); -} -class _ResponsePaneHeaderState extends State { @override Widget build(BuildContext context) { return Padding( @@ -82,10 +78,10 @@ class _ResponsePaneHeaderState extends State { text: "Response (", ), TextSpan( - text: "${widget.responseStatus}", + text: "$responseStatus", style: TextStyle( color: getResponseStatusCodeColor( - widget.responseStatus, + responseStatus, brightness: Theme.of(context).brightness, ), fontFamily: kCodeStyle.fontFamily, @@ -101,13 +97,13 @@ class _ResponsePaneHeaderState extends State { kHSpacer20, Expanded( child: Text( - widget.message ?? "", + message ?? "", softWrap: false, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleMedium!.copyWith( fontFamily: kCodeStyle.fontFamily, color: getResponseStatusCodeColor( - widget.responseStatus, + responseStatus, brightness: Theme.of(context).brightness, ), ), @@ -115,7 +111,7 @@ class _ResponsePaneHeaderState extends State { ), kHSpacer20, Text( - humanizeDuration(widget.time), + humanizeDuration(time), style: Theme.of(context).textTheme.titleMedium!.copyWith( fontFamily: kCodeStyle.fontFamily, color: Theme.of(context).colorScheme.secondary, @@ -208,7 +204,7 @@ class _ResponseTabViewState extends State } } -class ResponseHeadersHeader extends StatefulWidget { +class ResponseHeadersHeader extends StatelessWidget { const ResponseHeadersHeader({ super.key, required this.map, @@ -217,11 +213,7 @@ class ResponseHeadersHeader extends StatefulWidget { final Map map; final String name; - @override - State createState() => _ResponseHeadersHeaderState(); -} -class _ResponseHeadersHeaderState extends State { @override Widget build(BuildContext context) { return SizedBox( @@ -230,15 +222,15 @@ class _ResponseHeadersHeaderState extends State { children: [ Expanded( child: Text( - "${widget.name} (${widget.map.length} items)", + "$name (${map.length} items)", style: Theme.of(context).textTheme.labelLarge!.copyWith( fontWeight: FontWeight.bold, ), ), ), - if (widget.map.isNotEmpty) + if (map.isNotEmpty) CopyButton( - toCopy: kEncoder.convert(widget.map), + toCopy: kEncoder.convert(map), ), ], ), @@ -248,7 +240,7 @@ class _ResponseHeadersHeaderState extends State { const kHeaderRow = ["Header Name", "Header Value"]; -class ResponseHeaders extends StatefulWidget { +class ResponseHeaders extends StatelessWidget { const ResponseHeaders({ super.key, required this.responseHeaders, @@ -257,11 +249,7 @@ class ResponseHeaders extends StatefulWidget { final Map responseHeaders; final Map requestHeaders; - @override - State createState() => _ResponseHeadersState(); -} -class _ResponseHeadersState extends State { @override Widget build(BuildContext context) { return Padding( @@ -269,25 +257,25 @@ class _ResponseHeadersState extends State { child: ListView( children: [ ResponseHeadersHeader( - map: widget.responseHeaders, + map: responseHeaders, name: "Response Headers", ), - if (widget.responseHeaders.isNotEmpty) kVSpacer5, - if (widget.responseHeaders.isNotEmpty) + if (responseHeaders.isNotEmpty) kVSpacer5, + if (responseHeaders.isNotEmpty) MapTable( - map: widget.responseHeaders, + map: responseHeaders, colNames: kHeaderRow, firstColumnHeaderCase: true, ), kVSpacer10, ResponseHeadersHeader( - map: widget.requestHeaders, + map: requestHeaders, name: "Request Headers", ), - if (widget.requestHeaders.isNotEmpty) kVSpacer5, - if (widget.requestHeaders.isNotEmpty) + if (requestHeaders.isNotEmpty) kVSpacer5, + if (requestHeaders.isNotEmpty) MapTable( - map: widget.requestHeaders, + map: requestHeaders, colNames: kHeaderRow, firstColumnHeaderCase: true, ), @@ -297,21 +285,17 @@ class _ResponseHeadersState extends State { } } -class ResponseBody extends StatefulWidget { +class ResponseBody extends StatelessWidget { const ResponseBody({ super.key, this.activeRequestModel, }); final RequestModel? activeRequestModel; - @override - State createState() => _ResponseBodyState(); -} -class _ResponseBodyState extends State { @override Widget build(BuildContext context) { - final responseModel = widget.activeRequestModel?.responseModel; + final responseModel = activeRequestModel?.responseModel; if (responseModel == null) { return const ErrorMessage( message: @@ -349,7 +333,7 @@ class _ResponseBodyState extends State { } return BodySuccess( - key: Key("${widget.activeRequestModel!.id}-response"), + key: Key("${activeRequestModel!.id}-response"), mediaType: mediaType, options: options, bytes: responseModel.bodyBytes!, @@ -416,23 +400,20 @@ class _BodySuccessState extends State { (widget.options == kRawBodyViewOptions) ? const SizedBox() : SegmentedButton( - selectedIcon: showLabel - ? Icon( - kResponseBodyViewIcons[currentSeg]![kKeyIcon], - ) - : null, + style: const ButtonStyle( + padding: MaterialStatePropertyAll( + EdgeInsets.symmetric( + horizontal: 8, + ), + ), + ), + selectedIcon: Icon(currentSeg.icon), segments: widget.options .map>( (e) => ButtonSegment( value: e, - label: showLabel - ? Text( - kResponseBodyViewIcons[e]![kKeyName], - ) - : null, - icon: Icon( - kResponseBodyViewIcons[e]![kKeyIcon], - ), + label: Text(e.label), + icon: Icon(e.icon), ), ) .toList(), diff --git a/lib/widgets/tables.dart b/lib/widgets/tables.dart index 49d0021b..c238dae0 100644 --- a/lib/widgets/tables.dart +++ b/lib/widgets/tables.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; -class MapTable extends StatefulWidget { +class MapTable extends StatelessWidget { const MapTable( {super.key, required this.map, @@ -13,11 +13,6 @@ class MapTable extends StatefulWidget { final List colNames; final bool firstColumnHeaderCase; - @override - State createState() => _MapTableState(); -} - -class _MapTableState extends State { @override Widget build(BuildContext context) { return Table( @@ -33,7 +28,7 @@ class _MapTableState extends State { defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ TableRow( - children: widget.colNames + children: colNames .map( (e) => TableCell( verticalAlignment: TableCellVerticalAlignment.top, @@ -51,7 +46,7 @@ class _MapTableState extends State { ) .toList(), ), - ...widget.map.entries.map( + ...map.entries.map( (entry) => TableRow( children: [ TableCell( @@ -59,7 +54,7 @@ class _MapTableState extends State { child: Padding( padding: kP1, child: SelectableText( - widget.firstColumnHeaderCase + firstColumnHeaderCase ? formatHeaderCase(entry.key) : entry.key, style: kCodeStyle.copyWith( diff --git a/lib/widgets/textfields.dart b/lib/widgets/textfields.dart index ff7b7313..76d1a9dc 100644 --- a/lib/widgets/textfields.dart +++ b/lib/widgets/textfields.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; -class URLField extends StatefulWidget { +class URLField extends StatelessWidget { const URLField({ super.key, required this.activeId, @@ -13,21 +13,11 @@ class URLField extends StatefulWidget { final String? initialValue; final void Function(String)? onChanged; - @override - State createState() => _URLFieldState(); -} - -class _URLFieldState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { return TextFormField( - key: Key("url-${widget.activeId}"), - initialValue: widget.initialValue, + key: Key("url-$activeId"), + initialValue: initialValue, style: kCodeStyle, decoration: InputDecoration( hintText: kHintTextUrlCard, @@ -38,12 +28,12 @@ class _URLFieldState extends State { ), border: InputBorder.none, ), - onChanged: widget.onChanged, + onChanged: onChanged, ); } } -class CellField extends StatefulWidget { +class CellField extends StatelessWidget { const CellField({ super.key, required this.keyId, @@ -59,41 +49,36 @@ class CellField extends StatefulWidget { final void Function(String)? onChanged; final ColorScheme? colorScheme; - @override - State createState() => _CellFieldState(); -} - -class _CellFieldState extends State { @override Widget build(BuildContext context) { - var colorScheme = widget.colorScheme ?? Theme.of(context).colorScheme; + var clrScheme = colorScheme ?? Theme.of(context).colorScheme; return TextFormField( - key: Key(widget.keyId), - initialValue: widget.initialValue, + key: Key(keyId), + initialValue: initialValue, style: kCodeStyle.copyWith( - color: colorScheme.onSurface, + color: clrScheme.onSurface, ), decoration: InputDecoration( hintStyle: kCodeStyle.copyWith( - color: colorScheme.outline.withOpacity( + color: clrScheme.outline.withOpacity( kHintOpacity, ), ), - hintText: widget.hintText, + hintText: hintText, focusedBorder: UnderlineInputBorder( borderSide: BorderSide( - color: colorScheme.primary.withOpacity( + color: clrScheme.primary.withOpacity( kHintOpacity, ), ), ), enabledBorder: UnderlineInputBorder( borderSide: BorderSide( - color: colorScheme.surfaceVariant, + color: clrScheme.surfaceVariant, ), ), ), - onChanged: widget.onChanged, + onChanged: onChanged, ); } } diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index 861574d7..f2cf5c92 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -12,10 +12,14 @@ class MethodBox extends StatelessWidget { if (method == HTTPVerb.delete) { text = "DEL"; } + if (method == HTTPVerb.patch) { + text = "PAT"; + } return SizedBox( - width: 28, + width: 24, child: Text( text, + textAlign: TextAlign.center, style: TextStyle( fontSize: 8, fontWeight: FontWeight.bold, diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 23929f62..8c288953 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,22 +1,24 @@ -export 'editor.dart'; export 'buttons.dart'; -export 'tables.dart'; -export 'previewer.dart'; +export 'cards.dart'; +export 'checkbox.dart'; export 'code_previewer.dart'; export 'codegen_previewer.dart'; -export 'error_message.dart'; export 'dropdowns.dart'; -export 'splitviews.dart'; -export 'texts.dart'; -export 'textfields.dart'; +export 'editor.dart'; +export 'error_message.dart'; export 'headerfield.dart'; -export 'menus.dart'; -export 'cards.dart'; export 'intro_message.dart'; +export 'json_previewer.dart'; +export 'markdown.dart'; +export 'menus.dart'; +export 'previewer.dart'; export 'request_widgets.dart'; export 'response_widgets.dart'; export 'snackbars.dart'; -export 'markdown.dart'; -export 'uint8_audio_player.dart'; +export 'splitviews.dart'; +export 'tables.dart'; export 'tabs.dart'; -export 'json_previewer.dart'; +export 'textfields.dart'; +export 'texts.dart'; +export 'uint8_audio_player.dart'; +export 'window_caption.dart'; diff --git a/lib/widgets/window_caption.dart b/lib/widgets/window_caption.dart new file mode 100644 index 00000000..f26e7539 --- /dev/null +++ b/lib/widgets/window_caption.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +import 'package:window_manager/window_manager.dart'; + +const double kWindowCaptionHeight = 30; + +class WindowCaption extends StatefulWidget { + const WindowCaption({ + super.key, + this.backgroundColor, + this.brightness, + }); + + final Color? backgroundColor; + final Brightness? brightness; + + @override + State createState() => _WindowCaptionState(); +} + +class _WindowCaptionState extends State with WindowListener { + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onPanStart: (details) { + windowManager.startDragging(); + }, + child: const SizedBox( + height: double.infinity, + ), + ), + ), + WindowCaptionButton.minimize( + brightness: widget.brightness, + onPressed: () async { + bool isMinimized = await windowManager.isMinimized(); + if (isMinimized) { + windowManager.restore(); + } else { + windowManager.minimize(); + } + }, + ), + FutureBuilder( + future: windowManager.isMaximized(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data == true) { + return WindowCaptionButton.unmaximize( + brightness: widget.brightness, + onPressed: () { + windowManager.unmaximize(); + }, + ); + } + return WindowCaptionButton.maximize( + brightness: widget.brightness, + onPressed: () { + windowManager.maximize(); + }, + ); + }, + ), + WindowCaptionButton.close( + brightness: widget.brightness, + onPressed: () { + windowManager.close(); + }, + ), + ], + ); + } + + @override + void onWindowMaximize() { + setState(() {}); + } + + @override + void onWindowUnmaximize() { + setState(() {}); + } +} diff --git a/pubspec.lock b/pubspec.lock index 60631a20..5e8d5462 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" archive: dependency: transitive description: name: archive - sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03" + sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" url: "https://pub.dev" source: hosted - version: "3.4.6" + version: "3.4.9" args: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: audio_session - sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" + sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" url: "https://pub.dev" source: hosted - version: "0.1.16" + version: "0.1.18" axis_layout: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: barcode - sha256: "789f898eef0bd88312470bdb2cc996f895ad7dd5f89e9adde84b204546a90b45" + sha256: "2a8b2ee065f419c2aeda141436cc556d91ae772d220fd80679f4d431d6c2ab43" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.5" bidi: dependency: transitive description: @@ -101,26 +101,26 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" build_runner_core: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74 + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.6.3" + version: "8.8.1" characters: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -178,21 +178,21 @@ packages: source: hosted version: "1.1.1" code_builder: - dependency: transitive + dependency: "direct main" description: name: code_builder - sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.9.0" collection: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: transitive description: name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.6.4" + version: "1.7.2" crypto: dependency: transitive description: @@ -218,13 +218,13 @@ packages: source: hosted version: "3.0.3" dart_style: - dependency: transitive + dependency: "direct main" description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" davi: dependency: "direct main" description: @@ -265,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "4e42aacde3b993c5947467ab640882c56947d9d27342a5b6f2895b23956954a6" + url: "https://pub.dev" + source: hosted + version: "6.1.1" fixnum: dependency: transitive description: @@ -338,26 +346,42 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: ad76540d21c066228ee3f9d1dad64a9f7e46530e8bb7c85011a88bc1fd874bc5 + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - sha256: "8afc9a6aa6d8e8063523192ba837149dbf3d377a37c0b0fc579149a1fbd4a619" + sha256: "35108526a233cc0755664d445f8a6b4b61e6f8fe993b3658b80b4a26827fc196" url: "https://pub.dev" source: hosted - version: "0.6.18" + version: "0.6.18+2" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + url: "https://pub.dev" + source: hosted + version: "2.0.17" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: bdba94be666ecb1beeb0f5a748d96cdd6a37215f27e6b48c7673b95cecb800c8 + sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.9" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -380,10 +404,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "21bf2825311de65501d22e563e3d7605dff57fb5e6da982db785ae5372ff018a" + sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" freezed_annotation: dependency: "direct main" description: @@ -460,10 +484,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" http_multi_server: dependency: transitive description: @@ -523,11 +547,12 @@ packages: json_data_explorer: dependency: "direct main" description: - name: json_data_explorer - sha256: "303a00037b23963fd01be1b2dc509f14e9db2a40f852b0ce042d7635c22fd154" - url: "https://pub.dev" - source: hosted - version: "0.1.0" + path: "." + ref: "9fa58d7b51e65174ab11cbcae17bba88a4194dde" + resolved-ref: "9fa58d7b51e65174ab11cbcae17bba88a4194dde" + url: "https://github.com/foss42/json_data_explorer.git" + source: git + version: "0.1.2" json_serializable: dependency: "direct dev" description: @@ -540,10 +565,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b" + sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823 url: "https://pub.dev" source: hosted - version: "0.9.35" + version: "0.9.36" just_audio_mpv: dependency: "direct main" description: @@ -556,18 +581,18 @@ packages: dependency: transitive description: name: just_audio_platform_interface - sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df + sha256: c3dee0014248c97c91fe6299edb73dc4d6c6930a2f4f713579cd692d9e47f4a1 url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13 + sha256: "134356b0fe3d898293102b33b5fd618831ffdc72bb7a1b726140abdf22772b70" url: "https://pub.dev" source: hosted - version: "0.4.8" + version: "0.4.9" just_audio_windows: dependency: "direct main" description: @@ -628,10 +653,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -732,10 +757,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1" + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_foundation: dependency: transitive description: @@ -772,18 +797,18 @@ packages: dependency: transitive description: name: pdf - sha256: "9f75fc7f5580ea5e635b5724de58fb27f684c9ad03ed46fdc1aac768e4557315" + sha256: "93cbb2c06de9bab91844550f19896b2373e7a5ce25173995e7e5ec5e1741429d" url: "https://pub.dev" source: hosted - version: "3.10.4" + version: "3.10.7" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: @@ -796,18 +821,18 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.7" pointer_interceptor: dependency: transitive description: name: pointer_interceptor - sha256: "7626e034489820fd599380d2bb4d3f4a0a5e3529370b62bfce53ab736b91adb2" + sha256: adf7a637f97c077041d36801b43be08559fd4322d2127b3f20bb7be1b9eebc22 url: "https://pub.dev" source: hosted - version: "0.9.3+6" + version: "0.9.3+7" pointycastle: dependency: transitive description: @@ -828,18 +853,18 @@ packages: dependency: "direct main" description: name: printing - sha256: e7c383dca95ee7b88c02dc1c66638628d3dcdc2fb2cc47e7a595facd47e46b56 + sha256: ad39a42a5f83125952457dfd94f395c8cf0eb1f7759583dadb769be5c7f99d24 url: "https://pub.dev" source: hosted - version: "5.11.0" + version: "5.11.1" provider: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -868,10 +893,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "2af3d127a6e4e34b89b8f1f018086f5ded04b8e538174f0510bba3e4c0d878b1" + sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.9" rxdart: dependency: transitive description: @@ -937,10 +962,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -985,10 +1010,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1001,10 +1026,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1033,26 +1058,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" textwrap: dependency: transitive description: @@ -1081,74 +1106,98 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.2.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.2.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.2.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: "direct main" description: name: uuid - sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" + vector_graphics_compiler: + dependency: "direct main" + description: + name: vector_graphics_compiler + sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + url: "https://pub.dev" + source: hosted + version: "1.1.9+1" vector_math: dependency: transitive description: @@ -1161,10 +1210,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -1177,10 +1226,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -1201,10 +1250,10 @@ packages: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.1.1" window_manager: dependency: "direct main" description: @@ -1234,10 +1283,10 @@ packages: dependency: "direct main" description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -1247,5 +1296,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.3 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index b0f13300..bdb8665a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,12 +39,20 @@ dependencies: just_audio_windows: ^0.2.0 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 - printing: ^5.11.0 + printing: ^5.11.1 package_info_plus: ^4.1.0 flutter_typeahead: ^4.8.0 provider: ^6.0.5 - json_data_explorer: ^0.1.0 + json_data_explorer: + git: + url: https://github.com/foss42/json_data_explorer.git + ref: 9fa58d7b51e65174ab11cbcae17bba88a4194dde scrollable_positioned_list: ^0.2.3 + file_picker: ^6.1.1 + flutter_svg: ^2.0.9 + vector_graphics_compiler: ^1.1.9+1 + code_builder: ^4.9.0 + dart_style: ^2.3.4 dev_dependencies: flutter_test: diff --git a/test/codegen/codegen_test.dart b/test/codegen/codegen_test.dart new file mode 100644 index 00000000..63102e0a --- /dev/null +++ b/test/codegen/codegen_test.dart @@ -0,0 +1,239 @@ +import 'package:apidash/codegen/codegen.dart'; +import 'package:apidash/consts.dart'; +import '../request_models.dart'; +import 'package:test/test.dart'; + +void main() { + final codeGen = Codegen(); + + group('Test various Code generators', () { + test('cURL', () { + const expectedCode = r"""curl --url 'https://api.foss42.com'"""; + expect(codeGen.getCode(CodegenLanguage.curl, requestModelGet1, "https"), + expectedCode); + }); + + test('Dart Dio', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.get('https://api.foss42.com'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect( + codeGen.getCode(CodegenLanguage.dartDio, requestModelGet1, "https"), + expectedCode); + }); + + test('Dart HTTP', () { + const expectedCode = r"""import 'package:http/http.dart' as http; + +void main() async { + var uri = Uri.parse('https://api.foss42.com'); + + final response = await http.get(uri); + + int statusCode = response.statusCode; + if (statusCode >= 200 && statusCode < 300) { + print('Status Code: $statusCode'); + print('Response Body: ${response.body}'); + } else { + print('Error Status Code: $statusCode'); + print('Error Response Body: ${response.body}'); + } +} +"""; + expect( + codeGen.getCode(CodegenLanguage.dartHttp, requestModelGet1, "https"), + expectedCode); + }); + + test('HAR', () { + const expectedCode = r"""{ + "method": "GET", + "url": "https://api.foss42.com", + "httpVersion": "HTTP/1.1", + "queryString": [], + "headers": [] +}"""; + expect(codeGen.getCode(CodegenLanguage.har, requestModelGet1, "https"), + expectedCode); + }); + + test('JS Axios', () { + const expectedCode = r"""let config = { + url: 'https://api.foss42.com', + method: 'get' +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect( + codeGen.getCode(CodegenLanguage.jsAxios, requestModelGet1, "https"), + expectedCode); + }); + + test('JS Fetch', () { + const expectedCode = r"""let url = 'https://api.foss42.com'; + +let options = { + method: 'GET' +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect( + codeGen.getCode(CodegenLanguage.jsFetch, requestModelGet1, "https"), + expectedCode); + }); + + test('Kotlin OkHttp', () { + const expectedCode = r"""import okhttp3.OkHttpClient +import okhttp3.Request + +fun main() { + val client = OkHttpClient() + + val url = "https://api.foss42.com" + + val request = Request.Builder() + .url(url) + .get() + .build() + + val response = client.newCall(request).execute() + + println(response.code) + println(response.body?.string()) +} +"""; + expect( + codeGen.getCode( + CodegenLanguage.kotlinOkHttp, requestModelGet1, "https"), + expectedCode); + }); + + test('NodeJs Axios', () { + const expectedCode = r"""import axios from 'axios'; + +let config = { + url: 'https://api.foss42.com', + method: 'get' +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect( + codeGen.getCode( + CodegenLanguage.nodejsAxios, requestModelGet1, "https"), + expectedCode); + }); + + test('Nodejs Fetch', () { + const expectedCode = r"""import fetch from 'node-fetch'; + +let url = 'https://api.foss42.com'; + +let options = { + method: 'GET' +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect( + codeGen.getCode( + CodegenLanguage.nodejsFetch, requestModelGet1, "https"), + expectedCode); + }); + + test('Python http.client', () { + const expectedCode = r"""import http.client + +conn = http.client.HTTPSConnection("api.foss42.com") +conn.request("GET", "") + +res = conn.getresponse() +data = res.read() + +print(data.decode("utf-8")) +"""; + expect( + codeGen.getCode( + CodegenLanguage.pythonHttpClient, requestModelGet1, "https"), + expectedCode); + }); + + test('Python requests', () { + const expectedCode = r"""import requests + +url = 'https://api.foss42.com' + +response = requests.get(url) + +print('Status Code:', response.status_code) +print('Response Body:', response.text) +"""; + expect( + codeGen.getCode( + CodegenLanguage.pythonRequests, requestModelGet1, "https"), + expectedCode); + }); + }); +} diff --git a/test/codegen/curl_codegen_test.dart b/test/codegen/curl_codegen_test.dart index b67c8fa2..e9bb7f60 100644 --- a/test/codegen/curl_codegen_test.dart +++ b/test/codegen/curl_codegen_test.dart @@ -54,6 +54,37 @@ void main() { --header 'User-Agent: Test Agent'"""; expect(curlCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = + r"""curl --url 'https://api.foss42.com/humanize/social?num=8700000&add_space=true'"""; + expect(curlCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = + r"""curl --url 'https://api.foss42.com/humanize/social' \ + --header 'User-Agent: Test Agent'"""; + expect( + curlCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = + r"""curl --url 'https://api.foss42.com/humanize/social?num=8700000&digits=3' \ + --header 'User-Agent: Test Agent'"""; + expect(curlCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = + r"""curl --url 'https://api.foss42.com/humanize/social'"""; + expect(curlCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -100,6 +131,7 @@ void main() { expect(curlCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""curl --request PUT \ diff --git a/test/codegen/dart_dio_codegen_test.dart b/test/codegen/dart_dio_codegen_test.dart new file mode 100644 index 00000000..30b371fd --- /dev/null +++ b/test/codegen/dart_dio_codegen_test.dart @@ -0,0 +1,568 @@ +import 'package:apidash/codegen/dart/dio.dart'; +import 'package:test/test.dart'; + +import '../request_models.dart'; + +void main() { + final dartDioCodeGen = DartDioCodeGen(); + + group('GET Request', () { + test('GET 1', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.get('https://api.foss42.com'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet1, "https"), expectedCode); + }); + + test('GET 2', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = {'code': 'US'}; + final response = await dio.Dio.get( + 'https://api.foss42.com/country/data', + queryParameters: queryParams, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet2, "https"), expectedCode); + }); + + test('GET 3', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = {'code': 'IND'}; + final response = await dio.Dio.get( + 'https://api.foss42.com/country/data?code=US', + queryParameters: queryParams, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet3, "https"), expectedCode); + }); + + test('GET 4', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = { + 'num': '8700000', + 'digits': '3', + 'system': 'SS', + 'add_space': 'true', + 'trailing_zeros': 'true', + }; + final response = await dio.Dio.get( + 'https://api.foss42.com/humanize/social', + queryParameters: queryParams, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet4, "https"), expectedCode); + }); + + test('GET 5', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final headers = {'User-Agent': 'Test Agent'}; + final response = await dio.Dio.get( + 'https://api.github.com/repos/foss42/apidash', + options: Options(headers: headers), + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet5, "https"), expectedCode); + }); + + test('GET 6', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = {'raw': 'true'}; + final headers = {'User-Agent': 'Test Agent'}; + final response = await dio.Dio.get( + 'https://api.github.com/repos/foss42/apidash', + queryParameters: queryParams, + options: Options(headers: headers), + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet6, "https"), expectedCode); + }); + + test('GET 7', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.get('https://api.foss42.com'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet7, "https"), expectedCode); + }); + + test('GET 8', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = {'raw': 'true'}; + final headers = {'User-Agent': 'Test Agent'}; + final response = await dio.Dio.get( + 'https://api.github.com/repos/foss42/apidash', + queryParameters: queryParams, + options: Options(headers: headers), + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet8, "https"), expectedCode); + }); + + test('GET 9', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = { + 'num': '8700000', + 'add_space': 'true', + }; + final response = await dio.Dio.get( + 'https://api.foss42.com/humanize/social', + queryParameters: queryParams, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final headers = {'User-Agent': 'Test Agent'}; + final response = await dio.Dio.get( + 'https://api.foss42.com/humanize/social', + options: Options(headers: headers), + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect( + dartDioCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final queryParams = { + 'num': '8700000', + 'digits': '3', + }; + final headers = {'User-Agent': 'Test Agent'}; + final response = await dio.Dio.get( + 'https://api.foss42.com/humanize/social', + queryParameters: queryParams, + options: Options(headers: headers), + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.get('https://api.foss42.com/humanize/social'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); + }); + + group('HEAD Request', () { + test('HEAD 1', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.head('https://api.foss42.com'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelHead1, "https"), expectedCode); + }); + + test('HEAD 2', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.head('http://api.foss42.com'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelHead2, "http"), expectedCode); + }); + }); + + group('POST Request', () { + test('POST 1', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final data = r'''{ +"text": "I LOVE Flutter" +}'''; + final response = await dio.Dio.post( + 'https://api.foss42.com/case/lower', + data: data, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelPost1, "https"), expectedCode); + }); + + test('POST 2', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; +import 'dart:convert' as convert; + +void main() async { + try { + final data = convert.json.decode(r'''{ +"text": "I LOVE Flutter" +}'''); + final response = await dio.Dio.post( + 'https://api.foss42.com/case/lower', + data: data, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelPost2, "https"), expectedCode); + }); + + test('POST 3', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; +import 'dart:convert' as convert; + +void main() async { + try { + final headers = {'User-Agent': 'Test Agent'}; + final data = convert.json.decode(r'''{ +"text": "I LOVE Flutter" +}'''); + final response = await dio.Dio.post( + 'https://api.foss42.com/case/lower', + options: Options(headers: headers), + data: data, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelPost3, "https"), expectedCode); + }); + }); + + group('PUT Request', () { + test('PUT 1', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; +import 'dart:convert' as convert; + +void main() async { + try { + final data = convert.json.decode(r'''{ +"name": "morpheus", +"job": "zion resident" +}'''); + final response = await dio.Dio.put( + 'https://reqres.in/api/users/2', + data: data, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelPut1, "https"), expectedCode); + }); + }); + + group('PATCH Request', () { + test('PATCH 1', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; +import 'dart:convert' as convert; + +void main() async { + try { + final data = convert.json.decode(r'''{ +"name": "marfeus", +"job": "accountant" +}'''); + final response = await dio.Dio.patch( + 'https://reqres.in/api/users/2', + data: data, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect(dartDioCodeGen.getCode(requestModelPatch1, "https"), expectedCode); + }); + }); + + group('DELETE Request', () { + test('DELETE 1', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; + +void main() async { + try { + final response = await dio.Dio.delete('https://reqres.in/api/users/2'); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect( + dartDioCodeGen.getCode(requestModelDelete1, "https"), expectedCode); + }); + + test('DELETE 2', () { + const expectedCode = r"""import 'package:dio/dio.dart' as dio; +import 'dart:convert' as convert; + +void main() async { + try { + final data = convert.json.decode(r'''{ +"name": "marfeus", +"job": "accountant" +}'''); + final response = await dio.Dio.delete( + 'https://reqres.in/api/users/2', + data: data, + ); + print(response.statusCode); + print(response.data); + } on DioException catch (e, s) { + print(e.response?.statusCode); + print(e.response?.data); + print(s); + } catch (e, s) { + print(e); + print(s); + } +} +"""; + expect( + dartDioCodeGen.getCode(requestModelDelete2, "https"), expectedCode); + }); + }); +} diff --git a/test/codegen/dart_http_codegen_test.dart b/test/codegen/dart_http_codegen_test.dart index e384b628..55ab070a 100644 --- a/test/codegen/dart_http_codegen_test.dart +++ b/test/codegen/dart_http_codegen_test.dart @@ -1,5 +1,6 @@ import 'package:apidash/codegen/dart/http.dart'; import 'package:test/test.dart'; + import '../request_models.dart'; void main() { @@ -18,8 +19,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -34,9 +34,7 @@ void main() async { void main() async { var uri = Uri.parse('https://api.foss42.com/country/data'); - var queryParams = { - "code": "US" - }; + var queryParams = {'code': 'US'}; uri = uri.replace(queryParameters: queryParams); final response = await http.get(uri); @@ -45,13 +43,13 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } } """; + expect(dartHttpCodeGen.getCode(requestModelGet2, "https"), expectedCode); }); @@ -61,10 +59,8 @@ void main() async { void main() async { var uri = Uri.parse('https://api.foss42.com/country/data?code=US'); - var queryParams = { - "code": "IND" - }; - var urlQueryParams = Map.from(uri.queryParameters); + var queryParams = {'code': 'IND'}; + var urlQueryParams = Map.from(uri.queryParameters); urlQueryParams.addAll(queryParams); uri = uri.replace(queryParameters: urlQueryParams); @@ -74,8 +70,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -91,12 +86,12 @@ void main() async { var uri = Uri.parse('https://api.foss42.com/humanize/social'); var queryParams = { - "num": "8700000", - "digits": "3", - "system": "SS", - "add_space": "true", - "trailing_zeros": "true" - }; + 'num': '8700000', + 'digits': '3', + 'system': 'SS', + 'add_space': 'true', + 'trailing_zeros': 'true', + }; uri = uri.replace(queryParameters: queryParams); final response = await http.get(uri); @@ -105,8 +100,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -121,19 +115,18 @@ void main() async { void main() async { var uri = Uri.parse('https://api.github.com/repos/foss42/apidash'); - var headers = { - "User-Agent": "Test Agent" - }; + var headers = {'User-Agent': 'Test Agent'}; - final response = await http.get(uri, - headers: headers); + final response = await http.get( + uri, + headers: headers, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -148,24 +141,21 @@ void main() async { void main() async { var uri = Uri.parse('https://api.github.com/repos/foss42/apidash'); - var queryParams = { - "raw": "true" - }; + var queryParams = {'raw': 'true'}; uri = uri.replace(queryParameters: queryParams); - var headers = { - "User-Agent": "Test Agent" - }; + var headers = {'User-Agent': 'Test Agent'}; - final response = await http.get(uri, - headers: headers); + final response = await http.get( + uri, + headers: headers, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -186,8 +176,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -202,24 +191,21 @@ void main() async { void main() async { var uri = Uri.parse('https://api.github.com/repos/foss42/apidash'); - var queryParams = { - "raw": "true" - }; + var queryParams = {'raw': 'true'}; uri = uri.replace(queryParameters: queryParams); - var headers = { - "User-Agent": "Test Agent" - }; + var headers = {'User-Agent': 'Test Agent'}; - final response = await http.get(uri, - headers: headers); + final response = await http.get( + uri, + headers: headers, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -227,6 +213,117 @@ void main() async { """; expect(dartHttpCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""import 'package:http/http.dart' as http; + +void main() async { + var uri = Uri.parse('https://api.foss42.com/humanize/social'); + + var queryParams = { + 'num': '8700000', + 'add_space': 'true', + }; + uri = uri.replace(queryParameters: queryParams); + + final response = await http.get(uri); + + int statusCode = response.statusCode; + if (statusCode >= 200 && statusCode < 300) { + print('Status Code: $statusCode'); + print('Response Body: ${response.body}'); + } else { + print('Error Status Code: $statusCode'); + print('Error Response Body: ${response.body}'); + } +} +"""; + expect(dartHttpCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import 'package:http/http.dart' as http; + +void main() async { + var uri = Uri.parse('https://api.foss42.com/humanize/social'); + + var headers = {'User-Agent': 'Test Agent'}; + + final response = await http.get( + uri, + headers: headers, + ); + + int statusCode = response.statusCode; + if (statusCode >= 200 && statusCode < 300) { + print('Status Code: $statusCode'); + print('Response Body: ${response.body}'); + } else { + print('Error Status Code: $statusCode'); + print('Error Response Body: ${response.body}'); + } +} +"""; + expect( + dartHttpCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import 'package:http/http.dart' as http; + +void main() async { + var uri = Uri.parse('https://api.foss42.com/humanize/social'); + + var queryParams = { + 'num': '8700000', + 'digits': '3', + }; + uri = uri.replace(queryParameters: queryParams); + + var headers = {'User-Agent': 'Test Agent'}; + + final response = await http.get( + uri, + headers: headers, + ); + + int statusCode = response.statusCode; + if (statusCode >= 200 && statusCode < 300) { + print('Status Code: $statusCode'); + print('Response Body: ${response.body}'); + } else { + print('Error Status Code: $statusCode'); + print('Error Response Body: ${response.body}'); + } +} +"""; + expect(dartHttpCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import 'package:http/http.dart' as http; + +void main() async { + var uri = Uri.parse('https://api.foss42.com/humanize/social'); + + final response = await http.get(uri); + + int statusCode = response.statusCode; + if (statusCode >= 200 && statusCode < 300) { + print('Status Code: $statusCode'); + print('Response Body: ${response.body}'); + } else { + print('Error Status Code: $statusCode'); + print('Error Response Body: ${response.body}'); + } +} +"""; + expect(dartHttpCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -242,8 +339,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -264,8 +360,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -286,20 +381,19 @@ void main() async { "text": "I LOVE Flutter" }'''; - var headers = { - "content-type": "text/plain" - }; + var headers = {'content-type': 'text/plain'}; - final response = await http.post(uri, - headers: headers, - body: body); + final response = await http.post( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -318,20 +412,19 @@ void main() async { "text": "I LOVE Flutter" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.post(uri, - headers: headers, - body: body); + final response = await http.post( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -351,20 +444,21 @@ void main() async { }'''; var headers = { - "User-Agent": "Test Agent", - "content-type": "application/json" - }; + 'User-Agent': 'Test Agent', + 'content-type': 'application/json', + }; - final response = await http.post(uri, - headers: headers, - body: body); + final response = await http.post( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -373,6 +467,7 @@ void main() async { expect(dartHttpCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""import 'package:http/http.dart' as http; @@ -385,20 +480,19 @@ void main() async { "job": "zion resident" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.put(uri, - headers: headers, - body: body); + final response = await http.put( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -420,20 +514,19 @@ void main() async { "job": "accountant" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.patch(uri, - headers: headers, - body: body); + final response = await http.patch( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -457,8 +550,7 @@ void main() async { if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } @@ -479,20 +571,19 @@ void main() async { "job": "accountant" }'''; - var headers = { - "content-type": "application/json" - }; + var headers = {'content-type': 'application/json'}; - final response = await http.delete(uri, - headers: headers, - body: body); + final response = await http.delete( + uri, + headers: headers, + body: body, + ); int statusCode = response.statusCode; if (statusCode >= 200 && statusCode < 300) { print('Status Code: $statusCode'); print('Response Body: ${response.body}'); - } - else{ + } else { print('Error Status Code: $statusCode'); print('Error Response Body: ${response.body}'); } diff --git a/test/codegen/har_codegen_test.dart b/test/codegen/har_codegen_test.dart index cd9be868..8e6b7bb0 100644 --- a/test/codegen/har_codegen_test.dart +++ b/test/codegen/har_codegen_test.dart @@ -149,6 +149,83 @@ void main() { }"""; expect(harCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""{ + "method": "GET", + "url": "https://api.foss42.com/humanize/social?num=8700000&add_space=true", + "httpVersion": "HTTP/1.1", + "queryString": [ + { + "name": "num", + "value": "8700000" + }, + { + "name": "add_space", + "value": "true" + } + ], + "headers": [] +}"""; + expect(harCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""{ + "method": "GET", + "url": "https://api.foss42.com/humanize/social", + "httpVersion": "HTTP/1.1", + "queryString": [], + "headers": [ + { + "name": "User-Agent", + "value": "Test Agent" + } + ] +}"""; + expect( + harCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""{ + "method": "GET", + "url": "https://api.foss42.com/humanize/social?num=8700000&digits=3", + "httpVersion": "HTTP/1.1", + "queryString": [ + { + "name": "num", + "value": "8700000" + }, + { + "name": "digits", + "value": "3" + } + ], + "headers": [ + { + "name": "User-Agent", + "value": "Test Agent" + } + ] +}"""; + expect(harCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""{ + "method": "GET", + "url": "https://api.foss42.com/humanize/social", + "httpVersion": "HTTP/1.1", + "queryString": [], + "headers": [] +}"""; + expect(harCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -240,6 +317,7 @@ void main() { expect(harCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""{ diff --git a/test/codegen/js_axios_codegen_test.dart b/test/codegen/js_axios_codegen_test.dart index d809a122..9498da1c 100644 --- a/test/codegen/js_axios_codegen_test.dart +++ b/test/codegen/js_axios_codegen_test.dart @@ -201,6 +201,109 @@ axios(config) """; expect(axiosCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get', + params: { + "num": "8700000", + "add_space": "true" + } +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect(axiosCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get', + headers: { + "User-Agent": "Test Agent" + } +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect( + axiosCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get', + params: { + "num": "8700000", + "digits": "3" + }, + headers: { + "User-Agent": "Test Agent" + } +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect(axiosCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get' +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect(axiosCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -324,6 +427,7 @@ axios(config) expect(axiosCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""let config = { diff --git a/test/codegen/js_fetch_codegen_test.dart b/test/codegen/js_fetch_codegen_test.dart index 589f59e7..43d053f9 100644 --- a/test/codegen/js_fetch_codegen_test.dart +++ b/test/codegen/js_fetch_codegen_test.dart @@ -220,6 +220,121 @@ fetch(url, options) """; expect(fetchCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = + r"""let url = 'https://api.foss42.com/humanize/social?num=8700000&add_space=true'; + +let options = { + method: 'GET' +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect(fetchCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = + r"""let url = 'https://api.foss42.com/humanize/social'; + +let options = { + method: 'GET', + headers: { + "User-Agent": "Test Agent" + } +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect( + fetchCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = + r"""let url = 'https://api.foss42.com/humanize/social?num=8700000&digits=3'; + +let options = { + method: 'GET', + headers: { + "User-Agent": "Test Agent" + } +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect(fetchCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = + r"""let url = 'https://api.foss42.com/humanize/social'; + +let options = { + method: 'GET' +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect(fetchCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -366,6 +481,7 @@ fetch(url, options) expect(fetchCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""let url = 'https://reqres.in/api/users/2'; diff --git a/test/codegen/kotlin_okhttp_codegen_test.dart b/test/codegen/kotlin_okhttp_codegen_test.dart index f2205a1b..4f3220fa 100644 --- a/test/codegen/kotlin_okhttp_codegen_test.dart +++ b/test/codegen/kotlin_okhttp_codegen_test.dart @@ -219,6 +219,116 @@ fun main() { expect( kotlinOkHttpCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.HttpUrl.Companion.toHttpUrl + +fun main() { + val client = OkHttpClient() + + val url = "https://api.foss42.com/humanize/social".toHttpUrl().newBuilder() + .addQueryParameter("num", "8700000") + .addQueryParameter("add_space", "true") + .build() + + val request = Request.Builder() + .url(url) + .get() + .build() + + val response = client.newCall(request).execute() + + println(response.code) + println(response.body?.string()) +} +"""; + expect( + kotlinOkHttpCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import okhttp3.OkHttpClient +import okhttp3.Request + +fun main() { + val client = OkHttpClient() + + val url = "https://api.foss42.com/humanize/social" + + val request = Request.Builder() + .url(url) + .addHeader("User-Agent", "Test Agent") + .get() + .build() + + val response = client.newCall(request).execute() + + println(response.code) + println(response.body?.string()) +} +"""; + expect( + kotlinOkHttpCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.HttpUrl.Companion.toHttpUrl + +fun main() { + val client = OkHttpClient() + + val url = "https://api.foss42.com/humanize/social".toHttpUrl().newBuilder() + .addQueryParameter("num", "8700000") + .addQueryParameter("digits", "3") + .build() + + val request = Request.Builder() + .url(url) + .addHeader("User-Agent", "Test Agent") + .get() + .build() + + val response = client.newCall(request).execute() + + println(response.code) + println(response.body?.string()) +} +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModelGet11, "https"), + expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import okhttp3.OkHttpClient +import okhttp3.Request + +fun main() { + val client = OkHttpClient() + + val url = "https://api.foss42.com/humanize/social" + + val request = Request.Builder() + .url(url) + .get() + .build() + + val response = client.newCall(request).execute() + + println(response.code) + println(response.body?.string()) +} +"""; + expect(kotlinOkHttpCodeGen.getCode(requestModelGet12, "https"), + expectedCode); + }); }); group('HEAD Request', () { @@ -369,6 +479,7 @@ fun main() { expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r'''import okhttp3.OkHttpClient diff --git a/test/codegen/nodejs_axios_codegen_test.dart b/test/codegen/nodejs_axios_codegen_test.dart index 180edb8a..519f5ab5 100644 --- a/test/codegen/nodejs_axios_codegen_test.dart +++ b/test/codegen/nodejs_axios_codegen_test.dart @@ -217,6 +217,117 @@ axios(config) """; expect(axiosCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""import axios from 'axios'; + +let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get', + params: { + "num": "8700000", + "add_space": "true" + } +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect(axiosCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import axios from 'axios'; + +let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get', + headers: { + "User-Agent": "Test Agent" + } +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect( + axiosCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import axios from 'axios'; + +let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get', + params: { + "num": "8700000", + "digits": "3" + }, + headers: { + "User-Agent": "Test Agent" + } +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect(axiosCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import axios from 'axios'; + +let config = { + url: 'https://api.foss42.com/humanize/social', + method: 'get' +}; + +axios(config) + .then(function (response) { + // handle success + console.log(response.status); + console.log(response.data); + }) + .catch(function (error) { + // handle error + console.log(error.response.status); + console.log(error); + }); +"""; + expect(axiosCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -350,6 +461,7 @@ axios(config) expect(axiosCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""import axios from 'axios'; diff --git a/test/codegen/nodejs_fetch_codegen_test.dart b/test/codegen/nodejs_fetch_codegen_test.dart index 1a82d8a1..c06ceee5 100644 --- a/test/codegen/nodejs_fetch_codegen_test.dart +++ b/test/codegen/nodejs_fetch_codegen_test.dart @@ -230,6 +230,125 @@ fetch(url, options) """; expect(fetchCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""import fetch from 'node-fetch'; + +let url = 'https://api.foss42.com/humanize/social?num=8700000&add_space=true'; + +let options = { + method: 'GET' +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect(fetchCodeGen.getCode(requestModelGet9, "https"), expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import fetch from 'node-fetch'; + +let url = 'https://api.foss42.com/humanize/social'; + +let options = { + method: 'GET', + headers: { + "User-Agent": "Test Agent" + } +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect( + fetchCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import fetch from 'node-fetch'; + +let url = 'https://api.foss42.com/humanize/social?num=8700000&digits=3'; + +let options = { + method: 'GET', + headers: { + "User-Agent": "Test Agent" + } +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect(fetchCodeGen.getCode(requestModelGet11, "https"), expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import fetch from 'node-fetch'; + +let url = 'https://api.foss42.com/humanize/social'; + +let options = { + method: 'GET' +}; + +let status; +fetch(url, options) + .then(res => { + status = res.status; + return res.json() + }) + .then(body => { + console.log(status); + console.log(body); + }) + .catch(err => { + console.log(status); + console.error('error:' + err); + }); +"""; + expect(fetchCodeGen.getCode(requestModelGet12, "https"), expectedCode); + }); }); group('HEAD Request', () { @@ -386,6 +505,7 @@ fetch(url, options) expect(fetchCodeGen.getCode(requestModelPost3, "https"), expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""import fetch from 'node-fetch'; diff --git a/test/codegen/python_http_client_codegen_test.dart b/test/codegen/python_http_client_codegen_test.dart index f95d265d..c617305c 100644 --- a/test/codegen/python_http_client_codegen_test.dart +++ b/test/codegen/python_http_client_codegen_test.dart @@ -174,6 +174,94 @@ print(data.decode("utf-8")) expect(pythonHttpClientCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""import http.client +from urllib.parse import urlencode + +queryParams = { + "num": "8700000", + "add_space": "true" + } +queryParamsStr = '?' + urlencode(queryParams) + +conn = http.client.HTTPSConnection("api.foss42.com") +conn.request("GET", "/humanize/social" + queryParamsStr) + +res = conn.getresponse() +data = res.read() + +print(data.decode("utf-8")) +"""; + expect(pythonHttpClientCodeGen.getCode(requestModelGet9, "https"), + expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import http.client + +headers = { + "User-Agent": "Test Agent" + } + +conn = http.client.HTTPSConnection("api.foss42.com") +conn.request("GET", "/humanize/social", + headers= headers) + +res = conn.getresponse() +data = res.read() + +print(data.decode("utf-8")) +"""; + expect( + pythonHttpClientCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import http.client +from urllib.parse import urlencode + +queryParams = { + "num": "8700000", + "digits": "3" + } +queryParamsStr = '?' + urlencode(queryParams) + +headers = { + "User-Agent": "Test Agent" + } + +conn = http.client.HTTPSConnection("api.foss42.com") +conn.request("GET", "/humanize/social" + queryParamsStr, + headers= headers) + +res = conn.getresponse() +data = res.read() + +print(data.decode("utf-8")) +"""; + expect(pythonHttpClientCodeGen.getCode(requestModelGet11, "https"), + expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import http.client + +conn = http.client.HTTPSConnection("api.foss42.com") +conn.request("GET", "/humanize/social") + +res = conn.getresponse() +data = res.read() + +print(data.decode("utf-8")) +"""; + expect(pythonHttpClientCodeGen.getCode(requestModelGet12, "https"), + expectedCode); + }); }); group('HEAD Request', () { @@ -285,6 +373,7 @@ print(data.decode("utf-8")) expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""import http.client diff --git a/test/codegen/python_requests_codegen_test.dart b/test/codegen/python_requests_codegen_test.dart index 67e1f629..1387accf 100644 --- a/test/codegen/python_requests_codegen_test.dart +++ b/test/codegen/python_requests_codegen_test.dart @@ -153,6 +153,84 @@ print('Response Body:', response.text) expect(pythonRequestsCodeGen.getCode(requestModelGet8, "https"), expectedCode); }); + + test('GET 9', () { + const expectedCode = r"""import requests + +url = 'https://api.foss42.com/humanize/social' + +params = { + "num": "8700000", + "add_space": "true" + } + +response = requests.get(url, params=params) + +print('Status Code:', response.status_code) +print('Response Body:', response.text) +"""; + expect(pythonRequestsCodeGen.getCode(requestModelGet9, "https"), + expectedCode); + }); + + test('GET 10', () { + const expectedCode = r"""import requests + +url = 'https://api.foss42.com/humanize/social' + +headers = { + "User-Agent": "Test Agent" + } + +response = requests.get(url, headers=headers) + +print('Status Code:', response.status_code) +print('Response Body:', response.text) +"""; + expect( + pythonRequestsCodeGen.getCode( + requestModelGet10, + "https", + ), + expectedCode); + }); + + test('GET 11', () { + const expectedCode = r"""import requests + +url = 'https://api.foss42.com/humanize/social' + +params = { + "num": "8700000", + "digits": "3" + } + +headers = { + "User-Agent": "Test Agent" + } + +response = requests.get(url, params=params, headers=headers) + +print('Status Code:', response.status_code) +print('Response Body:', response.text) +"""; + expect(pythonRequestsCodeGen.getCode(requestModelGet11, "https"), + expectedCode); + }); + + test('GET 12', () { + const expectedCode = r"""import requests + +url = 'https://api.foss42.com/humanize/social' + +response = requests.get(url) + +print('Status Code:', response.status_code) +print('Response Body:', response.text) +"""; + expect(pythonRequestsCodeGen.getCode(requestModelGet12, "https"), + expectedCode); + }); }); group('HEAD Request', () { @@ -248,6 +326,7 @@ print('Response Body:', response.text) expectedCode); }); }); + group('PUT Request', () { test('PUT 1', () { const expectedCode = r"""import requests diff --git a/test/models/form_data_model_test.dart b/test/models/form_data_model_test.dart new file mode 100644 index 00000000..c8569a57 --- /dev/null +++ b/test/models/form_data_model_test.dart @@ -0,0 +1,44 @@ +import 'package:apidash/consts.dart'; +import 'package:test/test.dart'; +import 'package:apidash/models/form_data_model.dart'; + +void main() { + const fdmRow1 = FormDataModel( + name: "harry", + value: "23", + type: FormDataType.text, + ); + + test('Testing toString()', () { + const resultExpected = + 'FormDataModel(name: harry, value: 23, type: FormDataType.text)'; + expect(fdmRow1.toString(), resultExpected); + }); + + test('Testing toJson()', () { + const resultExpected = {"name": "harry", "value": "23", "type": "text"}; + expect(fdmRow1.toJson(), resultExpected); + }); + test('Testing fromJson()', () { + const resultExpected = fdmRow1; + expect( + FormDataModel.fromJson( + {"name": "harry", "value": "23", "type": "text"}), + resultExpected); + }); + + test('Testing copyWith()', () { + const resultExpected = FormDataModel( + name: "winter", + value: "26", + type: FormDataType.file, + ); + expect( + fdmRow1.copyWith(name: "winter", value: "26", type: FormDataType.file), + resultExpected); + }); + + test('Testing hashcode', () { + expect(fdmRow1.hashCode, greaterThan(0)); + }); +} diff --git a/test/models/name_value_model_test.dart b/test/models/name_value_model_test.dart index f451cea9..c1fd9f57 100644 --- a/test/models/name_value_model_test.dart +++ b/test/models/name_value_model_test.dart @@ -13,6 +13,11 @@ void main() { const resultExpected = {"name": "harry", "value": 23}; expect(nmRow1.toJson(), resultExpected); }); + test('Testing fromJson()', () { + const resultExpected = nmRow1; + expect(NameValueModel.fromJson({"name": "harry", "value": 23}), + resultExpected); + }); test('Testing copyWith()', () { const resultExpected = NameValueModel(name: "winter", value: "26"); diff --git a/test/models/request_model_test.dart b/test/models/request_model_test.dart index 1f03e809..626727de 100644 --- a/test/models/request_model_test.dart +++ b/test/models/request_model_test.dart @@ -106,11 +106,14 @@ void main() { 'content-length': '18', 'content-type': 'application/json; charset=utf-8' }, + 'isHeaderEnabledList': null, 'requestParams': null, + 'isParamEnabledList': null, "requestBodyContentType": 'json', "requestBody": '''{ "text":"WORLD" }''', + 'requestFormDataList': null, 'responseStatus': null, 'message': null, 'responseModel': null @@ -140,9 +143,12 @@ void main() { "Request Description: ", "Request Tab Index: 0", "Request Headers: [NameValueModel(name: content-length, value: 18), NameValueModel(name: content-type, value: application/json; charset=utf-8)]", + "Enabled Headers: null", "Request Params: null", + "Enabled Params: null", "Request Body Content Type: ContentType.json", 'Request Body: {\n"text":"WORLD"\n}', + 'Request FormData: null', "Response Status: null", "Response Message: null", "Response: null" @@ -150,6 +156,29 @@ void main() { test('Testing toString', () { expect(requestModelDup.toString(), requestModeDupString); }); + + test('Testing getters', () { + expect(requestModel.enabledRequestHeaders, const [ + NameValueModel(name: 'content-length', value: '18'), + NameValueModel( + name: 'content-type', value: 'application/json; charset=utf-8') + ]); + expect(requestModel.enabledRequestParams, null); + expect(requestModel.enabledHeadersMap, { + 'content-length': '18', + 'content-type': 'application/json; charset=utf-8' + }); + expect(requestModel.enabledParamsMap, {}); + expect(requestModel.headersMap, { + 'content-length': '18', + 'content-type': 'application/json; charset=utf-8' + }); + expect(requestModel.paramsMap, {}); + expect(requestModel.formDataMapList, []); + expect(requestModel.isFormDataRequest, false); + expect(requestModel.hasContentTypeHeader, true); + }); + test('Testing hashcode', () { expect(requestModel.hashCode, greaterThan(0)); }); diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart new file mode 100644 index 00000000..62745e62 --- /dev/null +++ b/test/models/settings_model_test.dart @@ -0,0 +1,74 @@ +import 'package:test/test.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/models/settings_model.dart'; +import 'package:apidash/consts.dart'; + +void main() { + const sm = SettingsModel( + isDark: false, + alwaysShowCollectionPaneScrollbar: true, + size: Size(300, 200), + offset: Offset(100, 150), + defaultUriScheme: "http", + defaultCodeGenLang: CodegenLanguage.curl, + saveResponses: true, + ); + + test('Testing toJson()', () { + const expectedResult = { + "isDark": false, + "alwaysShowCollectionPaneScrollbar": true, + "width": 300.0, + "height": 200.0, + "dx": 100.0, + "dy": 150.0, + "defaultUriScheme": "http", + "defaultCodeGenLang": "curl", + "saveResponses": true, + }; + expect(sm.toJson(), expectedResult); + }); + + test('Testing fromJson()', () { + const input = { + "isDark": false, + "alwaysShowCollectionPaneScrollbar": true, + "width": 300.0, + "height": 200.0, + "dx": 100.0, + "dy": 150.0, + "defaultUriScheme": "http", + "defaultCodeGenLang": "curl", + "saveResponses": true, + }; + expect(SettingsModel.fromJson(input), sm); + }); + + test('Testing copyWith()', () { + const expectedResult = SettingsModel( + isDark: true, + alwaysShowCollectionPaneScrollbar: true, + size: Size(300, 200), + offset: Offset(100, 150), + defaultUriScheme: "http", + defaultCodeGenLang: CodegenLanguage.curl, + saveResponses: false, + ); + expect( + sm.copyWith( + isDark: true, + saveResponses: false, + ), + expectedResult); + }); + + test('Testing toString()', () { + const expectedResult = + "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true}"; + expect(sm.toString(), expectedResult); + }); + + test('Testing hashcode', () { + expect(sm.hashCode, greaterThan(0)); + }); +} diff --git a/test/request_models.dart b/test/request_models.dart index ede411bb..adf7e0e8 100644 --- a/test/request_models.dart +++ b/test/request_models.dart @@ -90,6 +90,94 @@ const requestModelGet8 = RequestModel( ], ); +/// GET request model with some params enabled +const requestModelGet9 = RequestModel( + id: 'enabledParams', + url: 'https://api.foss42.com/humanize/social', + method: HTTPVerb.get, + requestParams: [ + NameValueModel(name: 'num', value: '8700000'), + NameValueModel(name: 'digits', value: '3'), + NameValueModel(name: 'system', value: 'SS'), + NameValueModel(name: 'add_space', value: 'true'), + ], + isParamEnabledList: [ + true, + false, + false, + true, + ], +); + +/// GET Request model with some headers enabled +const requestModelGet10 = RequestModel( + id: 'enabledParams', + url: 'https://api.foss42.com/humanize/social', + method: HTTPVerb.get, + requestHeaders: [ + NameValueModel(name: 'User-Agent', value: 'Test Agent'), + NameValueModel(name: 'Content-Type', value: 'application/json'), + ], + isHeaderEnabledList: [ + true, + false, + ], +); + +/// GET Request model with some headers & URL parameters enabled +const requestModelGet11 = RequestModel( + id: 'enabledRows', + url: 'https://api.foss42.com/humanize/social', + method: HTTPVerb.get, + requestParams: [ + NameValueModel(name: 'num', value: '8700000'), + NameValueModel(name: 'digits', value: '3'), + NameValueModel(name: 'system', value: 'SS'), + NameValueModel(name: 'add_space', value: 'true'), + ], + requestHeaders: [ + NameValueModel(name: 'User-Agent', value: 'Test Agent'), + NameValueModel(name: 'Content-Type', value: 'application/json'), + ], + isParamEnabledList: [ + true, + true, + false, + false, + ], + isHeaderEnabledList: [ + true, + false, + ], +); + +/// Request model with all headers & URL parameters disabled +const requestModelGet12 = RequestModel( + id: 'disabledRows', + url: 'https://api.foss42.com/humanize/social', + method: HTTPVerb.get, + requestParams: [ + NameValueModel(name: 'num', value: '8700000'), + NameValueModel(name: 'digits', value: '3'), + NameValueModel(name: 'system', value: 'SS'), + NameValueModel(name: 'add_space', value: 'true'), + ], + requestHeaders: [ + NameValueModel(name: 'User-Agent', value: 'Test Agent'), + NameValueModel(name: 'Content-Type', value: 'application/json'), + ], + isParamEnabledList: [ + false, + false, + false, + false, + ], + isHeaderEnabledList: [ + false, + false, + ], +); + /// Basic HEAD request model const requestModelHead1 = RequestModel( id: 'head1', diff --git a/test/utils/convert_utils_test.dart b/test/utils/convert_utils_test.dart index 26be2c10..e632bd3a 100644 --- a/test/utils/convert_utils_test.dart +++ b/test/utils/convert_utils_test.dart @@ -1,6 +1,8 @@ +import 'package:apidash/consts.dart'; import 'package:test/test.dart'; import 'package:apidash/utils/convert_utils.dart'; import 'package:apidash/models/name_value_model.dart'; +import 'package:apidash/models/form_data_model.dart'; void main() { group("Testing humanizeDuration function", () { @@ -106,6 +108,92 @@ void main() { }); }); + group("Testing rowsToFormDataMapList", () { + test('Testing for null', () { + expect(rowsToFormDataMapList(null), null); + }); + test('Testing with a map value', () { + const input = [ + FormDataModel(name: "text", value: "abc", type: FormDataType.file), + FormDataModel(name: "lang", value: "eng", type: FormDataType.file), + FormDataModel(name: "code", value: "1", type: FormDataType.text) + ]; + const expectedResult = [ + {"name": "text", "value": "abc", "type": "file"}, + {"name": "lang", "value": "eng", "type": "file"}, + {"name": "code", "value": "1", "type": "text"} + ]; + expect(rowsToFormDataMapList(input), expectedResult); + }); + }); + + group("Testing mapListToFormDataModelRows", () { + test('Testing for null', () { + expect(mapListToFormDataModelRows(null), null); + }); + test('Testing with a map value', () { + const input = [ + {"name": "text", "value": "abc", "type": "file"}, + {"name": "lang", "value": "eng", "type": "file"}, + {"name": "code", "value": "1", "type": "text"} + ]; + const expectedResult = [ + FormDataModel(name: "text", value: "abc", type: FormDataType.file), + FormDataModel(name: "lang", value: "eng", type: FormDataType.file), + FormDataModel(name: "code", value: "1", type: FormDataType.text) + ]; + expect(mapListToFormDataModelRows(input), expectedResult); + }); + }); + + group("Testing getFormDataType", () { + test('Testing for null', () { + expect(getFormDataType(null), FormDataType.text); + }); + test('Testing with a map value', () { + const input = "file"; + const expectedResult = FormDataType.file; + expect(getFormDataType(input), expectedResult); + }); + }); + + group("Testing jsonMapToBytes", () { + test('Testing for null', () { + expect(jsonMapToBytes(null), []); + }); + test('Testing with a map value', () { + Map value1 = {"a": "1"}; + const result1Expected = [ + 123, + 10, + 32, + 32, + 34, + 97, + 34, + 58, + 32, + 34, + 49, + 34, + 10, + 125 + ]; + expect(jsonMapToBytes(value1), result1Expected); + }); + }); + + group("Testing stringToBytes", () { + test('Testing for null', () { + expect(stringToBytes(null), null); + }); + test('Testing with a stringToBytes value', () { + String value1 = "ab"; + const result1Expected = [97, 98]; + expect(stringToBytes(value1), result1Expected); + }); + }); + group("Testing padMultilineString", () { String text1 = '''Using API Dash, you can draft API requests via an easy to use GUI which allows you to: @@ -127,4 +215,39 @@ Easily manipulate and play around with request inputs like headers, query parame expect(padMultilineString(text1, 10), text1FirstLineNotPaddedExpected); }); }); + + group("Test getEnabledRows", () { + test('Testing for null', () { + expect(getEnabledRows(null, null), null); + }); + test('Testing for empty list', () { + expect(getEnabledRows([], []), []); + }); + const kvRow1 = NameValueModel(name: "code", value: "IN"); + const kvRow2 = NameValueModel(name: "lang", value: "eng"); + const kvRow3 = NameValueModel(name: "version", value: 0.1); + const kvRow4 = NameValueModel(name: "month", value: 4); + test('Testing with isRowEnabledList null', () { + expect(getEnabledRows([kvRow1, kvRow2, kvRow3, kvRow4], null), + [kvRow1, kvRow2, kvRow3, kvRow4]); + }); + test('Testing for list with all enabled', () { + expect( + getEnabledRows( + [kvRow1, kvRow2, kvRow3, kvRow4], [true, true, true, true]), + [kvRow1, kvRow2, kvRow3, kvRow4]); + }); + test('Testing for list with all disabled', () { + expect( + getEnabledRows( + [kvRow1, kvRow2, kvRow3, kvRow4], [false, false, false, false]), + []); + }); + test('Testing for list with some disabled', () { + expect( + getEnabledRows( + [kvRow1, kvRow2, kvRow3, kvRow4], [true, false, true, false]), + [kvRow1, kvRow3]); + }); + }); } diff --git a/test/utils/file_utils_test.dart b/test/utils/file_utils_test.dart index fef80ace..18e2816a 100644 --- a/test/utils/file_utils_test.dart +++ b/test/utils/file_utils_test.dart @@ -3,15 +3,21 @@ import 'package:apidash/utils/file_utils.dart'; void main() { group( - "Testing x function", + "Testing File Utils", () { - /*test('Test case 2', () { - expect(showButtonLabelsInViewCodePane(350), false); + test('Test getFileExtension', () { + String mimetype = "text/csv"; + expect(getFileExtension(mimetype), "csv"); }); - test('Test case 3', () { - expect(showButtonLabelsInViewCodePane(450), true); - });*/ + test('Test getShortPath', () { + String path = "A/B/C/D.csv"; + expect(getShortPath(path), ".../C/D.csv"); + }); + + test('Test getTempFileName', () { + expect(getTempFileName().length, greaterThan(0)); + }); }, ); } diff --git a/test/utils/har_utils_test.dart b/test/utils/har_utils_test.dart new file mode 100644 index 00000000..0dd16d62 --- /dev/null +++ b/test/utils/har_utils_test.dart @@ -0,0 +1,323 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/utils/har_utils.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import '../request_models.dart'; + +void main() { + group( + "Testing HAR Utils", + () { + test('Test collectionToHAR', () async { + Map expectedResult = { + 'log': { + 'creator': { + 'comment': + 'For support, check out API Dash repo - https://github.com/foss42/apidash', + 'version': '1.0', + 'name': 'API Dash' + }, + 'entries': [ + { + 'startedDateTime': 'ABC', + 'comment': 'id:get6', + 'serverIPAddress': '', + 'time': 0, + 'timings': { + 'connect': -1, + 'comment': '', + 'blocked': -1, + 'dns': -1, + 'receive': 0, + 'send': 0, + 'wait': 0, + 'ssl': -1 + }, + 'response': { + 'status': 200, + 'statusText': 'OK', + 'httpVersion': 'HTTP/1.1', + 'cookies': [], + 'headers': [], + 'content': { + 'size': 0, + 'mimeType': '', + 'comment': '', + 'text': '' + }, + 'redirectURL': '', + 'headersSize': 0, + 'bodySize': 0, + 'comment': '' + }, + 'request': { + 'method': 'GET', + 'url': 'https://api.github.com/repos/foss42/apidash?raw=true', + 'httpVersion': 'HTTP/1.1', + 'queryString': [ + {'name': 'raw', 'value': 'true', 'comment': ''} + ], + 'headers': [ + {'name': 'User-Agent', 'value': 'Test Agent', 'comment': ''} + ], + 'comment': '', + 'cookies': [], + 'headersSize': -1, + 'bodySize': 0 + }, + 'cache': {} + }, + { + 'startedDateTime': 'ABC', + 'comment': 'id:enabledRows', + 'serverIPAddress': '', + 'time': 0, + 'timings': { + 'connect': -1, + 'comment': '', + 'blocked': -1, + 'dns': -1, + 'receive': 0, + 'send': 0, + 'wait': 0, + 'ssl': -1 + }, + 'response': { + 'status': 200, + 'statusText': 'OK', + 'httpVersion': 'HTTP/1.1', + 'cookies': [], + 'headers': [], + 'content': { + 'size': 0, + 'mimeType': '', + 'comment': '', + 'text': '' + }, + 'redirectURL': '', + 'headersSize': 0, + 'bodySize': 0, + 'comment': '' + }, + 'request': { + 'method': 'GET', + 'url': + 'https://api.foss42.com/humanize/social?num=8700000&digits=3&system=SS&add_space=true', + 'httpVersion': 'HTTP/1.1', + 'queryString': [ + {'name': 'num', 'value': '8700000', 'comment': ''}, + {'name': 'digits', 'value': '3', 'comment': ''}, + {'name': 'system', 'value': 'SS', 'comment': ''}, + {'name': 'add_space', 'value': 'true', 'comment': ''} + ], + 'headers': [ + { + 'name': 'User-Agent', + 'value': 'Test Agent', + 'comment': '' + }, + { + 'name': 'Content-Type', + 'value': 'application/json', + 'comment': '' + } + ], + 'comment': '', + 'cookies': [], + 'headersSize': -1, + 'bodySize': 0 + }, + 'cache': {} + }, + { + 'startedDateTime': 'ABC', + 'comment': 'id:post3', + 'serverIPAddress': '', + 'time': 0, + 'timings': { + 'connect': -1, + 'comment': '', + 'blocked': -1, + 'dns': -1, + 'receive': 0, + 'send': 0, + 'wait': 0, + 'ssl': -1 + }, + 'response': { + 'status': 200, + 'statusText': 'OK', + 'httpVersion': 'HTTP/1.1', + 'cookies': [], + 'headers': [], + 'content': { + 'size': 0, + 'mimeType': '', + 'comment': '', + 'text': '' + }, + 'redirectURL': '', + 'headersSize': 0, + 'bodySize': 0, + 'comment': '' + }, + 'request': { + 'method': 'POST', + 'url': 'https://api.foss42.com/case/lower', + 'httpVersion': 'HTTP/1.1', + 'queryString': [], + 'headers': [ + { + 'name': 'Content-Type', + 'value': 'application/json', + 'comment': '' + }, + {'name': 'User-Agent', 'value': 'Test Agent', 'comment': ''} + ], + 'postData': { + 'mimeType': 'application/json', + 'text': '{\n' + '"text": "I LOVE Flutter"\n' + '}', + 'comment': '' + }, + 'comment': '', + 'cookies': [], + 'headersSize': -1, + 'bodySize': 28 + }, + 'cache': {} + } + ], + 'comment': '', + 'browser': {'version': '1.0', 'comment': '', 'name': 'API Dash'}, + 'version': '1.2' + } + }; + PackageInfo.setMockInitialValues( + appName: "apidash", + packageName: "dev.apidash.apidash", + version: "1.0", + buildNumber: "3", + buildSignature: "XYZ"); + var result = await collectionToHAR([ + requestModelGet6, + requestModelGet11, + requestModelPost3, + ]); + result['log']['entries'][0]['startedDateTime'] = 'ABC'; + result['log']['entries'][1]['startedDateTime'] = 'ABC'; + result['log']['entries'][2]['startedDateTime'] = 'ABC'; + expect(result, expectedResult); + }); + + test('Test requestModelToHARJsonRequest', () { + Map expectedResult = { + 'method': 'GET', + 'url': 'https://api.github.com/repos/foss42/apidash?raw=true', + 'httpVersion': 'HTTP/1.1', + 'queryString': [ + {'name': 'raw', 'value': 'true'} + ], + 'headers': [ + {'name': 'User-Agent', 'value': 'Test Agent'} + ] + }; + expect(requestModelToHARJsonRequest(requestModelGet6), expectedResult); + }); + + test('Test requestModelToHARJsonRequest exportMode=true', () { + Map expectedResult = { + 'method': 'GET', + 'url': 'https://api.github.com/repos/foss42/apidash?raw=true', + 'httpVersion': 'HTTP/1.1', + 'queryString': [ + {'name': 'raw', 'value': 'true', 'comment': ''} + ], + 'headers': [ + {'name': 'User-Agent', 'value': 'Test Agent', 'comment': ''} + ], + 'comment': '', + 'cookies': [], + 'headersSize': -1, + 'bodySize': 0 + }; + expect( + requestModelToHARJsonRequest( + requestModelGet6, + exportMode: true, + ), + expectedResult); + }); + + test('Test requestModelToHARJsonRequest exportMode=true', () { + Map expectedResult = { + 'method': 'POST', + 'url': 'https://api.foss42.com/case/lower', + 'httpVersion': 'HTTP/1.1', + 'queryString': [], + 'headers': [ + {'name': 'Content-Type', 'value': 'application/json', 'comment': ''} + ], + 'postData': { + 'mimeType': 'application/json', + 'text': '{\n' + '"text": "I LOVE Flutter"\n' + '}', + 'comment': '' + }, + 'comment': '', + 'cookies': [], + 'headersSize': -1, + 'bodySize': 28 + }; + expect( + requestModelToHARJsonRequest( + requestModelPost2, + exportMode: true, + ), + expectedResult); + }); + + test('Test requestModelToHARJsonRequest useEnabled=false', () { + Map expectedResult = { + 'method': 'GET', + 'url': + 'https://api.foss42.com/humanize/social?num=8700000&digits=3&system=SS&add_space=true', + 'httpVersion': 'HTTP/1.1', + 'queryString': [ + {'name': 'num', 'value': '8700000'}, + {'name': 'digits', 'value': '3'}, + {'name': 'system', 'value': 'SS'}, + {'name': 'add_space', 'value': 'true'} + ], + 'headers': [ + {'name': 'User-Agent', 'value': 'Test Agent'}, + {'name': 'Content-Type', 'value': 'application/json'} + ] + }; + expect(requestModelToHARJsonRequest(requestModelGet11), expectedResult); + }); + + test('Test requestModelToHARJsonRequest useEnabled=true', () { + Map expectedResult = { + 'method': 'GET', + 'url': 'https://api.foss42.com/humanize/social?num=8700000&digits=3', + 'httpVersion': 'HTTP/1.1', + 'queryString': [ + {'name': 'num', 'value': '8700000'}, + {'name': 'digits', 'value': '3'} + ], + 'headers': [ + {'name': 'User-Agent', 'value': 'Test Agent'} + ] + }; + expect( + requestModelToHARJsonRequest( + requestModelGet11, + useEnabled: true, + ), + expectedResult); + }); + }, + ); +} diff --git a/test/utils/header_utils_test.dart b/test/utils/header_utils_test.dart new file mode 100644 index 00000000..73c15721 --- /dev/null +++ b/test/utils/header_utils_test.dart @@ -0,0 +1,141 @@ +import 'package:apidash/utils/header_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group("Testing getHeaderSuggestions function", () { + test("Testing using Allow-Headers", () { + String pattern = "Allow-Headers"; + List expected = ["Access-Control-Allow-Headers"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Allow-Methods", () { + String pattern = "Allow-Methods"; + List expected = ["Access-Control-Allow-Methods"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Allow-Origin", () { + String pattern = "Allow-Origin"; + List expected = ["Access-Control-Allow-Origin"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Request-Method", () { + String pattern = "Request-Method"; + List expected = ["Access-Control-Request-Method"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Max-Age", () { + String pattern = "Max-Age"; + List expected = ["Access-Control-Max-Age"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Access-Control-Allow-Headers", () { + String pattern = "Access-Control-Allow-Headers"; + List expected = ["Access-Control-Allow-Headers"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Access-Control-Allow-Methods", () { + String pattern = "Access-Control-Allow-Methods"; + List expected = ["Access-Control-Allow-Methods"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Access-Control-Allow-Origin", () { + String pattern = "Access-Control-Allow-Origin"; + List expected = ["Access-Control-Allow-Origin"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Access-Control-Request-Method", () { + String pattern = "Access-Control-Request-Method"; + List expected = ["Access-Control-Request-Method"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Access-Control-Max-Age", () { + String pattern = "Access-Control-Max-Age"; + List expected = ["Access-Control-Max-Age"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Content-Type", () { + String pattern = "Content-Type"; + List expected = ['Content-Type', 'X-Content-Type-Options']; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using Expires", () { + String pattern = "Expires"; + List expected = ["Expires"]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using 'Access-Control' pattern", () { + String pattern = "Access-Control"; + List expected = [ + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Max-Age", + "Access-Control-Request-Headers", + "Access-Control-Request-Method" + ]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using 'allow-' pattern", () { + String pattern = 'allow-'; + List expected = [ + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin" + ]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using 'content' pattern", () { + String pattern = "content"; + List expected = [ + 'Content-Disposition', + 'Content-Encoding', + 'Content-Length', + 'Content-Security-Policy', + 'Content-Type', + 'X-Content-Type-Options' + ]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing using 'x-' pattern", () { + String pattern = "x-"; + List expected = [ + "Access-Control-Max-Age", + "X-Api-Key", + "X-Content-Type-Options", + "X-CSRF-Token", + "X-Forwarded-For", + "X-Frame-Options", + "X-Requested-With", + "X-XSS-Protection" + ]; + expect(getHeaderSuggestions(pattern), expected); + }); + + test("Testing for 'origin' pattern", () { + String pattern = "origin"; + List expected = [ + 'Access-Control-Allow-Origin', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Origin' + ]; + expect(getHeaderSuggestions(pattern), expected); + }); + }); +} diff --git a/test/utils/http_utils_test.dart b/test/utils/http_utils_test.dart index 2cc04763..55129bcc 100644 --- a/test/utils/http_utils_test.dart +++ b/test/utils/http_utils_test.dart @@ -268,7 +268,7 @@ void main() { test('Testing getResponseBodyViewOptions for image/svg+xml', () { MediaType mediaType5 = MediaType("image", "svg+xml"); var result5 = getResponseBodyViewOptions(mediaType5); - expect(result5.$1, kCodeRawBodyViewOptions); + expect(result5.$1, kPreviewRawBodyViewOptions); expect(result5.$2, "xml"); }); test('Testing getResponseBodyViewOptions for application/xhtml+xml', () { diff --git a/test/widget_test.dart b/test/widget_test.dart index ab73b3a2..48eb1d93 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1 +1,6 @@ +import 'package:apidash/main.dart'; +import 'package:apidash/app.dart'; +import 'package:apidash/common/utils.dart'; +import 'package:apidash/screens/screens.dart'; + void main() {} diff --git a/test/widgets/buttons_test.dart b/test/widgets/buttons_test.dart index 55eaa096..91677be8 100644 --- a/test/widgets/buttons_test.dart +++ b/test/widgets/buttons_test.dart @@ -1,7 +1,8 @@ -import 'package:apidash/consts.dart'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:apidash/widgets/buttons.dart'; +import 'package:apidash/consts.dart'; import '../test_consts.dart'; void main() { @@ -53,6 +54,7 @@ void main() { await tester.tap(button1); expect(changedValue, 'Send'); }); + testWidgets('Testing for Send Request button when sentRequestId is not null', (tester) async { await tester.pumpWidget( @@ -76,6 +78,7 @@ void main() { expect(tester.widget(button1).enabled, isFalse); }); + testWidgets('Testing for Send Request button when sentRequestId = activeId', (tester) async { await tester.pumpWidget( @@ -99,4 +102,113 @@ void main() { expect(tester.widget(button1).enabled, isFalse); }); + + testWidgets('Testing for Save in Downloads button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Save in Downloads button', + theme: kThemeDataLight, + home: const Scaffold( + body: SaveInDownloadsButton(), + ), + ), + ); + + expect(find.byIcon(Icons.download), findsOneWidget); + expect(find.text("Download"), findsOneWidget); + + final button1 = find.byType(TextButton); + expect(button1, findsOneWidget); + + expect(tester.widget(button1).enabled, isFalse); + }); + + testWidgets('Testing for Save in Downloads button 2', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Save in Downloads button', + theme: kThemeDataLight, + home: Scaffold( + body: SaveInDownloadsButton( + content: Uint8List.fromList([1]), + ), + ), + ), + ); + + expect(find.byIcon(Icons.download), findsOneWidget); + expect(find.text("Download"), findsOneWidget); + + final button1 = find.byType(TextButton); + expect(button1, findsOneWidget); + + expect(tester.widget(button1).enabled, isTrue); + }); + + testWidgets('Testing for Repo button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Repo button', + theme: kThemeDataLight, + home: const Scaffold( + body: RepoButton( + icon: Icons.code, + ), + ), + ), + ); + + expect(find.byIcon(Icons.code), findsOneWidget); + expect(find.text("GitHub"), findsOneWidget); + }); + + testWidgets('Testing for Repo button icon = null', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Repo button', + theme: kThemeDataLight, + home: const Scaffold( + body: RepoButton(), + ), + ), + ); + + expect(find.byIcon(Icons.code), findsNothing); + expect(find.text("GitHub"), findsOneWidget); + + final button1 = find.byType(FilledButton); + expect(button1, findsOneWidget); + + expect(tester.widget(button1).enabled, isTrue); + }); + + testWidgets('Testing for Discord button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Discord button', + theme: kThemeDataLight, + home: const Scaffold( + body: DiscordButton(), + ), + ), + ); + + expect(find.byIcon(Icons.discord), findsOneWidget); + expect(find.text("Discord Server"), findsOneWidget); + }); + + testWidgets('Testing for Save button', (tester) async { + await tester.pumpWidget( + MaterialApp( + title: 'Save button', + theme: kThemeDataLight, + home: const Scaffold( + body: SaveButton(), + ), + ), + ); + + expect(find.byIcon(Icons.save), findsOneWidget); + expect(find.text("Save"), findsOneWidget); + }); } diff --git a/test/widgets/checkbox_test.dart b/test/widgets/checkbox_test.dart new file mode 100644 index 00000000..fc6167a0 --- /dev/null +++ b/test/widgets/checkbox_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/checkbox.dart'; + +void main() { + testWidgets('Testing for Checkbox', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Checkbox Widget', + home: Scaffold( + body: CheckBox( + keyId: "1", + value: false, + onChanged: (value) { + changedValue = value; + }, + ), + ), + ), + ); + + expect(find.byKey(const Key("1")), findsOneWidget); + var box = find.byKey(const Key("1")); + await tester.tap(box); + await tester.pump(); + await tester.pumpAndSettle(); + expect(changedValue, true); + }); +} diff --git a/test/widgets/dropdowns_test.dart b/test/widgets/dropdowns_test.dart index d6922bfa..7d304ce8 100644 --- a/test/widgets/dropdowns_test.dart +++ b/test/widgets/dropdowns_test.dart @@ -87,4 +87,88 @@ void main() { expect(changedValue, ContentType.text); }); + + testWidgets('Testing Dropdown for FormData', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Dropdown FormData Type testing', + theme: kThemeDataLight, + home: Scaffold( + body: Center( + child: Column( + children: [ + DropdownButtonFormData( + formDataType: FormDataType.file, + onChanged: (value) { + changedValue = value!; + }, + ), + ], + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); + expect(find.byType(DropdownButton), findsOneWidget); + expect( + (tester.widget(find.byType(DropdownButton)) + as DropdownButton) + .value, + equals(FormDataType.file)); + + await tester.tap(find.text('file')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + await tester.tap(find.text('text').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(changedValue, FormDataType.text); + }); + + testWidgets('Testing Dropdown for Codegen', (tester) async { + dynamic changedValue; + await tester.pumpWidget( + MaterialApp( + title: 'Dropdown Codegen Type testing', + theme: kThemeDataLight, + home: Scaffold( + body: Center( + child: Column( + children: [ + DropdownButtonCodegenLanguage( + codegenLanguage: CodegenLanguage.curl, + onChanged: (value) { + changedValue = value!; + }, + ), + ], + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.unfold_more_rounded), findsOneWidget); + expect(find.byType(DropdownButton), findsOneWidget); + expect( + (tester.widget(find.byType(DropdownButton)) + as DropdownButton) + .value, + equals(CodegenLanguage.curl)); + + await tester.tap(find.text('cURL')); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + await tester.tap(find.text('Dart (dio)').last); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(changedValue, CodegenLanguage.dartDio); + }); } diff --git a/test/widgets/form_data_field_test.dart b/test/widgets/form_data_field_test.dart new file mode 100644 index 00000000..b5748370 --- /dev/null +++ b/test/widgets/form_data_field_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/form_data_field.dart'; + +void main() { + testWidgets('Testing for Form Data Widget', (tester) async { + await tester.pumpWidget( + const MaterialApp( + title: 'Form Data Field Widget', + home: Scaffold( + body: FormDataField( + keyId: "1", + initialValue: "Test Field", + ), + ), + ), + ); + + expect(find.text("Test Field"), findsOneWidget); + }); +} diff --git a/test/widgets/headerfield_test.dart b/test/widgets/headerfield_test.dart new file mode 100644 index 00000000..63a179f2 --- /dev/null +++ b/test/widgets/headerfield_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/headerfield.dart'; + +void main() { + testWidgets('Testing Header Field', (tester) async { + await tester.pumpWidget( + const MaterialApp( + title: 'Header Field', + home: Scaffold( + body: Column(children: [ + HeaderField( + keyId: "1", + initialValue: "X", + ) + ]), + ), + ), + ); + + expect(find.byKey(const Key("1")), findsOneWidget); + expect(find.text('X'), findsOneWidget); + }); +} diff --git a/test/widgets/markdown_test.dart b/test/widgets/markdown_test.dart new file mode 100644 index 00000000..79cb579a --- /dev/null +++ b/test/widgets/markdown_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/markdown.dart'; + +void main() { + testWidgets('Testing CustomMarkdown', (tester) async { + const markdown = CustomMarkdown(data: """Is a markdown ~`star on github`~ + + #br + #br + + ~`github repo`~ ~`Discord Server`~"""); + await tester.pumpWidget(markdown); + //expectTextStrings(tester.allWidgets, ['Data1']); + }, skip: true); +} diff --git a/test/widgets/previewer_test.dart b/test/widgets/previewer_test.dart index c98a9070..0d2f68cd 100644 --- a/test/widgets/previewer_test.dart +++ b/test/widgets/previewer_test.dart @@ -4,6 +4,7 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'package:flutter/foundation.dart'; import 'package:printing/printing.dart' show PdfPreview; +import 'package:flutter_svg/flutter_svg.dart' show SvgPicture; import '../test_consts.dart'; void main() { @@ -169,4 +170,65 @@ void main() { await tester.pumpAndSettle(); expect(find.text(kAudioError), findsOneWidget); }); + + testWidgets('Testing when type/subtype is image/svg+xml', (tester) async { + String rawSvg = + """ + + + + + + + + + + + + + + + + + + +"""; + + await tester.pumpWidget( + MaterialApp( + title: 'Previewer', + home: Scaffold( + body: Previewer( + type: 'image', + subtype: 'svg+xml', + bytes: Uint8List.fromList([]), + body: rawSvg, + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text(kSvgError), findsNothing); + expect(find.byType(SvgPicture), findsOneWidget); + }); + + testWidgets('Testing when type/subtype is image/svg+xml corrupted', + (tester) async { + String rawSvg = "rwsjhdws"; + await tester.pumpWidget( + MaterialApp( + title: 'Previewer', + home: Scaffold( + body: Previewer( + type: 'image', + subtype: 'svg+xml', + bytes: Uint8List.fromList([]), + body: rawSvg, + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text(kSvgError), findsOneWidget); + }); } diff --git a/test/widgets/window_caption_test.dart b/test/widgets/window_caption_test.dart new file mode 100644 index 00000000..ab48cea9 --- /dev/null +++ b/test/widgets/window_caption_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:apidash/widgets/window_caption.dart'; + +void main() { + testWidgets('Testing for Window caption', (tester) async { + await tester.pumpWidget( + const MaterialApp( + title: 'Window caption', + home: Scaffold( + body: WindowCaption(), + ), + ), + ); + + final wd = find.byType(GestureDetector); + expect(wd, findsAny); + }); +}