diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index da184793a..12d91d502 100644 --- a/lib/models/history_meta_model.g.dart +++ b/lib/models/history_meta_model.g.dart @@ -35,6 +35,7 @@ Map _$$HistoryMetaModelImplToJson( const _$APITypeEnumMap = { APIType.rest: 'rest', APIType.graphql: 'graphql', + APIType.mqtt: 'mqtt', }; const _$HTTPVerbEnumMap = { diff --git a/lib/models/mqtt_request_model.dart b/lib/models/mqtt_request_model.dart new file mode 100644 index 000000000..1a2143a4d --- /dev/null +++ b/lib/models/mqtt_request_model.dart @@ -0,0 +1,42 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'mqtt_request_model.freezed.dart'; +part 'mqtt_request_model.g.dart'; + +@freezed +class MQTTRequestModel with _$MQTTRequestModel { + const factory MQTTRequestModel({ + @Default("") String brokerUrl, + @Default(1883) int port, + @Default("") String clientId, + @Default("") String username, + @Default("") String password, + @Default(60) int keepAlive, + @Default(false) bool cleanSession, + @Default(3) int connectTimeout, + @Default([]) List topics, + @Default("") String publishTopic, + @Default("") String publishPayload, + @Default(0) int publishQos, + @Default(false) bool publishRetain, + }) = _MQTTRequestModel; + + factory MQTTRequestModel.fromJson(Map json) => + _$MQTTRequestModelFromJson(json); +} + +@freezed +class MQTTTopicModel with _$MQTTTopicModel { + const factory MQTTTopicModel({ + required String topic, + @Default(0) int qos, + @Default(false) bool subscribe, + @Default("") String description, + }) = _MQTTTopicModel; + + factory MQTTTopicModel.fromJson(Map json) => + _$MQTTTopicModelFromJson(json); +} + +const kMQTTRequestEmptyModel = MQTTRequestModel(); +const kMQTTTopicEmptyModel = MQTTTopicModel(topic: ""); \ No newline at end of file diff --git a/lib/models/mqtt_request_model.freezed.dart b/lib/models/mqtt_request_model.freezed.dart new file mode 100644 index 000000000..291daf7ca --- /dev/null +++ b/lib/models/mqtt_request_model.freezed.dart @@ -0,0 +1,666 @@ +// 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_request_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#adding-getters-and-methods-to-our-models'); + +MQTTRequestModel _$MQTTRequestModelFromJson(Map json) { + return _MQTTRequestModel.fromJson(json); +} + +/// @nodoc +mixin _$MQTTRequestModel { + String get brokerUrl => throw _privateConstructorUsedError; + int get port => throw _privateConstructorUsedError; + String get clientId => throw _privateConstructorUsedError; + String get username => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + int get keepAlive => throw _privateConstructorUsedError; + bool get cleanSession => throw _privateConstructorUsedError; + int get connectTimeout => throw _privateConstructorUsedError; + List get topics => throw _privateConstructorUsedError; + String get publishTopic => throw _privateConstructorUsedError; + String get publishPayload => throw _privateConstructorUsedError; + int get publishQos => throw _privateConstructorUsedError; + bool get publishRetain => throw _privateConstructorUsedError; + + /// Serializes this MQTTRequestModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of MQTTRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MQTTRequestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MQTTRequestModelCopyWith<$Res> { + factory $MQTTRequestModelCopyWith( + MQTTRequestModel value, $Res Function(MQTTRequestModel) then) = + _$MQTTRequestModelCopyWithImpl<$Res, MQTTRequestModel>; + @useResult + $Res call( + {String brokerUrl, + int port, + String clientId, + String username, + String password, + int keepAlive, + bool cleanSession, + int connectTimeout, + List topics, + String publishTopic, + String publishPayload, + int publishQos, + bool publishRetain}); +} + +/// @nodoc +class _$MQTTRequestModelCopyWithImpl<$Res, $Val extends MQTTRequestModel> + implements $MQTTRequestModelCopyWith<$Res> { + _$MQTTRequestModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MQTTRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? brokerUrl = null, + Object? port = null, + Object? clientId = null, + Object? username = null, + Object? password = null, + Object? keepAlive = null, + Object? cleanSession = null, + Object? connectTimeout = null, + Object? topics = null, + Object? publishTopic = null, + Object? publishPayload = null, + Object? publishQos = null, + Object? publishRetain = null, + }) { + return _then(_value.copyWith( + brokerUrl: null == brokerUrl + ? _value.brokerUrl + : brokerUrl // ignore: cast_nullable_to_non_nullable + as String, + port: null == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as int, + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + keepAlive: null == keepAlive + ? _value.keepAlive + : keepAlive // ignore: cast_nullable_to_non_nullable + as int, + cleanSession: null == cleanSession + ? _value.cleanSession + : cleanSession // ignore: cast_nullable_to_non_nullable + as bool, + connectTimeout: null == connectTimeout + ? _value.connectTimeout + : connectTimeout // ignore: cast_nullable_to_non_nullable + as int, + topics: null == topics + ? _value.topics + : topics // ignore: cast_nullable_to_non_nullable + as List, + publishTopic: null == publishTopic + ? _value.publishTopic + : publishTopic // ignore: cast_nullable_to_non_nullable + as String, + publishPayload: null == publishPayload + ? _value.publishPayload + : publishPayload // ignore: cast_nullable_to_non_nullable + as String, + publishQos: null == publishQos + ? _value.publishQos + : publishQos // ignore: cast_nullable_to_non_nullable + as int, + publishRetain: null == publishRetain + ? _value.publishRetain + : publishRetain // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$MQTTRequestModelImplCopyWith<$Res> + implements $MQTTRequestModelCopyWith<$Res> { + factory _$$MQTTRequestModelImplCopyWith(_$MQTTRequestModelImpl value, + $Res Function(_$MQTTRequestModelImpl) then) = + __$$MQTTRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String brokerUrl, + int port, + String clientId, + String username, + String password, + int keepAlive, + bool cleanSession, + int connectTimeout, + List topics, + String publishTopic, + String publishPayload, + int publishQos, + bool publishRetain}); +} + +/// @nodoc +class __$$MQTTRequestModelImplCopyWithImpl<$Res> + extends _$MQTTRequestModelCopyWithImpl<$Res, _$MQTTRequestModelImpl> + implements _$$MQTTRequestModelImplCopyWith<$Res> { + __$$MQTTRequestModelImplCopyWithImpl(_$MQTTRequestModelImpl _value, + $Res Function(_$MQTTRequestModelImpl) _then) + : super(_value, _then); + + /// Create a copy of MQTTRequestModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? brokerUrl = null, + Object? port = null, + Object? clientId = null, + Object? username = null, + Object? password = null, + Object? keepAlive = null, + Object? cleanSession = null, + Object? connectTimeout = null, + Object? topics = null, + Object? publishTopic = null, + Object? publishPayload = null, + Object? publishQos = null, + Object? publishRetain = null, + }) { + return _then(_$MQTTRequestModelImpl( + brokerUrl: null == brokerUrl + ? _value.brokerUrl + : brokerUrl // ignore: cast_nullable_to_non_nullable + as String, + port: null == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as int, + clientId: null == clientId + ? _value.clientId + : clientId // ignore: cast_nullable_to_non_nullable + as String, + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + keepAlive: null == keepAlive + ? _value.keepAlive + : keepAlive // ignore: cast_nullable_to_non_nullable + as int, + cleanSession: null == cleanSession + ? _value.cleanSession + : cleanSession // ignore: cast_nullable_to_non_nullable + as bool, + connectTimeout: null == connectTimeout + ? _value.connectTimeout + : connectTimeout // ignore: cast_nullable_to_non_nullable + as int, + topics: null == topics + ? _value._topics + : topics // ignore: cast_nullable_to_non_nullable + as List, + publishTopic: null == publishTopic + ? _value.publishTopic + : publishTopic // ignore: cast_nullable_to_non_nullable + as String, + publishPayload: null == publishPayload + ? _value.publishPayload + : publishPayload // ignore: cast_nullable_to_non_nullable + as String, + publishQos: null == publishQos + ? _value.publishQos + : publishQos // ignore: cast_nullable_to_non_nullable + as int, + publishRetain: null == publishRetain + ? _value.publishRetain + : publishRetain // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$MQTTRequestModelImpl implements _MQTTRequestModel { + const _$MQTTRequestModelImpl( + {this.brokerUrl = "", + this.port = 1883, + this.clientId = "", + this.username = "", + this.password = "", + this.keepAlive = 60, + this.cleanSession = false, + this.connectTimeout = 3, + final List topics = const [], + this.publishTopic = "", + this.publishPayload = "", + this.publishQos = 0, + this.publishRetain = false}) + : _topics = topics; + + factory _$MQTTRequestModelImpl.fromJson(Map json) => + _$$MQTTRequestModelImplFromJson(json); + + @override + @JsonKey() + final String brokerUrl; + @override + @JsonKey() + final int port; + @override + @JsonKey() + final String clientId; + @override + @JsonKey() + final String username; + @override + @JsonKey() + final String password; + @override + @JsonKey() + final int keepAlive; + @override + @JsonKey() + final bool cleanSession; + @override + @JsonKey() + final int connectTimeout; + final List _topics; + @override + @JsonKey() + List get topics { + if (_topics is EqualUnmodifiableListView) return _topics; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_topics); + } + + @override + @JsonKey() + final String publishTopic; + @override + @JsonKey() + final String publishPayload; + @override + @JsonKey() + final int publishQos; + @override + @JsonKey() + final bool publishRetain; + + @override + String toString() { + return 'MQTTRequestModel(brokerUrl: $brokerUrl, port: $port, clientId: $clientId, username: $username, password: $password, keepAlive: $keepAlive, cleanSession: $cleanSession, connectTimeout: $connectTimeout, topics: $topics, publishTopic: $publishTopic, publishPayload: $publishPayload, publishQos: $publishQos, publishRetain: $publishRetain)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MQTTRequestModelImpl && + (identical(other.brokerUrl, brokerUrl) || + other.brokerUrl == brokerUrl) && + (identical(other.port, port) || other.port == port) && + (identical(other.clientId, clientId) || + other.clientId == clientId) && + (identical(other.username, username) || + other.username == username) && + (identical(other.password, password) || + other.password == password) && + (identical(other.keepAlive, keepAlive) || + other.keepAlive == keepAlive) && + (identical(other.cleanSession, cleanSession) || + other.cleanSession == cleanSession) && + (identical(other.connectTimeout, connectTimeout) || + other.connectTimeout == connectTimeout) && + const DeepCollectionEquality().equals(other._topics, _topics) && + (identical(other.publishTopic, publishTopic) || + other.publishTopic == publishTopic) && + (identical(other.publishPayload, publishPayload) || + other.publishPayload == publishPayload) && + (identical(other.publishQos, publishQos) || + other.publishQos == publishQos) && + (identical(other.publishRetain, publishRetain) || + other.publishRetain == publishRetain)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + brokerUrl, + port, + clientId, + username, + password, + keepAlive, + cleanSession, + connectTimeout, + const DeepCollectionEquality().hash(_topics), + publishTopic, + publishPayload, + publishQos, + publishRetain); + + /// Create a copy of MQTTRequestModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MQTTRequestModelImplCopyWith<_$MQTTRequestModelImpl> get copyWith => + __$$MQTTRequestModelImplCopyWithImpl<_$MQTTRequestModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$MQTTRequestModelImplToJson( + this, + ); + } +} + +abstract class _MQTTRequestModel implements MQTTRequestModel { + const factory _MQTTRequestModel( + {final String brokerUrl, + final int port, + final String clientId, + final String username, + final String password, + final int keepAlive, + final bool cleanSession, + final int connectTimeout, + final List topics, + final String publishTopic, + final String publishPayload, + final int publishQos, + final bool publishRetain}) = _$MQTTRequestModelImpl; + + factory _MQTTRequestModel.fromJson(Map json) = + _$MQTTRequestModelImpl.fromJson; + + @override + String get brokerUrl; + @override + int get port; + @override + String get clientId; + @override + String get username; + @override + String get password; + @override + int get keepAlive; + @override + bool get cleanSession; + @override + int get connectTimeout; + @override + List get topics; + @override + String get publishTopic; + @override + String get publishPayload; + @override + int get publishQos; + @override + bool get publishRetain; + + /// Create a copy of MQTTRequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MQTTRequestModelImplCopyWith<_$MQTTRequestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +MQTTTopicModel _$MQTTTopicModelFromJson(Map json) { + return _MQTTTopicModel.fromJson(json); +} + +/// @nodoc +mixin _$MQTTTopicModel { + String get topic => throw _privateConstructorUsedError; + int get qos => throw _privateConstructorUsedError; + bool get subscribe => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + + /// Serializes this MQTTTopicModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of MQTTTopicModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $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 topic, 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; + + /// Create a copy of MQTTTopicModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? topic = null, + Object? qos = null, + Object? subscribe = null, + Object? description = null, + }) { + return _then(_value.copyWith( + topic: null == topic + ? _value.topic + : topic // 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 topic, 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); + + /// Create a copy of MQTTTopicModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? topic = null, + Object? qos = null, + Object? subscribe = null, + Object? description = null, + }) { + return _then(_$MQTTTopicModelImpl( + topic: null == topic + ? _value.topic + : topic // 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 implements _MQTTTopicModel { + const _$MQTTTopicModelImpl( + {required this.topic, + this.qos = 0, + this.subscribe = false, + this.description = ""}); + + factory _$MQTTTopicModelImpl.fromJson(Map json) => + _$$MQTTTopicModelImplFromJson(json); + + @override + final String topic; + @override + @JsonKey() + final int qos; + @override + @JsonKey() + final bool subscribe; + @override + @JsonKey() + final String description; + + @override + String toString() { + return 'MQTTTopicModel(topic: $topic, qos: $qos, subscribe: $subscribe, description: $description)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MQTTTopicModelImpl && + (identical(other.topic, topic) || other.topic == topic) && + (identical(other.qos, qos) || other.qos == qos) && + (identical(other.subscribe, subscribe) || + other.subscribe == subscribe) && + (identical(other.description, description) || + other.description == description)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, topic, qos, subscribe, description); + + /// Create a copy of MQTTTopicModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @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 topic, + final int qos, + final bool subscribe, + final String description}) = _$MQTTTopicModelImpl; + + factory _MQTTTopicModel.fromJson(Map json) = + _$MQTTTopicModelImpl.fromJson; + + @override + String get topic; + @override + int get qos; + @override + bool get subscribe; + @override + String get description; + + /// Create a copy of MQTTTopicModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MQTTTopicModelImplCopyWith<_$MQTTTopicModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/mqtt_request_model.g.dart b/lib/models/mqtt_request_model.g.dart new file mode 100644 index 000000000..1ae975378 --- /dev/null +++ b/lib/models/mqtt_request_model.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mqtt_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$MQTTRequestModelImpl _$$MQTTRequestModelImplFromJson( + Map json) => + _$MQTTRequestModelImpl( + brokerUrl: json['brokerUrl'] as String? ?? "", + port: (json['port'] as num?)?.toInt() ?? 1883, + clientId: json['clientId'] as String? ?? "", + username: json['username'] as String? ?? "", + password: json['password'] as String? ?? "", + keepAlive: (json['keepAlive'] as num?)?.toInt() ?? 60, + cleanSession: json['cleanSession'] as bool? ?? false, + connectTimeout: (json['connectTimeout'] as num?)?.toInt() ?? 3, + topics: (json['topics'] as List?) + ?.map((e) => MQTTTopicModel.fromJson(e as Map)) + .toList() ?? + const [], + publishTopic: json['publishTopic'] as String? ?? "", + publishPayload: json['publishPayload'] as String? ?? "", + publishQos: (json['publishQos'] as num?)?.toInt() ?? 0, + publishRetain: json['publishRetain'] as bool? ?? false, + ); + +Map _$$MQTTRequestModelImplToJson( + _$MQTTRequestModelImpl instance) => + { + 'brokerUrl': instance.brokerUrl, + 'port': instance.port, + 'clientId': instance.clientId, + 'username': instance.username, + 'password': instance.password, + 'keepAlive': instance.keepAlive, + 'cleanSession': instance.cleanSession, + 'connectTimeout': instance.connectTimeout, + 'topics': instance.topics, + 'publishTopic': instance.publishTopic, + 'publishPayload': instance.publishPayload, + 'publishQos': instance.publishQos, + 'publishRetain': instance.publishRetain, + }; + +_$MQTTTopicModelImpl _$$MQTTTopicModelImplFromJson(Map json) => + _$MQTTTopicModelImpl( + topic: json['topic'] as String, + qos: (json['qos'] as num?)?.toInt() ?? 0, + subscribe: json['subscribe'] as bool? ?? false, + description: json['description'] as String? ?? "", + ); + +Map _$$MQTTTopicModelImplToJson( + _$MQTTTopicModelImpl instance) => + { + 'topic': instance.topic, + 'qos': instance.qos, + 'subscribe': instance.subscribe, + 'description': instance.description, + }; diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index 4d63c2adc..130b58831 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -1,4 +1,6 @@ import 'package:apidash_core/apidash_core.dart'; +import 'mqtt_request_model.dart'; +import '../services/mqtt_service.dart' show MQTTConnectionState; part 'request_model.freezed.dart'; @@ -24,6 +26,8 @@ class RequestModel with _$RequestModel { @JsonKey(includeToJson: false) DateTime? sendingTime, String? preRequestScript, String? postRequestScript, + @JsonKey(includeFromJson: false, includeToJson: false) MQTTConnectionState? mqttConnectionState, + @JsonKey(includeFromJson: false, includeToJson: false) MQTTRequestModel? mqttRequestModel, }) = _RequestModel; factory RequestModel.fromJson(Map json) => diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index 72f607bd5..f2d53c65d 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -36,7 +36,13 @@ mixin _$RequestModel { @JsonKey(includeToJson: false) DateTime? get sendingTime => throw _privateConstructorUsedError; String? get preRequestScript => throw _privateConstructorUsedError; - String? get postRequestScript => throw _privateConstructorUsedError; + String? get postRequestScript => + throw _privateConstructorUsedError; + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTConnectionState? get mqttConnectionState => + throw _privateConstructorUsedError; + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTRequestModel? get mqttRequestModel => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -67,10 +73,15 @@ abstract class $RequestModelCopyWith<$Res> { @JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTConnectionState? mqttConnectionState, + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTRequestModel? mqttRequestModel}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + $MQTTRequestModelCopyWith<$Res>? get mqttRequestModel; } /// @nodoc @@ -101,6 +112,8 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? sendingTime = freezed, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? mqttConnectionState = freezed, + Object? mqttRequestModel = freezed, }) { return _then(_value.copyWith( id: null == id @@ -155,6 +168,14 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + mqttConnectionState: freezed == mqttConnectionState + ? _value.mqttConnectionState + : mqttConnectionState // ignore: cast_nullable_to_non_nullable + as MQTTConnectionState?, + mqttRequestModel: freezed == mqttRequestModel + ? _value.mqttRequestModel + : mqttRequestModel // ignore: cast_nullable_to_non_nullable + as MQTTRequestModel?, ) as $Val); } @@ -185,6 +206,20 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> return _then(_value.copyWith(httpResponseModel: value) as $Val); }); } + + /// Create a copy of RequestModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MQTTRequestModelCopyWith<$Res>? get mqttRequestModel { + if (_value.mqttRequestModel == null) { + return null; + } + + return $MQTTRequestModelCopyWith<$Res>(_value.mqttRequestModel!, (value) { + return _then(_value.copyWith(mqttRequestModel: value) as $Val); + }); + } } /// @nodoc @@ -208,12 +243,18 @@ abstract class _$$RequestModelImplCopyWith<$Res> @JsonKey(includeToJson: false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, String? preRequestScript, - String? postRequestScript}); + String? postRequestScript, + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTConnectionState? mqttConnectionState, + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTRequestModel? mqttRequestModel}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @override $HttpResponseModelCopyWith<$Res>? get httpResponseModel; + @override + $MQTTRequestModelCopyWith<$Res>? get mqttRequestModel; } /// @nodoc @@ -242,6 +283,8 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? sendingTime = freezed, Object? preRequestScript = freezed, Object? postRequestScript = freezed, + Object? mqttConnectionState = freezed, + Object? mqttRequestModel = freezed, }) { return _then(_$RequestModelImpl( id: null == id @@ -295,6 +338,14 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.postRequestScript : postRequestScript // ignore: cast_nullable_to_non_nullable as String?, + mqttConnectionState: freezed == mqttConnectionState + ? _value.mqttConnectionState + : mqttConnectionState // ignore: cast_nullable_to_non_nullable + as MQTTConnectionState?, + mqttRequestModel: freezed == mqttRequestModel + ? _value.mqttRequestModel + : mqttRequestModel // ignore: cast_nullable_to_non_nullable + as MQTTRequestModel?, )); } } @@ -316,7 +367,11 @@ class _$RequestModelImpl implements _RequestModel { @JsonKey(includeToJson: false) this.isWorking = false, @JsonKey(includeToJson: false) this.sendingTime, this.preRequestScript, - this.postRequestScript}); + this.postRequestScript, + @JsonKey(includeFromJson: false, includeToJson: false) + this.mqttConnectionState, + @JsonKey(includeFromJson: false, includeToJson: false) + this.mqttRequestModel}); factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -353,10 +408,17 @@ class _$RequestModelImpl implements _RequestModel { final String? preRequestScript; @override final String? postRequestScript; +// MQTT support (move to end) + @override + @JsonKey(includeFromJson: false, includeToJson: false) + final MQTTConnectionState? mqttConnectionState; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + final MQTTRequestModel? mqttRequestModel; @override String toString() { - return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)'; + return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript, mqttConnectionState: $mqttConnectionState, mqttRequestModel: $mqttRequestModel)'; } @override @@ -385,7 +447,11 @@ class _$RequestModelImpl implements _RequestModel { (identical(other.preRequestScript, preRequestScript) || other.preRequestScript == preRequestScript) && (identical(other.postRequestScript, postRequestScript) || - other.postRequestScript == postRequestScript)); + other.postRequestScript == postRequestScript) && + (identical(other.mqttConnectionState, mqttConnectionState) || + other.mqttConnectionState == mqttConnectionState) && + (identical(other.mqttRequestModel, mqttRequestModel) || + other.mqttRequestModel == mqttRequestModel)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -404,7 +470,9 @@ class _$RequestModelImpl implements _RequestModel { isWorking, sendingTime, preRequestScript, - postRequestScript); + postRequestScript, + mqttConnectionState, + mqttRequestModel); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -436,7 +504,11 @@ abstract class _RequestModel implements RequestModel { @JsonKey(includeToJson: false) final bool isWorking, @JsonKey(includeToJson: false) final DateTime? sendingTime, final String? preRequestScript, - final String? postRequestScript}) = _$RequestModelImpl; + final String? postRequestScript, + @JsonKey(includeFromJson: false, includeToJson: false) + final MQTTConnectionState? mqttConnectionState, + @JsonKey(includeFromJson: false, includeToJson: false) + final MQTTRequestModel? mqttRequestModel}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -469,7 +541,13 @@ abstract class _RequestModel implements RequestModel { @override String? get preRequestScript; @override - String? get postRequestScript; + String? get postRequestScript; // MQTT support (move to end) + @override + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTConnectionState? get mqttConnectionState; + @override + @JsonKey(includeFromJson: false, includeToJson: false) + MQTTRequestModel? get mqttRequestModel; /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/request_model.g.dart b/lib/models/request_model.g.dart index d4f6ec206..5043c81d9 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -48,4 +48,5 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => const _$APITypeEnumMap = { APIType.rest: 'rest', APIType.graphql: 'graphql', + APIType.mqtt: 'mqtt', }; diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 263575745..09a42b604 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/consts.dart'; import 'providers.dart'; import '../models/models.dart'; +import '../models/mqtt_request_model.dart'; +import '../services/mqtt_service.dart' show MQTTConnectionState; import '../services/services.dart'; import '../utils/utils.dart'; @@ -68,6 +70,7 @@ class CollectionStateNotifier final newRequestModel = RequestModel( id: id, httpRequestModel: const HttpRequestModel(), + mqttRequestModel: const MQTTRequestModel(), ); var map = {...state!}; map[id] = newRequestModel; @@ -225,6 +228,8 @@ class CollectionStateNotifier HttpResponseModel? httpResponseModel, String? preRequestScript, String? postRequestScript, + MQTTRequestModel? mqttRequestModel, + MQTTConnectionState? mqttConnectionState, }) { final rId = id ?? ref.read(selectedIdStateProvider); if (rId == null) { @@ -233,6 +238,13 @@ class CollectionStateNotifier } var currentModel = state![rId]!; var currentHttpRequestModel = currentModel.httpRequestModel; + + // Initialize MQTT request model if API type is changed to MQTT + MQTTRequestModel? finalMqttRequestModel = mqttRequestModel ?? currentModel.mqttRequestModel; + if (apiType == APIType.mqtt && finalMqttRequestModel == null) { + finalMqttRequestModel = const MQTTRequestModel(); + } + final newModel = currentModel.copyWith( apiType: apiType ?? currentModel.apiType, name: name ?? currentModel.name, @@ -259,6 +271,8 @@ class CollectionStateNotifier httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel, preRequestScript: preRequestScript ?? currentModel.preRequestScript, postRequestScript: postRequestScript ?? currentModel.postRequestScript, + mqttRequestModel: finalMqttRequestModel, + mqttConnectionState: mqttConnectionState ?? currentModel.mqttConnectionState, ); var map = {...state!}; @@ -267,6 +281,18 @@ class CollectionStateNotifier unsave(); } + void updateMQTTState({ + String? id, + MQTTRequestModel? mqttRequestModel, + MQTTConnectionState? mqttConnectionState, + }) { + update( + id: id, + mqttRequestModel: mqttRequestModel, + mqttConnectionState: mqttConnectionState, + ); + } + Future sendRequest() async { final requestId = ref.read(selectedIdStateProvider); ref.read(codePaneVisibleStateProvider.notifier).state = false; @@ -279,7 +305,7 @@ class CollectionStateNotifier } RequestModel? requestModel = state![requestId]; - if (requestModel?.httpRequestModel == null) { + if (requestModel?.httpRequestModel == null && requestModel?.apiType != APIType.mqtt) { return; } @@ -302,6 +328,13 @@ class CollectionStateNotifier } APIType apiType = executionRequestModel.apiType; + + // Handle MQTT requests + if (apiType == APIType.mqtt) { + await _handleMQTTRequest(requestId, requestModel); + return; + } + HttpRequestModel substitutedHttpRequestModel = getSubstitutedHttpRequestModel(executionRequestModel.httpRequestModel!); @@ -389,6 +422,65 @@ class CollectionStateNotifier unsave(); } + Future _handleMQTTRequest(String requestId, RequestModel requestModel) async { + final mqttService = ref.read(mqttServiceProvider); + final mqttModel = requestModel.mqttRequestModel; + + if (mqttModel == null) { + return; + } + + // set current model's isWorking to true and update state + var map = {...state!}; + map[requestId] = requestModel.copyWith( + isWorking: true, + sendingTime: DateTime.now(), + ); + state = map; + + try { + final isConnected = mqttService.currentState.isConnected; + + if (isConnected) { + // Disconnect if already connected + await mqttService.disconnect(); + final newRequestModel = requestModel.copyWith( + mqttConnectionState: mqttService.currentState, + isWorking: false, + responseStatus: 200, + message: 'Disconnected from MQTT broker', + ); + map = {...state!}; + map[requestId] = newRequestModel; + state = map; + } else { + // Connect to MQTT broker + final success = await mqttService.connect(mqttModel); + final newRequestModel = requestModel.copyWith( + mqttConnectionState: mqttService.currentState, + isWorking: false, + responseStatus: success ? 200 : -1, + message: success ? 'Connected to MQTT broker' : 'Failed to connect to MQTT broker', + ); + map = {...state!}; + map[requestId] = newRequestModel; + state = map; + } + } catch (e) { + final newRequestModel = requestModel.copyWith( + mqttConnectionState: mqttService.currentState, + isWorking: false, + responseStatus: -1, + message: 'MQTT error: $e', + ); + map = {...state!}; + map[requestId] = newRequestModel; + state = map; + } + + unsave(); + } + void cancelRequest() { final id = ref.read(selectedIdStateProvider); cancelHttpRequest(id); @@ -428,6 +520,11 @@ class CollectionStateNotifier httpRequestModel: const HttpRequestModel(), ); } + if (requestModel.mqttRequestModel == null) { + requestModel = requestModel.copyWith( + mqttRequestModel: const MQTTRequestModel(), + ); + } data[id] = requestModel; } } diff --git a/lib/providers/mqtt_providers.dart b/lib/providers/mqtt_providers.dart new file mode 100644 index 000000000..2390043a3 --- /dev/null +++ b/lib/providers/mqtt_providers.dart @@ -0,0 +1,57 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; +import '../services/mqtt_service.dart'; +import '../models/mqtt_request_model.dart'; +import '../services/mqtt_service.dart' show MQTTConnectionState; +import 'collection_providers.dart'; + +final mqttServiceProvider = Provider((ref) { + final service = MQTTService(); + ref.onDispose(() { + service.dispose(); + }); + return service; +}); + +final mqttConnectionStateProvider = StreamProvider((ref) { + final mqttService = ref.watch(mqttServiceProvider); + return mqttService.stateStream; +}); + +final mqttRequestProvider = StateProvider((ref) { + return kMQTTRequestEmptyModel; +}); + +final mqttTopicsProvider = StateProvider>((ref) { + final request = ref.watch(mqttRequestProvider); + return request.topics; +}); + +final mqttMessagesProvider = StateProvider>((ref) { + final connectionState = ref.watch(mqttConnectionStateProvider); + return connectionState.value?.messages ?? []; +}); + +// Provider to update RequestModel with MQTT state changes +final mqttStateUpdaterProvider = Provider((ref) { + final mqttService = ref.watch(mqttServiceProvider); + final collectionNotifier = ref.watch(collectionStateNotifierProvider.notifier); + final selectedId = ref.watch(selectedIdStateProvider); + + // Listen to MQTT state changes and update the current RequestModel + ref.listen(mqttConnectionStateProvider, (previous, next) { + if (next.hasValue && selectedId != null) { + final currentState = ref.read(collectionStateNotifierProvider); + final currentModel = currentState?[selectedId]; + + if (currentModel != null && currentModel.apiType == APIType.mqtt) { + collectionNotifier.updateMQTTState( + id: selectedId, + mqttConnectionState: next.value, + ); + } + } + }); + + return mqttService; +}); \ No newline at end of file diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 29fc6e595..422c8ccf4 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -3,3 +3,4 @@ export 'environment_providers.dart'; export 'history_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; +export 'mqtt_providers.dart'; diff --git a/lib/screens/common_widgets/code_pane.dart b/lib/screens/common_widgets/code_pane.dart index 654b94403..c62e67c98 100644 --- a/lib/screens/common_widgets/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -47,6 +47,11 @@ class CodePane extends ConsumerWidget { message: "Code generation for GraphQL is currently not available.", ); } + if (substitutedRequestModel.apiType == APIType.mqtt) { + return const ErrorMessage( + message: "Code generation for MQTT is not available.", + ); + } if (code == null) { return const ErrorMessage( message: "An error was encountered while generating code. $kRaiseIssue", diff --git a/lib/screens/common_widgets/envfield_url.dart b/lib/screens/common_widgets/envfield_url.dart index 4757c870c..4d53a745c 100644 --- a/lib/screens/common_widgets/envfield_url.dart +++ b/lib/screens/common_widgets/envfield_url.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; import 'env_trigger_field.dart'; +// these changes are made to accomodate MQTT requirements in URL card + + class EnvURLField extends StatelessWidget { const EnvURLField({ super.key, @@ -11,6 +14,7 @@ class EnvURLField extends StatelessWidget { this.onChanged, this.onFieldSubmitted, this.focusNode, + this.decoration, }); final String selectedId; @@ -18,6 +22,7 @@ class EnvURLField extends StatelessWidget { final void Function(String)? onChanged; final void Function(String)? onFieldSubmitted; final FocusNode? focusNode; + final InputDecoration? decoration; @override Widget build(BuildContext context) { @@ -26,7 +31,7 @@ class EnvURLField extends StatelessWidget { initialValue: initialValue, focusNode: focusNode, style: kCodeStyle, - decoration: InputDecoration( + decoration: decoration ?? InputDecoration( hintText: kHintTextUrlCard, hintStyle: kCodeStyle.copyWith( color: Theme.of(context).colorScheme.outlineVariant, diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index 6766ad1b0..d70085035 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -127,6 +127,10 @@ class HistoryRequestPane extends ConsumerWidget { const HistoryScriptsTab(), ], ), + APIType.mqtt => Padding( + padding: const EdgeInsets.all(16.0), + child: Center(child: Text('MQTT History Request (TODO)')), + ), _ => kSizedBoxEmpty, }; } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_properties_table.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_properties_table.dart new file mode 100644 index 000000000..3b779730d --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_properties_table.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/consts.dart'; + +class MQTTPropertiesTable extends StatefulWidget { + const MQTTPropertiesTable({super.key}); + + @override + State createState() => _MQTTPropertiesTableState(); +} + +class _MQTTPropertiesTableState extends State { + List<_PropertyRow> rows = [ + _PropertyRow(enabled: true, name: '', value: ''), + ]; + + void _addRow() { + setState(() { + rows.add(_PropertyRow(enabled: false, name: '', value: '')); + }); + } + + void _removeRow(int index) { + setState(() { + rows.removeAt(index); + }); + } + + @override + Widget build(BuildContext context) { + final clrScheme = Theme.of(context).colorScheme; + + final List columns = [ + const DataColumn2( + label: Text(''), + fixedWidth: 30, + ), + const DataColumn2( + label: Text('Property Name'), + ), + const DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + const DataColumn2( + label: Text('Property Value'), + ), + const DataColumn2( + label: Text(''), + fixedWidth: 32, + ), + ]; + + final fieldDecoration = InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.outlineVariant, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + ); + + final List dataRows = rows + .map( + (row) => DataRow( + cells: [ + DataCell( + Checkbox( + value: row.enabled, + onChanged: (val) { + setState(() { + row.enabled = val ?? false; + }); + }, + ), + ), + DataCell( + TextFormField( + initialValue: row.name, + decoration: fieldDecoration, + onChanged: (val) { + row.name = val; + }, + ), + ), + const DataCell( + Text('='), + ), + DataCell( + TextFormField( + initialValue: row.value, + decoration: fieldDecoration, + onChanged: (val) { + row.value = val; + }, + ), + ), + DataCell( + IconButton( + icon: const Icon(Icons.delete, color: Colors.redAccent), + onPressed: () => _removeRow(rows.indexOf(row)), + ), + ), + ], + ), + ) + .toList(); + + return Stack( + children: [ + Container( + margin: kPh10t10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + 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, + ), + ), + ), + if (!kIsMobile) kVSpacer40, + ], + ), + ), + if (!kIsMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: _addRow, + icon: const Icon(Icons.add), + label: const Text( + 'Add Property', + style: kTextStyleButton, + ), + ), + ), + ), + ], + ); + } +} + +class _PropertyRow { + bool enabled; + String name; + String value; + _PropertyRow({required this.enabled, required this.name, required this.value}); +} \ No newline at end of file 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 000000000..c40e51a37 --- /dev/null +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/mqtt/mqtt_request_pane.dart @@ -0,0 +1,467 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../../../widgets/mqtt/mqtt_connection_config.dart'; +import 'mqtt_properties_table.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/models/mqtt_request_model.dart'; +import 'package:apidash/services/mqtt_service.dart' show MQTTConnectionState; +import 'package:flutter/foundation.dart' show kIsMobile; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash_design_system/tokens/measurements.dart'; +import 'package:apidash_design_system/tokens/typography.dart'; +import 'package:apidash/widgets/request_pane.dart'; + +class EditMQTTRequestPane extends ConsumerStatefulWidget { + const EditMQTTRequestPane({super.key}); + + @override + ConsumerState createState() => _EditMQTTRequestPaneState(); +} + +class _EditMQTTRequestPaneState extends ConsumerState with SingleTickerProviderStateMixin { + final clientIdController = TextEditingController(); + final usernameController = TextEditingController(); + final passwordController = TextEditingController(); + final topicController = TextEditingController(); + final payloadController = TextEditingController(); + late TabController _tabController; + String? _sendToTopic; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + _initializeControllers(); + } + + void _initializeControllers() { + final selectedRequestModel = ref.read(selectedRequestModelProvider); + final mqttModel = selectedRequestModel?.mqttRequestModel; + if (mqttModel != null) { + clientIdController.text = mqttModel.clientId; + usernameController.text = mqttModel.username; + passwordController.text = mqttModel.password; + } + } + + @override + void dispose() { + clientIdController.dispose(); + usernameController.dispose(); + passwordController.dispose(); + topicController.dispose(); + payloadController.dispose(); + _tabController.dispose(); + super.dispose(); + } + + + + void _handlePublish() async { + final selectedId = ref.read(selectedIdStateProvider); + if (selectedId == null) { + return; + } + + final mqttService = ref.read(mqttServiceProvider); + final mqttModel = ref.read(selectedRequestModelProvider)?.mqttRequestModel; + + final sendTopic = _selectedSendTopic(mqttModel?.topics ?? []); + if (mqttModel == null || sendTopic == null || payloadController.text.isEmpty) { + return; + } + + try { + final success = await mqttService.publish( + sendTopic, + payloadController.text, + qos: 0, + retain: false, + ); + + if (success) { + // Update the MQTT state in RequestModel + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: selectedId, + mqttConnectionState: mqttService.currentState, + ); + + // Clear the form after successful publish + topicController.clear(); + payloadController.clear(); + } + } catch (e) { + // Handle error silently + } + } + + void _addTopicRow() { + final selectedId = ref.read(selectedIdStateProvider); + if (selectedId != null) { + final currentModel = ref.read(selectedRequestModelProvider); + final topics = List.from(currentModel?.mqttRequestModel?.topics ?? []); + topics.add(const MQTTTopicModel(topic: '', qos: 0, subscribe: true)); + final updatedModel = currentModel!.mqttRequestModel!.copyWith(topics: topics); + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: selectedId, + mqttRequestModel: updatedModel, + ); + } + } + + void _updateTopicField(int index, {String? topic, int? qos, bool? subscribe, String? description}) { + final selectedId = ref.read(selectedIdStateProvider); + if (selectedId != null) { + final currentModel = ref.read(selectedRequestModelProvider); + final topics = List.from(currentModel?.mqttRequestModel?.topics ?? []); + final old = topics[index]; + final updated = old.copyWith( + topic: topic ?? old.topic, + qos: qos ?? old.qos, + subscribe: subscribe ?? old.subscribe, + description: description ?? old.description, + ); + topics[index] = updated; + final updatedModel = currentModel!.mqttRequestModel!.copyWith(topics: topics); + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: selectedId, + mqttRequestModel: updatedModel, + ); + // Subscribe/unsubscribe immediately if connected, but do NOT disconnect + final mqttService = ref.read(mqttServiceProvider); + if (mqttService.isConnected && subscribe != null && subscribe != old.subscribe) { + if (subscribe) { + mqttService.subscribe(updated.topic, updated.qos); + } else { + mqttService.unsubscribe(updated.topic); + } + } + } + } + + void _removeTopic(int index) { + final selectedId = ref.read(selectedIdStateProvider); + if (selectedId != null) { + final currentModel = ref.read(selectedRequestModelProvider); + final topics = List.from(currentModel?.mqttRequestModel?.topics ?? []); + topics.removeAt(index); + final updatedModel = currentModel!.mqttRequestModel!.copyWith(topics: topics); + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: selectedId, + mqttRequestModel: updatedModel, + ); + } + } + + @override + Widget build(BuildContext context) { + final selectedRequestModel = ref.watch(selectedRequestModelProvider); + final mqttState = selectedRequestModel?.mqttConnectionState; + final mqttModel = selectedRequestModel?.mqttRequestModel; + final isConnected = mqttState?.isConnected ?? false; + final topics = mqttModel?.topics ?? []; + + // Consume the MQTT state updater provider to ensure state updates + ref.watch(mqttStateUpdaterProvider); + + // Listen to MQTT state changes and update RequestModel + ref.listen(mqttConnectionStateProvider, (previous, next) { + if (next.hasValue && next.value != previous?.value) { + final selectedId = ref.read(selectedIdStateProvider); + if (selectedId != null) { + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: selectedId, + mqttConnectionState: next.value, + ); + } + } + }); + + return RequestPane( + selectedId: ref.watch(selectedIdStateProvider), + codePaneVisible: false, // MQTT does not have code view toggle + tabIndex: _tabController.index, + onTapTabBar: (index) { + setState(() { + _tabController.index = index; + }); + }, + tabLabels: const [ + 'Message', + 'Connection Configuration', + 'Properties', + ], + showIndicators: const [false, false, false], + children: [ + // Message Tab + Padding( + padding: const EdgeInsets.all(8), + child: Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Content Type Dropdown + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text("Select Content Type: "), + DropdownButton( + value: "json", + items: const [ + DropdownMenuItem(value: "json", child: Text("json")), + DropdownMenuItem(value: "text", child: Text("text")), + ], + onChanged: (val) { + // TODO: Handle content type change( in another PR content types were present thats why this dropdown is added) + }, + ), + const SizedBox(width: 16), + // Send-to topic dropdown + DropdownButton( + value: _selectedSendTopic(topics), + hint: const Text('Send to topic'), + items: topics.where((t) => t.subscribe).map((t) => DropdownMenuItem( + value: t.topic, + child: Text(t.topic), + )).toList(), + onChanged: (val) { + setState(() { + _sendToTopic = val; + }); + }, + ), + ], + ), + const SizedBox(height: 12), + // Message Body Editor + TextField( + controller: payloadController, + decoration: const InputDecoration( + hintText: '{"name":"John", "age":30, "car":2}', + border: OutlineInputBorder(), + ), + minLines: 6, + maxLines: 12, + enabled: isConnected, + ), + const SizedBox(height: 12), + // Connect/Disconnect and Send Button + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!isConnected) + ElevatedButton( + onPressed: () { + ref.read(collectionStateNotifierProvider.notifier).sendRequest(); + }, + child: const Text('Connect'), + ), + if (isConnected) + ElevatedButton( + onPressed: () async { + final selectedId = ref.read(selectedIdStateProvider); + if (selectedId != null) { + final mqttService = ref.read(mqttServiceProvider); + await mqttService.disconnect(); + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: selectedId, + mqttConnectionState: mqttService.currentState, + ); + } + }, + child: const Text('Disconnect'), + ), + if (isConnected) + const SizedBox(width: 16), + if (isConnected) + ElevatedButton( + onPressed: _handlePublish, + child: const Text('Send'), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: MQTTTopicsTable( + topics: topics, + onUpdate: _updateTopicField, + onAdd: _addTopicRow, + isMobile: kIsMobile, + onDelete: _removeTopic, + ), + ), + ], + ), + ), + ), + ), + // Connection Configuration Tab + Padding( + padding: const EdgeInsets.all(8), + child: MQTTConnectionConfig( + clientIdController: clientIdController, + usernameController: usernameController, + passwordController: passwordController, + isConnected: isConnected, + onConnect: () { + // Connect functionality + }, + onDisconnect: () { + // Disconnect functionality + }, + ), + ), + // Properties Tab + const Padding( + padding: EdgeInsets.all(8), + child: MQTTPropertiesTable(), + ), + ], + ); + } + + + String? _selectedSendTopic(List topics) { + if (_sendToTopic != null && topics.any((t) => t.topic == _sendToTopic && t.subscribe)) { + return _sendToTopic; + } + final first = topics.firstWhere((t) => t.subscribe, orElse: () => const MQTTTopicModel(topic: '')); + return first.topic.isNotEmpty ? first.topic : null; + } +} + +class MQTTTopicsTable extends StatelessWidget { + final List topics; + final void Function(int, {String? topic, int? qos, bool? subscribe, String? description}) onUpdate; + final VoidCallback onAdd; + final bool isMobile; + final void Function(int) onDelete; + + const MQTTTopicsTable({ + super.key, + required this.topics, + required this.onUpdate, + required this.onAdd, + required this.isMobile, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + margin: kPh10t10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + 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: const [ + DataColumn2(label: Text('Topics')), + DataColumn2(label: Text('QoS'), fixedWidth: 60), + DataColumn2(label: Text('Subscribe'), fixedWidth: 90), + DataColumn2(label: Text('Description')), + DataColumn2(label: Text(''), fixedWidth: 32), + ], + rows: topics.isEmpty + ? [ + const DataRow(cells: [ + DataCell(Text('No topics configured')), + DataCell.empty, + DataCell.empty, + DataCell.empty, + DataCell.empty, + ]), + ] + : List.generate( + topics.length, + (index) { + final topic = topics[index]; + return DataRow( + cells: [ + DataCell( + TextFormField( + initialValue: topic.topic, + decoration: const InputDecoration( + hintText: 'Topic', + border: InputBorder.none, + ), + onChanged: (val) => onUpdate(index, topic: val), + ), + ), + DataCell( + DropdownButton( + value: topic.qos, + items: const [ + DropdownMenuItem(value: 0, child: Text('0')), + DropdownMenuItem(value: 1, child: Text('1')), + DropdownMenuItem(value: 2, child: Text('2')), + ], + onChanged: (val) { + if (val != null) onUpdate(index, qos: val); + }, + ), + ), + DataCell( + Switch( + value: topic.subscribe, + onChanged: (val) => onUpdate(index, subscribe: val), + ), + ), + DataCell( + TextFormField( + initialValue: topic.description, + decoration: const InputDecoration( + hintText: 'Add description', + border: InputBorder.none, + ), + onChanged: (val) => onUpdate(index, description: val), + ), + ), + DataCell( + IconButton( + icon: const Icon(Icons.delete, color: Colors.redAccent), + onPressed: () => onDelete(index), + ), + ), + ], + ); + }, + ), + ), + ), + ), + if (!isMobile) kVSpacer40, + ], + ), + ), + if (!isMobile) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: kPb15, + child: ElevatedButton.icon( + onPressed: onAdd, + icon: const Icon(Icons.add), + label: const Text('Add Topics'), + ), + ), + ), + ], + ); + } +} \ No newline at end of file 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 9c852c71b..7189c4cc2 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 @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'request_pane_graphql.dart'; import 'request_pane_rest.dart'; +import 'mqtt/mqtt_request_pane.dart'; class EditRequestPane extends ConsumerWidget { const EditRequestPane({super.key}); @@ -17,6 +18,7 @@ class EditRequestPane extends ConsumerWidget { return switch (apiType) { APIType.rest => const EditRestRequestPane(), APIType.graphql => const EditGraphQLRequestPane(), + APIType.mqtt => const EditMQTTRequestPane(), _ => kSizedBoxEmpty, }; } 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 367bb4128..2ae7f2276 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 @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import '../../../../services/mqtt_service.dart' show MQTTConnectionState, MQTTEventType; class ResponsePane extends ConsumerWidget { const ResponsePane({super.key}); @@ -95,6 +96,105 @@ class ResponseBodyTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedRequestModel = ref.watch(selectedRequestModelProvider); + if (selectedRequestModel?.apiType == APIType.mqtt) { + final mqttState = selectedRequestModel?.mqttConnectionState; + if (mqttState == null) { + return const Center(child: Text('No MQTT connection info.')); + } + // Show event log + final eventLog = mqttState.eventLog; + if (eventLog.isEmpty) { + return const Center(child: Text('No MQTT events yet.')); + } + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: eventLog.length, + itemBuilder: (context, index) { + final event = eventLog[index]; + IconData icon; + Color color; + switch (event.type) { + case MQTTEventType.connect: + icon = Icons.link; + color = Colors.green; + break; + case MQTTEventType.disconnect: + icon = Icons.link_off; + color = Colors.red; + break; + case MQTTEventType.subscribe: + icon = Icons.subscriptions; + color = Colors.blue; + break; + case MQTTEventType.unsubscribe: + icon = Icons.unsubscribe; + color = Colors.orange; + break; + case MQTTEventType.send: + icon = Icons.arrow_upward; + color = Colors.blue; + break; + case MQTTEventType.receive: + icon = Icons.arrow_downward; + color = Colors.green; + break; + case MQTTEventType.error: + icon = Icons.error; + color = Colors.red; + break; + default: + icon = Icons.info; + color = Colors.grey; + } + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + leading: Icon(icon, color: color), + title: Row( + children: [ + if (event.topic != null && event.topic!.isNotEmpty) + Text( + event.topic!, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (event.topic != null && event.topic!.isNotEmpty) + const SizedBox(width: 8), + if (event.type == MQTTEventType.send) + const Text('Sent'), + if (event.type == MQTTEventType.receive) + const Text('Received'), + if (event.type == MQTTEventType.connect) + const Text('Connected'), + if (event.type == MQTTEventType.disconnect) + const Text('Disconnected'), + if (event.type == MQTTEventType.subscribe) + const Text('Subscribed'), + if (event.type == MQTTEventType.unsubscribe) + const Text('Unsubscribed'), + if (event.type == MQTTEventType.error) + const Text('Error'), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (event.type == MQTTEventType.send || event.type == MQTTEventType.receive) + if (event.payload != null && event.payload!.isNotEmpty) + Text(event.payload!), + if (event.type != MQTTEventType.send && event.type != MQTTEventType.receive) + if (event.description != null && event.description!.isNotEmpty) + Text(event.description!, style: const TextStyle(color: Colors.grey)), + Text( + '${event.timestamp.hour.toString().padLeft(2, '0')}:${event.timestamp.minute.toString().padLeft(2, '0')}:${event.timestamp.second.toString().padLeft(2, '0')}', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ); + }, + ); + } return ResponseBody( selectedRequestModel: selectedRequestModel, ); diff --git a/lib/screens/home_page/editor_pane/url_card.dart b/lib/screens/home_page/editor_pane/url_card.dart index 829bc5c97..c8d7554ba 100644 --- a/lib/screens/home_page/editor_pane/url_card.dart +++ b/lib/screens/home_page/editor_pane/url_card.dart @@ -4,7 +4,10 @@ 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 '../../common_widgets/common_widgets.dart'; +import '../../../models/mqtt_request_model.dart'; +import '../../../services/mqtt_service.dart' show MQTTConnectionState; class EditorPaneRequestURLCard extends ConsumerWidget { const EditorPaneRequestURLCard({super.key}); @@ -12,8 +15,11 @@ class EditorPaneRequestURLCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(selectedIdStateProvider); + final selectedRequestModel = ref.watch(selectedRequestModelProvider); final apiType = ref .watch(selectedRequestModelProvider.select((value) => value?.apiType)); + final mqttModel = selectedRequestModel?.mqttRequestModel; + final mqttState = selectedRequestModel?.mqttConnectionState; return Card( color: kColorTransparent, surfaceTintColor: kColorTransparent, @@ -29,44 +35,111 @@ class EditorPaneRequestURLCard extends ConsumerWidget { vertical: 5, horizontal: !context.isMediumWindow ? 20 : 6, ), - child: context.isMediumWindow - ? Row( - children: [ - switch (apiType) { - APIType.rest => const DropdownButtonHTTPMethod(), - APIType.graphql => kSizedBoxEmpty, - null => kSizedBoxEmpty, + child: Row( + children: [ + switch (apiType) { + APIType.rest => const DropdownButtonHTTPMethod(), + _ => const SizedBox.shrink(), + }, + if (apiType == APIType.rest) kHSpacer5, + Expanded( + child: switch (apiType) { + APIType.mqtt => EnvURLField( + selectedId: ref.watch(selectedIdStateProvider)!, + initialValue: mqttModel?.brokerUrl, + onChanged: (value) { + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: ref.watch(selectedIdStateProvider), + mqttRequestModel: mqttModel!.copyWith(brokerUrl: value), + ); }, - switch (apiType) { - APIType.rest => kHSpacer5, - _ => kHSpacer8, + onFieldSubmitted: (value) { + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: ref.watch(selectedIdStateProvider), + mqttRequestModel: mqttModel!.copyWith(brokerUrl: value), + ); + ref.read(collectionStateNotifierProvider.notifier).sendRequest(); }, - const Expanded( - child: URLTextField(), + focusNode: null, + decoration: const InputDecoration( + hintText: 'Broker URL', + labelText: 'Broker URL', + border: InputBorder.none, ), - ], - ) - : Row( - children: [ - switch (apiType) { - APIType.rest => const DropdownButtonHTTPMethod(), - APIType.graphql => kSizedBoxEmpty, - null => kSizedBoxEmpty, + ), + _ => EnvURLField( + selectedId: ref.watch(selectedIdStateProvider)!, + initialValue: ref + .read(collectionStateNotifierProvider.notifier) + .getRequestModel(ref.watch(selectedIdStateProvider)!) + ?.httpRequestModel + ?.url, + onChanged: (value) { + ref.read(collectionStateNotifierProvider.notifier).update(url: value); }, - switch (apiType) { - APIType.rest => kHSpacer20, - _ => kHSpacer8, + onFieldSubmitted: (value) { + ref.read(collectionStateNotifierProvider.notifier).sendRequest(); }, - const Expanded( - child: URLTextField(), - ), - kHSpacer20, - const SizedBox( - height: 36, - child: SendRequestButton(), - ) - ], + focusNode: null, + ), + }, + ), + if (apiType == APIType.mqtt && mqttModel != null) ...[ + kHSpacer20, + SizedBox( + width: 220, + child: Row( + children: [ + SizedBox( + width: 70, + child: TextField( + controller: TextEditingController(text: mqttModel.port.toString()), + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Port', + border: OutlineInputBorder(), + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), + ), + onChanged: (val) { + final port = int.tryParse(val) ?? mqttModel.port; + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: ref.watch(selectedIdStateProvider), + mqttRequestModel: mqttModel.copyWith(port: port), + ); + }, + ), + ), + const SizedBox(width: 12), + SizedBox( + width: 120, + child: TextField( + controller: TextEditingController(text: mqttModel.clientId), + decoration: const InputDecoration( + labelText: 'Client ID', + border: OutlineInputBorder(), + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 8), + ), + onChanged: (val) { + ref.read(collectionStateNotifierProvider.notifier).updateMQTTState( + id: ref.watch(selectedIdStateProvider), + mqttRequestModel: mqttModel.copyWith(clientId: val), + ); + }, + ), + ), + ], + ), ), + ], + kHSpacer20, + const SizedBox( + height: 36, + child: SendRequestButton(), + ), + ], + ), ), ); } diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart new file mode 100644 index 000000000..b74befa6f --- /dev/null +++ b/lib/services/mqtt_service.dart @@ -0,0 +1,335 @@ +import 'dart:async'; +import 'package:mqtt_client/mqtt_client.dart'; +import 'package:mqtt_client/mqtt_server_client.dart'; +import '../models/mqtt_request_model.dart'; + +enum MQTTEventType { connect, disconnect, subscribe, unsubscribe, send, receive, error } + +class MQTTEvent { + final DateTime timestamp; + final MQTTEventType type; + final String? topic; + final String? payload; + final String? description; + + MQTTEvent({ + required this.timestamp, + required this.type, + this.topic, + this.payload, + this.description, + }); +} + +class MQTTConnectionState { + final bool isConnected; + final String? error; + final List messages; + final List eventLog; + + const MQTTConnectionState({ + this.isConnected = false, + this.error, + this.messages = const [], + this.eventLog = const [], + }); + + MQTTConnectionState copyWith({ + bool? isConnected, + String? error, + List? messages, + List? eventLog, + }) { + return MQTTConnectionState( + isConnected: isConnected ?? this.isConnected, + error: error ?? this.error, + messages: messages ?? this.messages, + eventLog: eventLog ?? this.eventLog, + ); + } +} + +class MQTTMessage { + final String topic; + final String payload; + final DateTime timestamp; + final bool isIncoming; + + const MQTTMessage({ + required this.topic, + required this.payload, + required this.timestamp, + required this.isIncoming, + }); +} + +class MQTTService { + MqttClient? _client; + MQTTConnectionState _state = const MQTTConnectionState(); + final StreamController _stateController = + StreamController.broadcast(); + final List _messages = []; + final List _eventLog = []; + + bool get isConnected => _client != null && _client!.connectionStatus?.state == MqttConnectionState.connected; + + Stream get stateStream => _stateController.stream; + MQTTConnectionState get currentState => _state; + + void _addEvent(MQTTEvent event) { + _eventLog.add(event); + // Keep only last 200 events + if (_eventLog.length > 200) { + _eventLog.removeAt(0); + } + _updateState(_state.copyWith(eventLog: List.from(_eventLog))); + } + + Future connect(MQTTRequestModel request) async { + print('[MQTT] Attempting to connect:'); + print(' Broker URL: ${request.brokerUrl}'); + print(' Port: ${request.port}'); + print(' Client ID: ${request.clientId}'); + print(' Username: ${request.username}'); + print(' Password: ${request.password.isNotEmpty ? "***" : "(empty)"}'); + print(' Topics to subscribe: ${request.topics.map((t) => '${t.topic} (qos: ${t.qos}, sub: ${t.subscribe})').toList()}'); + _eventLog.clear(); // Clear log for new session + try { + // Parse broker URL, add scheme if missing + String brokerUrl = request.brokerUrl.trim(); + if (!brokerUrl.contains('://')) { + brokerUrl = 'mqtt://$brokerUrl'; + } + final uri = Uri.parse(brokerUrl); + final isWebSocket = uri.scheme == 'ws' || uri.scheme == 'wss'; + + // Create client based on platform and protocol + if (isWebSocket) { + _client = MqttServerClient(uri.toString(), 'apidash_${DateTime.now().millisecondsSinceEpoch}'); + } else { + _client = MqttServerClient(uri.host, 'apidash_${DateTime.now().millisecondsSinceEpoch}'); + _client!.port = request.port == 0 ? 1883 : request.port; + } + + // Configure client + _client!.keepAlivePeriod = request.keepAlive; + _client!.connectTimeoutPeriod = request.connectTimeout * 1000; + _client!.onDisconnected = _onDisconnected; + _client!.onConnected = _onConnected; + _client!.onSubscribed = _onSubscribed; + + // Set up connection message + _client!.connectionMessage = MqttConnectMessage() + .withClientIdentifier(request.clientId.isNotEmpty ? request.clientId : 'apidash_client_${DateTime.now().millisecondsSinceEpoch}') + .withWillTopic('apidash/disconnect') + .withWillMessage('Client disconnected') + .startClean() + .withWillQos(MqttQos.atLeastOnce); + + // Connect + print('[MQTT] Connecting...'); + await _client!.connect(); + + if (_client!.connectionStatus!.state == MqttConnectionState.connected) { + print('[MQTT] Connected successfully!'); + _updateState(_state.copyWith(isConnected: true, error: null)); + + // Subscribe to topics + for (final topic in request.topics.where((t) => t.subscribe)) { + await subscribe(topic.topic, topic.qos); + } + + // Set up message listener + _client!.updates!.listen(_onMessageReceived); + + return true; + } else { + final error = 'Failed to connect: ${_client!.connectionStatus}'; + print('[MQTT] $error'); + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.error, + description: error, + )); + _updateState(_state.copyWith(isConnected: false, error: error)); + return false; + } + } catch (e) { + final error = 'Connection error: $e'; + print('[MQTT] $error'); + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.error, + description: error, + )); + _updateState(_state.copyWith(isConnected: false, error: error)); + return false; + } + } + + Future disconnect() async { + if (_client != null && _client!.connectionStatus!.state == MqttConnectionState.connected) { + _client!.disconnect(); + } + _client = null; + _updateState(const MQTTConnectionState()); + } + + Future subscribe(String topic, int qos) async { + if (_client == null || _client!.connectionStatus!.state != MqttConnectionState.connected) { + return false; + } + + try { + _client!.subscribe(topic, MqttQos.values[qos]); + return true; + } catch (e) { + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.error, + topic: topic, + description: 'Subscribe error: $e', + )); + _updateState(_state.copyWith(error: 'Subscribe error: $e')); + return false; + } + } + + Future unsubscribe(String topic) async { + if (_client == null || _client!.connectionStatus!.state != MqttConnectionState.connected) { + return false; + } + + try { + _client!.unsubscribe(topic); + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.unsubscribe, + topic: topic, + description: "Unsubscribed from topic $topic", + )); + return true; + } catch (e) { + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.error, + topic: topic, + description: 'Unsubscribe error: $e', + )); + _updateState(_state.copyWith(error: 'Unsubscribe error: $e')); + return false; + } + } + + Future publish(String topic, String payload, {int qos = 0, bool retain = false}) async { + if (_client == null || _client!.connectionStatus!.state != MqttConnectionState.connected) { + return false; + } + + try { + final message = MqttClientPayloadBuilder(); + message.addString(payload); + _client!.publishMessage(topic, MqttQos.values[qos], message.payload!, retain: retain); + + // Add outgoing message to history + final outgoingMessage = MQTTMessage( + topic: topic, + payload: payload, + timestamp: DateTime.now(), + isIncoming: false, + ); + _addMessage(outgoingMessage); + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.send, + topic: topic, + payload: payload, + description: "Message sent to $topic", + )); + + return true; + } catch (e) { + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.error, + topic: topic, + description: 'Publish error: $e', + )); + _updateState(_state.copyWith(error: 'Publish error: $e')); + return false; + } + } + + void _onConnected() { + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.connect, + description: "Connected to broker", + )); + _updateState(_state.copyWith(isConnected: true, error: null)); + } + + void _onDisconnected() { + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.disconnect, + description: "Disconnected from broker", + )); + _updateState(_state.copyWith(isConnected: false)); + } + + void _onSubscribed(String topic) { + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.subscribe, + topic: topic, + description: "Subscribed to topic $topic", + )); + // Topic subscription successful + } + + void _onMessageReceived(List> messages) { + for (final message in messages) { + String payload = ''; + try { + final mqttMsg = message.payload as MqttPublishMessage; + final pt = MqttPublishPayload.bytesToStringAsString(mqttMsg.payload.message); + payload = pt; + } catch (e) { + payload = message.payload.toString(); + } + print('[MQTT] Received message on topic: ${message.topic}, payload: $payload'); + final incomingMessage = MQTTMessage( + topic: message.topic, + payload: payload, + timestamp: DateTime.now(), + isIncoming: true, + ); + _addMessage(incomingMessage); + _addEvent(MQTTEvent( + timestamp: DateTime.now(), + type: MQTTEventType.receive, + topic: message.topic, + payload: payload, + description: "Message received from ${message.topic}", + )); + } + } + + void _addMessage(MQTTMessage message) { + _messages.add(message); + + _updateState(_state.copyWith(messages: List.from(_messages))); + } + + void _updateState(MQTTConnectionState newState) { + + _state = newState.copyWith(eventLog: List.from(_eventLog)); + _stateController.add(_state); + } + + void dispose() { + disconnect(); + _stateController.close(); + } +} \ No newline at end of file diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 68eaa82c4..8c279733e 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -36,6 +36,7 @@ Color getAPIColor( method, ), APIType.graphql => kColorGQL, + APIType.mqtt => Colors.orange, // Or define kColorMQTT if you have one }; if (brightness == Brightness.dark) { col = getDarkModeColor(col); diff --git a/lib/widgets/mqtt/mqtt_connection_config.dart b/lib/widgets/mqtt/mqtt_connection_config.dart new file mode 100644 index 000000000..3721ff60d --- /dev/null +++ b/lib/widgets/mqtt/mqtt_connection_config.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:apidash/consts.dart'; + +class MQTTConnectionConfig extends StatelessWidget { + final TextEditingController clientIdController; + final TextEditingController usernameController; + final TextEditingController passwordController; + final bool isConnected; + final VoidCallback onConnect; + final VoidCallback onDisconnect; + + const MQTTConnectionConfig({ + super.key, + required this.clientIdController, + required this.usernameController, + required this.passwordController, + required this.isConnected, + required this.onConnect, + required this.onDisconnect, + }); + + @override + Widget build(BuildContext context) { + final clrScheme = Theme.of(context).colorScheme; + + final List columns = [ + const DataColumn2( + label: Text(''), + fixedWidth: 30, + ), + const DataColumn2( + label: Text('Parameter'), + ), + const DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + const DataColumn2( + label: Text('Value'), + ), + const DataColumn2( + label: Text(''), + fixedWidth: 32, + ), + ]; + + final fieldDecoration = InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.outlineVariant, + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + ); + + final List dataRows = [ + DataRow( + cells: [ + const DataCell(SizedBox.shrink()), + const DataCell(Text('Username')), + const DataCell(Text('=')), + DataCell( + TextFormField( + controller: usernameController, + decoration: fieldDecoration.copyWith( + hintText: 'Optional', + ), + enabled: !isConnected, + onChanged: (value) { + // Update username in RequestModel.mqttRequestModel + // (Assume context has access to ref or pass a callback) + }, + ), + ), + const DataCell(SizedBox.shrink()), + ], + ), + DataRow( + cells: [ + const DataCell(SizedBox.shrink()), + const DataCell(Text('Password')), + const DataCell(Text('=')), + DataCell( + TextFormField( + controller: passwordController, + decoration: fieldDecoration.copyWith( + hintText: 'Optional', + ), + obscureText: true, + enabled: !isConnected, + onChanged: (value) { + // Update password in RequestModel.mqttRequestModel + }, + ), + ), + const DataCell(SizedBox.shrink()), + ], + ), + // ... Keep Alive, Last-Will, etc. ... + ]; + + return Column( + children: [ + Expanded( + child: Container( + margin: kPh10t10, + 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, + ), + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index c64a71b27..60aacc69e 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -20,6 +20,7 @@ class SidebarRequestCardTextBox extends StatelessWidget { switch (apiType) { APIType.rest => method.abbr, APIType.graphql => apiType.abbr, + APIType.mqtt => apiType.abbr, }, textAlign: TextAlign.center, style: TextStyle( diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 778dba2df..05937e730 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -2,7 +2,8 @@ import 'dart:convert'; enum APIType { rest("HTTP", "HTTP"), - graphql("GraphQL", "GQL"); + graphql("GraphQL", "GQL"), + mqtt("MQTT", "MQTT"); const APIType(this.label, this.abbr); final String label; diff --git a/packages/better_networking/lib/services/http_service.dart b/packages/better_networking/lib/services/http_service.dart index 844ef69be..711f19895 100644 --- a/packages/better_networking/lib/services/http_service.dart +++ b/packages/better_networking/lib/services/http_service.dart @@ -21,6 +21,10 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest( SupportedUriSchemes defaultUriScheme = kDefaultUriScheme, bool noSSL = false, }) async { + if (apiType == APIType.mqtt) { + // MQTT is not supported in this HTTP service + return (null, null, 'MQTT protocol is not supported in HTTP service.'); + } if (httpClientManager.wasRequestCancelled(requestId)) { httpClientManager.removeCancelledRequest(requestId); } diff --git a/packages/better_networking/lib/utils/http_request_utils.dart b/packages/better_networking/lib/utils/http_request_utils.dart index c13fab75b..6bf20d901 100644 --- a/packages/better_networking/lib/utils/http_request_utils.dart +++ b/packages/better_networking/lib/utils/http_request_utils.dart @@ -93,6 +93,7 @@ String? getRequestBody(APIType type, HttpRequestModel httpRequestModel) { ? httpRequestModel.body : null, APIType.graphql => getGraphQLBody(httpRequestModel), + APIType.mqtt => null, }; } diff --git a/pubspec.yaml b/pubspec.yaml index 0ef4fae78..078ef92b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: git: url: https://github.com/google/flutter-desktop-embedding.git path: plugins/window_size + mqtt_client: ^9.6.1 dependency_overrides: extended_text_field: ^16.0.0