diff --git a/lib/consts.dart b/lib/consts.dart index a7607dfc..fe49e404 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -124,6 +124,7 @@ const kVSpacer40 = SizedBox(height: 40); const kTabAnimationDuration = Duration(milliseconds: 200); const kTabHeight = 32.0; const kHeaderHeight = 32.0; +const kTopicHeaderHeight = 48.0; const kSegmentHeight = 24.0; const kTextButtonMinWidth = 44.0; @@ -253,12 +254,22 @@ final kColorHttpMethodDelete = Colors.red.shade800; enum RequestItemMenuOption { edit, delete, duplicate } +enum ProtocolType { http, mqttv3 } + +enum RealtimeConnectionState { + connected, + connecting, + disconnected, + disconnecting +} + enum HTTPVerb { get, head, post, put, patch, delete } enum FormDataType { text, file } -const kSupportedUriSchemes = ["https", "http"]; +const kSupportedUriSchemes = ["https", "http", "mqtt", "mqtts", "ws", "wss"]; const kDefaultUriScheme = "https"; + const kMethodsWithBody = [ HTTPVerb.post, HTTPVerb.put, @@ -266,6 +277,7 @@ const kMethodsWithBody = [ HTTPVerb.delete, ]; +const kDefaultProtocolType = ProtocolType.http; const kDefaultHttpMethod = HTTPVerb.get; const kDefaultContentType = ContentType.json; @@ -529,10 +541,15 @@ const kCsvError = "There seems to be an issue rendering this CSV. Please raise an issue in API Dash GitHub repo so that we can resolve it."; const kHintTextUrlCard = "Enter API endpoint like https://$kDefaultUri/"; +const kHintTextClientIdCard = "Enter Client ID"; const kLabelPlusNew = "+ New"; const kLabelSend = "Send"; const kLabelSending = "Sending.."; const kLabelBusy = "Busy"; +const kLabelConnect = "Connect"; +const kLabelDisconnect = "Disconnect"; +const kLabelConnecting = "Connecting.."; +const kLabelDisconnecting = "Disconnecting.."; const kLabelCopy = "Copy"; const kLabelSave = "Save"; const kLabelDownload = "Download"; diff --git a/lib/models/models.dart b/lib/models/models.dart index 66d6f6ce..cd8ad9fb 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -3,3 +3,5 @@ export 'request_model.dart'; export 'response_model.dart'; export 'settings_model.dart'; export 'form_data_model.dart'; +export 'mqtt/mqtt_request_model.dart'; +export 'mqtt/mqtt_response_model.dart'; diff --git a/lib/models/mqtt/mqtt_request_model.dart b/lib/models/mqtt/mqtt_request_model.dart new file mode 100644 index 00000000..22c93605 --- /dev/null +++ b/lib/models/mqtt/mqtt_request_model.dart @@ -0,0 +1,238 @@ +import 'package:flutter/foundation.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; + +@immutable +class MQTTRequestModel { + const MQTTRequestModel({ + this.username, + this.password, + this.keepAlive, + this.lwTopic, + this.lwMessage, + required this.lwQos, + required this.lwRetain, + required this.id, + required this.clientId, + this.url = "", + this.name = "", + this.description = "", + this.requestTabIndex = 0, + this.responseStatus, + this.message, + this.responseModel, + }); + + final String id; + final String clientId; + final String url; + final String name; + final String description; + final int requestTabIndex; + final int? responseStatus; + final String? message; + final String? username; + final String? password; + final int? keepAlive; + final String? lwTopic; + final String? lwMessage; + final int lwQos; + final bool lwRetain; + + final MQTTResponseModel? responseModel; + + MQTTRequestModel duplicate({ + required String id, + }) { + return MQTTRequestModel( + id: id, + url: "", + name: "$name (copy)", + description: "", + requestTabIndex: 0, + responseStatus: null, + message: null, + username: null, + password: null, + keepAlive: null, + lwTopic: null, + lwMessage: null, + lwQos: 0, + lwRetain: false, + responseModel: null, + clientId: "", + ); + } + + MQTTRequestModel copyWith({ + String? id, + HTTPVerb? method, + String? url, + String? name, + String? description, + int? requestTabIndex, + int? responseStatus, + String? message, + MQTTResponseModel? responseModel, + String? username, + String? password, + int? keepAlive, + String? lwTopic, + String? lwMessage, + int? lwQos, + bool? lwRetain, + String? clientId, + }) { + return MQTTRequestModel( + id: id ?? this.id, + url: url ?? this.url, + name: name ?? this.name, + description: description ?? this.description, + requestTabIndex: requestTabIndex ?? this.requestTabIndex, + responseStatus: responseStatus ?? this.responseStatus, + message: message ?? this.message, + responseModel: responseModel ?? this.responseModel, + username: username ?? this.username, + password: password ?? this.password, + keepAlive: keepAlive ?? this.keepAlive, + lwTopic: lwTopic ?? this.lwTopic, + lwMessage: lwMessage ?? this.lwMessage, + lwQos: lwQos ?? this.lwQos, + lwRetain: lwRetain ?? this.lwRetain, + clientId: clientId ?? this.clientId, + ); + } + + factory MQTTRequestModel.fromJson(Map data) { + MQTTResponseModel? responseModel; + + final id = data["id"] as String; + final url = data["url"] as String; + final name = data["name"] as String?; + final description = data["description"] as String?; + final username = data["username"] as String?; + final password = data["password"] as String?; + final keepAlive = data["keepAlive"] as int?; + final lwTopic = data["lwTopic"] as String?; + final lwMessage = data["lwMessage"] as String?; + final lwQos = data["lwQos"] as int; + final lwRetain = data["lwRetain"] as bool; + final responseStatus = data["responseStatus"] as int?; + final message = data["message"] as String?; + final clientId = data["clientId"] as String; + final responseModelJson = data["responseModel"]; + + if (responseModelJson != null) { + responseModel = MQTTResponseModel.fromJson( + Map.from(responseModelJson)); + } else { + responseModel = null; + } + + return MQTTRequestModel( + id: id, + url: url, + clientId: clientId, + name: name ?? "", + description: description ?? "", + requestTabIndex: 0, + responseStatus: responseStatus, + message: message, + username: username ?? "", + password: password ?? "", + keepAlive: keepAlive, + lwTopic: lwTopic, + lwMessage: lwMessage, + lwQos: lwQos, + lwRetain: lwRetain, + responseModel: responseModel, + ); + } + + Map toJson({bool includeResponse = true}) { + return { + "id": id, + "url": url, + "name": name, + "description": description, + "clientId": clientId, + "username": username, + "password": password, + "keepAlive": keepAlive, + "lwTopic": lwTopic, + "lwMessage": lwMessage, + "lwQos": lwQos, + "lwRetain": lwRetain, + "responseStatus": includeResponse ? responseStatus : null, + "message": includeResponse ? message : null, + "responseModel": includeResponse ? responseModel?.toJson() : null, + }; + } + + @override + String toString() { + return [ + "Request Id: $id", + "Request URL: $url", + "Request Name: $name", + "Request Description: $description", + "Client ID: $clientId", + "Username: $username", + "Password: $password", + "Keep Alive: ${keepAlive.toString()}", + "Last Will Topic: $lwTopic", + "Last Will Message: $lwMessage", + "Last Will QoS: ${lwQos.toString()}", + "Last Will Retain: $lwRetain", + "Request Tab Index: ${requestTabIndex.toString()}", + "Response Status: $responseStatus", + "Response Message: $message", + "Response: ${responseModel.toString()}" + ].join("\n"); + } + + @override + bool operator ==(Object other) { + return other is MQTTRequestModel && + other.runtimeType == runtimeType && + other.id == id && + other.url == url && + clientId == other.clientId && + other.name == name && + other.description == description && + other.username == username && + other.password == password && + other.keepAlive == keepAlive && + other.lwTopic == lwTopic && + other.lwMessage == lwMessage && + other.lwQos == lwQos && + other.lwRetain == lwRetain && + other.requestTabIndex == requestTabIndex && + other.responseStatus == responseStatus && + other.message == message && + other.responseModel == responseModel; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + id, + url, + clientId, + name, + description, + username, + password, + keepAlive, + lwTopic, + lwMessage, + lwQos, + lwRetain, + requestTabIndex, + responseStatus, + message, + responseModel, + ); + } +} diff --git a/lib/models/mqtt/mqtt_response_model.dart b/lib/models/mqtt/mqtt_response_model.dart new file mode 100644 index 00000000..6de8fc6a --- /dev/null +++ b/lib/models/mqtt/mqtt_response_model.dart @@ -0,0 +1,83 @@ +import 'package:flutter/foundation.dart'; +import 'package:mqtt_client/mqtt_client.dart'; + +@immutable +class MQTTResponseModel { + const MQTTResponseModel({ + this.topic, + this.mqttHeader, + this.payload, + this.lwRetain, + this.time, + }); + + final String? topic; + final MqttHeader? mqttHeader; + final String? payload; + final bool? lwRetain; + final Duration? time; + + factory MQTTResponseModel.fromJson(Map data) { + Duration? timeElapsed; + final topic = data["topic"] as String?; + final mqttHeader = data["mqttHeader"] as MqttHeader?; + final payload = data["payload"] as String?; + final lwRetain = data["lwRetain"] as bool?; + final time = data["time"] as int?; + + if (time != null) { + timeElapsed = Duration(microseconds: time); + } + return MQTTResponseModel( + topic: topic, + mqttHeader: mqttHeader, + payload: payload, + lwRetain: lwRetain, + time: timeElapsed, + ); + } + + Map toJson() { + return { + "topic": topic, + "mqttHeader": mqttHeader, + "payload": payload, + "lwRetain": lwRetain, + "time": time?.inMicroseconds, + }; + } + + @override + String toString() { + return [ + "Topic: $topic", + "MQTT Header: $mqttHeader", + "Payload: $payload", + "Last Will Retain: $lwRetain", + "Response Time: $time", + ].join("\n"); + } + + @override + bool operator ==(Object other) { + return other is MQTTResponseModel && + other.runtimeType == runtimeType && + other.topic == topic && + other.mqttHeader == mqttHeader && + other.payload == payload && + other.lwRetain == lwRetain && + other.time == time; + } + + @override + int get hashCode { + return Object.hash( + runtimeType, + topic, + mqttHeader, + payload, + lwRetain, + time, + ); + } +} diff --git a/lib/models/mqtt/mqtt_topic_model.dart b/lib/models/mqtt/mqtt_topic_model.dart new file mode 100644 index 00000000..fdc9a499 --- /dev/null +++ b/lib/models/mqtt/mqtt_topic_model.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:flutter/foundation.dart'; + +part 'mqtt_topic_model.freezed.dart'; + +part 'mqtt_topic_model.g.dart'; + +@freezed +class MQTTTopicModel with _$MQTTTopicModel { + const factory MQTTTopicModel({ + required String name, + required int qos, + required bool subscribe, + required String description, + }) = _MQTTTopicModel; + + factory MQTTTopicModel.fromJson(Map json) => + _$MQTTTopicModelFromJson(json); +} + +const kMQTTTopicEmptyModel = + MQTTTopicModel(name: "", qos: 0, subscribe: false, description: ""); diff --git a/lib/models/mqtt/mqtt_topic_model.freezed.dart b/lib/models/mqtt/mqtt_topic_model.freezed.dart new file mode 100644 index 00000000..fcc2b011 --- /dev/null +++ b/lib/models/mqtt/mqtt_topic_model.freezed.dart @@ -0,0 +1,224 @@ +// 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 'mqtt_topic_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'); + +MQTTTopicModel _$MQTTTopicModelFromJson(Map json) { + return _MQTTTopicModel.fromJson(json); +} + +/// @nodoc +mixin _$MQTTTopicModel { + String get name => throw _privateConstructorUsedError; + int get qos => throw _privateConstructorUsedError; + bool get subscribe => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MQTTTopicModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MQTTTopicModelCopyWith<$Res> { + factory $MQTTTopicModelCopyWith( + MQTTTopicModel value, $Res Function(MQTTTopicModel) then) = + _$MQTTTopicModelCopyWithImpl<$Res, MQTTTopicModel>; + @useResult + $Res call({String name, int qos, bool subscribe, String description}); +} + +/// @nodoc +class _$MQTTTopicModelCopyWithImpl<$Res, $Val extends MQTTTopicModel> + implements $MQTTTopicModelCopyWith<$Res> { + _$MQTTTopicModelCopyWithImpl(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? qos = null, + Object? subscribe = null, + Object? description = null, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + qos: null == qos + ? _value.qos + : qos // ignore: cast_nullable_to_non_nullable + as int, + subscribe: null == subscribe + ? _value.subscribe + : subscribe // ignore: cast_nullable_to_non_nullable + as bool, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MQTTTopicModelImplCopyWith<$Res> + implements $MQTTTopicModelCopyWith<$Res> { + factory _$$MQTTTopicModelImplCopyWith(_$MQTTTopicModelImpl value, + $Res Function(_$MQTTTopicModelImpl) then) = + __$$MQTTTopicModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String name, int qos, bool subscribe, String description}); +} + +/// @nodoc +class __$$MQTTTopicModelImplCopyWithImpl<$Res> + extends _$MQTTTopicModelCopyWithImpl<$Res, _$MQTTTopicModelImpl> + implements _$$MQTTTopicModelImplCopyWith<$Res> { + __$$MQTTTopicModelImplCopyWithImpl( + _$MQTTTopicModelImpl _value, $Res Function(_$MQTTTopicModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? qos = null, + Object? subscribe = null, + Object? description = null, + }) { + return _then(_$MQTTTopicModelImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + qos: null == qos + ? _value.qos + : qos // ignore: cast_nullable_to_non_nullable + as int, + subscribe: null == subscribe + ? _value.subscribe + : subscribe // ignore: cast_nullable_to_non_nullable + as bool, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MQTTTopicModelImpl + with DiagnosticableTreeMixin + implements _MQTTTopicModel { + const _$MQTTTopicModelImpl( + {required this.name, + required this.qos, + required this.subscribe, + required this.description}); + + factory _$MQTTTopicModelImpl.fromJson(Map json) => + _$$MQTTTopicModelImplFromJson(json); + + @override + final String name; + @override + final int qos; + @override + final bool subscribe; + @override + final String description; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'MQTTTopicModel(name: $name, qos: $qos, subscribe: $subscribe, description: $description)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'MQTTTopicModel')) + ..add(DiagnosticsProperty('name', name)) + ..add(DiagnosticsProperty('qos', qos)) + ..add(DiagnosticsProperty('subscribe', subscribe)) + ..add(DiagnosticsProperty('description', description)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MQTTTopicModelImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.qos, qos) || other.qos == qos) && + (identical(other.subscribe, subscribe) || + other.subscribe == subscribe) && + (identical(other.description, description) || + other.description == description)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => + Object.hash(runtimeType, name, qos, subscribe, description); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$MQTTTopicModelImplCopyWith<_$MQTTTopicModelImpl> get copyWith => + __$$MQTTTopicModelImplCopyWithImpl<_$MQTTTopicModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MQTTTopicModelImplToJson( + this, + ); + } +} + +abstract class _MQTTTopicModel implements MQTTTopicModel { + const factory _MQTTTopicModel( + {required final String name, + required final int qos, + required final bool subscribe, + required final String description}) = _$MQTTTopicModelImpl; + + factory _MQTTTopicModel.fromJson(Map json) = + _$MQTTTopicModelImpl.fromJson; + + @override + String get name; + @override + int get qos; + @override + bool get subscribe; + @override + String get description; + @override + @JsonKey(ignore: true) + _$$MQTTTopicModelImplCopyWith<_$MQTTTopicModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/mqtt/mqtt_topic_model.g.dart b/lib/models/mqtt/mqtt_topic_model.g.dart new file mode 100644 index 00000000..eefe995b --- /dev/null +++ b/lib/models/mqtt/mqtt_topic_model.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mqtt_topic_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MQTTTopicModelImpl _$$MQTTTopicModelImplFromJson(Map json) => + _$MQTTTopicModelImpl( + name: json['name'] as String, + qos: json['qos'] as int, + subscribe: json['subscribe'] as bool, + description: json['description'] as String, + ); + +Map _$$MQTTTopicModelImplToJson( + _$MQTTTopicModelImpl instance) => + { + 'name': instance.name, + 'qos': instance.qos, + 'subscribe': instance.subscribe, + 'description': instance.description, + }; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index d8b9d6af..1cfd1649 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -15,6 +15,7 @@ import 'models.dart'; class RequestModel { const RequestModel({ required this.id, + this.protocol = kDefaultProtocolType, this.method = HTTPVerb.get, this.url = "", this.name = "", @@ -35,6 +36,7 @@ class RequestModel { }); final String id; + final ProtocolType protocol; final HTTPVerb method; final String url; final String name; @@ -104,6 +106,7 @@ class RequestModel { method: method, url: url, name: name ?? "${this.name} (copy)", + protocol: protocol, description: description, requestTabIndex: requestTabIndex ?? 0, requestHeaders: requestHeaders != null ? [...requestHeaders!] : null, @@ -121,6 +124,7 @@ class RequestModel { RequestModel copyWith({ String? id, + ProtocolType? protocol, HTTPVerb? method, String? url, String? name, @@ -146,6 +150,7 @@ class RequestModel { var formDataList = requestFormDataList ?? this.requestFormDataList; return RequestModel( id: id ?? this.id, + protocol: protocol ?? this.protocol, method: method ?? this.method, url: url ?? this.url, name: name ?? this.name, @@ -168,11 +173,17 @@ class RequestModel { } factory RequestModel.fromJson(Map data) { + ProtocolType protocol; HTTPVerb method; ContentType requestBodyContentType; ResponseModel? responseModel; final id = data["id"] as String; + try { + protocol = ProtocolType.values.byName(data["protocol"] as String); + } catch (e) { + protocol = kDefaultProtocolType; + } try { method = HTTPVerb.values.byName(data["method"] as String); } catch (e) { @@ -206,6 +217,7 @@ class RequestModel { return RequestModel( id: id, + protocol: protocol, method: method, url: url, name: name ?? "", @@ -233,6 +245,7 @@ class RequestModel { Map toJson({bool includeResponse = true}) { return { "id": id, + "protocol": protocol.name, "method": method.name, "url": url, "name": name, @@ -254,6 +267,7 @@ class RequestModel { String toString() { return [ "Request Id: $id", + "Request Protocol: ${protocol.name}", "Request Method: ${method.name}", "Request URL: $url", "Request Name: $name", @@ -277,6 +291,7 @@ class RequestModel { return other is RequestModel && other.runtimeType == runtimeType && other.id == id && + other.protocol == protocol && other.method == method && other.url == url && other.name == name && @@ -299,6 +314,7 @@ class RequestModel { return Object.hash( runtimeType, id, + protocol, method, url, name, diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index ec4d36f9..2c39ebea 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -1,14 +1,28 @@ +import 'package:apidash/models/mqtt/mqtt_topic_model.dart'; +import 'package:apidash/services/mqtt_service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; +import 'settings_providers.dart'; +import 'ui_providers.dart'; import 'package:http/http.dart' as http; import '../consts.dart'; import '../models/models.dart'; -import '../services/services.dart' show hiveHandler, HiveHandler, request; +import '../services/services.dart' + show HiveHandler, connectToMqttServer, hiveHandler, request; import '../utils/utils.dart' show getNewUuid, collectionToHAR; import 'settings_providers.dart'; import 'ui_providers.dart'; final selectedIdStateProvider = StateProvider((ref) => null); +final sentRequestIdStateProvider = StateProvider((ref) => null); + +final realtimeConnectionStateProvider = StateProvider( + (ref) => RealtimeConnectionState.disconnected); +final realtimeHistoryStateProvider = + StateProvider>>((ref) => []); +final clientIdStateProvider = StateProvider((ref) => null); final selectedRequestModelProvider = StateProvider((ref) { final selectedId = ref.watch(selectedIdStateProvider); @@ -28,6 +42,10 @@ final requestSequenceProvider = StateProvider>((ref) { final StateNotifierProvider?> collectionStateNotifierProvider = StateNotifierProvider((ref) => CollectionStateNotifier(ref, hiveHandler)); +final subscribedTopicsStateProvider = + StateProvider>((ref) => []); +final messageTopicStateProvider = StateProvider((ref) => null); +late MqttServerClient mqttClient; class CollectionStateNotifier extends StateNotifier?> { @@ -136,6 +154,7 @@ class CollectionStateNotifier void update( String id, { + ProtocolType? protocol, HTTPVerb? method, String? url, String? name, @@ -153,22 +172,22 @@ class CollectionStateNotifier ResponseModel? responseModel, }) { final newModel = state![id]!.copyWith( - method: method, - url: url, - name: name, - description: description, - requestTabIndex: requestTabIndex, - requestHeaders: requestHeaders, - requestParams: requestParams, - isHeaderEnabledList: isHeaderEnabledList, - isParamEnabledList: isParamEnabledList, - requestBodyContentType: requestBodyContentType, - requestBody: requestBody, - requestFormDataList: requestFormDataList, - responseStatus: responseStatus, - message: message, - responseModel: responseModel, - ); + protocol: protocol, + method: method, + url: url, + name: name, + description: description, + requestTabIndex: requestTabIndex, + requestHeaders: requestHeaders, + requestParams: requestParams, + isHeaderEnabledList: isHeaderEnabledList, + isParamEnabledList: isParamEnabledList, + requestBodyContentType: requestBodyContentType, + requestBody: requestBody, + requestFormDataList: requestFormDataList, + responseStatus: responseStatus, + message: message, + responseModel: responseModel); //print(newModel); var map = {...state!}; map[id] = newModel; @@ -176,6 +195,139 @@ class CollectionStateNotifier ref.read(hasUnsavedChangesProvider.notifier).state = true; } + Future connectToBroker(String id) async { + RequestModel requestModel = state![id]!; + late final RequestModel newRequestModel; + ref.read(realtimeConnectionStateProvider.notifier).state = + RealtimeConnectionState.connecting; + ref.read(subscribedTopicsStateProvider.notifier).state = []; + ref.read(selectedIdStateProvider.notifier).state = id; + ref.read(realtimeHistoryStateProvider.notifier).state = []; + ref.read(sentRequestIdStateProvider.notifier).state = id; + ref.read(codePaneVisibleStateProvider.notifier).state = false; + String? clientId = ref.read(clientIdStateProvider.notifier).state; + + try { + RequestModel requestModel = state![id]!; + mqttClient = await connectToMqttServer( + broker: requestModel.url, clientId: clientId); + + var map = {...state!}; + state = map; + } catch (e) { + newRequestModel = requestModel.copyWith( + responseStatus: -1, + message: e.toString(), + ); + var map = {...state!}; + map[id] = newRequestModel; + state = map; + ref.read(sentRequestIdStateProvider.notifier).state = null; + ref.read(realtimeConnectionStateProvider.notifier).state = + RealtimeConnectionState.disconnected; + } + if (mqttClient.connectionStatus?.state == MqttConnectionState.connected) { + ref.read(realtimeHistoryStateProvider.notifier).state = [ + { + "direction": 'info', + "message": 'Connected to broker', + }, + ...ref.read(realtimeHistoryStateProvider) + ]; + mqttClient.onDisconnected = () { + ref.read(realtimeHistoryStateProvider.notifier).state = [ + { + "direction": 'info', + "message": 'Disconnected from broker', + }, + ...ref.read(realtimeHistoryStateProvider) + ]; + ref.read(realtimeConnectionStateProvider.notifier).state = + RealtimeConnectionState.disconnected; + }; + mqttClient.updates?.listen( + (List> c) { + final MqttPublishMessage recMess = c[0].payload as MqttPublishMessage; + final String pt = + MqttPublishPayload.bytesToStringAsString(recMess.payload.message); + + // Update the history + ref.read(realtimeHistoryStateProvider.notifier).state = [ + { + "direction": 'receive', + "message": "${c[0].topic}: $pt", + }, + ...ref.read(realtimeHistoryStateProvider), + ]; + }, + ); + newRequestModel = requestModel.copyWith( + responseStatus: 1, // 1 is for connected + message: 'Connected', + ); + var map = {...state!}; + map[id] = newRequestModel; + state = map; + ref.read(realtimeConnectionStateProvider.notifier).state = + RealtimeConnectionState.connected; + } + } + + Future sendMessage(String id) async { + ref.read(subscribedTopicsStateProvider.notifier).state = []; + ref.read(codePaneVisibleStateProvider.notifier).state = false; + String? topic = ref.watch(messageTopicStateProvider.notifier).state; + RequestModel requestModel = state![id]!; + publishMessage( + client: mqttClient, + topic: topic!, + message: requestModel.requestBody!, + qos: MqttQos.atLeastOnce); + // Update the history + ref.read(realtimeHistoryStateProvider.notifier).state = [ + { + "direction": 'send', + "message": '$topic: ${requestModel.requestBody!}', + }, + ...ref.read(realtimeHistoryStateProvider), + ]; + } + + Future subscribeTopic(String topic, MqttQos qosLevel) async { + subscribeToTopic(mqttClient, topic, qosLevel); + ref.read(realtimeHistoryStateProvider.notifier).state = [ + { + "direction": 'info', + "message": 'Subscribed to the topic $topic from broker', + }, + ...ref.read(realtimeHistoryStateProvider) + ]; + } + + Future unsubscribeTopic(String topic) async { + mqttClient.unsubscribe(topic); + ref.read(realtimeHistoryStateProvider.notifier).state = [ + { + "direction": 'info', + "message": 'Unsubscribed to the topic $topic from broker', + }, + ...ref.read(realtimeHistoryStateProvider) + ]; + } + + Future disconnectFromBroker(String id) async { + ref.read(realtimeConnectionStateProvider.notifier).state = + RealtimeConnectionState.disconnecting; + ref.read(subscribedTopicsStateProvider.notifier).state = []; + ref.read(selectedIdStateProvider.notifier).state = id; + ref.read(codePaneVisibleStateProvider.notifier).state = false; + await disconnectFromMqttServer(mqttClient); + + ref.read(realtimeConnectionStateProvider.notifier).state = + RealtimeConnectionState.disconnected; + ref.read(sentRequestIdStateProvider.notifier).state = null; + } + Future sendRequest(String id) async { ref.read(codePaneVisibleStateProvider.notifier).state = false; final defaultUriScheme = ref.read( diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 934c5b66..dfb359f1 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -218,6 +218,7 @@ class RequestItem extends ConsumerWidget { return SidebarRequestCard( id: id, + protocol: requestModel.protocol, method: requestModel.method, name: requestModel.name, url: requestModel.url, diff --git a/lib/screens/home_page/editor_pane/details_card/details_card.dart b/lib/screens/home_page/editor_pane/details_card/details_card.dart index e14c139c..597cac60 100644 --- a/lib/screens/home_page/editor_pane/details_card/details_card.dart +++ b/lib/screens/home_page/editor_pane/details_card/details_card.dart @@ -1,3 +1,6 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_pane.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_response_pane.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -12,10 +15,18 @@ class EditorPaneRequestDetailsCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final codePaneVisible = ref.watch(codePaneVisibleStateProvider); + + final protocol = ref.watch(selectedRequestModelProvider)?.protocol; return RequestDetailsCard( child: EqualSplitView( - leftWidget: const EditRequestPane(), - rightWidget: codePaneVisible ? const CodePane() : const ResponsePane(), + leftWidget: protocol == ProtocolType.mqttv3 + ? const MQTTEditRequestPane() + : const EditRequestPane(), + rightWidget: codePaneVisible + ? const CodePane() + : protocol == ProtocolType.mqttv3 + ? const MQTTResponsePane() + : const ResponsePane(), ), ); } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_connection_config.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_connection_config.dart new file mode 100644 index 00000000..0e8048c6 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_connection_config.dart @@ -0,0 +1,172 @@ +import 'dart:math'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +class MQTTConnectionConfigParams extends ConsumerStatefulWidget { + const MQTTConnectionConfigParams({super.key}); + + @override + ConsumerState createState() => + EditRequestURLParamsState(); +} + +class EditRequestURLParamsState + extends ConsumerState { + final random = Random.secure(); + List headerRows = [ + const NameValueModel(name: 'Username', value: ''), + const NameValueModel(name: 'Password', value: ''), + const NameValueModel(name: 'Keep Alive', value: ''), + const NameValueModel(name: 'Last-Will Topic', value: ''), + const NameValueModel(name: 'Last-Will Message', value: ''), + const NameValueModel(name: 'Last-Will QoS', value: ''), + ]; + late List isRowEnabledList; + late int seed; + + @override + void initState() { + super.initState(); + seed = random.nextInt(kRandMax); + } + + void _onFieldChange(String selectedId) { + ref.read(collectionStateNotifierProvider.notifier).update( + selectedId, + requestParams: headerRows, + isParamEnabledList: isRowEnabledList, + ); + } + + @override + Widget build(BuildContext context) { + final selectedId = ref.watch(selectedIdStateProvider); + final length = ref.watch(selectedRequestModelProvider + .select((value) => value?.requestParams?.length)); + var rP = ref.read(selectedRequestModelProvider)?.requestParams; + bool isHeadersEmpty = rP == null || rP.isEmpty; + headerRows = isHeadersEmpty ? headerRows : rP; + isRowEnabledList = + ref.read(selectedRequestModelProvider)?.isParamEnabledList ?? + List.filled(headerRows.length, true, growable: true); + List columns = const [ + DataColumn2( + label: Text(kNameHeader), + ), + DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + DataColumn2( + label: Text(kNameValue), + ), + ]; + + List dataRows = List.generate(headerRows.length, (index) { + return DataRow(cells: [ + DataCell( + Text( + headerRows[index].name, + style: kCodeStyle, + ), + ), + DataCell( + Text( + "=", + style: kCodeStyle, + ), + ), + DataCell( + CellField( + keyId: "$selectedId-$index-params-v-$seed", + initialValue: headerRows[index].value, + hintText: "Add Value", + onChanged: (value) { + headerRows[index] = headerRows[index].copyWith(value: value); + _onFieldChange(selectedId!); + }, + colorScheme: Theme.of(context).colorScheme, + ), + ), + ]); + }); + // // DaviModel model = DaviModel( + // rows: rows, + // columns: [ + // DaviColumn( + // name: 'URL Parameter', + // width: 70, + // grow: 1, + // cellBuilder: (_, row) { + // int idx = row.index; + // return Text( + // rows[idx].name, + // style: kCodeStyle, + // ); + // }, + // sortable: false, + // ), + // DaviColumn( + // width: 30, + // cellBuilder: (_, row) { + // return Text( + // "=", + // style: kCodeStyle, + // ); + // }, + // ), + // DaviColumn( + // name: 'Value', + // grow: 1, + // cellBuilder: (_, row) { + // int idx = row.index; + // return CellField( + // keyId: "$selectedId-$idx-params-v-$seed", + // initialValue: rows[idx].value, + // hintText: "Add Value", + // onChanged: (value) { + // rows[idx] = rows[idx].copyWith(value: value); + // _onFieldChange(selectedId!); + // }, + // colorScheme: Theme.of(context).colorScheme, + // ); + // }, + // sortable: false, + // ), + // ], + // ); + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: Column( + children: [ + Expanded( + child: Theme( + data: Theme.of(context) + .copyWith(scrollbarTheme: kDataTableScrollbarTheme), + child: DataTable2( + columnSpacing: 12, + dividerThickness: 0, + horizontalMargin: 0, + headingRowHeight: 0, + dataRowHeight: kDataTableRowHeight, + bottomMargin: kDataTableBottomPadding, + isVerticalScrollBarVisible: true, + columns: columns, + rows: dataRows, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_message_body.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_message_body.dart new file mode 100644 index 00000000..ceccdcc7 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_message_body.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import '../request_form_data.dart'; + +class EditMessageBody extends ConsumerWidget { + const EditMessageBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final requestModel = ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(selectedId!); + final contentType = ref.watch(selectedRequestModelProvider + .select((value) => value?.requestBodyContentType)); + + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + ), + margin: kPt5o10, + child: Column( + children: [ + SizedBox( + height: kTopicHeaderHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Select Content Type:", + ), + const DropdownButtonBodyContentType(), + kHSpacer20, + Expanded( + child: Padding( + padding: kMaterialListPadding, + child: TopicField( + value: + ref.watch(messageTopicStateProvider.notifier).state ?? + "", + hintText: "Topic", + onChanged: (String value) { + ref.read(messageTopicStateProvider.notifier).state = + value; + }, + ), + ), + ) + ], + ), + ), + Expanded( + child: switch (contentType) { + ContentType.formdata => const FormDataWidget(), + // TODO: Fix JsonTextFieldEditor & plug it here + ContentType.json => TextFieldEditor( + key: Key("$selectedId-json-body"), + fieldKey: "$selectedId-json-body-editor", + initialValue: requestModel?.requestBody, + onChanged: (String value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId, requestBody: value); + }, + ), + _ => TextFieldEditor( + key: Key("$selectedId-body"), + fieldKey: "$selectedId-body-editor", + initialValue: requestModel?.requestBody, + onChanged: (String value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId, requestBody: value); + }, + ), + }, + ), + const Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.only(top: 20, bottom: 20), + child: SendButton(), + ), + ), + Divider( + color: Theme.of(context).colorScheme.surfaceVariant, + height: 1, + thickness: 1, + ) + ], + ), + ); + } +} + +class DropdownButtonBodyContentType extends ConsumerWidget { + const DropdownButtonBodyContentType({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final requestBodyContentType = ref.watch(selectedRequestModelProvider + .select((value) => value?.requestBodyContentType)); + return DropdownButtonContentType( + contentType: requestBodyContentType, + onChanged: (ContentType? value) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId!, requestBodyContentType: value); + }, + ); + } +} + +class SendButton extends ConsumerWidget { + const SendButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final sentRequestId = ref.watch(sentRequestIdStateProvider); + return SendMessageButton( + selectedId: selectedId, + sentRequestId: sentRequestId, + onTap: () { + ref + .read(collectionStateNotifierProvider.notifier) + .sendMessage(selectedId!); + }, + ); + } +} + +class TopicField extends StatelessWidget { + const TopicField({ + super.key, + required this.value, + this.hintText, + this.onChanged, + this.colorScheme, + }); + + final String? hintText; + final String value; + final void Function(String)? onChanged; + final ColorScheme? colorScheme; + + @override + Widget build(BuildContext context) { + var clrScheme = colorScheme ?? Theme.of(context).colorScheme; + return TextFormField( + initialValue: value, + style: kCodeStyle.copyWith( + color: clrScheme.onSurface, + ), + decoration: InputDecoration( + hintStyle: kCodeStyle.copyWith( + color: clrScheme.outline.withOpacity( + kHintOpacity, + ), + ), + hintText: hintText, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceVariant, + ), + ), + ), + onChanged: onChanged, + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_pane.dart new file mode 100644 index 00000000..5f8b6bbd --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_pane.dart @@ -0,0 +1,69 @@ +import 'package:apidash/consts.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_connection_config.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_message_body.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_properties.dart'; +import 'package:apidash/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_topics_pane.dart'; +import 'package:apidash/widgets/mqtt/mqtt_request_widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; + +class MQTTEditRequestPane extends ConsumerWidget { + const MQTTEditRequestPane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final codePaneVisible = ref.watch(codePaneVisibleStateProvider); + final tabIndex = ref.watch( + selectedRequestModelProvider.select((value) => value?.requestTabIndex)); + + final headerLength = ref.watch(selectedRequestModelProvider + .select((value) => value?.headersMap.length)); + final paramLength = ref.watch(selectedRequestModelProvider + .select((value) => value?.paramsMap.length)); + final bodyLength = ref.watch(selectedRequestModelProvider + .select((value) => value?.requestBody?.length)); + + return Column( + children: [ + Expanded( + child: MQTTRequestPane( + selectedId: selectedId, + tabIndex: tabIndex, + onTapTabBar: (index) { + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId!, requestTabIndex: index); + }, + showIndicators: [ + bodyLength != null && bodyLength > 0, + paramLength != null && paramLength > 0, + headerLength != null && headerLength > 0, + ], + tabLabels: const [ + 'Message', + 'Connection Configuration', + 'Properties' + ], + children: const [ + EditMessageBody(), + MQTTConnectionConfigParams(), + MQTTEditRequestProperties(), + ], + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: const MQTTTopicsPane(), + ), + ) + ], + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_properties.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_properties.dart new file mode 100644 index 00000000..0be8f862 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_properties.dart @@ -0,0 +1,217 @@ +import 'dart:math'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +class MQTTEditRequestProperties extends ConsumerStatefulWidget { + const MQTTEditRequestProperties({super.key}); + + @override + ConsumerState createState() => + MQTTEditRequestPropertiesState(); +} + +class MQTTEditRequestPropertiesState + extends ConsumerState { + final random = Random.secure(); + late List headerRows; + late List isRowEnabledList; + late int seed; + + @override + void initState() { + super.initState(); + seed = random.nextInt(kRandMax); + } + + void _onFieldChange(String selectedId) { + ref.read(collectionStateNotifierProvider.notifier).update( + selectedId, + requestHeaders: headerRows, + isHeaderEnabledList: isRowEnabledList, + ); + } + + @override + Widget build(BuildContext context) { + final selectedId = ref.watch(selectedIdStateProvider); + final length = ref.watch(selectedRequestModelProvider + .select((value) => value?.requestHeaders?.length)); + var rH = ref.read(selectedRequestModelProvider)?.requestHeaders; + headerRows = (rH == null || rH.isEmpty) + ? [ + kNameValueEmptyModel, + ] + : rH; + isRowEnabledList = + ref.read(selectedRequestModelProvider)?.isHeaderEnabledList ?? + List.filled(headerRows.length, true, growable: true); + bool isAddingRow = false; + return Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: Column( + children: [ + Expanded( + child: DataTable2( + columnSpacing: 12, + dividerThickness: 0, + horizontalMargin: 0, + headingRowHeight: 0, + dataRowHeight: kDataTableRowHeight, + bottomMargin: kDataTableBottomPadding, + isVerticalScrollBarVisible: true, + columns: const [ + DataColumn2( + label: Text('Checkbox'), + size: ColumnSize.S, + ), + DataColumn2( + label: Text('Header Name'), + size: ColumnSize.L, + ), + DataColumn2( + size: ColumnSize.S, + label: Text('='), + ), + DataColumn2( + size: ColumnSize.L, + label: Text('Header Value'), + ), + DataColumn2( + size: ColumnSize.S, + label: Text(" "), + ), + ], + rows: List.generate( + headerRows.length, + (index) { + bool isLast = index + 1 == headerRows.length; + return DataRow( + key: ValueKey("$selectedId-$index-headers-row-$seed"), + cells: [ + DataCell( + CheckBox( + keyId: "$selectedId-$index-headers-c-$seed", + value: isRowEnabledList[index], + onChanged: isLast + ? null + : (value) { + setState(() { + isRowEnabledList[index] = value!; + }); + _onFieldChange(selectedId!); + }, + colorScheme: Theme.of(context).colorScheme, + ), + ), + DataCell( + HeaderField( + keyId: "$selectedId-$index-headers-k-$seed", + initialValue: headerRows[index].name, + hintText: kHintAddName, + onChanged: (value) { + headerRows[index] = + headerRows[index].copyWith(name: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + isRowEnabledList[index] = true; + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + } + _onFieldChange(selectedId!); + }, + colorScheme: Theme.of(context).colorScheme, + ), + ), + DataCell( + Center( + child: Text( + "=", + style: kCodeStyle, + ), + ), + ), + DataCell( + CellField( + keyId: "$selectedId-$index-headers-v-$seed", + initialValue: headerRows[index].value, + hintText: kHintAddValue, + onChanged: (value) { + headerRows[index] = + headerRows[index].copyWith(value: value); + if (isLast && !isAddingRow) { + isAddingRow = true; + isRowEnabledList[index] = true; + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + } + _onFieldChange(selectedId!); + }, + colorScheme: Theme.of(context).colorScheme, + ), + ), + DataCell( + InkWell( + onTap: isLast + ? null + : () { + seed = random.nextInt(kRandMax); + if (headerRows.length == 2) { + setState(() { + headerRows = [ + kNameValueEmptyModel, + ]; + isRowEnabledList = [false]; + }); + } else { + headerRows.removeAt(index); + isRowEnabledList.removeAt(index); + } + _onFieldChange(selectedId!); + }, + child: + Theme.of(context).brightness == Brightness.dark + ? kIconRemoveDark + : kIconRemoveLight, + ), + ), + ], + ); + }, + ), + )), + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: ElevatedButton.icon( + onPressed: () { + headerRows.add(kNameValueEmptyModel); + isRowEnabledList.add(false); + _onFieldChange(selectedId!); + }, + icon: const Icon(Icons.add), + label: const Text( + "Add Properties", + style: kTextStyleButton, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_response_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_response_pane.dart new file mode 100644 index 00000000..979ba273 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_response_pane.dart @@ -0,0 +1,144 @@ +import 'package:apidash/providers/collection_providers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class MQTTResponsePane extends ConsumerWidget { + const MQTTResponsePane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final sentRequestId = ref.watch(sentRequestIdStateProvider); + final connectionState = ref.watch(realtimeConnectionStateProvider); + final responseStatus = ref.watch( + selectedRequestModelProvider.select((value) => value?.responseStatus)); + final message = ref + .watch(selectedRequestModelProvider.select((value) => value?.message)); + final startSendingTime = ref.watch( + selectedRequestModelProvider.select((value) => value?.sendingTime)); + if (responseStatus == -1) { + return ErrorMessage(message: '$message. $kUnexpectedRaiseIssue'); + } + if (connectionState == RealtimeConnectionState.connecting || + connectionState == RealtimeConnectionState.disconnecting) { + return SendingWidget( + startSendingTime: DateTime.now(), + ); + } + if (sentRequestId == null) { + return const NotSentWidget(); + } + + return const MQTTResponseDetails(); + } +} + +class MQTTResponseDetails extends ConsumerWidget { + const MQTTResponseDetails({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final responseStatus = ref.watch( + selectedRequestModelProvider.select((value) => value?.responseStatus)); + final message = ref + .watch(selectedRequestModelProvider.select((value) => value?.message)); + final responseModel = ref.watch( + selectedRequestModelProvider.select((value) => value?.responseModel)); + return Column( + children: [ + Padding( + padding: kPh20v10, + child: SizedBox( + height: kHeaderHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Response", + style: Theme.of(context).textTheme.titleMedium, + ), + // FilledButton.tonalIcon( + // onPressed: widget.onPressedCodeButton, + // icon: Icon( + // widget.codePaneVisible + // ? Icons.code_off_rounded + // : Icons.code_rounded, + // ), + // label: SizedBox( + // width: 75, + // child: Text( + // widget.codePaneVisible ? "Hide Code" : "View Code"), + // ), + // ), + ], + ), + ), + ), + Expanded(child: MQTTResponseView()), + ], + ); + } +} + +class ResponseTabs extends ConsumerWidget { + const ResponseTabs({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + return ResponseTabView( + selectedId: selectedId, + children: const [ + ResponseBodyTab(), + ], + ); + } +} + +class ResponseBodyTab extends ConsumerWidget { + const ResponseBodyTab({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedRequestModel = ref.watch(selectedRequestModelProvider); + return ResponseBody( + selectedRequestModel: selectedRequestModel, + ); + } +} + +class MQTTResponseView extends ConsumerWidget { + const MQTTResponseView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final history = ref.watch(realtimeHistoryStateProvider); + return ListView.builder( + itemCount: history.length, + itemBuilder: (context, index) { + return ListTile( + leading: history[index]['direction'] == 'receive' + ? const Icon( + Icons.arrow_downward, + color: Colors.green, + ) + : history[index]['direction'] == 'send' + ? const Icon( + Icons.arrow_upward, + color: Colors.blue, + ) + : Icon( + Icons.info, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + title: Text(history[index]['message'] ?? ''), + ); + }, + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_topics_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_topics_pane.dart new file mode 100644 index 00000000..ab734295 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_topics_pane.dart @@ -0,0 +1,317 @@ +import 'dart:math'; +import 'package:apidash/models/mqtt/mqtt_topic_model.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import 'package:mqtt_client/mqtt_client.dart'; + +class MQTTTopicsPane extends ConsumerStatefulWidget { + const MQTTTopicsPane({super.key}); + + @override + ConsumerState createState() => MQTTTopicsPaneState(); +} + +class MQTTTopicsPaneState extends ConsumerState { + final random = Random.secure(); + List rows = [ + const MQTTTopicModel(name: '', qos: 0, subscribe: false, description: ''), + // Add more topics as needed + ]; + late int seed; + List descriptionControllers = []; + List nameControllers = []; + late List isRowEnabledList; + + @override + void initState() { + super.initState(); + seed = random.nextInt(kRandMax); + for (var row in rows) { + descriptionControllers.add(TextEditingController(text: row.description)); + } + for (var row in rows) { + nameControllers.add(TextEditingController(text: row.description)); + } + } + + void _onFieldChange(String selectedId) { + // ref.read(collectionStateNotifierProvider.notifier).update( + // selectedId, + // requestHeaders: rows, + // isHeaderEnabledList: isRowEnabledList, + // ); + ref.read(subscribedTopicsStateProvider.notifier).state = rows; + } + + @override + void dispose() { + for (var controller in descriptionControllers) { + controller.dispose(); + } + for (var controller in nameControllers) { + controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final selectedId = ref.watch(selectedIdStateProvider); + final realtimeConnectionState = ref.watch(realtimeConnectionStateProvider); + isRowEnabledList = + ref.read(selectedRequestModelProvider)?.isHeaderEnabledList ?? + List.filled(rows.length, true, growable: true); + List columns = const [ + DataColumn2( + size: ColumnSize.L, + label: Text( + 'Topics', + ), + ), + DataColumn2( + size: ColumnSize.S, + label: Text('QoS'), + ), + DataColumn2(label: Text('Subscribe'), size: ColumnSize.S), + DataColumn2( + label: Text('Description'), + ), + ]; + List dataRows = List.generate( + rows.length, + (index) => DataRow( + cells: [ + DataCell( + TextField( + decoration: const InputDecoration( + hintText: 'Add Topic Name', + ), + controller: nameControllers[index], // Use topic name controller + + onChanged: (value) { + setState(() { + rows[index] = rows[index].copyWith(name: value); + }); + }, + ), + ), + DataCell( + DropdownButtonQos( + qos: rows[index].qos, + onChanged: (value) { + setState(() { + rows[index] = rows[index].copyWith(qos: value!); + }); + _onFieldChange(selectedId!); + }, + ), + ), + DataCell( + Switch( + value: rows[index].subscribe, + onChanged: (value) { + MqttQos qos = rows[index].qos == 0 + ? MqttQos.atMostOnce + : rows[index].qos == 1 + ? MqttQos.atLeastOnce + : MqttQos.exactlyOnce; + String topicName = rows[index].name; + setState(() { + if (realtimeConnectionState == + RealtimeConnectionState.connected && + topicName.isNotEmpty) { + rows[index] = rows[index].copyWith(subscribe: value); + _onFieldChange(selectedId!); + if (value) { + ref + .read(collectionStateNotifierProvider.notifier) + .subscribeTopic(topicName, qos); + } else { + ref + .read(collectionStateNotifierProvider.notifier) + .unsubscribeTopic(topicName); + } + } + }); + }, + ), + ), + DataCell( + TextField( + decoration: const InputDecoration( + hintText: 'Add Description', + ), + controller: descriptionControllers[index], + onChanged: (value) { + setState(() { + rows[index] = rows[index].copyWith(description: value); + }); + }, + ), + ), + ], + ), + ); + // DaviModel model = DaviModel( + // rows: rows, + // columns: [ + // DaviColumn( + // name: 'Topics', + // width: 70, + // grow: 1, + // cellBuilder: (_, row) { + // int idx = row.index; + // return CellField( + // keyId: "$selectedId-$idx-description-$seed", + // initialValue: rows[idx].description, + // hintText: "Add Topic Name", + // onChanged: (value) { + // setState(() { + // rows[idx] = rows[idx].copyWith(name: value); + // // print(rows); + // }); + // }, + // colorScheme: Theme.of(context).colorScheme); + // }, + // sortable: false, + // ), + // DaviColumn( + // resizable: false, + // name: 'QoS', + // width: 50, + // cellBuilder: (_, row) { + // int idx = row.index; + // return DropdownButtonQos( + // qos: rows[idx].qos, + // onChanged: (value) { + // setState(() { + // rows[idx] = rows[idx].copyWith(qos: value!); + // }); + // _onFieldChange(selectedId!); + // }, + // ); + // }, + // ), + // DaviColumn( + // resizable: false, + // name: 'Subscribe', + // width: 100, + // cellBuilder: (_, row) { + // int idx = row.index; + // return Switch( + // value: rows[idx].subscribe, + // onChanged: (value) { + // MqttQos qos = rows[idx].qos == 0 + // ? MqttQos.atMostOnce + // : rows[idx].qos == 1 + // ? MqttQos.atLeastOnce + // : MqttQos.exactlyOnce; + // String topicName = rows[idx].name; + // setState(() { + // if (realtimeConnectionState == + // RealtimeConnectionState.connected && + // topicName.isNotEmpty) { + // rows[idx] = rows[idx].copyWith(subscribe: value); + // _onFieldChange(selectedId!); + // if (value) { + // ref + // .read(collectionStateNotifierProvider.notifier) + // .subscribeTopic(topicName, qos); + // } else { + // ref + // .read(collectionStateNotifierProvider.notifier) + // .unsubscribeTopic(topicName); + // } + // } + // }); + // }, + // ); + // // return CheckBox( + // // keyId: "$selectedId-$idx-subscribe-c-$seed", + // // value: isRowEnabledList[idx], + // // onChanged: (value) { + // // isRowEnabledList[idx] = value!; + // // _onFieldChange(selectedId!); + // // }, + // // colorScheme: Theme.of(context).colorScheme, + // // ); + // }), + // DaviColumn( + // name: 'Description', + // width: 10, + // grow: 1, + // cellBuilder: (_, row) { + // int idx = row.index; + // return CellField( + // keyId: "$selectedId-$idx-description-$seed", + // initialValue: rows[idx].description, + // hintText: "Add description", + // onChanged: (value) { + // setState(() { + // rows[idx] = rows[idx].copyWith(description: value); + // }); + // }, + // colorScheme: Theme.of(context).colorScheme); + // }, + // ) + // ], + // ); + return Stack( + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: kBorderRadius12, + ), + margin: kP10, + child: Column( + children: [ + Expanded( + child: Theme( + data: Theme.of(context) + .copyWith(scrollbarTheme: kDataTableScrollbarTheme), + child: DataTable2( + columnSpacing: 12, + dividerThickness: 0, + horizontalMargin: 0, + dataRowHeight: kDataTableRowHeight, + bottomMargin: kDataTableBottomPadding, + isVerticalScrollBarVisible: true, + columns: columns, + rows: dataRows, + ), + ), + ), + ], + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: ElevatedButton.icon( + onPressed: () { + setState(() { + rows.add(kMQTTTopicEmptyModel); + nameControllers.add(TextEditingController()); + descriptionControllers.add(TextEditingController()); + _onFieldChange(selectedId!); + }); + }, + icon: const Icon(Icons.add), + label: const Text( + "Add Topics", + style: kTextStyleButton, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart index 337f0785..a27809ba 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_pane.dart @@ -44,6 +44,7 @@ class EditRequestPane extends ConsumerWidget { headerLength > 0, hasBody, ], + tabLabels: const ['URL Params', 'Headers', 'Body'], children: const [ EditRequestURLParams(), EditRequestHeaders(), diff --git a/lib/screens/home_page/editor_pane/details_card/response_pane.dart b/lib/screens/home_page/editor_pane/details_card/response_pane.dart index 17a03ea7..2daca1df 100644 --- a/lib/screens/home_page/editor_pane/details_card/response_pane.dart +++ b/lib/screens/home_page/editor_pane/details_card/response_pane.dart @@ -1,3 +1,4 @@ +import 'package:apidash/providers/collection_providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index ce264454..3a2bac03 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -40,6 +40,8 @@ class RequestEditorTopBar extends ConsumerWidget { ), child: Row( children: [ + const DropdownButtonProtocol(), + kHSpacer20, Expanded( child: Text( name ?? "", diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 012d067c..25009555 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -4,11 +4,14 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -class EditorPaneRequestURLCard extends StatelessWidget { +class EditorPaneRequestURLCard extends ConsumerWidget { const EditorPaneRequestURLCard({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); + ref.read(realtimeConnectionStateProvider); return Card( elevation: 0, shape: RoundedRectangleBorder( @@ -17,28 +20,79 @@ class EditorPaneRequestURLCard extends StatelessWidget { ), borderRadius: kBorderRadius12, ), - child: const Padding( - padding: EdgeInsets.symmetric( + child: Padding( + padding: const EdgeInsets.symmetric( vertical: 5, horizontal: 20, ), child: Row( - children: [ - DropdownButtonHTTPMethod(), - kHSpacer20, - Expanded( - child: URLTextField(), - ), - kHSpacer20, - SizedBox( - height: 36, - child: SendButton(), - ), - ], + children: getWidgets(protocol!), ), ), ); } + + List getWidgets(ProtocolType protocol) { + final List httpURLWidgets = [ + const DropdownButtonHTTPMethod(), + kHSpacer20, + const Expanded( + child: URLTextField(), + ), + kHSpacer20, + const SizedBox( + height: 36, + child: SendButton(), + ), + ]; + + final List mqttv3URLWidgets = [ + const Expanded( + child: URLTextField(), + ), + kHSpacer20, + const VerticalDivider( + thickness: 1, + width: 1, + color: Colors.white, + ), + const Expanded(child: ClientIdField()), + const SizedBox( + height: 36, + child: ConnectButton(), + ), + ]; + switch (protocol) { + case ProtocolType.http: + return httpURLWidgets; + case ProtocolType.mqttv3: + return mqttv3URLWidgets; + default: + return httpURLWidgets; + } + } +} + +class DropdownButtonProtocol extends ConsumerWidget { + const DropdownButtonProtocol({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final protocol = ref + .watch(selectedRequestModelProvider.select((value) => value?.protocol)); + return DropdownButtonProtocolType( + protocolType: protocol, + onChanged: (ProtocolType? value) { + final selectedId = ref.read(selectedRequestModelProvider)!.id; + ref + .read(collectionStateNotifierProvider.notifier) + .update(selectedId, protocol: value); + print('protocol: $value'); + }, + ); + } } class DropdownButtonHTTPMethod extends ConsumerWidget { @@ -66,11 +120,12 @@ class URLTextField extends ConsumerWidget { const URLTextField({ super.key, }); - @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); return URLField( + enabled: ref.watch(realtimeConnectionStateProvider) == + RealtimeConnectionState.disconnected, selectedId: selectedId!, initialValue: ref .read(collectionStateNotifierProvider.notifier) @@ -111,3 +166,70 @@ class SendButton extends ConsumerWidget { ); } } + +class ConnectButton extends ConsumerWidget { + const ConnectButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedIdStateProvider); + final sentRequestId = ref.watch(sentRequestIdStateProvider); + final connectionState = ref.watch(realtimeConnectionStateProvider); + return ConnectRequestButton( + selectedId: selectedId, + sentRequestId: sentRequestId, + onConnect: () { + ref + .read(collectionStateNotifierProvider.notifier) + .connectToBroker(selectedId!); + }, + onDisconnect: () { + ref + .read(collectionStateNotifierProvider.notifier) + .disconnectFromBroker(selectedId!); + }, + connectionState: connectionState!, + // onTap: () { + // ref + // .read(collectionStateNotifierProvider.notifier) + // .sendRequest(selectedId!); + // }, + ); + } +} + +class ClientIdField extends ConsumerWidget { + const ClientIdField({ + super.key, + this.selectedId, + this.initialValue, + }); + + final String? selectedId; + final String? initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return TextFormField( + key: Key("url-$selectedId"), + enabled: ref.watch(realtimeConnectionStateProvider) == + RealtimeConnectionState.disconnected, + initialValue: initialValue, + style: kCodeStyle, + decoration: InputDecoration( + hintText: kHintTextClientIdCard, + hintStyle: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + ), + border: InputBorder.none, + ), + onChanged: (value) { + ref.read(clientIdStateProvider.notifier).state = value; + }, + ); + } +} diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart new file mode 100644 index 00000000..fd7c5e95 --- /dev/null +++ b/lib/services/mqtt_service.dart @@ -0,0 +1,33 @@ +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; + +Future connectToMqttServer({ + required String broker, + String? clientId, +}) async { + final client = MqttServerClient(broker, clientId ?? ''); + client.setProtocolV311(); + await client.connect(); + return client; +} + +Future publishMessage({ + required MqttServerClient client, + required String topic, + required String message, + required MqttQos qos, +}) async { + final builder = MqttClientPayloadBuilder(); + builder.addString(message); + + client.publishMessage(topic, qos, builder.payload!); +} + +Future subscribeToTopic( + MqttServerClient client, String topic, MqttQos qosLevel) async { + client.subscribe(topic, qosLevel); +} + +Future disconnectFromMqttServer(MqttServerClient client) async { + client.disconnect(); +} diff --git a/lib/services/services.dart b/lib/services/services.dart index a7cf03fd..938070ba 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -1,3 +1,4 @@ export 'http_service.dart'; export 'hive_services.dart'; export 'window_services.dart'; +export 'mqtt_service.dart'; diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index 41bcddb9..d1b24877 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -83,6 +83,105 @@ class SendRequestButton extends StatelessWidget { } } +class SendMessageButton extends StatelessWidget { + const SendMessageButton({ + super.key, + required this.selectedId, + required this.sentRequestId, + required this.onTap, + }); + + final String? selectedId; + final String? sentRequestId; + final void Function() onTap; + + @override + Widget build(BuildContext context) { + bool disable = sentRequestId == null; + return FilledButton( + onPressed: disable ? null : onTap, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + disable + ? (sentRequestId != null ? kLabelSending : kLabelBusy) + : kLabelSend, + style: kTextStyleButton, + ), + if (!disable) kHSpacer10, + if (!disable) + const Icon( + size: 16, + Icons.send, + ), + ], + ), + ); + } +} + +class ConnectRequestButton extends StatelessWidget { + const ConnectRequestButton({ + super.key, + required this.selectedId, + required this.sentRequestId, + required this.onConnect, + required this.onDisconnect, + required this.connectionState, + }); + + final String? selectedId; + final String? sentRequestId; + final void Function() onConnect; + final void Function() onDisconnect; + final RealtimeConnectionState connectionState; + + @override + Widget build(BuildContext context) { + bool disable = connectionState == RealtimeConnectionState.connecting || + connectionState == RealtimeConnectionState.disconnecting; + String label; + void Function()? onTap; + + switch (connectionState) { + case RealtimeConnectionState.disconnected: + label = kLabelConnect; + onTap = onConnect; + break; + case RealtimeConnectionState.connecting: + label = kLabelConnecting; + break; + case RealtimeConnectionState.connected: + label = kLabelDisconnect; + onTap = onDisconnect; + break; + case RealtimeConnectionState.disconnecting: + label = kLabelDisconnecting; + break; + } + + return FilledButton( + onPressed: disable ? null : onTap, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: kTextStyleButton, + ), + if (!disable) kHSpacer10, + if (!disable) + const Icon( + size: 16, + Icons.send, + ), + ], + ), + ); + } +} + class SaveInDownloadsButton extends StatelessWidget { const SaveInDownloadsButton({ super.key, diff --git a/lib/widgets/cards.dart b/lib/widgets/cards.dart index 943227ce..39a4cf9d 100644 --- a/lib/widgets/cards.dart +++ b/lib/widgets/cards.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart'; import 'menus.dart' show RequestCardMenu; -import 'texts.dart' show MethodBox; +import 'texts.dart' show MethodBox, ProtocolBox; class SidebarRequestCard extends StatelessWidget { const SidebarRequestCard({ super.key, required this.id, + required this.protocol, required this.method, this.name, this.url, @@ -24,6 +25,7 @@ class SidebarRequestCard extends StatelessWidget { }); final String id; + final ProtocolType protocol; final String? name; final String? url; final HTTPVerb method; @@ -82,7 +84,9 @@ class SidebarRequestCard extends StatelessWidget { height: 20, child: Row( children: [ - MethodBox(method: method), + protocol == ProtocolType.http + ? MethodBox(method: method) + : ProtocolBox(protocol: protocol), kHSpacer4, Expanded( child: inEditMode diff --git a/lib/widgets/dropdowns.dart b/lib/widgets/dropdowns.dart index 1be902e9..9af991d1 100644 --- a/lib/widgets/dropdowns.dart +++ b/lib/widgets/dropdowns.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; +import 'package:mqtt_client/mqtt_client.dart'; class DropdownButtonHttpMethod extends StatelessWidget { const DropdownButtonHttpMethod({ @@ -47,6 +48,51 @@ class DropdownButtonHttpMethod extends StatelessWidget { } } +class DropdownButtonProtocolType extends StatelessWidget { + const DropdownButtonProtocolType({ + super.key, + this.protocolType, + this.onChanged, + }); + + final ProtocolType? protocolType; + final void Function(ProtocolType? value)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + focusColor: surfaceColor, + value: protocolType, + icon: const Icon(Icons.unfold_more_rounded), + elevation: 4, + underline: Container( + height: 0, + ), + borderRadius: kBorderRadius12, + onChanged: onChanged, + items: ProtocolType.values + .map>((ProtocolType value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + value.name.toUpperCase(), + style: kCodeStyle.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).brightness == Brightness.dark + ? getDarkModeColor(Colors.white) + : Colors.black, + ), + ), + ), + ); + }).toList(), + ); + } +} + class DropdownButtonContentType extends StatelessWidget { const DropdownButtonContentType({ super.key, @@ -191,3 +237,49 @@ class DropdownButtonCodegenLanguage extends StatelessWidget { ); } } + +class DropdownButtonQos extends StatelessWidget { + const DropdownButtonQos({ + super.key, + this.qos, + this.onChanged, + }); + + final int? qos; + final void Function(int?)? onChanged; + + @override + Widget build(BuildContext context) { + final surfaceColor = Theme.of(context).colorScheme.surface; + return DropdownButton( + // dropdownColor: Theme.of(context).colorScheme.surfaceVariant, + focusColor: surfaceColor, + value: qos, + icon: const Icon( + Icons.unfold_more_rounded, + size: 16, + ), + elevation: 4, + style: kCodeStyle, + underline: Container( + height: 0, + ), + onChanged: onChanged, + borderRadius: kBorderRadius12, + items: [0, 1, 2].map>((int value) { + return DropdownMenuItem( + value: value, + child: Padding( + padding: kPs8, + child: Text( + value.toString(), + style: kCodeStyle.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/widgets/mqtt/mqtt_request_widgets.dart b/lib/widgets/mqtt/mqtt_request_widgets.dart new file mode 100644 index 00000000..16a221b3 --- /dev/null +++ b/lib/widgets/mqtt/mqtt_request_widgets.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import '../tabs.dart'; + +class MQTTRequestPane extends StatefulWidget { + const MQTTRequestPane({ + super.key, + required this.selectedId, + this.tabIndex, + this.onTapTabBar, + required this.children, + this.showIndicators = const [false, false, false], + required this.tabLabels, + }); + + final List tabLabels; + final String? selectedId; + final int? tabIndex; + final void Function(int)? onTapTabBar; + final List children; + final List showIndicators; + + @override + State createState() => _MQTTRequestPaneState(); +} + +class _MQTTRequestPaneState extends State + with TickerProviderStateMixin { + late final TabController _controller; + + @override + void initState() { + super.initState(); + _controller = TabController( + length: 3, + animationDuration: kTabAnimationDuration, + vsync: this, + ); + } + + @override + Widget build(BuildContext context) { + if (widget.tabIndex != null) { + _controller.index = widget.tabIndex!; + } + return Column( + children: [ + Padding( + padding: kPh20v10, + child: SizedBox( + height: kHeaderHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Request", + style: Theme.of(context).textTheme.titleMedium, + ), + // FilledButton.tonalIcon( + // onPressed: widget.onPressedCodeButton, + // icon: Icon( + // widget.codePaneVisible + // ? Icons.code_off_rounded + // : Icons.code_rounded, + // ), + // label: SizedBox( + // width: 75, + // child: Text( + // widget.codePaneVisible ? "Hide Code" : "View Code"), + // ), + // ), + ], + ), + ), + ), + TabBar( + key: Key(widget.selectedId!), + controller: _controller, + overlayColor: kColorTransparentState, + onTap: widget.onTapTabBar, + tabs: [ + TabLabel( + text: widget.tabLabels[0], + showIndicator: widget.showIndicators[0], + ), + TabLabel( + text: widget.tabLabels[1], + showIndicator: widget.showIndicators[1], + ), + TabLabel( + text: widget.tabLabels[2], + showIndicator: widget.showIndicators[2], + ), + ], + ), + Expanded( + child: TabBarView( + controller: _controller, + physics: const NeverScrollableScrollPhysics(), + children: widget.children, + ), + ), + ], + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index 747cc8f4..90b425a9 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -12,8 +12,10 @@ class RequestPane extends StatefulWidget { this.onTapTabBar, required this.children, this.showIndicators = const [false, false, false], + required this.tabLabels, }); + final List tabLabels; final String? selectedId; final bool codePaneVisible; final int? tabIndex; @@ -80,15 +82,15 @@ class _RequestPaneState extends State onTap: widget.onTapTabBar, tabs: [ TabLabel( - text: kLabelURLParams, + text: widget.tabLabels[0], showIndicator: widget.showIndicators[0], ), TabLabel( - text: kLabelHeaders, + text: widget.tabLabels[1], showIndicator: widget.showIndicators[1], ), TabLabel( - text: kLabelBody, + text: widget.tabLabels[2], showIndicator: widget.showIndicators[2], ), ], diff --git a/lib/widgets/textfields.dart b/lib/widgets/textfields.dart index 671be6ba..e69d4beb 100644 --- a/lib/widgets/textfields.dart +++ b/lib/widgets/textfields.dart @@ -5,12 +5,14 @@ class URLField extends StatelessWidget { const URLField({ super.key, required this.selectedId, + required this.enabled, this.initialValue, this.onChanged, this.onFieldSubmitted, }); final String selectedId; + final bool enabled; final String? initialValue; final void Function(String)? onChanged; final void Function(String)? onFieldSubmitted; @@ -18,6 +20,7 @@ class URLField extends StatelessWidget { @override Widget build(BuildContext context) { return TextFormField( + enabled: enabled, key: Key("url-$selectedId"), initialValue: initialValue, style: kCodeStyle, diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index f2cf5c92..55578f11 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -32,3 +32,27 @@ class MethodBox extends StatelessWidget { ); } } + +class ProtocolBox extends StatelessWidget { + const ProtocolBox({super.key, required this.protocol}); + final ProtocolType protocol; + + @override + Widget build(BuildContext context) { + String text = protocol.name.toUpperCase(); + return SizedBox( + width: 24, + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 8, + fontWeight: FontWeight.bold, + color: Theme.of(context).brightness == Brightness.dark + ? getDarkModeColor(Colors.white) + : Colors.black, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index d6235dec..4bc3f573 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" archive: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -45,18 +45,26 @@ packages: dependency: transitive description: name: audio_session - sha256: "6fdf255ed3af86535c96452c33ecff1245990bb25a605bfb1958661ccc3d467f" + sha256: a49af9981eec5d7cd73b37bacb6ee73f8143a6a9f9bd5b6021e6c346b9b6cf4e url: "https://pub.dev" source: hosted version: "0.1.18" + axis_layout: + dependency: transitive + description: + name: axis_layout + sha256: "9ba44f279f39121065d811e72da892de86f5613d68eb0b295f60d021ea8f2a59" + url: "https://pub.dev" + source: hosted + version: "1.0.1" barcode: dependency: transitive description: name: barcode - sha256: "2a8b2ee065f419c2aeda141436cc556d91ae772d220fd80679f4d431d6c2ab43" + sha256: "91b143666f7bb13636f716b6d4e412e372ab15ff7969799af8c9e30a382e9385" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.6" bidi: dependency: transitive description: @@ -109,18 +117,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -133,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.2" characters: dependency: transitive description: @@ -145,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -225,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" csv: dependency: "direct main" description: @@ -244,11 +268,19 @@ packages: data_table_2: dependency: "direct main" description: - name: data_table_2 - sha256: fdb0551f103f1daf837bddfde14619fd9e683408833a618c9afabeb533fce88c + name: davi + sha256: "4105870281c4c33e8e017e21e212b96fd2637b4c1a35b2a56f14aaa4acdf6f0d" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + event_bus: + dependency: transitive + description: + name: event_bus + sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" url: "https://pub.dev" source: hosted - version: "2.5.11" + version: "2.0.0" eventify: dependency: transitive description: @@ -261,10 +293,10 @@ packages: dependency: transitive description: name: extended_text_field - sha256: ed9655c70a47a54c7cc689cf7f89a2bde9ab7b530150b4d1808b7aa7eb8cdf90 + sha256: d064097c774eab3a7f2aad1db32533611a71ed52305b3afd3e0364642a1a61f7 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "13.1.0" extended_text_library: dependency: transitive description: @@ -285,10 +317,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -434,18 +466,26 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - sha256: cb44f7831b23a6bdd0f501718b0d2e8045cbc625a15f668af37ddb80314821db + sha256: "31c12de79262b5431c5492e9c89948aa789158435f707d3519a7fdef6af28af7" url: "https://pub.dev" source: hosted version: "0.6.21" + 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: @@ -568,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.4" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" html_unescape: dependency: transitive description: @@ -604,10 +652,10 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.7" io: dependency: transitive description: @@ -669,10 +717,10 @@ packages: dependency: "direct main" description: name: just_audio - sha256: b607cd1a43bac03d85c3aaee00448ff4a589ef2a77104e3d409889ff079bf823 + sha256: b7cb6bbf3750caa924d03f432ba401ec300fd90936b3f73a9b33d58b1e96286b url: "https://pub.dev" source: hosted - version: "0.9.36" + version: "0.9.37" just_audio_mpv: dependency: "direct main" description: @@ -789,10 +837,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mime_dart: dependency: "direct main" description: @@ -809,6 +857,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.1" + mqtt_client: + dependency: "direct main" + description: + name: mqtt_client + sha256: aa2bed04e239b9199ce95ae2553cb16a2c39110a5f56b21ce8c09be3261ea3d2 + url: "https://pub.dev" + source: hosted + version: "10.2.0" multi_split_view: dependency: "direct main" description: @@ -885,18 +941,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -909,10 +965,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -925,10 +981,10 @@ packages: dependency: transitive description: name: pdf - sha256: "93cbb2c06de9bab91844550f19896b2373e7a5ce25173995e7e5ec5e1741429d" + sha256: "243f05342fc0bdf140eba5b069398985cdbdd3dbb1d776cf43d5ea29cc570ba6" url: "https://pub.dev" source: hosted - version: "3.10.7" + version: "3.10.8" pdf_widget_wrapper: dependency: transitive description: @@ -949,18 +1005,18 @@ packages: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointer_interceptor: dependency: transitive description: @@ -989,18 +1045,18 @@ packages: dependency: transitive description: name: pointer_interceptor_web - sha256: "9386e064097fd16419e935c23f08f35b58e6aaec155dd39bd6a003b88f9c14b4" + sha256: a6237528b46c411d8d55cdfad8fcb3269fc4cbb26060b14bff94879165887d1e url: "https://pub.dev" source: hosted - version: "0.10.1+2" + version: "0.10.2" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -1262,6 +1318,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" url_launcher: dependency: "direct main" description: @@ -1274,18 +1346,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3 + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.5" url_launcher_linux: dependency: transitive description: @@ -1306,18 +1378,18 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" url_launcher_windows: dependency: transitive description: @@ -1366,46 +1438,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - video_player: - dependency: "direct main" - description: - name: video_player - sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23 - url: "https://pub.dev" - source: hosted - version: "2.8.3" - video_player_android: - dependency: transitive - description: - name: video_player_android - sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" - url: "https://pub.dev" - source: hosted - version: "2.4.12" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - video_player_platform_interface: - dependency: "direct main" - description: - name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" - url: "https://pub.dev" - source: hosted - version: "6.2.2" - video_player_web: - dependency: transitive - description: - name: video_player_web - sha256: "41245cef5ef29c4585dbabcbcbe9b209e34376642c7576cabf11b4ad9289d6e4" - url: "https://pub.dev" - source: hosted - version: "2.3.0" vm_service: dependency: transitive description: @@ -1434,10 +1466,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" webkit_inspection_protocol: dependency: transitive description: @@ -1450,10 +1482,10 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.4.0" window_manager: dependency: "direct main" description: @@ -1467,7 +1499,7 @@ packages: description: path: "plugins/window_size" ref: HEAD - resolved-ref: "6c66ad23ee79749f30a8eece542cf54eaf157ed8" + resolved-ref: eb3964990cf19629c89ff8cb4a37640c7b3d5601 url: "https://github.com/google/flutter-desktop-embedding.git" source: git version: "0.1.0" @@ -1475,10 +1507,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 528cda82..9b2b69e0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: dart_style: ^2.3.6 json_text_field: ^1.1.0 csv: ^6.0.0 + mqtt_client: ^10.2.0 data_table_2: ^2.5.11 file_selector: ^1.0.3