From d5feb0b091aa338411b9d60b985a782f115e5095 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 14 Jul 2024 23:16:29 +0530 Subject: [PATCH 01/13] wip: history request groups --- lib/consts.dart | 3 +- lib/models/history_meta_model.dart | 21 ++ lib/models/history_meta_model.freezed.dart | 261 ++++++++++++++++++ lib/models/history_meta_model.g.dart | 38 +++ lib/models/history_request_model.dart | 23 ++ lib/models/history_request_model.freezed.dart | 258 +++++++++++++++++ lib/models/history_request_model.g.dart | 27 ++ lib/models/models.dart | 2 + lib/providers/collection_providers.dart | 16 ++ lib/providers/history_providers.dart | 91 ++++++ lib/providers/providers.dart | 1 + lib/screens/about_dialog.dart | 29 -- lib/screens/dashboard.dart | 6 +- lib/screens/envvar/environment_editor.dart | 56 ++-- lib/screens/history/history_details.dart | 10 + lib/screens/history/history_page.dart | 41 +++ lib/screens/history/history_pane.dart | 96 +++++++ lib/screens/history/history_requests.dart | 10 + lib/screens/history/history_viewer.dart | 35 +++ .../home_page/editor_pane/editor_request.dart | 2 +- lib/screens/mobile/dashboard.dart | 15 +- lib/screens/settings_page.dart | 1 - lib/services/hive_services.dart | 30 ++ lib/utils/convert_utils.dart | 8 + lib/utils/history_utils.dart | 86 ++++++ lib/utils/utils.dart | 1 + lib/widgets/card_sidebar_history.dart | 110 ++++++++ lib/widgets/splitview_history.dart | 65 +++++ lib/widgets/widgets.dart | 2 + pubspec.lock | 8 + pubspec.yaml | 3 +- 31 files changed, 1284 insertions(+), 71 deletions(-) create mode 100644 lib/models/history_meta_model.dart create mode 100644 lib/models/history_meta_model.freezed.dart create mode 100644 lib/models/history_meta_model.g.dart create mode 100644 lib/models/history_request_model.dart create mode 100644 lib/models/history_request_model.freezed.dart create mode 100644 lib/models/history_request_model.g.dart create mode 100644 lib/providers/history_providers.dart delete mode 100644 lib/screens/about_dialog.dart create mode 100644 lib/screens/history/history_details.dart create mode 100644 lib/screens/history/history_page.dart create mode 100644 lib/screens/history/history_pane.dart create mode 100644 lib/screens/history/history_requests.dart create mode 100644 lib/screens/history/history_viewer.dart create mode 100644 lib/utils/history_utils.dart create mode 100644 lib/widgets/card_sidebar_history.dart create mode 100644 lib/widgets/splitview_history.dart diff --git a/lib/consts.dart b/lib/consts.dart index 481a0665..63e1946f 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -68,6 +68,7 @@ const kFormDataButtonLabelTextStyle = TextStyle( const kTextStylePopupMenuItem = TextStyle(fontSize: 16); const kBorderRadius4 = BorderRadius.all(Radius.circular(4)); +const kBorderRadius6 = BorderRadius.all(Radius.circular(6)); const kBorderRadius8 = BorderRadius.all(Radius.circular(8)); final kBorderRadius10 = BorderRadius.circular(10); const kBorderRadius12 = BorderRadius.all(Radius.circular(12)); @@ -88,7 +89,7 @@ const kPv8 = EdgeInsets.symmetric(vertical: 8); const kPv6 = EdgeInsets.symmetric(vertical: 6); const kPv2 = EdgeInsets.symmetric(vertical: 2); const kPh2 = EdgeInsets.symmetric(horizontal: 2); -const kPt24o8 = EdgeInsets.only(top: 24, left: 8.0, right: 8.0, bottom: 8.0); +const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = EdgeInsets.only(left: 10.0, right: 10.0, top: 5.0, bottom: 10.0); const kPh4 = EdgeInsets.symmetric(horizontal: 4); diff --git a/lib/models/history_meta_model.dart b/lib/models/history_meta_model.dart new file mode 100644 index 00000000..58e68aee --- /dev/null +++ b/lib/models/history_meta_model.dart @@ -0,0 +1,21 @@ +import 'package:apidash/consts.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'history_meta_model.freezed.dart'; + +part 'history_meta_model.g.dart'; + +@freezed +class HistoryMetaModel with _$HistoryMetaModel { + const factory HistoryMetaModel({ + required String historyId, + @Default("") String name, + required String url, + required HTTPVerb method, + required int responseStatus, + required DateTime timeStamp, + }) = _HistoryMetaModel; + + factory HistoryMetaModel.fromJson(Map json) => + _$HistoryMetaModelFromJson(json); +} diff --git a/lib/models/history_meta_model.freezed.dart b/lib/models/history_meta_model.freezed.dart new file mode 100644 index 00000000..fb7ba2eb --- /dev/null +++ b/lib/models/history_meta_model.freezed.dart @@ -0,0 +1,261 @@ +// 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 'history_meta_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'); + +HistoryMetaModel _$HistoryMetaModelFromJson(Map json) { + return _HistoryMetaModel.fromJson(json); +} + +/// @nodoc +mixin _$HistoryMetaModel { + String get historyId => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + HTTPVerb get method => throw _privateConstructorUsedError; + int get responseStatus => throw _privateConstructorUsedError; + DateTime get timeStamp => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $HistoryMetaModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HistoryMetaModelCopyWith<$Res> { + factory $HistoryMetaModelCopyWith( + HistoryMetaModel value, $Res Function(HistoryMetaModel) then) = + _$HistoryMetaModelCopyWithImpl<$Res, HistoryMetaModel>; + @useResult + $Res call( + {String historyId, + String name, + String url, + HTTPVerb method, + int responseStatus, + DateTime timeStamp}); +} + +/// @nodoc +class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel> + implements $HistoryMetaModelCopyWith<$Res> { + _$HistoryMetaModelCopyWithImpl(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? historyId = null, + Object? name = null, + Object? url = null, + Object? method = null, + Object? responseStatus = null, + Object? timeStamp = null, + }) { + return _then(_value.copyWith( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as HTTPVerb, + responseStatus: null == responseStatus + ? _value.responseStatus + : responseStatus // ignore: cast_nullable_to_non_nullable + as int, + timeStamp: null == timeStamp + ? _value.timeStamp + : timeStamp // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HistoryMetaModelImplCopyWith<$Res> + implements $HistoryMetaModelCopyWith<$Res> { + factory _$$HistoryMetaModelImplCopyWith(_$HistoryMetaModelImpl value, + $Res Function(_$HistoryMetaModelImpl) then) = + __$$HistoryMetaModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String historyId, + String name, + String url, + HTTPVerb method, + int responseStatus, + DateTime timeStamp}); +} + +/// @nodoc +class __$$HistoryMetaModelImplCopyWithImpl<$Res> + extends _$HistoryMetaModelCopyWithImpl<$Res, _$HistoryMetaModelImpl> + implements _$$HistoryMetaModelImplCopyWith<$Res> { + __$$HistoryMetaModelImplCopyWithImpl(_$HistoryMetaModelImpl _value, + $Res Function(_$HistoryMetaModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? historyId = null, + Object? name = null, + Object? url = null, + Object? method = null, + Object? responseStatus = null, + Object? timeStamp = null, + }) { + return _then(_$HistoryMetaModelImpl( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + method: null == method + ? _value.method + : method // ignore: cast_nullable_to_non_nullable + as HTTPVerb, + responseStatus: null == responseStatus + ? _value.responseStatus + : responseStatus // ignore: cast_nullable_to_non_nullable + as int, + timeStamp: null == timeStamp + ? _value.timeStamp + : timeStamp // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$HistoryMetaModelImpl implements _HistoryMetaModel { + const _$HistoryMetaModelImpl( + {required this.historyId, + this.name = "", + required this.url, + required this.method, + required this.responseStatus, + required this.timeStamp}); + + factory _$HistoryMetaModelImpl.fromJson(Map json) => + _$$HistoryMetaModelImplFromJson(json); + + @override + final String historyId; + @override + @JsonKey() + final String name; + @override + final String url; + @override + final HTTPVerb method; + @override + final int responseStatus; + @override + final DateTime timeStamp; + + @override + String toString() { + return 'HistoryMetaModel(historyId: $historyId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HistoryMetaModelImpl && + (identical(other.historyId, historyId) || + other.historyId == historyId) && + (identical(other.name, name) || other.name == name) && + (identical(other.url, url) || other.url == url) && + (identical(other.method, method) || other.method == method) && + (identical(other.responseStatus, responseStatus) || + other.responseStatus == responseStatus) && + (identical(other.timeStamp, timeStamp) || + other.timeStamp == timeStamp)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, historyId, name, url, method, responseStatus, timeStamp); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HistoryMetaModelImplCopyWith<_$HistoryMetaModelImpl> get copyWith => + __$$HistoryMetaModelImplCopyWithImpl<_$HistoryMetaModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HistoryMetaModelImplToJson( + this, + ); + } +} + +abstract class _HistoryMetaModel implements HistoryMetaModel { + const factory _HistoryMetaModel( + {required final String historyId, + final String name, + required final String url, + required final HTTPVerb method, + required final int responseStatus, + required final DateTime timeStamp}) = _$HistoryMetaModelImpl; + + factory _HistoryMetaModel.fromJson(Map json) = + _$HistoryMetaModelImpl.fromJson; + + @override + String get historyId; + @override + String get name; + @override + String get url; + @override + HTTPVerb get method; + @override + int get responseStatus; + @override + DateTime get timeStamp; + @override + @JsonKey(ignore: true) + _$$HistoryMetaModelImplCopyWith<_$HistoryMetaModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart new file mode 100644 index 00000000..67975edc --- /dev/null +++ b/lib/models/history_meta_model.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'history_meta_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$HistoryMetaModelImpl _$$HistoryMetaModelImplFromJson( + Map json) => + _$HistoryMetaModelImpl( + historyId: json['historyId'] as String, + name: json['name'] as String? ?? "", + url: json['url'] as String, + method: $enumDecode(_$HTTPVerbEnumMap, json['method']), + responseStatus: (json['responseStatus'] as num).toInt(), + timeStamp: DateTime.parse(json['timeStamp'] as String), + ); + +Map _$$HistoryMetaModelImplToJson( + _$HistoryMetaModelImpl instance) => + { + 'historyId': instance.historyId, + 'name': instance.name, + 'url': instance.url, + 'method': _$HTTPVerbEnumMap[instance.method]!, + 'responseStatus': instance.responseStatus, + 'timeStamp': instance.timeStamp.toIso8601String(), + }; + +const _$HTTPVerbEnumMap = { + HTTPVerb.get: 'get', + HTTPVerb.head: 'head', + HTTPVerb.post: 'post', + HTTPVerb.put: 'put', + HTTPVerb.patch: 'patch', + HTTPVerb.delete: 'delete', +}; diff --git a/lib/models/history_request_model.dart b/lib/models/history_request_model.dart new file mode 100644 index 00000000..5a9a2b53 --- /dev/null +++ b/lib/models/history_request_model.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'models.dart'; + +part 'history_request_model.freezed.dart'; + +part 'history_request_model.g.dart'; + +@freezed +class HistoryRequestModel with _$HistoryRequestModel { + @JsonSerializable( + explicitToJson: true, + anyMap: true, + ) + const factory HistoryRequestModel({ + required String historyId, + required HistoryMetaModel metaData, + required HttpRequestModel httpRequestModel, + required HttpResponseModel httpResponseModel, + }) = _HistoryRequestModel; + + factory HistoryRequestModel.fromJson(Map json) => + _$HistoryRequestModelFromJson(json); +} diff --git a/lib/models/history_request_model.freezed.dart b/lib/models/history_request_model.freezed.dart new file mode 100644 index 00000000..afdacfdd --- /dev/null +++ b/lib/models/history_request_model.freezed.dart @@ -0,0 +1,258 @@ +// 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 'history_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'); + +HistoryRequestModel _$HistoryRequestModelFromJson(Map json) { + return _HistoryRequestModel.fromJson(json); +} + +/// @nodoc +mixin _$HistoryRequestModel { + String get historyId => throw _privateConstructorUsedError; + HistoryMetaModel get metaData => throw _privateConstructorUsedError; + HttpRequestModel get httpRequestModel => throw _privateConstructorUsedError; + HttpResponseModel get httpResponseModel => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $HistoryRequestModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HistoryRequestModelCopyWith<$Res> { + factory $HistoryRequestModelCopyWith( + HistoryRequestModel value, $Res Function(HistoryRequestModel) then) = + _$HistoryRequestModelCopyWithImpl<$Res, HistoryRequestModel>; + @useResult + $Res call( + {String historyId, + HistoryMetaModel metaData, + HttpRequestModel httpRequestModel, + HttpResponseModel httpResponseModel}); + + $HistoryMetaModelCopyWith<$Res> get metaData; + $HttpRequestModelCopyWith<$Res> get httpRequestModel; + $HttpResponseModelCopyWith<$Res> get httpResponseModel; +} + +/// @nodoc +class _$HistoryRequestModelCopyWithImpl<$Res, $Val extends HistoryRequestModel> + implements $HistoryRequestModelCopyWith<$Res> { + _$HistoryRequestModelCopyWithImpl(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? historyId = null, + Object? metaData = null, + Object? httpRequestModel = null, + Object? httpResponseModel = null, + }) { + return _then(_value.copyWith( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + metaData: null == metaData + ? _value.metaData + : metaData // ignore: cast_nullable_to_non_nullable + as HistoryMetaModel, + httpRequestModel: null == httpRequestModel + ? _value.httpRequestModel + : httpRequestModel // ignore: cast_nullable_to_non_nullable + as HttpRequestModel, + httpResponseModel: null == httpResponseModel + ? _value.httpResponseModel + : httpResponseModel // ignore: cast_nullable_to_non_nullable + as HttpResponseModel, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $HistoryMetaModelCopyWith<$Res> get metaData { + return $HistoryMetaModelCopyWith<$Res>(_value.metaData, (value) { + return _then(_value.copyWith(metaData: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $HttpRequestModelCopyWith<$Res> get httpRequestModel { + return $HttpRequestModelCopyWith<$Res>(_value.httpRequestModel, (value) { + return _then(_value.copyWith(httpRequestModel: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $HttpResponseModelCopyWith<$Res> get httpResponseModel { + return $HttpResponseModelCopyWith<$Res>(_value.httpResponseModel, (value) { + return _then(_value.copyWith(httpResponseModel: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$HistoryRequestModelImplCopyWith<$Res> + implements $HistoryRequestModelCopyWith<$Res> { + factory _$$HistoryRequestModelImplCopyWith(_$HistoryRequestModelImpl value, + $Res Function(_$HistoryRequestModelImpl) then) = + __$$HistoryRequestModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String historyId, + HistoryMetaModel metaData, + HttpRequestModel httpRequestModel, + HttpResponseModel httpResponseModel}); + + @override + $HistoryMetaModelCopyWith<$Res> get metaData; + @override + $HttpRequestModelCopyWith<$Res> get httpRequestModel; + @override + $HttpResponseModelCopyWith<$Res> get httpResponseModel; +} + +/// @nodoc +class __$$HistoryRequestModelImplCopyWithImpl<$Res> + extends _$HistoryRequestModelCopyWithImpl<$Res, _$HistoryRequestModelImpl> + implements _$$HistoryRequestModelImplCopyWith<$Res> { + __$$HistoryRequestModelImplCopyWithImpl(_$HistoryRequestModelImpl _value, + $Res Function(_$HistoryRequestModelImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? historyId = null, + Object? metaData = null, + Object? httpRequestModel = null, + Object? httpResponseModel = null, + }) { + return _then(_$HistoryRequestModelImpl( + historyId: null == historyId + ? _value.historyId + : historyId // ignore: cast_nullable_to_non_nullable + as String, + metaData: null == metaData + ? _value.metaData + : metaData // ignore: cast_nullable_to_non_nullable + as HistoryMetaModel, + httpRequestModel: null == httpRequestModel + ? _value.httpRequestModel + : httpRequestModel // ignore: cast_nullable_to_non_nullable + as HttpRequestModel, + httpResponseModel: null == httpResponseModel + ? _value.httpResponseModel + : httpResponseModel // ignore: cast_nullable_to_non_nullable + as HttpResponseModel, + )); + } +} + +/// @nodoc + +@JsonSerializable(explicitToJson: true, anyMap: true) +class _$HistoryRequestModelImpl implements _HistoryRequestModel { + const _$HistoryRequestModelImpl( + {required this.historyId, + required this.metaData, + required this.httpRequestModel, + required this.httpResponseModel}); + + factory _$HistoryRequestModelImpl.fromJson(Map json) => + _$$HistoryRequestModelImplFromJson(json); + + @override + final String historyId; + @override + final HistoryMetaModel metaData; + @override + final HttpRequestModel httpRequestModel; + @override + final HttpResponseModel httpResponseModel; + + @override + String toString() { + return 'HistoryRequestModel(historyId: $historyId, metaData: $metaData, httpRequestModel: $httpRequestModel, httpResponseModel: $httpResponseModel)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HistoryRequestModelImpl && + (identical(other.historyId, historyId) || + other.historyId == historyId) && + (identical(other.metaData, metaData) || + other.metaData == metaData) && + (identical(other.httpRequestModel, httpRequestModel) || + other.httpRequestModel == httpRequestModel) && + (identical(other.httpResponseModel, httpResponseModel) || + other.httpResponseModel == httpResponseModel)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, historyId, metaData, httpRequestModel, httpResponseModel); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HistoryRequestModelImplCopyWith<_$HistoryRequestModelImpl> get copyWith => + __$$HistoryRequestModelImplCopyWithImpl<_$HistoryRequestModelImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HistoryRequestModelImplToJson( + this, + ); + } +} + +abstract class _HistoryRequestModel implements HistoryRequestModel { + const factory _HistoryRequestModel( + {required final String historyId, + required final HistoryMetaModel metaData, + required final HttpRequestModel httpRequestModel, + required final HttpResponseModel httpResponseModel}) = + _$HistoryRequestModelImpl; + + factory _HistoryRequestModel.fromJson(Map json) = + _$HistoryRequestModelImpl.fromJson; + + @override + String get historyId; + @override + HistoryMetaModel get metaData; + @override + HttpRequestModel get httpRequestModel; + @override + HttpResponseModel get httpResponseModel; + @override + @JsonKey(ignore: true) + _$$HistoryRequestModelImplCopyWith<_$HistoryRequestModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/history_request_model.g.dart b/lib/models/history_request_model.g.dart new file mode 100644 index 00000000..830d7a4c --- /dev/null +++ b/lib/models/history_request_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'history_request_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$HistoryRequestModelImpl _$$HistoryRequestModelImplFromJson(Map json) => + _$HistoryRequestModelImpl( + historyId: json['historyId'] as String, + metaData: HistoryMetaModel.fromJson( + Map.from(json['metaData'] as Map)), + httpRequestModel: HttpRequestModel.fromJson( + Map.from(json['httpRequestModel'] as Map)), + httpResponseModel: HttpResponseModel.fromJson( + Map.from(json['httpResponseModel'] as Map)), + ); + +Map _$$HistoryRequestModelImplToJson( + _$HistoryRequestModelImpl instance) => + { + 'historyId': instance.historyId, + 'metaData': instance.metaData.toJson(), + 'httpRequestModel': instance.httpRequestModel.toJson(), + 'httpResponseModel': instance.httpResponseModel.toJson(), + }; diff --git a/lib/models/models.dart b/lib/models/models.dart index 63949949..2471d12c 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -1,5 +1,7 @@ export 'environment_model.dart'; export 'form_data_model.dart'; +export 'history_meta_model.dart'; +export 'history_request_model.dart'; export 'http_request_model.dart'; export 'http_response_model.dart'; export 'name_value_model.dart'; diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 5230e39f..d86d0836 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -240,12 +240,28 @@ class CollectionStateNotifier httpResponseModel: responseModel, isWorking: false, ); + String newHistoryId = getNewUuid(); + HistoryRequestModel model = HistoryRequestModel( + historyId: newHistoryId, + metaData: HistoryMetaModel( + historyId: newHistoryId, + name: requestModel.name, + url: substitutedHttpRequestModel.url, + method: substitutedHttpRequestModel.method, + responseStatus: statusCode, + timeStamp: DateTime.now(), + ), + httpRequestModel: substitutedHttpRequestModel, + httpResponseModel: responseModel, + ); + ref.read(historyMetaStateNotifier.notifier).addHistoryRequest(model); } // update state with response data map = {...state!}; map[id] = newRequestModel; state = map; + ref.read(hasUnsavedChangesProvider.notifier).state = true; } diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart new file mode 100644 index 00000000..3f7d7c95 --- /dev/null +++ b/lib/providers/history_providers.dart @@ -0,0 +1,91 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/models/models.dart'; +import '../services/services.dart' show hiveHandler, HiveHandler; +import '../utils/history_utils.dart'; + +final selectedHistoryIdStateProvider = StateProvider((ref) => null); + +final selectedRequestGroupStateProvider = StateProvider((ref) { + final selectedHistoryId = ref.watch(selectedHistoryIdStateProvider); + if (selectedHistoryId == null) { + return null; + } + final historyMetaState = ref.read(historyMetaStateNotifier); + return getHistoryRequestKey(historyMetaState![selectedHistoryId]!); +}); + +final selectedHistoryRequestModelProvider = + StateProvider((ref) => null); + +final historySequenceProvider = + StateProvider>?>((ref) { + final historyMetas = ref.watch(historyMetaStateNotifier); + return getTemporalGroups(historyMetas?.values.toList()); +}); + +final StateNotifierProvider?> historyMetaStateNotifier = + StateNotifierProvider((ref) => HistoryMetaStateNotifier(ref, hiveHandler)); + +class HistoryMetaStateNotifier + extends StateNotifier?> { + HistoryMetaStateNotifier(this.ref, this.hiveHandler) : super(null) { + var status = loadHistoryMetas(); + Future.microtask(() { + if (status) { + final temporalGroups = getTemporalGroups(state?.values.toList()); + final latestRequestId = getLatestRequestId(temporalGroups); + if (latestRequestId != null) { + loadHistoryRequest(latestRequestId); + } + } + }); + } + + final Ref ref; + final HiveHandler hiveHandler; + + bool loadHistoryMetas() { + List? historyIds = hiveHandler.getHistoryIds(); + if (historyIds == null || historyIds.isEmpty) { + state = null; + return false; + } else { + Map historyMetaMap = {}; + for (var historyId in historyIds) { + var jsonModel = hiveHandler.getHistoryMeta(historyId); + if (jsonModel != null) { + var jsonMap = Map.from(jsonModel); + var historyMetaModelFromJson = HistoryMetaModel.fromJson(jsonMap); + historyMetaMap[historyId] = historyMetaModelFromJson; + } + } + state = historyMetaMap; + return true; + } + } + + Future loadHistoryRequest(String id) async { + var jsonModel = await hiveHandler.getHistoryRequest(id); + if (jsonModel != null) { + var jsonMap = Map.from(jsonModel); + var historyRequestModelFromJson = HistoryRequestModel.fromJson(jsonMap); + ref.read(selectedHistoryRequestModelProvider.notifier).state = + historyRequestModelFromJson; + ref.read(selectedHistoryIdStateProvider.notifier).state = id; + } + } + + void addHistoryRequest(HistoryRequestModel model) async { + final id = model.historyId; + state = { + ...state ?? {}, + id: model.metaData, + }; + final List updatedHistoryKeys = + state == null ? [id] : [...state!.keys, id]; + hiveHandler.setHistoryIds(updatedHistoryKeys); + hiveHandler.setHistoryMeta(id, model.metaData.toJson()); + await hiveHandler.setHistoryRequest(id, model.toJson()); + } +} diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 32f808ae..29fc6e59 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,4 +1,5 @@ export 'collection_providers.dart'; export 'environment_providers.dart'; +export 'history_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; diff --git a/lib/screens/about_dialog.dart b/lib/screens/about_dialog.dart deleted file mode 100644 index 692b6743..00000000 --- a/lib/screens/about_dialog.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:apidash/consts.dart'; -import 'package:flutter/material.dart'; -import 'package:apidash/widgets/widgets.dart'; - -showAboutAppDialog( - BuildContext context, -) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - contentPadding: kPt20 + kPh20, - content: Container( - width: double.infinity, - height: double.infinity, - constraints: const BoxConstraints(maxWidth: 540, maxHeight: 544), - child: const IntroMessage(), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("Close"), - ), - ], - ); - }); -} diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index a841ca6f..c9b57475 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,4 +1,4 @@ -import 'package:apidash/screens/about_dialog.dart'; +import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -118,7 +118,9 @@ class Dashboard extends ConsumerWidget { EnvironmentPage( scaffoldKey: mobileScaffoldKey, ), - const SizedBox(), + HistoryPage( + scaffoldKey: mobileScaffoldKey, + ), const SettingsPage(), ], ), diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index 53b17206..b4591e35 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -19,7 +19,7 @@ class EnvironmentEditor extends ConsumerWidget { padding: context.isMediumWindow ? kPb10 : (kIsMacOS || kIsWindows) - ? kPt24o8 + ? kPt28o8 : kP8, child: Column( children: [ @@ -65,34 +65,40 @@ class EnvironmentEditor extends ConsumerWidget { kVSpacer5, Expanded( child: Container( - padding: context.isMediumWindow ? null : kPv6, margin: context.isMediumWindow ? null : kP4, - decoration: context.isMediumWindow - ? null - : BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - width: 1, - ), - borderRadius: kBorderRadius12, - ), - child: const Column( - children: [ - kHSpacer40, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Card( + margin: EdgeInsets.zero, + color: kColorTransparent, + surfaceTintColor: kColorTransparent, + shape: RoundedRectangleBorder( + side: BorderSide( + color: + Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius12, + ), + elevation: 0, + child: const Padding( + padding: kPv6, + child: Column( children: [ - SizedBox(width: 30), - Text("Variable"), - SizedBox(width: 30), - Text("Value"), - SizedBox(width: 40), + kHSpacer40, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 30), + Text("Variable"), + SizedBox(width: 30), + Text("Value"), + SizedBox(width: 40), + ], + ), + kHSpacer40, + Divider(), + Expanded(child: EditEnvironmentVariables()) ], ), - kHSpacer40, - Divider(), - Expanded(child: EditEnvironmentVariables()) - ], + ), ), ), ), diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart new file mode 100644 index 00000000..e32ca2db --- /dev/null +++ b/lib/screens/history/history_details.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HistoryDetails extends StatelessWidget { + const HistoryDetails({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart new file mode 100644 index 00000000..69f105ab --- /dev/null +++ b/lib/screens/history/history_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/providers/providers.dart'; +import 'history_pane.dart'; +import 'history_viewer.dart'; + +class HistoryPage extends ConsumerWidget { + const HistoryPage({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; + @override + Widget build(BuildContext context, WidgetRef ref) { + final historyModel = ref.watch(selectedHistoryRequestModelProvider); + if (context.isMediumWindow) { + return DrawerSplitView( + scaffoldKey: scaffoldKey, + mainContent: const HistoryViewer(), + title: Text(historyModel?.historyId ?? 'History'), + leftDrawerContent: const HistoryPane(), + actions: const [SizedBox(width: 16)], + onDrawerChanged: (value) => + ref.read(leftDrawerStateProvider.notifier).state = value, + ); + } + return const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: HistoryPane(), + mainWidget: HistoryViewer(), + ), + ), + ], + ); + } +} diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart new file mode 100644 index 00000000..7b32149c --- /dev/null +++ b/lib/screens/history/history_pane.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class HistoryPane extends ConsumerWidget { + const HistoryPane({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: (!context.isMediumWindow && kIsMacOS + ? kP24CollectionPane + : kP8CollectionPane) + + (context.isMediumWindow ? kPb70 : EdgeInsets.zero), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [kVSpacer10, Expanded(child: HistoryList()), kVSpacer5], + ), + ); + } +} + +class HistoryList extends HookConsumerWidget { + const HistoryList({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final historySequence = ref.watch(historySequenceProvider); + final alwaysShowHistoryPaneScrollbar = ref.watch(settingsProvider + .select((value) => value.alwaysShowCollectionPaneScrollbar)); + final List? sortedHistoryKeys = historySequence?.keys.toList(); + sortedHistoryKeys?.sort((a, b) => b.compareTo(a)); + ScrollController scrollController = useScrollController(); + return Scrollbar( + controller: scrollController, + thumbVisibility: alwaysShowHistoryPaneScrollbar, + radius: const Radius.circular(12), + child: ListView( + padding: context.isMediumWindow + ? EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + right: 8, + ) + : kPe8, + controller: scrollController, + children: sortedHistoryKeys != null + ? sortedHistoryKeys.map((date) { + var items = historySequence![date]!; + final requestGroups = getRequestGroups(items); + return Column( + children: [ + ExpansionTile( + title: Text( + humanizeDate(date), + ), + children: requestGroups.values.map((item) { + return Padding( + padding: kPv2 + kPh4, + child: SidebarHistoryCard( + id: item.first.historyId, + models: item, + method: item.first.method, + selectedId: + ref.watch(selectedRequestGroupStateProvider), + requestGroupSize: item.length, + onTap: () { + ref + .read(historyMetaStateNotifier.notifier) + .loadHistoryRequest(item.first.historyId); + }, + ), + ); + }).toList(), + ), + ], + ); + }).toList() + : [ + const Text( + 'No history', + style: TextStyle(color: Colors.grey), + ) + ], + ), + ); + } +} diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart new file mode 100644 index 00000000..a38b09f1 --- /dev/null +++ b/lib/screens/history/history_requests.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class HistoryRequests extends StatelessWidget { + const HistoryRequests({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/screens/history/history_viewer.dart b/lib/screens/history/history_viewer.dart new file mode 100644 index 00000000..4dbb5faf --- /dev/null +++ b/lib/screens/history/history_viewer.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import 'history_details.dart'; +import 'history_requests.dart'; + +class HistoryViewer extends StatelessWidget { + const HistoryViewer({super.key}); + + @override + Widget build(BuildContext context) { + if (context.isMediumWindow) { + return const HistoryDetails(); + } + return Padding( + padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8, + child: Card( + color: kColorTransparent, + surfaceTintColor: kColorTransparent, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius12, + ), + elevation: 0, + child: const HistorySplitView( + sidebarWidget: HistoryRequests(), + mainWidget: HistoryDetails(), + ), + ), + ); + } +} diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index 9d01fe2f..52f49a3f 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -27,7 +27,7 @@ class RequestEditor extends StatelessWidget { ), ) : Padding( - padding: kIsMacOS || kIsWindows ? kPt24o8 : kP8, + padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8, child: const Column( children: [ RequestEditorTopBar(), diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index 9dbcae9c..a74e6094 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -1,3 +1,4 @@ +import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -39,7 +40,7 @@ class _MobileDashboardState extends ConsumerState { ), if (context.isMediumWindow) AnimatedPositioned( - bottom: railIdx > 1 + bottom: railIdx > 2 ? 0 : isLeftDrawerOpen ? 0 @@ -72,17 +73,9 @@ class PageBranch extends ConsumerWidget { return EnvironmentPage( scaffoldKey: scaffoldKey, ); - // case 2: - // // TODO: Implement history page - // return const PageBase( - // title: 'History', - // scaffoldBody: SizedBox(), - // ); case 2: - // TODO: Implement history page - return const PageBase( - title: 'History', - scaffoldBody: SizedBox(), + return HistoryPage( + scaffoldKey: scaffoldKey, ); case 3: return const PageBase( diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index 98c9f529..c7426524 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -1,4 +1,3 @@ -import 'package:apidash/screens/about_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 0b5a86fc..c6a3cc40 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -3,9 +3,14 @@ import 'package:hive_flutter/hive_flutter.dart'; const String kDataBox = "apidash-data"; const String kKeyDataBoxIds = "ids"; + const String kEnvironmentBox = "apidash-environments"; const String kKeyEnvironmentBoxIds = "environmentIds"; +const String kHistoryMetaBox = "apidash-history-meta"; +const String kHistoryBoxIds = "historyIds"; +const String kHistoryLazyBox = "apidash-history-lazy"; + const String kSettingsBox = "apidash-settings"; Future openBoxes() async { @@ -13,6 +18,8 @@ Future openBoxes() async { await Hive.openBox(kDataBox); await Hive.openBox(kSettingsBox); await Hive.openBox(kEnvironmentBox); + await Hive.openBox(kHistoryMetaBox); + await Hive.openLazyBox(kHistoryLazyBox); } (Size?, Offset?) getInitialSize() { @@ -38,11 +45,15 @@ class HiveHandler { late final Box dataBox; late final Box settingsBox; late final Box environmentBox; + late final Box historyMetaBox; + late final LazyBox historyLazyBox; HiveHandler() { dataBox = Hive.box(kDataBox); settingsBox = Hive.box(kSettingsBox); environmentBox = Hive.box(kEnvironmentBox); + historyMetaBox = Hive.box(kHistoryMetaBox); + historyLazyBox = Hive.lazyBox(kHistoryLazyBox); } Map get settings => settingsBox.toMap(); @@ -69,6 +80,25 @@ class HiveHandler { Future deleteEnvironment(String id) => environmentBox.delete(id); + dynamic getHistoryIds() => historyMetaBox.get(kHistoryBoxIds); + Future setHistoryIds(List? ids) => + historyMetaBox.put(kHistoryBoxIds, ids); + + dynamic getHistoryMeta(String id) => historyMetaBox.get(id); + Future setHistoryMeta( + String id, Map? historyMetaJson) => + historyMetaBox.put(id, historyMetaJson); + + Future deleteHistoryMeta(String id) => historyMetaBox.delete(id); + + Future getHistoryRequest(String id) async => + await historyLazyBox.get(id); + Future setHistoryRequest( + String id, Map? historyRequestJsoon) => + historyLazyBox.put(id, historyRequestJsoon); + + Future deleteHistoryReqyest(String id) => historyLazyBox.delete(id); + Future clear() async { await dataBox.clear(); await environmentBox.clear(); diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index e855568b..735ace25 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -1,10 +1,18 @@ import 'dart:typed_data'; import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:intl/intl.dart'; import '../models/models.dart'; import '../consts.dart'; import 'package:http/http.dart' as http; +String humanizeDate(DateTime? date) { + if (date == null) { + return ""; + } + return DateFormat('MMMM d, yyyy').format(date); +} + String humanizeDuration(Duration? duration) { if (duration == null) { return ""; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart new file mode 100644 index 00000000..b1d62fb5 --- /dev/null +++ b/lib/utils/history_utils.dart @@ -0,0 +1,86 @@ +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/convert_utils.dart'; + +DateTime stripTime(DateTime dateTime) { + return DateTime(dateTime.year, dateTime.month, dateTime.day); +} + +String getHistoryRequestName(HistoryMetaModel model) { + if (model.name.isNotEmpty) { + return model.name; + } else { + return model.url; + } +} + +String getHistoryRequestKey(HistoryMetaModel model) { + String timeStamp = humanizeDate(model.timeStamp); + if (model.name.isNotEmpty) { + return model.name + model.method.name + timeStamp; + } else { + return model.url + model.method.name + timeStamp; + } +} + +String? getLatestRequestId( + Map> temporalGroups) { + if (temporalGroups.isEmpty) { + return null; + } + List keys = temporalGroups.keys.toList(); + keys.sort((a, b) => b.compareTo(a)); + return temporalGroups[keys.first]!.first.historyId; +} + +DateTime getDateTimeKey(List keys, DateTime currentKey) { + if (keys.isEmpty) return currentKey; + for (DateTime key in keys) { + if (key.year == currentKey.year && + key.month == currentKey.month && + key.day == currentKey.day) { + return key; + } + } + return stripTime(currentKey); +} + +Map> getTemporalGroups( + List? models) { + Map> temporalGroups = {}; + if (models?.isEmpty ?? true) { + return temporalGroups; + } + for (HistoryMetaModel model in models!) { + List existingKeys = temporalGroups.keys.toList(); + DateTime key = getDateTimeKey(existingKeys, model.timeStamp); + if (existingKeys.contains(key)) { + temporalGroups[key]!.add(model); + } else { + temporalGroups[stripTime(key)] = [model]; + } + } + temporalGroups.forEach((key, value) { + value.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); + }); + return temporalGroups; +} + +Map> getRequestGroups( + List? models) { + Map> historyGroups = {}; + if (models?.isEmpty ?? true) { + return historyGroups; + } + for (HistoryMetaModel model in models!) { + String key = getHistoryRequestKey(model); + if (historyGroups.containsKey(key)) { + historyGroups[key]!.add(model); + } else { + historyGroups[key] = [model]; + } + } + historyGroups.forEach((key, value) { + value.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); + }); + return historyGroups; +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 1857825d..d38e34b8 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,6 +1,7 @@ export 'ui_utils.dart'; export 'convert_utils.dart'; export 'header_utils.dart'; +export 'history_utils.dart'; export 'http_utils.dart'; export 'file_utils.dart'; export 'window_utils.dart'; diff --git a/lib/widgets/card_sidebar_history.dart b/lib/widgets/card_sidebar_history.dart new file mode 100644 index 00000000..4a5e638d --- /dev/null +++ b/lib/widgets/card_sidebar_history.dart @@ -0,0 +1,110 @@ +import 'package:apidash/models/history_meta_model.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'texts.dart' show MethodBox; + +class SidebarHistoryCard extends StatelessWidget { + const SidebarHistoryCard({ + super.key, + required this.id, + required this.models, + required this.method, + this.selectedId, + this.requestGroupSize = 1, + this.onTap, + }); + + final String id; + final List models; + final HTTPVerb method; + final String? selectedId; + final int requestGroupSize; + final Function()? onTap; + + @override + Widget build(BuildContext context) { + final Color color = Theme.of(context).colorScheme.surface; + final Color colorVariant = + Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5); + final model = models.first; + final Color surfaceTint = Theme.of(context).colorScheme.primary; + bool isSelected = selectedId == getHistoryRequestKey(model); + final String name = getHistoryRequestName(model); + return Tooltip( + message: name, + triggerMode: TooltipTriggerMode.manual, + waitDuration: const Duration(seconds: 1), + child: Card( + shape: const RoundedRectangleBorder( + borderRadius: kBorderRadius8, + ), + elevation: isSelected ? 1 : 0, + surfaceTintColor: isSelected ? surfaceTint : null, + color: isSelected + ? Theme.of(context).colorScheme.brightness == Brightness.dark + ? colorVariant + : color + : color, + margin: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + borderRadius: kBorderRadius8, + hoverColor: colorVariant, + focusColor: colorVariant.withOpacity(0.5), + child: Padding( + padding: const EdgeInsets.only( + left: 6, + right: 6, + top: 5, + bottom: 5, + ), + child: SizedBox( + height: 20, + child: Row( + children: [ + MethodBox(method: method), + kHSpacer4, + Expanded( + child: Text( + name, + softWrap: false, + overflow: TextOverflow.fade, + ), + ), + requestGroupSize > 1 ? kHSpacer4 : const SizedBox.shrink(), + Visibility( + visible: requestGroupSize > 1, + child: Container( + padding: kPh4, + constraints: const BoxConstraints(minWidth: 24), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: kBorderRadius6, + ), + child: Center( + child: Text( + requestGroupSize == 2 + ? requestGroupSize.toString() + : "9+", + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + )), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/splitview_history.dart b/lib/widgets/splitview_history.dart new file mode 100644 index 00000000..698a76db --- /dev/null +++ b/lib/widgets/splitview_history.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:multi_split_view/multi_split_view.dart'; +import 'package:apidash/consts.dart'; + +class HistorySplitView extends StatefulWidget { + const HistorySplitView({ + super.key, + required this.sidebarWidget, + required this.mainWidget, + }); + + final Widget sidebarWidget; + final Widget mainWidget; + + @override + HistorySplitViewState createState() => HistorySplitViewState(); +} + +class HistorySplitViewState extends State { + final MultiSplitViewController _controller = MultiSplitViewController( + areas: [ + Area(id: "sidebar", min: 200, size: 220, max: 300), + Area(id: "main"), + ], + ); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MultiSplitViewTheme( + data: MultiSplitViewThemeData( + dividerThickness: 3, + dividerPainter: DividerPainters.background( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + highlightedColor: Theme.of(context).colorScheme.outline.withOpacity( + kHintOpacity, + ), + animationEnabled: false, + ), + ), + child: MultiSplitView( + controller: _controller, + sizeOverflowPolicy: SizeOverflowPolicy.shrinkFirst, + sizeUnderflowPolicy: SizeUnderflowPolicy.stretchLast, + builder: (context, area) { + return switch (area.id) { + "sidebar" => widget.sidebarWidget, + "main" => widget.mainWidget, + _ => Container(), + }; + }, + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 15f36376..cf8c84fe 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -6,6 +6,7 @@ export 'button_save_download.dart'; export 'button_send.dart'; export 'card_request_details.dart'; export 'card_sidebar_environment.dart'; +export 'card_sidebar_history.dart'; export 'card_sidebar_request.dart'; export 'checkbox.dart'; export 'code_previewer.dart'; @@ -42,6 +43,7 @@ export 'snackbars.dart'; export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; +export 'splitview_history.dart'; export 'suggestions_menu.dart'; export 'tables.dart'; export 'tabs.dart'; diff --git a/pubspec.lock b/pubspec.lock index 0e8ed727..47f739eb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -648,6 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d7b371b..1199ac67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: just_audio_mpv: ^0.1.7 just_audio_windows: ^0.2.0 freezed_annotation: ^2.4.1 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 printing: ^5.12.0 package_info_plus: ^8.0.0 flutter_typeahead: ^5.2.0 @@ -64,6 +64,7 @@ dependencies: flutter_hooks: ^0.20.5 flutter_portal: ^1.1.4 mention_tag_text_field: ^0.0.5 + intl: ^0.19.0 dependency_overrides: web: ^0.5.0 From cad6c97f89ce126d07ac3b3b1a56c70006f46c05 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Fri, 19 Jul 2024 18:11:37 +0530 Subject: [PATCH 02/13] wip: history details pane --- lib/consts.dart | 3 + lib/providers/history_providers.dart | 4 +- lib/screens/common_widgets/button_navbar.dart | 4 +- lib/screens/envvar/environment_editor.dart | 17 +++-- .../history/details_pane/url_card.dart | 66 +++++++++++++++++ lib/screens/history/history_details.dart | 36 +++++++++- lib/screens/history/history_pane.dart | 16 ++--- lib/screens/history/history_requests.dart | 28 +++++++- lib/screens/mobile/navbar.dart | 9 --- .../requests_page/request_response_tabs.dart | 72 +++---------------- lib/utils/history_utils.dart | 17 +++++ lib/widgets/card_sidebar_history.dart | 11 ++- lib/widgets/error_message.dart | 66 ++++++++--------- lib/widgets/field_raw.dart | 3 + lib/widgets/splitview_history.dart | 2 +- lib/widgets/tabbar_request_response.dart | 56 +++++++++++++++ lib/widgets/widgets.dart | 1 + 17 files changed, 275 insertions(+), 136 deletions(-) create mode 100644 lib/screens/history/details_pane/url_card.dart create mode 100644 lib/widgets/tabbar_request_response.dart diff --git a/lib/consts.dart b/lib/consts.dart index 63e1946f..4d87cdf7 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -128,6 +128,9 @@ const kPt8 = EdgeInsets.only( const kPt20 = EdgeInsets.only( top: 20, ); +const kPt24 = EdgeInsets.only( + top: 24, +); const kPt28 = EdgeInsets.only( top: 28, ); diff --git a/lib/providers/history_providers.dart b/lib/providers/history_providers.dart index 3f7d7c95..6f5d23df 100644 --- a/lib/providers/history_providers.dart +++ b/lib/providers/history_providers.dart @@ -5,12 +5,12 @@ import '../utils/history_utils.dart'; final selectedHistoryIdStateProvider = StateProvider((ref) => null); -final selectedRequestGroupStateProvider = StateProvider((ref) { +final selectedRequestGroupIdStateProvider = StateProvider((ref) { final selectedHistoryId = ref.watch(selectedHistoryIdStateProvider); + final historyMetaState = ref.read(historyMetaStateNotifier); if (selectedHistoryId == null) { return null; } - final historyMetaState = ref.read(historyMetaStateNotifier); return getHistoryRequestKey(historyMetaState![selectedHistoryId]!); }); diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index 1daf31cd..2cefc201 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -37,7 +37,7 @@ class NavbarButton extends ConsumerWidget { if (buttonIdx != null) { ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; - if (railIdx > 1 && buttonIdx! <= 1) { + if (railIdx > 2 && buttonIdx! <= 2) { ref.read(leftDrawerStateProvider.notifier).state = false; } } @@ -62,7 +62,7 @@ class NavbarButton extends ConsumerWidget { if (buttonIdx != null) { ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; - if (railIdx > 1 && buttonIdx! <= 1) { + if (railIdx > 2 && buttonIdx! <= 2) { ref.read(leftDrawerStateProvider.notifier).state = false; } diff --git a/lib/screens/envvar/environment_editor.dart b/lib/screens/envvar/environment_editor.dart index b4591e35..934d7ad8 100644 --- a/lib/screens/envvar/environment_editor.dart +++ b/lib/screens/envvar/environment_editor.dart @@ -70,13 +70,16 @@ class EnvironmentEditor extends ConsumerWidget { margin: EdgeInsets.zero, color: kColorTransparent, surfaceTintColor: kColorTransparent, - shape: RoundedRectangleBorder( - side: BorderSide( - color: - Theme.of(context).colorScheme.surfaceContainerHighest, - ), - borderRadius: kBorderRadius12, - ), + shape: context.isMediumWindow + ? null + : RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest, + ), + borderRadius: kBorderRadius12, + ), elevation: 0, child: const Padding( padding: kPv6, diff --git a/lib/screens/history/details_pane/url_card.dart b/lib/screens/history/details_pane/url_card.dart new file mode 100644 index 00000000..af5d4a41 --- /dev/null +++ b/lib/screens/history/details_pane/url_card.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class HistoryURLCard extends StatelessWidget { + const HistoryURLCard({ + super.key, + required this.method, + required this.url, + }); + + final HTTPVerb method; + final String url; + + @override + Widget build(BuildContext context) { + final fontSize = Theme.of(context).textTheme.titleMedium?.fontSize; + return LayoutBuilder(builder: (context, constraints) { + final isCompact = constraints.maxWidth <= kMinWindowSize.width; + return Card( + color: kColorTransparent, + surfaceTintColor: kColorTransparent, + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + borderRadius: kBorderRadius8, + ), + child: Padding( + padding: EdgeInsets.symmetric( + vertical: 12, + horizontal: isCompact ? 10 : 16, + ), + child: Row( + children: [ + isCompact ? const SizedBox.shrink() : kHSpacer10, + Text( + method.name.toUpperCase(), + style: kCodeStyle.copyWith( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: getHTTPMethodColor( + method, + brightness: Theme.of(context).brightness, + ), + ), + ), + isCompact ? kHSpacer10 : kHSpacer20, + Expanded( + child: RawTextField( + readOnly: true, + controller: TextEditingController(text: url), + style: kCodeStyle.copyWith( + fontSize: fontSize, + ), + ), + ) + ], + ), + ), + ); + }); + } +} diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index e32ca2db..cee748ba 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -1,10 +1,42 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; +import './details_pane/url_card.dart'; -class HistoryDetails extends StatelessWidget { +class HistoryDetails extends StatefulHookConsumerWidget { const HistoryDetails({super.key}); + @override + ConsumerState createState() => _HistoryDetailsState(); +} + +class _HistoryDetailsState extends ConsumerState + with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return Container(); + final selectedHistoryRequest = + ref.watch(selectedHistoryRequestModelProvider); + final metaData = selectedHistoryRequest?.metaData; + + final TabController controller = + useTabController(initialLength: 2, vsync: this); + + return selectedHistoryRequest != null + ? Column( + children: [ + Padding( + padding: kP4, + child: HistoryURLCard( + method: metaData!.method, url: metaData.url)), + kVSpacer10, + RequestResponseTabbar( + controller: controller, + ), + ], + ) + : const Text("No Request Selected"); } } diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index 7b32149c..00620f92 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -15,9 +15,7 @@ class HistoryPane extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Padding( - padding: (!context.isMediumWindow && kIsMacOS - ? kP24CollectionPane - : kP8CollectionPane) + + padding: (!context.isMediumWindow && kIsMacOS ? kPt24 : kPt8) + (context.isMediumWindow ? kPb70 : EdgeInsets.zero), child: const Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -34,6 +32,7 @@ class HistoryList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); final historySequence = ref.watch(historySequenceProvider); final alwaysShowHistoryPaneScrollbar = ref.watch(settingsProvider .select((value) => value.alwaysShowCollectionPaneScrollbar)); @@ -45,12 +44,7 @@ class HistoryList extends HookConsumerWidget { thumbVisibility: alwaysShowHistoryPaneScrollbar, radius: const Radius.circular(12), child: ListView( - padding: context.isMediumWindow - ? EdgeInsets.only( - bottom: MediaQuery.paddingOf(context).bottom, - right: 8, - ) - : kPe8, + padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom), controller: scrollController, children: sortedHistoryKeys != null ? sortedHistoryKeys.map((date) { @@ -69,8 +63,8 @@ class HistoryList extends HookConsumerWidget { id: item.first.historyId, models: item, method: item.first.method, - selectedId: - ref.watch(selectedRequestGroupStateProvider), + isSelected: selectedGroupId == + getHistoryRequestKey(item.first), requestGroupSize: item.length, onTap: () { ref diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index a38b09f1..1a80c2be 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -1,10 +1,32 @@ import 'package:flutter/material.dart'; +import 'package:apidash/providers/history_providers.dart'; +import 'package:apidash/utils/history_utils.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class HistoryRequests extends StatelessWidget { +class HistoryRequests extends ConsumerWidget { const HistoryRequests({super.key}); @override - Widget build(BuildContext context) { - return Container(); + Widget build(BuildContext context, WidgetRef ref) { + final selectedRequestId = ref.watch(selectedHistoryIdStateProvider); + final selectedRequest = ref.read(selectedHistoryRequestModelProvider); + final historyMetas = ref.read(historyMetaStateNotifier); + final requestGroup = getRequestGroup( + historyMetas?.values.toList(), selectedRequest?.metaData); + return Column( + children: requestGroup + .map((request) => SidebarHistoryCard( + id: request.historyId, + method: request.method, + isSelected: selectedRequestId == request.historyId, + onTap: () { + ref.read(selectedHistoryIdStateProvider.notifier).state = + request.historyId; + }, + models: [request], + )) + .toList(), + ); } } diff --git a/lib/screens/mobile/navbar.dart b/lib/screens/mobile/navbar.dart index 04caf61a..5449f45f 100644 --- a/lib/screens/mobile/navbar.dart +++ b/lib/screens/mobile/navbar.dart @@ -57,15 +57,6 @@ class BottomNavBar extends ConsumerWidget { label: 'History', ), ), - // Expanded( - // child: NavbarButton( - // railIdx: railIdx, - // buttonIdx: 2, - // selectedIcon: Icons.history, - // icon: Icons.history_outlined, - // label: 'History', - // ), - // ), Expanded( child: NavbarButton( railIdx: railIdx, diff --git a/lib/screens/mobile/requests_page/request_response_tabs.dart b/lib/screens/mobile/requests_page/request_response_tabs.dart index 38fab16a..36ad6c11 100644 --- a/lib/screens/mobile/requests_page/request_response_tabs.dart +++ b/lib/screens/mobile/requests_page/request_response_tabs.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import '../../home_page/editor_pane/details_card/response_pane.dart'; import '../../home_page/editor_pane/editor_request.dart'; @@ -30,72 +31,21 @@ class RequestResponseTabs extends StatelessWidget { } } -class RequestResponseTabbar extends StatelessWidget { - const RequestResponseTabbar({ - super.key, - required this.controller, - }); - - final TabController controller; - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - width: kReqResTabWidth, - height: kReqResTabHeight, - decoration: BoxDecoration( - borderRadius: kBorderRadius20, - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - ), - child: ClipRRect( - borderRadius: kBorderRadius20, - child: TabBar( - dividerColor: Colors.transparent, - indicatorWeight: 0.0, - indicatorSize: TabBarIndicatorSize.tab, - unselectedLabelColor: - Theme.of(context).colorScheme.onSurface.withOpacity(0.4), - labelStyle: kTextStyleTab.copyWith( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimary, - ), - unselectedLabelStyle: kTextStyleTab, - splashBorderRadius: kBorderRadius20, - indicator: BoxDecoration( - borderRadius: kBorderRadius20, - color: Theme.of(context).colorScheme.primary, - ), - controller: controller, - tabs: const [ - Tab( - text: kLabelRequest, - ), - Tab( - text: kLabelResponse, - ), - ], - ), - ), - ), - ); - } -} - class RequestResponseTabviews extends StatelessWidget { const RequestResponseTabviews({super.key, required this.controller}); final TabController controller; @override Widget build(BuildContext context) { - return TabBarView(controller: controller, children: const [ - RequestEditor(), - Padding( - padding: kPt8, - child: ResponsePane(), - ), - ]); + return TabBarView( + controller: controller, + children: const [ + RequestEditor(), + Padding( + padding: kPt8, + child: ResponsePane(), + ), + ], + ); } } diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index b1d62fb5..de04cd24 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -84,3 +84,20 @@ Map> getRequestGroups( }); return historyGroups; } + +List getRequestGroup( + List? models, HistoryMetaModel? selectedModel) { + List requestGroup = []; + if (selectedModel == null || (models?.isEmpty ?? true)) { + return requestGroup; + } + String selectedModelKey = getHistoryRequestKey(selectedModel); + for (HistoryMetaModel model in models!) { + String key = getHistoryRequestKey(model); + if (key == selectedModelKey) { + requestGroup.add(model); + } + } + requestGroup.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); + return requestGroup; +} diff --git a/lib/widgets/card_sidebar_history.dart b/lib/widgets/card_sidebar_history.dart index 4a5e638d..be6387b3 100644 --- a/lib/widgets/card_sidebar_history.dart +++ b/lib/widgets/card_sidebar_history.dart @@ -10,7 +10,7 @@ class SidebarHistoryCard extends StatelessWidget { required this.id, required this.models, required this.method, - this.selectedId, + this.isSelected = false, this.requestGroupSize = 1, this.onTap, }); @@ -18,7 +18,7 @@ class SidebarHistoryCard extends StatelessWidget { final String id; final List models; final HTTPVerb method; - final String? selectedId; + final bool isSelected; final int requestGroupSize; final Function()? onTap; @@ -29,7 +29,6 @@ class SidebarHistoryCard extends StatelessWidget { Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5); final model = models.first; final Color surfaceTint = Theme.of(context).colorScheme.primary; - bool isSelected = selectedId == getHistoryRequestKey(model); final String name = getHistoryRequestName(model); return Tooltip( message: name, @@ -84,9 +83,9 @@ class SidebarHistoryCard extends StatelessWidget { ), child: Center( child: Text( - requestGroupSize == 2 - ? requestGroupSize.toString() - : "9+", + requestGroupSize > 9 + ? "9+" + : requestGroupSize.toString(), style: Theme.of(context) .textTheme .labelSmall diff --git a/lib/widgets/error_message.dart b/lib/widgets/error_message.dart index b6e9cf46..ed8a2cb8 100644 --- a/lib/widgets/error_message.dart +++ b/lib/widgets/error_message.dart @@ -20,38 +20,40 @@ class ErrorMessage extends StatelessWidget { return Padding( padding: kPh20v10, child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - showIcon - ? Icon( - Icons.warning_rounded, - size: 40, - color: color, - ) - : const SizedBox(), - SelectableText( - message ?? 'An error occurred. $kUnexpectedRaiseIssue', - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(color: color), - ), - kVSpacer20, - showIssueButton - ? FilledButton.tonalIcon( - onPressed: () { - launchUrl(Uri.parse(kGitUrl)); - }, - icon: const Icon(Icons.arrow_outward_rounded), - label: Text( - 'Raise Issue', - style: Theme.of(context).textTheme.titleMedium, - ), - ) - : const SizedBox(), - ], + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + showIcon + ? Icon( + Icons.warning_rounded, + size: 40, + color: color, + ) + : const SizedBox(), + SelectableText( + message ?? 'An error occurred. $kUnexpectedRaiseIssue', + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: color), + ), + kVSpacer20, + showIssueButton + ? FilledButton.tonalIcon( + onPressed: () { + launchUrl(Uri.parse(kGitUrl)); + }, + icon: const Icon(Icons.arrow_outward_rounded), + label: Text( + 'Raise Issue', + style: Theme.of(context).textTheme.titleMedium, + ), + ) + : const SizedBox(), + ], + ), ), ), ); diff --git a/lib/widgets/field_raw.dart b/lib/widgets/field_raw.dart index 8adf6fd0..1f216b74 100644 --- a/lib/widgets/field_raw.dart +++ b/lib/widgets/field_raw.dart @@ -8,16 +8,19 @@ class RawTextField extends StatelessWidget { this.controller, this.hintText, this.style, + this.readOnly = false, }); final void Function(String)? onChanged; final TextEditingController? controller; final String? hintText; final TextStyle? style; + final bool readOnly; @override Widget build(BuildContext context) { return TextField( + readOnly: readOnly, controller: controller, onChanged: onChanged, style: style, diff --git a/lib/widgets/splitview_history.dart b/lib/widgets/splitview_history.dart index 698a76db..b955eaeb 100644 --- a/lib/widgets/splitview_history.dart +++ b/lib/widgets/splitview_history.dart @@ -19,7 +19,7 @@ class HistorySplitView extends StatefulWidget { class HistorySplitViewState extends State { final MultiSplitViewController _controller = MultiSplitViewController( areas: [ - Area(id: "sidebar", min: 200, size: 220, max: 300), + Area(id: "sidebar", min: 200, size: 250, max: 300), Area(id: "main"), ], ); diff --git a/lib/widgets/tabbar_request_response.dart b/lib/widgets/tabbar_request_response.dart new file mode 100644 index 00000000..5a117120 --- /dev/null +++ b/lib/widgets/tabbar_request_response.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class RequestResponseTabbar extends StatelessWidget { + const RequestResponseTabbar({ + super.key, + required this.controller, + }); + + final TabController controller; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: kReqResTabWidth, + height: kReqResTabHeight, + decoration: BoxDecoration( + borderRadius: kBorderRadius20, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + ), + ), + child: ClipRRect( + borderRadius: kBorderRadius20, + child: TabBar( + dividerColor: Colors.transparent, + indicatorWeight: 0.0, + indicatorSize: TabBarIndicatorSize.tab, + unselectedLabelColor: + Theme.of(context).colorScheme.onSurface.withOpacity(0.4), + labelStyle: kTextStyleTab.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onPrimary, + ), + unselectedLabelStyle: kTextStyleTab, + splashBorderRadius: kBorderRadius20, + indicator: BoxDecoration( + borderRadius: kBorderRadius20, + color: Theme.of(context).colorScheme.primary, + ), + controller: controller, + tabs: const [ + Tab( + text: kLabelRequest, + ), + Tab( + text: kLabelResponse, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index cf8c84fe..81a7018d 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -45,6 +45,7 @@ export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; export 'suggestions_menu.dart'; +export 'tabbar_request_response.dart'; export 'tables.dart'; export 'tabs.dart'; export 'texts.dart'; From f8ede1edc876ba309be8a486a9cf55e8a2b46f6c Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sat, 20 Jul 2024 22:05:08 +0530 Subject: [PATCH 03/13] wip: history panes --- lib/providers/ui_providers.dart | 1 + .../code_pane.dart | 14 ++- .../common_widgets/common_widgets.dart | 1 + .../details_pane/his_request_pane.dart | 60 +++++++++++ .../details_pane/his_response_pane.dart | 48 +++++++++ .../history/details_pane/url_card.dart | 5 +- lib/screens/history/history_details.dart | 69 +++++++++--- lib/screens/history/history_page.dart | 6 +- lib/screens/history/history_pane.dart | 9 +- lib/screens/history/history_requests.dart | 23 ++-- .../details_card/details_card.dart | 2 +- .../mobile/requests_page/requests_page.dart | 1 - lib/utils/convert_utils.dart | 7 ++ lib/utils/history_utils.dart | 14 ++- lib/widgets/card_history_request.dart | 65 +++++++++++ lib/widgets/field_read_only.dart | 30 ++++++ lib/widgets/request_widgets.dart | 43 +++----- lib/widgets/response_widgets.dart | 9 +- lib/widgets/{tables.dart => table_map.dart} | 0 lib/widgets/table_request.dart | 101 ++++++++++++++++++ lib/widgets/texts.dart | 21 ++++ lib/widgets/widgets.dart | 6 +- .../csharp_rest_sharp_codgen_test.dart | 2 +- test/codegen/go_http_codegen_test.dart | 2 +- test/providers/ui_providers_test.dart | 3 - test/widgets/tables_test.dart | 2 +- 26 files changed, 473 insertions(+), 71 deletions(-) rename lib/screens/{home_page/editor_pane/details_card => common_widgets}/code_pane.dart (79%) create mode 100644 lib/screens/history/details_pane/his_request_pane.dart create mode 100644 lib/screens/history/details_pane/his_response_pane.dart create mode 100644 lib/widgets/card_history_request.dart create mode 100644 lib/widgets/field_read_only.dart rename lib/widgets/{tables.dart => table_map.dart} (100%) create mode 100644 lib/widgets/table_request.dart diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 9b6cc3e0..0478b040 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -9,6 +9,7 @@ final navRailIndexStateProvider = StateProvider((ref) => 0); final selectedIdEditStateProvider = StateProvider((ref) => null); final environmentFieldEditStateProvider = StateProvider((ref) => null); final codePaneVisibleStateProvider = StateProvider((ref) => false); +final historyCodePaneVisibleStateProvider = StateProvider((ref) => false); final saveDataStateProvider = StateProvider((ref) => false); final clearDataStateProvider = StateProvider((ref) => false); final hasUnsavedChangesProvider = StateProvider((ref) => false); diff --git a/lib/screens/home_page/editor_pane/details_card/code_pane.dart b/lib/screens/common_widgets/code_pane.dart similarity index 79% rename from lib/screens/home_page/editor_pane/details_card/code_pane.dart rename to lib/screens/common_widgets/code_pane.dart index f61c10a6..1da84542 100644 --- a/lib/screens/home_page/editor_pane/details_card/code_pane.dart +++ b/lib/screens/common_widgets/code_pane.dart @@ -9,14 +9,24 @@ import 'package:apidash/consts.dart'; final Codegen codegen = Codegen(); class CodePane extends ConsumerWidget { - const CodePane({super.key}); + const CodePane({ + super.key, + this.isHistoryRequest = false, + }); + + final bool isHistoryRequest; @override Widget build(BuildContext context, WidgetRef ref) { final CodegenLanguage codegenLanguage = ref.watch(codegenLanguageStateProvider); - final selectedRequestModel = ref.watch(selectedRequestModelProvider); + final selectedHistoryRequestModel = + ref.watch(selectedHistoryRequestModelProvider); + + final selectedRequestModel = isHistoryRequest + ? getRequestModelFromHistoryModel(selectedHistoryRequestModel!) + : ref.watch(selectedRequestModelProvider); final defaultUriScheme = ref.watch(settingsProvider.select((value) => value.defaultUriScheme)); diff --git a/lib/screens/common_widgets/common_widgets.dart b/lib/screens/common_widgets/common_widgets.dart index 6c08887a..02a1f583 100644 --- a/lib/screens/common_widgets/common_widgets.dart +++ b/lib/screens/common_widgets/common_widgets.dart @@ -1,4 +1,5 @@ export 'button_navbar.dart'; +export 'code_pane.dart'; export 'editor_title.dart'; export 'editor_title_actions.dart'; export 'envfield_url.dart'; diff --git a/lib/screens/history/details_pane/his_request_pane.dart b/lib/screens/history/details_pane/his_request_pane.dart new file mode 100644 index 00000000..6378a871 --- /dev/null +++ b/lib/screens/history/details_pane/his_request_pane.dart @@ -0,0 +1,60 @@ +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 HistoryRequestPane extends ConsumerWidget { + const HistoryRequestPane({ + super.key, + this.isCompact = false, + }); + + final bool isCompact; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedHistoryIdStateProvider); + final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); + + final headersMap = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.httpRequestModel.headersMap)) ?? + {}; + final headerLength = headersMap.length; + + final paramsMap = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.httpRequestModel.paramsMap)) ?? + {}; + final paramLength = paramsMap.length; + + final hasBody = ref.watch(selectedHistoryRequestModelProvider + .select((value) => value?.httpRequestModel.hasBody)) ?? + false; + + return RequestPane( + selectedId: selectedId, + codePaneVisible: codePaneVisible, + onPressedCodeButton: () { + ref.read(historyCodePaneVisibleStateProvider.notifier).state = + !codePaneVisible; + }, + showViewCodeButton: !isCompact, + showIndicators: [ + paramLength > 0, + headerLength > 0, + hasBody, + ], + children: [ + RequestDataTable( + rows: paramsMap, + keyName: kNameURLParam, + ), + RequestDataTable( + rows: headersMap, + keyName: kNameHeader, + ), + const SizedBox(), + ], + ); + } +} diff --git a/lib/screens/history/details_pane/his_response_pane.dart b/lib/screens/history/details_pane/his_response_pane.dart new file mode 100644 index 00000000..f90390cb --- /dev/null +++ b/lib/screens/history/details_pane/his_response_pane.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; + +class HistoryResponsePane extends ConsumerWidget { + const HistoryResponsePane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedId = ref.watch(selectedHistoryIdStateProvider); + final selectedHistoryRequest = + ref.watch(selectedHistoryRequestModelProvider); + final historyHttpResponseModel = selectedHistoryRequest?.httpResponseModel; + + if (selectedId != null) { + final requestModel = + getRequestModelFromHistoryModel(selectedHistoryRequest!); + return Column( + children: [ + ResponsePaneHeader( + responseStatus: historyHttpResponseModel?.statusCode, + message: kResponseCodeReasons[historyHttpResponseModel?.statusCode], + time: historyHttpResponseModel?.time, + ), + Expanded( + child: ResponseTabView( + selectedId: selectedId, + children: [ + ResponseBody( + selectedRequestModel: requestModel, + ), + ResponseHeaders( + responseHeaders: historyHttpResponseModel?.headers ?? {}, + requestHeaders: + historyHttpResponseModel?.requestHeaders ?? {}, + ), + ], + ), + ), + ], + ); + } + return const Text("No Request Selected"); + } +} diff --git a/lib/screens/history/details_pane/url_card.dart b/lib/screens/history/details_pane/url_card.dart index af5d4a41..cb8ec0da 100644 --- a/lib/screens/history/details_pane/url_card.dart +++ b/lib/screens/history/details_pane/url_card.dart @@ -49,9 +49,8 @@ class HistoryURLCard extends StatelessWidget { ), isCompact ? kHSpacer10 : kHSpacer20, Expanded( - child: RawTextField( - readOnly: true, - controller: TextEditingController(text: url), + child: ReadOnlyTextField( + initialValue: url, style: kCodeStyle.copyWith( fontSize: fontSize, ), diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index cee748ba..7498fa5f 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -4,7 +4,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; -import './details_pane/url_card.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; +import 'details_pane/url_card.dart'; +import 'details_pane/his_request_pane.dart'; +import 'details_pane/his_response_pane.dart'; class HistoryDetails extends StatefulHookConsumerWidget { const HistoryDetails({super.key}); @@ -21,22 +24,62 @@ class _HistoryDetailsState extends ConsumerState ref.watch(selectedHistoryRequestModelProvider); final metaData = selectedHistoryRequest?.metaData; + final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); + final TabController controller = useTabController(initialLength: 2, vsync: this); return selectedHistoryRequest != null - ? Column( - children: [ - Padding( - padding: kP4, - child: HistoryURLCard( - method: metaData!.method, url: metaData.url)), - kVSpacer10, - RequestResponseTabbar( - controller: controller, - ), - ], + ? LayoutBuilder( + builder: (context, constraints) { + final isCompact = constraints.maxWidth < kMediumWindowWidth; + return Column( + children: [ + kVSpacer5, + Padding( + padding: kPh4, + child: HistoryURLCard( + method: metaData!.method, + url: metaData.url, + )), + kVSpacer10, + if (isCompact) ...[ + RequestResponseTabbar( + controller: controller, + ), + kVSpacer10, + Expanded( + child: TabBarView( + controller: controller, + children: [ + HistoryRequestPane( + isCompact: isCompact, + ), + const HistoryResponsePane(), + ], + )) + ] else ...[ + Expanded( + child: Padding( + padding: kPh4, + child: RequestDetailsCard( + child: EqualSplitView( + leftWidget: HistoryRequestPane( + isCompact: isCompact, + ), + rightWidget: codePaneVisible + ? const CodePane(isHistoryRequest: true) + : const HistoryResponsePane(), + ), + ), + ), + ), + kVSpacer8, + ] + ], + ); + }, ) - : const Text("No Request Selected"); + : const SizedBox.shrink(); } } diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 69f105ab..0f288fb7 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; @@ -16,11 +17,14 @@ class HistoryPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final historyModel = ref.watch(selectedHistoryRequestModelProvider); + final title = historyModel != null + ? getHistoryRequestName(historyModel.metaData) + : 'History'; if (context.isMediumWindow) { return DrawerSplitView( scaffoldKey: scaffoldKey, mainContent: const HistoryViewer(), - title: Text(historyModel?.historyId ?? 'History'), + title: Text(title), leftDrawerContent: const HistoryPane(), actions: const [SizedBox(width: 16)], onDrawerChanged: (value) => diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index 00620f92..be7e36d7 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -56,6 +56,7 @@ class HistoryList extends HookConsumerWidget { title: Text( humanizeDate(date), ), + initiallyExpanded: true, children: requestGroups.values.map((item) { return Padding( padding: kPv2 + kPh4, @@ -79,9 +80,11 @@ class HistoryList extends HookConsumerWidget { ); }).toList() : [ - const Text( - 'No history', - style: TextStyle(color: Colors.grey), + const Center( + child: Text( + 'No history', + style: TextStyle(color: Colors.grey), + ), ) ], ), diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index 1a80c2be..f0bc10cf 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -1,3 +1,4 @@ +import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:apidash/providers/history_providers.dart'; import 'package:apidash/utils/history_utils.dart'; @@ -10,23 +11,29 @@ class HistoryRequests extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedRequestId = ref.watch(selectedHistoryIdStateProvider); - final selectedRequest = ref.read(selectedHistoryRequestModelProvider); - final historyMetas = ref.read(historyMetaStateNotifier); + final selectedRequest = ref.watch(selectedHistoryRequestModelProvider); + final historyMetas = ref.watch(historyMetaStateNotifier); final requestGroup = getRequestGroup( historyMetas?.values.toList(), selectedRequest?.metaData); return Column( - children: requestGroup - .map((request) => SidebarHistoryCard( + children: [ + kVSpacer20, + ...requestGroup.map((request) => Padding( + padding: kPv2 + kPh4, + child: HistoryRequestCard( id: request.historyId, - method: request.method, + model: request, isSelected: selectedRequestId == request.historyId, onTap: () { ref.read(selectedHistoryIdStateProvider.notifier).state = request.historyId; + ref + .read(historyMetaStateNotifier.notifier) + .loadHistoryRequest(request.historyId); }, - models: [request], - )) - .toList(), + ), + )) + ], ); } } 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..739f7475 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 @@ -2,9 +2,9 @@ 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/screens/common_widgets/common_widgets.dart'; import 'request_pane/request_pane.dart'; import 'response_pane.dart'; -import 'code_pane.dart'; class EditorPaneRequestDetailsCard extends ConsumerWidget { const EditorPaneRequestDetailsCard({super.key}); diff --git a/lib/screens/mobile/requests_page/requests_page.dart b/lib/screens/mobile/requests_page/requests_page.dart index 86d4d70d..4164356c 100644 --- a/lib/screens/mobile/requests_page/requests_page.dart +++ b/lib/screens/mobile/requests_page/requests_page.dart @@ -7,7 +7,6 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../home_page/collection_pane.dart'; import '../../home_page/editor_pane/url_card.dart'; -import '../../home_page/editor_pane/details_card/code_pane.dart'; import '../../home_page/editor_pane/editor_default.dart'; import '../../common_widgets/common_widgets.dart'; import '../widgets/page_base.dart'; diff --git a/lib/utils/convert_utils.dart b/lib/utils/convert_utils.dart index 735ace25..c772134e 100644 --- a/lib/utils/convert_utils.dart +++ b/lib/utils/convert_utils.dart @@ -13,6 +13,13 @@ String humanizeDate(DateTime? date) { return DateFormat('MMMM d, yyyy').format(date); } +String humanizeTime(DateTime? time) { + if (time == null) { + return ""; + } + return DateFormat('hh:mm:ss a').format(time); +} + String humanizeDuration(Duration? duration) { if (duration == null) { return ""; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index de04cd24..acbea853 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -1,10 +1,22 @@ -import 'package:apidash/models/models.dart'; import 'package:apidash/utils/convert_utils.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; DateTime stripTime(DateTime dateTime) { return DateTime(dateTime.year, dateTime.month, dateTime.day); } +RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) { + return RequestModel( + id: model.historyId, + name: model.metaData.name, + responseStatus: model.httpResponseModel.statusCode, + message: kResponseCodeReasons[model.httpResponseModel.statusCode], + httpRequestModel: model.httpRequestModel, + httpResponseModel: model.httpResponseModel, + ); +} + String getHistoryRequestName(HistoryMetaModel model) { if (model.name.isNotEmpty) { return model.name; diff --git a/lib/widgets/card_history_request.dart b/lib/widgets/card_history_request.dart new file mode 100644 index 00000000..a76e477c --- /dev/null +++ b/lib/widgets/card_history_request.dart @@ -0,0 +1,65 @@ +import 'package:apidash/models/history_meta_model.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'texts.dart'; + +class HistoryRequestCard extends StatelessWidget { + const HistoryRequestCard({ + super.key, + required this.id, + required this.model, + this.isSelected = false, + this.onTap, + }); + + final String id; + final HistoryMetaModel model; + final bool isSelected; + final Function()? onTap; + + @override + Widget build(BuildContext context) { + final Color color = Theme.of(context).colorScheme.surface; + final Color colorVariant = + Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5); + final Color surfaceTint = Theme.of(context).colorScheme.primary; + return Card( + shape: const ContinuousRectangleBorder(borderRadius: kBorderRadius12), + elevation: isSelected ? 1 : 0, + surfaceTintColor: isSelected ? surfaceTint : null, + color: isSelected + ? Theme.of(context).colorScheme.brightness == Brightness.dark + ? colorVariant + : color + : color, + margin: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + borderRadius: kBorderRadius6, + hoverColor: colorVariant, + focusColor: colorVariant.withOpacity(0.5), + child: Padding( + padding: kPv6 + kPh8, + child: SizedBox( + height: 20, + child: Row( + children: [ + Expanded( + child: Text( + humanizeTime(model.timeStamp), + softWrap: false, + overflow: TextOverflow.fade, + style: kCodeStyle, + ), + ), + kHSpacer4, + StatusCode(statusCode: model.responseStatus), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/field_read_only.dart b/lib/widgets/field_read_only.dart new file mode 100644 index 00000000..abdb0b4e --- /dev/null +++ b/lib/widgets/field_read_only.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class ReadOnlyTextField extends StatelessWidget { + const ReadOnlyTextField({ + super.key, + this.initialValue, + this.style, + this.decoration, + }); + + final String? initialValue; + final TextStyle? style; + final InputDecoration? decoration; + + @override + Widget build(BuildContext context) { + return TextField( + readOnly: true, + controller: TextEditingController(text: initialValue), + style: style, + decoration: decoration ?? + const InputDecoration( + isDense: true, + border: InputBorder.none, + contentPadding: kPv8, + ), + ); + } +} diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index 368bba5f..1e7b7ccf 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/consts.dart'; import 'tabs.dart'; -import 'package:apidash/extensions/extensions.dart'; -class RequestPane extends StatefulWidget { +class RequestPane extends StatefulHookWidget { const RequestPane({ super.key, required this.selectedId, @@ -13,6 +14,7 @@ class RequestPane extends StatefulWidget { this.onTapTabBar, required this.children, this.showIndicators = const [false, false, false], + this.showViewCodeButton, }); final String? selectedId; @@ -22,6 +24,7 @@ class RequestPane extends StatefulWidget { final void Function(int)? onTapTabBar; final List children; final List showIndicators; + final bool? showViewCodeButton; @override State createState() => _RequestPaneState(); @@ -29,28 +32,19 @@ class RequestPane extends StatefulWidget { class _RequestPaneState 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) { + final TabController controller = useTabController( + initialLength: 3, + vsync: this, + ); if (widget.tabIndex != null) { - _controller.index = widget.tabIndex!; + controller.index = widget.tabIndex!; } return Column( children: [ - context.isMediumWindow - ? const SizedBox.shrink() - : Padding( + (widget.showViewCodeButton ?? !context.isMediumWindow) + ? Padding( padding: kP8, child: SizedBox( height: kHeaderHeight, @@ -76,10 +70,11 @@ class _RequestPaneState extends State ], ), ), - ), + ) + : const SizedBox.shrink(), TabBar( key: Key(widget.selectedId!), - controller: _controller, + controller: controller, overlayColor: kColorTransparentState, labelPadding: kPh2, onTap: widget.onTapTabBar, @@ -101,7 +96,7 @@ class _RequestPaneState extends State kVSpacer5, Expanded( child: TabBarView( - controller: _controller, + controller: controller, physics: const NeverScrollableScrollPhysics(), children: widget.children, ), @@ -109,10 +104,4 @@ class _RequestPaneState extends State ], ); } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } } diff --git a/lib/widgets/response_widgets.dart b/lib/widgets/response_widgets.dart index ce07c100..0df9343b 100644 --- a/lib/widgets/response_widgets.dart +++ b/lib/widgets/response_widgets.dart @@ -128,6 +128,7 @@ class ResponsePaneHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final bool showClearButton = onClearResponse != null; return Padding( padding: kPv8, child: SizedBox( @@ -159,9 +160,11 @@ class ResponsePaneHeader extends StatelessWidget { ), ), kHSpacer10, - ClearResponseButton( - onPressed: onClearResponse, - ) + showClearButton + ? ClearResponseButton( + onPressed: onClearResponse, + ) + : const SizedBox.shrink(), ], ), ), diff --git a/lib/widgets/tables.dart b/lib/widgets/table_map.dart similarity index 100% rename from lib/widgets/tables.dart rename to lib/widgets/table_map.dart diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart new file mode 100644 index 00000000..bdde7c47 --- /dev/null +++ b/lib/widgets/table_request.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/consts.dart'; + +class RequestDataTable extends StatelessWidget { + const RequestDataTable({ + super.key, + required this.rows, + this.keyName, + this.valueName, + }); + + final Map rows; + final String? keyName; + final String? valueName; + + @override + Widget build(BuildContext context) { + final clrScheme = Theme.of(context).colorScheme; + + final List columns = [ + DataColumn2( + label: Text(keyName ?? kNameField), + ), + const DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + DataColumn2( + label: Text(valueName ?? kNameValue), + ), + ]; + + final fieldDecoration = InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + ); + + final List dataRows = rows.entries + .map( + (MapEntry entry) => DataRow( + cells: [ + DataCell( + ReadOnlyTextField( + initialValue: entry.key, + decoration: fieldDecoration, + ), + ), + const DataCell( + Text('='), + ), + DataCell( + ReadOnlyTextField( + initialValue: entry.value, + decoration: fieldDecoration, + ), + ), + ], + ), + ) + .toList(); + + return Container( + 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, + ), + ), + ), + kVSpacer40, + ], + ), + ); + } +} diff --git a/lib/widgets/texts.dart b/lib/widgets/texts.dart index f2cf5c92..71753bfc 100644 --- a/lib/widgets/texts.dart +++ b/lib/widgets/texts.dart @@ -32,3 +32,24 @@ class MethodBox extends StatelessWidget { ); } } + +class StatusCode extends StatelessWidget { + const StatusCode({super.key, required this.statusCode, this.style}); + final int statusCode; + final TextStyle? style; + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + final Color color = + getResponseStatusCodeColor(statusCode, brightness: brightness); + return Text( + statusCode.toString(), + style: style?.copyWith(color: color) ?? + Theme.of(context).textTheme.bodyMedium?.copyWith( + fontFamily: kCodeStyle.fontFamily, + color: color, + ), + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index b80b6cf9..9f0323aa 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -4,6 +4,7 @@ export 'button_discord.dart'; export 'button_repo.dart'; export 'button_save_download.dart'; export 'button_send.dart'; +export 'card_history_request.dart'; export 'card_request_details.dart'; export 'card_sidebar_environment.dart'; export 'card_sidebar_history.dart'; @@ -28,6 +29,7 @@ export 'field_cell.dart'; export 'field_header.dart'; export 'field_json_search.dart'; export 'field_raw.dart'; +export 'field_read_only.dart'; export 'field_url.dart'; export 'intro_message.dart'; export 'json_previewer.dart'; @@ -46,9 +48,9 @@ export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; -export 'suggestions_menu.dart'; export 'tabbar_request_response.dart'; -export 'tables.dart'; +export 'table_map.dart'; +export 'table_request.dart'; export 'tabs.dart'; export 'texts.dart'; export 'uint8_audio_player.dart'; diff --git a/test/codegen/csharp_rest_sharp_codgen_test.dart b/test/codegen/csharp_rest_sharp_codgen_test.dart index 6522cbee..df16f928 100644 --- a/test/codegen/csharp_rest_sharp_codgen_test.dart +++ b/test/codegen/csharp_rest_sharp_codgen_test.dart @@ -1,5 +1,5 @@ import 'package:apidash/consts.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../models/request_models.dart'; diff --git a/test/codegen/go_http_codegen_test.dart b/test/codegen/go_http_codegen_test.dart index 08492cf1..89e4c010 100644 --- a/test/codegen/go_http_codegen_test.dart +++ b/test/codegen/go_http_codegen_test.dart @@ -1,6 +1,6 @@ import 'package:apidash/codegen/codegen.dart'; import 'package:apidash/consts.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart'; +import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:test/test.dart'; import '../models/request_models.dart'; diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 6ebf7745..5854f768 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,12 +1,9 @@ import 'dart:io'; -import 'package:spot/spot.dart'; -import 'package:apidash/consts.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/screens/dashboard.dart'; import 'package:apidash/screens/envvar/environment_page.dart'; import 'package:apidash/screens/home_page/collection_pane.dart'; -import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/details_card/response_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_default.dart'; import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; diff --git a/test/widgets/tables_test.dart b/test/widgets/tables_test.dart index 70102cfe..46b4bc1a 100644 --- a/test/widgets/tables_test.dart +++ b/test/widgets/tables_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:apidash/widgets/tables.dart'; +import 'package:apidash/widgets/table_map.dart'; void main() { Map mapInput = { From d9d60961f76d8272626a291852a90bedf930264c Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 21 Jul 2024 04:34:12 +0530 Subject: [PATCH 04/13] wip: history requests sheet --- lib/consts.dart | 6 +- .../common_widgets/sidebar_header.dart | 2 +- lib/screens/history/history_details.dart | 4 +- lib/screens/history/history_page.dart | 17 +- lib/screens/history/history_pane.dart | 150 +++++++++++++----- lib/screens/history/history_requests.dart | 136 +++++++++++++++- .../history_widgets/his_bottombar.dart | 75 +++++++++ .../his_request_pane.dart | 0 .../his_response_pane.dart | 0 .../history_widgets/his_sidebar_header.dart | 50 ++++++ .../his_url_card.dart} | 0 .../history_widgets/history_widgets.dart | 5 + 12 files changed, 380 insertions(+), 65 deletions(-) create mode 100644 lib/screens/history/history_widgets/his_bottombar.dart rename lib/screens/history/{details_pane => history_widgets}/his_request_pane.dart (100%) rename lib/screens/history/{details_pane => history_widgets}/his_response_pane.dart (100%) create mode 100644 lib/screens/history/history_widgets/his_sidebar_header.dart rename lib/screens/history/{details_pane/url_card.dart => history_widgets/his_url_card.dart} (100%) create mode 100644 lib/screens/history/history_widgets/history_widgets.dart diff --git a/lib/consts.dart b/lib/consts.dart index 03172793..63be0705 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -100,13 +100,15 @@ const kP6 = EdgeInsets.all(6); const kP8 = EdgeInsets.all(8); const kPs8 = EdgeInsets.only(left: 8); const kPs2 = EdgeInsets.only(left: 2); +const kPe4 = EdgeInsets.only(right: 4); const kPe8 = EdgeInsets.only(right: 8.0); const kPh20v5 = EdgeInsets.symmetric(horizontal: 20, vertical: 5); const kPh20v10 = EdgeInsets.symmetric(horizontal: 20, vertical: 10); const kP10 = EdgeInsets.all(10); -const kPv8 = EdgeInsets.symmetric(vertical: 8); -const kPv6 = EdgeInsets.symmetric(vertical: 6); const kPv2 = EdgeInsets.symmetric(vertical: 2); +const kPv6 = EdgeInsets.symmetric(vertical: 6); +const kPv8 = EdgeInsets.symmetric(vertical: 8); +const kPv10 = EdgeInsets.symmetric(vertical: 10); const kPh2 = EdgeInsets.symmetric(horizontal: 2); const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = diff --git a/lib/screens/common_widgets/sidebar_header.dart b/lib/screens/common_widgets/sidebar_header.dart index 4e6b16f7..1a26f0ea 100644 --- a/lib/screens/common_widgets/sidebar_header.dart +++ b/lib/screens/common_widgets/sidebar_header.dart @@ -47,7 +47,7 @@ class SidebarHeader extends ConsumerWidget { ? IconButton( style: IconButton.styleFrom( padding: const EdgeInsets.all(4), - minimumSize: const Size(30, 30), + minimumSize: const Size(36, 36), ), onPressed: () { mobileScaffoldKey.currentState?.closeDrawer(); diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index 7498fa5f..10f02251 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -5,9 +5,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; -import 'details_pane/url_card.dart'; -import 'details_pane/his_request_pane.dart'; -import 'details_pane/his_response_pane.dart'; +import 'history_widgets/history_widgets.dart'; class HistoryDetails extends StatefulHookConsumerWidget { const HistoryDetails({super.key}); diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 0f288fb7..3787ce4f 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -6,6 +6,7 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; +import 'history_widgets/history_widgets.dart'; class HistoryPage extends ConsumerWidget { const HistoryPage({ @@ -22,14 +23,14 @@ class HistoryPage extends ConsumerWidget { : 'History'; if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, - mainContent: const HistoryViewer(), - title: Text(title), - leftDrawerContent: const HistoryPane(), - actions: const [SizedBox(width: 16)], - onDrawerChanged: (value) => - ref.read(leftDrawerStateProvider.notifier).state = value, - ); + scaffoldKey: scaffoldKey, + mainContent: const HistoryViewer(), + title: Text(title), + leftDrawerContent: const HistoryPane(), + actions: const [SizedBox(width: 16)], + onDrawerChanged: (value) => + ref.read(leftDrawerStateProvider.notifier).state = value, + bottomNavigationBar: const HistoryPageBottombar()); } return const Column( children: [ diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index be7e36d7..d206dabb 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -4,8 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/consts.dart'; +import 'history_widgets/history_widgets.dart'; class HistoryPane extends ConsumerWidget { const HistoryPane({ @@ -19,7 +21,11 @@ class HistoryPane extends ConsumerWidget { (context.isMediumWindow ? kPb70 : EdgeInsets.zero), child: const Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [kVSpacer10, Expanded(child: HistoryList()), kVSpacer5], + children: [ + HistorySidebarHeader(), + Expanded(child: HistoryList()), + kVSpacer5, + ], ), ); } @@ -32,7 +38,6 @@ class HistoryList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); final historySequence = ref.watch(historySequenceProvider); final alwaysShowHistoryPaneScrollbar = ref.watch(settingsProvider .select((value) => value.alwaysShowCollectionPaneScrollbar)); @@ -43,51 +48,108 @@ class HistoryList extends HookConsumerWidget { controller: scrollController, thumbVisibility: alwaysShowHistoryPaneScrollbar, radius: const Radius.circular(12), - child: ListView( + child: ListView.separated( padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom), controller: scrollController, - children: sortedHistoryKeys != null - ? sortedHistoryKeys.map((date) { - var items = historySequence![date]!; - final requestGroups = getRequestGroups(items); - return Column( - children: [ - ExpansionTile( - title: Text( - humanizeDate(date), - ), - initiallyExpanded: true, - children: requestGroups.values.map((item) { - return Padding( - padding: kPv2 + kPh4, - child: SidebarHistoryCard( - id: item.first.historyId, - models: item, - method: item.first.method, - isSelected: selectedGroupId == - getHistoryRequestKey(item.first), - requestGroupSize: item.length, - onTap: () { - ref - .read(historyMetaStateNotifier.notifier) - .loadHistoryRequest(item.first.historyId); - }, - ), - ); - }).toList(), - ), - ], - ); - }).toList() - : [ - const Center( - child: Text( - 'No history', - style: TextStyle(color: Colors.grey), - ), - ) - ], + itemCount: sortedHistoryKeys?.length ?? 0, + separatorBuilder: (context, index) => Divider( + height: 0, + thickness: 2, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + itemBuilder: (context, index) { + var items = historySequence![sortedHistoryKeys![index]]!; + final requestGroups = getRequestGroups(items); + return Padding( + padding: kPv2, + child: HistoryExpansionTile( + date: sortedHistoryKeys[index], + requestGroups: requestGroups, + ), + ); + }, ), ); } } + +class HistoryExpansionTile extends StatefulHookConsumerWidget { + const HistoryExpansionTile({ + super.key, + required this.requestGroups, + required this.date, + }); + + final Map> requestGroups; + final DateTime date; + + @override + ConsumerState createState() => + _HistoryExpansionTileState(); +} + +class _HistoryExpansionTileState extends ConsumerState + with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + final animationController = useAnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + final animation = Tween(begin: 0.25, end: 0.0).animate(animationController); + final colorScheme = Theme.of(context).colorScheme; + final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); + return ExpansionTile( + dense: true, + title: Row( + children: [ + RotationTransition( + turns: animation, + child: Icon( + Icons.chevron_right_rounded, + size: 20, + color: colorScheme.onSurface.withOpacity(0.6), + )), + kHSpacer5, + Text( + humanizeDate(widget.date), + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + onExpansionChanged: (value) { + if (value) { + animationController.reverse(); + } else { + animationController.forward(); + } + }, + trailing: const SizedBox.shrink(), + tilePadding: kPh8, + shape: const RoundedRectangleBorder(), + collapsedBackgroundColor: colorScheme.surfaceContainerLow, + initiallyExpanded: true, + childrenPadding: kPv8 + kPe4, + children: widget.requestGroups.values.map((item) { + return Padding( + padding: kPv2 + kPh4, + child: SidebarHistoryCard( + id: item.first.historyId, + models: item, + method: item.first.method, + isSelected: selectedGroupId == getHistoryRequestKey(item.first), + requestGroupSize: item.length, + onTap: () { + ref + .read(historyMetaStateNotifier.notifier) + .loadHistoryRequest(item.first.historyId); + }, + ), + ); + }).toList(), + ); + } +} diff --git a/lib/screens/history/history_requests.dart b/lib/screens/history/history_requests.dart index f0bc10cf..7927cd3c 100644 --- a/lib/screens/history/history_requests.dart +++ b/lib/screens/history/history_requests.dart @@ -1,12 +1,20 @@ -import 'package:apidash/consts.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/providers/history_providers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/history_utils.dart'; import 'package:apidash/widgets/widgets.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/consts.dart'; class HistoryRequests extends ConsumerWidget { - const HistoryRequests({super.key}); + const HistoryRequests({ + super.key, + this.scrollController, + this.onSelect, + }); + + final ScrollController? scrollController; + final Function()? onSelect; @override Widget build(BuildContext context, WidgetRef ref) { @@ -15,9 +23,12 @@ class HistoryRequests extends ConsumerWidget { final historyMetas = ref.watch(historyMetaStateNotifier); final requestGroup = getRequestGroup( historyMetas?.values.toList(), selectedRequest?.metaData); - return Column( + return ListView( + shrinkWrap: true, + controller: scrollController, + padding: kPh4, children: [ - kVSpacer20, + kVSpacer10, ...requestGroup.map((request) => Padding( padding: kPv2 + kPh4, child: HistoryRequestCard( @@ -30,10 +41,121 @@ class HistoryRequests extends ConsumerWidget { ref .read(historyMetaStateNotifier.notifier) .loadHistoryRequest(request.historyId); + onSelect?.call(); }, ), - )) + )), + kVSpacer10, ], ); } } + +class HistorRequestsScrollableSheet extends StatefulWidget { + const HistorRequestsScrollableSheet({ + super.key, + }); + + @override + State createState() => + _HistorRequestsScrollableSheetState(); +} + +class _HistorRequestsScrollableSheetState + extends State { + double sheetPosition = 0.5; + final double dragSensitivity = 600; + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: sheetPosition, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + Grabber( + onVerticalDragUpdate: (DragUpdateDetails details) { + setState(() { + sheetPosition -= details.delta.dy / dragSensitivity; + if (sheetPosition < 0.25) { + sheetPosition = 0.25; + } + if (sheetPosition > 0.9) { + sheetPosition = 0.9; + } + }); + }, + isOnDesktopAndWeb: isOnDesktopAndWeb, + ), + Expanded( + child: HistoryRequests( + scrollController: scrollController, + onSelect: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + }); + } + + bool get isOnDesktopAndWeb { + if (kIsWeb) { + return true; + } + switch (defaultTargetPlatform) { + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + return true; + case TargetPlatform.android: + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + return false; + } + } +} + +class Grabber extends StatelessWidget { + const Grabber({ + super.key, + required this.onVerticalDragUpdate, + required this.isOnDesktopAndWeb, + }); + + final ValueChanged onVerticalDragUpdate; + final bool isOnDesktopAndWeb; + + @override + Widget build(BuildContext context) { + if (!isOnDesktopAndWeb) { + return const SizedBox.shrink(); + } + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return GestureDetector( + onVerticalDragUpdate: onVerticalDragUpdate, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), topRight: Radius.circular(16)), + ), + child: Align( + alignment: Alignment.topCenter, + child: Container( + margin: kPv10, + width: 80.0, + height: 6.0, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/history/history_widgets/his_bottombar.dart b/lib/screens/history/history_widgets/his_bottombar.dart new file mode 100644 index 00000000..5f146cc5 --- /dev/null +++ b/lib/screens/history/history_widgets/his_bottombar.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; +import '../history_requests.dart'; + +class HistoryPageBottombar extends ConsumerWidget { + const HistoryPageBottombar({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedRequestModel = ref.watch(selectedHistoryRequestModelProvider); + final historyMetas = ref.watch(historyMetaStateNotifier); + final requestGroup = getRequestGroup( + historyMetas?.values.toList(), selectedRequestModel?.metaData); + final requestCount = requestGroup.length; + return Padding( + padding: MediaQuery.of(context).viewInsets, + child: Container( + height: 60 + MediaQuery.paddingOf(context).bottom, + width: MediaQuery.sizeOf(context).width, + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + left: 16, + right: 16, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.onInverseSurface, + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + requestCount > 1 + ? Badge( + label: Text( + requestCount > 9 ? '9 +' : requestCount.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.secondaryContainer, + ), + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 500), + child: const HistorRequestsScrollableSheet()); + }, + ); + }, + icon: const Icon( + Icons.keyboard_arrow_up_rounded, + ), + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ); + } +} diff --git a/lib/screens/history/details_pane/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart similarity index 100% rename from lib/screens/history/details_pane/his_request_pane.dart rename to lib/screens/history/history_widgets/his_request_pane.dart diff --git a/lib/screens/history/details_pane/his_response_pane.dart b/lib/screens/history/history_widgets/his_response_pane.dart similarity index 100% rename from lib/screens/history/details_pane/his_response_pane.dart rename to lib/screens/history/history_widgets/his_response_pane.dart diff --git a/lib/screens/history/history_widgets/his_sidebar_header.dart b/lib/screens/history/history_widgets/his_sidebar_header.dart new file mode 100644 index 00000000..e431c4b7 --- /dev/null +++ b/lib/screens/history/history_widgets/his_sidebar_header.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; + +class HistorySidebarHeader extends ConsumerWidget { + const HistorySidebarHeader({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mobileScaffoldKey = ref.read(mobileScaffoldKeyStateProvider); + return Padding( + padding: kPe4, + child: Row( + children: [ + kHSpacer10, + Text( + "History", + style: Theme.of(context).textTheme.titleMedium, + ), + const Spacer(), + IconButton( + tooltip: "Auto Delete Settings", + style: IconButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.primary, + ), + onPressed: () {}, + icon: const Icon( + Icons.auto_delete_outlined, + size: 20, + ), + ), + context.width <= kMinWindowSize.width + ? IconButton( + style: IconButton.styleFrom( + padding: const EdgeInsets.all(4), + minimumSize: const Size(36, 36), + ), + onPressed: () { + mobileScaffoldKey.currentState?.closeDrawer(); + }, + icon: const Icon(Icons.chevron_left), + ) + : const SizedBox.shrink(), + ], + ), + ); + } +} diff --git a/lib/screens/history/details_pane/url_card.dart b/lib/screens/history/history_widgets/his_url_card.dart similarity index 100% rename from lib/screens/history/details_pane/url_card.dart rename to lib/screens/history/history_widgets/his_url_card.dart diff --git a/lib/screens/history/history_widgets/history_widgets.dart b/lib/screens/history/history_widgets/history_widgets.dart new file mode 100644 index 00000000..dc8712ff --- /dev/null +++ b/lib/screens/history/history_widgets/history_widgets.dart @@ -0,0 +1,5 @@ +export 'his_bottombar.dart'; +export 'his_request_pane.dart'; +export 'his_response_pane.dart'; +export 'his_sidebar_header.dart'; +export 'his_url_card.dart'; From 2fc02eadd150901f886794c1ca3722f7d8d8656c Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 21 Jul 2024 19:55:32 +0530 Subject: [PATCH 05/13] feat: history of requests --- lib/consts.dart | 21 ++- lib/models/history_meta_model.dart | 1 + lib/models/history_meta_model.freezed.dart | 27 +++- lib/models/history_meta_model.g.dart | 2 + lib/providers/collection_providers.dart | 27 ++++ lib/screens/common_widgets/button_navbar.dart | 43 +++--- lib/screens/history/history_details.dart | 43 ++++-- lib/screens/history/history_page.dart | 17 ++- lib/screens/history/history_pane.dart | 12 +- .../history_widgets/his_action_buttons.dart | 47 +++++++ .../history_widgets/his_bottombar.dart | 131 +++++++++++------- .../requests_page/request_response_tabs.dart | 11 +- lib/widgets/button_group_filled.dart | 60 ++++++++ ...st_response.dart => tabbar_segmented.dart} | 24 ++-- lib/widgets/table_request.dart | 10 ++ lib/widgets/widgets.dart | 3 +- 16 files changed, 355 insertions(+), 124 deletions(-) create mode 100644 lib/screens/history/history_widgets/his_action_buttons.dart create mode 100644 lib/widgets/button_group_filled.dart rename lib/widgets/{tabbar_request_response.dart => tabbar_segmented.dart} (77%) diff --git a/lib/consts.dart b/lib/consts.dart index 63be0705..ed2bc4e1 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -168,6 +168,7 @@ const kPb15 = EdgeInsets.only( const kPb70 = EdgeInsets.only( bottom: 70, ); +const kHSpacer2 = SizedBox(width: 2); const kHSpacer4 = SizedBox(width: 4); const kHSpacer5 = SizedBox(width: 5); const kHSpacer10 = SizedBox(width: 10); @@ -191,8 +192,8 @@ const kRandMax = 100000; const kSuggestionsMenuWidth = 300.0; const kSuggestionsMenuMaxHeight = 200.0; -const kReqResTabWidth = 280.0; -const kReqResTabHeight = 32.0; +const kSegmentedTabWidth = 140.0; +const kSegmentedTabHeight = 32.0; const kDataTableScrollbarTheme = ScrollbarThemeData( crossAxisMargin: -4, @@ -316,6 +317,20 @@ final kColorHttpMethodPut = Colors.amber.shade900; final kColorHttpMethodPatch = kColorHttpMethodPut; final kColorHttpMethodDelete = Colors.red.shade800; +class ButtonData { + ButtonData({ + required this.label, + required this.icon, + this.onPressed, + this.tooltip = "", + }); + + final String label; + final IconData icon; + final VoidCallback? onPressed; + final String tooltip; +} + enum ItemMenuOption { edit("Rename"), delete("Delete"), @@ -724,6 +739,8 @@ const kLabelSave = "Save"; const kLabelDownload = "Download"; const kLabelSaving = "Saving"; const kLabelSaved = "Saved"; +const kLabelCode = "Code"; +const kLabelDuplicate = "Duplicate"; // Request Pane const kLabelRequest = "Request"; const kLabelHideCode = "Hide Code"; diff --git a/lib/models/history_meta_model.dart b/lib/models/history_meta_model.dart index 58e68aee..9188b6ad 100644 --- a/lib/models/history_meta_model.dart +++ b/lib/models/history_meta_model.dart @@ -9,6 +9,7 @@ part 'history_meta_model.g.dart'; class HistoryMetaModel with _$HistoryMetaModel { const factory HistoryMetaModel({ required String historyId, + required String requestId, @Default("") String name, required String url, required HTTPVerb method, diff --git a/lib/models/history_meta_model.freezed.dart b/lib/models/history_meta_model.freezed.dart index fb7ba2eb..41901461 100644 --- a/lib/models/history_meta_model.freezed.dart +++ b/lib/models/history_meta_model.freezed.dart @@ -21,6 +21,7 @@ HistoryMetaModel _$HistoryMetaModelFromJson(Map json) { /// @nodoc mixin _$HistoryMetaModel { String get historyId => throw _privateConstructorUsedError; + String get requestId => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; String get url => throw _privateConstructorUsedError; HTTPVerb get method => throw _privateConstructorUsedError; @@ -41,6 +42,7 @@ abstract class $HistoryMetaModelCopyWith<$Res> { @useResult $Res call( {String historyId, + String requestId, String name, String url, HTTPVerb method, @@ -62,6 +64,7 @@ class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel> @override $Res call({ Object? historyId = null, + Object? requestId = null, Object? name = null, Object? url = null, Object? method = null, @@ -73,6 +76,10 @@ class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel> ? _value.historyId : historyId // ignore: cast_nullable_to_non_nullable as String, + requestId: null == requestId + ? _value.requestId + : requestId // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -107,6 +114,7 @@ abstract class _$$HistoryMetaModelImplCopyWith<$Res> @useResult $Res call( {String historyId, + String requestId, String name, String url, HTTPVerb method, @@ -126,6 +134,7 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res> @override $Res call({ Object? historyId = null, + Object? requestId = null, Object? name = null, Object? url = null, Object? method = null, @@ -137,6 +146,10 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res> ? _value.historyId : historyId // ignore: cast_nullable_to_non_nullable as String, + requestId: null == requestId + ? _value.requestId + : requestId // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -166,6 +179,7 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res> class _$HistoryMetaModelImpl implements _HistoryMetaModel { const _$HistoryMetaModelImpl( {required this.historyId, + required this.requestId, this.name = "", required this.url, required this.method, @@ -178,6 +192,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { @override final String historyId; @override + final String requestId; + @override @JsonKey() final String name; @override @@ -191,7 +207,7 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { @override String toString() { - return 'HistoryMetaModel(historyId: $historyId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)'; + return 'HistoryMetaModel(historyId: $historyId, requestId: $requestId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)'; } @override @@ -201,6 +217,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { other is _$HistoryMetaModelImpl && (identical(other.historyId, historyId) || other.historyId == historyId) && + (identical(other.requestId, requestId) || + other.requestId == requestId) && (identical(other.name, name) || other.name == name) && (identical(other.url, url) || other.url == url) && (identical(other.method, method) || other.method == method) && @@ -212,8 +230,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, historyId, name, url, method, responseStatus, timeStamp); + int get hashCode => Object.hash(runtimeType, historyId, requestId, name, url, + method, responseStatus, timeStamp); @JsonKey(ignore: true) @override @@ -233,6 +251,7 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel { abstract class _HistoryMetaModel implements HistoryMetaModel { const factory _HistoryMetaModel( {required final String historyId, + required final String requestId, final String name, required final String url, required final HTTPVerb method, @@ -245,6 +264,8 @@ abstract class _HistoryMetaModel implements HistoryMetaModel { @override String get historyId; @override + String get requestId; + @override String get name; @override String get url; diff --git a/lib/models/history_meta_model.g.dart b/lib/models/history_meta_model.g.dart index 67975edc..3d8af833 100644 --- a/lib/models/history_meta_model.g.dart +++ b/lib/models/history_meta_model.g.dart @@ -10,6 +10,7 @@ _$HistoryMetaModelImpl _$$HistoryMetaModelImplFromJson( Map json) => _$HistoryMetaModelImpl( historyId: json['historyId'] as String, + requestId: json['requestId'] as String, name: json['name'] as String? ?? "", url: json['url'] as String, method: $enumDecode(_$HTTPVerbEnumMap, json['method']), @@ -21,6 +22,7 @@ Map _$$HistoryMetaModelImplToJson( _$HistoryMetaModelImpl instance) => { 'historyId': instance.historyId, + 'requestId': instance.requestId, 'name': instance.name, 'url': instance.url, 'method': _$HTTPVerbEnumMap[instance.method]!, diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 3bb2258d..02eead40 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -159,6 +159,32 @@ class CollectionStateNotifier ref.read(hasUnsavedChangesProvider.notifier).state = true; } + void duplicateFromHistory(HistoryRequestModel historyRequestModel) { + final newId = getNewUuid(); + + var itemIds = ref.read(requestSequenceProvider); + var currentModel = historyRequestModel; + final newModel = RequestModel( + id: newId, + name: "${currentModel.metaData.name} (history)", + httpRequestModel: currentModel.httpRequestModel, + responseStatus: currentModel.metaData.responseStatus, + message: kResponseCodeReasons[currentModel.metaData.responseStatus], + httpResponseModel: currentModel.httpResponseModel, + isWorking: false, + sendingTime: null, + ); + + itemIds.insert(0, newId); + var map = {...state!}; + map[newId] = newModel; + state = map; + + ref.read(requestSequenceProvider.notifier).state = [...itemIds]; + ref.read(selectedIdStateProvider.notifier).state = newId; + ref.read(hasUnsavedChangesProvider.notifier).state = true; + } + void update( String id, { HTTPVerb? method, @@ -261,6 +287,7 @@ class CollectionStateNotifier historyId: newHistoryId, metaData: HistoryMetaModel( historyId: newHistoryId, + requestId: id, name: requestModel.name, url: substitutedHttpRequestModel.url, method: substitutedHttpRequestModel.method, diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index 2cefc201..f18c695c 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -27,22 +27,27 @@ class NavbarButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final bool isSelected = railIdx == buttonIdx; final Size size = isCompact ? const Size(56, 32) : const Size(65, 32); + var onPress = isSelected + ? null + : () { + if (buttonIdx != null) { + ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; + if ((railIdx > 2 && buttonIdx! <= 2) || + !(ref + .read(mobileScaffoldKeyStateProvider) + .currentState + ?.isDrawerOpen ?? + true)) { + ref.read(leftDrawerStateProvider.notifier).state = false; + } + } + onTap?.call(); + }; return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( behavior: HitTestBehavior.translucent, - onTap: isSelected - ? null - : () { - if (buttonIdx != null) { - ref.read(navRailIndexStateProvider.notifier).state = - buttonIdx!; - if (railIdx > 2 && buttonIdx! <= 2) { - ref.read(leftDrawerStateProvider.notifier).state = false; - } - } - onTap?.call(); - }, + onTap: onPress, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -56,19 +61,7 @@ class NavbarButton extends ConsumerWidget { : TextButton.styleFrom( fixedSize: size, ), - onPressed: isSelected - ? null - : () { - if (buttonIdx != null) { - ref.read(navRailIndexStateProvider.notifier).state = - buttonIdx!; - if (railIdx > 2 && buttonIdx! <= 2) { - ref.read(leftDrawerStateProvider.notifier).state = - false; - } - } - onTap?.call(); - }, + onPressed: onPress, child: Icon( isSelected ? selectedIcon : icon, color: Theme.of(context).colorScheme.onSurfaceVariant, diff --git a/lib/screens/history/history_details.dart b/lib/screens/history/history_details.dart index 10f02251..ebdf81c1 100644 --- a/lib/screens/history/history_details.dart +++ b/lib/screens/history/history_details.dart @@ -25,7 +25,7 @@ class _HistoryDetailsState extends ConsumerState final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider); final TabController controller = - useTabController(initialLength: 2, vsync: this); + useTabController(initialLength: 3, vsync: this); return selectedHistoryRequest != null ? LayoutBuilder( @@ -42,28 +42,45 @@ class _HistoryDetailsState extends ConsumerState )), kVSpacer10, if (isCompact) ...[ - RequestResponseTabbar( + SegmentedTabbar( controller: controller, + tabs: const [ + Tab(text: kLabelRequest), + Tab(text: kLabelResponse), + Tab(text: kLabelCode), + ], ), kVSpacer10, Expanded( - child: TabBarView( - controller: controller, - children: [ - HistoryRequestPane( - isCompact: isCompact, - ), - const HistoryResponsePane(), - ], - )) + child: TabBarView( + controller: controller, + children: [ + HistoryRequestPane( + isCompact: isCompact, + ), + const HistoryResponsePane(), + const CodePane( + isHistoryRequest: true, + ), + ], + ), + ), + const HistoryPageBottombar() ] else ...[ Expanded( child: Padding( padding: kPh4, child: RequestDetailsCard( child: EqualSplitView( - leftWidget: HistoryRequestPane( - isCompact: isCompact, + leftWidget: Column( + children: [ + Expanded( + child: HistoryRequestPane( + isCompact: isCompact, + ), + ), + const HistoryPageBottombar(), + ], ), rightWidget: codePaneVisible ? const CodePane(isHistoryRequest: true) diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 3787ce4f..0f288fb7 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -6,7 +6,6 @@ import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; -import 'history_widgets/history_widgets.dart'; class HistoryPage extends ConsumerWidget { const HistoryPage({ @@ -23,14 +22,14 @@ class HistoryPage extends ConsumerWidget { : 'History'; if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, - mainContent: const HistoryViewer(), - title: Text(title), - leftDrawerContent: const HistoryPane(), - actions: const [SizedBox(width: 16)], - onDrawerChanged: (value) => - ref.read(leftDrawerStateProvider.notifier).state = value, - bottomNavigationBar: const HistoryPageBottombar()); + scaffoldKey: scaffoldKey, + mainContent: const HistoryViewer(), + title: Text(title), + leftDrawerContent: const HistoryPane(), + actions: const [SizedBox(width: 16)], + onDrawerChanged: (value) => + ref.read(leftDrawerStateProvider.notifier).state = value, + ); } return const Column( children: [ diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index d206dabb..8b090b95 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -65,6 +65,7 @@ class HistoryList extends HookConsumerWidget { child: HistoryExpansionTile( date: sortedHistoryKeys[index], requestGroups: requestGroups, + initiallyExpanded: index == 0, ), ); }, @@ -78,10 +79,12 @@ class HistoryExpansionTile extends StatefulHookConsumerWidget { super.key, required this.requestGroups, required this.date, + this.initiallyExpanded = false, }); final Map> requestGroups; final DateTime date; + final bool initiallyExpanded; @override ConsumerState createState() => @@ -95,8 +98,9 @@ class _HistoryExpansionTileState extends ConsumerState final animationController = useAnimationController( duration: const Duration(milliseconds: 200), vsync: this, + initialValue: widget.initiallyExpanded ? 1.0 : 0.0, ); - final animation = Tween(begin: 0.25, end: 0.0).animate(animationController); + final animation = Tween(begin: 0.0, end: 0.25).animate(animationController); final colorScheme = Theme.of(context).colorScheme; final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider); return ExpansionTile( @@ -122,16 +126,16 @@ class _HistoryExpansionTileState extends ConsumerState ), onExpansionChanged: (value) { if (value) { - animationController.reverse(); - } else { animationController.forward(); + } else { + animationController.reverse(); } }, trailing: const SizedBox.shrink(), tilePadding: kPh8, shape: const RoundedRectangleBorder(), collapsedBackgroundColor: colorScheme.surfaceContainerLow, - initiallyExpanded: true, + initiallyExpanded: widget.initiallyExpanded, childrenPadding: kPv8 + kPe4, children: widget.requestGroups.values.map((item) { return Padding( diff --git a/lib/screens/history/history_widgets/his_action_buttons.dart b/lib/screens/history/history_widgets/his_action_buttons.dart new file mode 100644 index 00000000..436c9557 --- /dev/null +++ b/lib/screens/history/history_widgets/his_action_buttons.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_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 HistoryActionButtons extends ConsumerWidget { + const HistoryActionButtons({super.key, this.historyRequestModel}); + + final HistoryRequestModel? historyRequestModel; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final collectionStateNotifier = ref.watch(collectionStateNotifierProvider); + final isAvailable = collectionStateNotifier?.values.any((element) => + element.id == historyRequestModel?.metaData.requestId) ?? + false; + final requestId = historyRequestModel?.metaData.requestId; + return FilledButtonGroup(buttons: [ + ButtonData( + icon: Icons.copy_rounded, + label: kLabelDuplicate, + onPressed: requestId != null + ? () { + ref + .read(collectionStateNotifierProvider.notifier) + .duplicateFromHistory(historyRequestModel!); + ref.read(navRailIndexStateProvider.notifier).state = 0; + } + : null, + tooltip: "Duplicate Request", + ), + ButtonData( + icon: Icons.north_east_rounded, + label: kLabelRequest, + onPressed: isAvailable && requestId != null + ? () { + ref.read(selectedIdStateProvider.notifier).state = requestId; + ref.read(navRailIndexStateProvider.notifier).state = 0; + } + : null, + tooltip: isAvailable ? "Go to Request" : "Couldn't find Request", + ), + ]); + } +} diff --git a/lib/screens/history/history_widgets/his_bottombar.dart b/lib/screens/history/history_widgets/his_bottombar.dart index 5f146cc5..9945b7bc 100644 --- a/lib/screens/history/history_widgets/his_bottombar.dart +++ b/lib/screens/history/history_widgets/his_bottombar.dart @@ -1,8 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import '../history_requests.dart'; +import 'his_action_buttons.dart'; class HistoryPageBottombar extends ConsumerWidget { const HistoryPageBottombar({ @@ -16,59 +19,87 @@ class HistoryPageBottombar extends ConsumerWidget { final requestGroup = getRequestGroup( historyMetas?.values.toList(), selectedRequestModel?.metaData); final requestCount = requestGroup.length; - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - height: 60 + MediaQuery.paddingOf(context).bottom, - width: MediaQuery.sizeOf(context).width, - padding: EdgeInsets.only( - bottom: MediaQuery.paddingOf(context).bottom, - left: 16, - right: 16, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.onInverseSurface, - width: 1, - ), + + return Container( + height: 60 + MediaQuery.paddingOf(context).bottom, + width: MediaQuery.sizeOf(context).width, + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + left: 16, + right: 16, + ), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + top: BorderSide( + color: Theme.of(context).colorScheme.onInverseSurface, + width: 1, ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - requestCount > 1 - ? Badge( - label: Text( - requestCount > 9 ? '9 +' : requestCount.toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - child: IconButton( - style: IconButton.styleFrom( - backgroundColor: - Theme.of(context).colorScheme.secondaryContainer, - ), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) { - return ConstrainedBox( - constraints: - const BoxConstraints(maxWidth: 500), - child: const HistorRequestsScrollableSheet()); - }, - ); - }, - icon: const Icon( - Icons.keyboard_arrow_up_rounded, - ), - ), - ) - : const SizedBox.shrink(), - ], + ), + child: context.isMediumWindow + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + HistoryActionButtons(historyRequestModel: selectedRequestModel), + HistorySheetButton(requestCount: requestCount) + ], + ) + : Center( + child: HistoryActionButtons( + historyRequestModel: selectedRequestModel)), + ); + } +} + +class HistorySheetButton extends StatelessWidget { + const HistorySheetButton({ + super.key, + required this.requestCount, + }); + + final int requestCount; + + @override + Widget build(BuildContext context) { + final isCompact = context.isCompactWindow; + const icon = Icon(Icons.keyboard_arrow_up_rounded); + return Badge( + isLabelVisible: requestCount > 1, + label: Text( + requestCount > 9 ? '9+' : requestCount.toString(), + style: const TextStyle(fontWeight: FontWeight.bold), + ), + child: FilledButton.tonal( + style: FilledButton.styleFrom( + minimumSize: const Size(44, 44), + padding: isCompact ? kP4 : const EdgeInsets.fromLTRB(16, 12, 8, 12), ), + onPressed: requestCount > 1 + ? () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: const HistorRequestsScrollableSheet()); + }, + ); + } + : null, + child: isCompact + ? icon + : const Row( + children: [ + Text( + "Show All", + style: kTextStyleButton, + ), + kHSpacer5, + icon, + ], + ), ), ); } diff --git a/lib/screens/mobile/requests_page/request_response_tabs.dart b/lib/screens/mobile/requests_page/request_response_tabs.dart index 36ad6c11..34a0ed90 100644 --- a/lib/screens/mobile/requests_page/request_response_tabs.dart +++ b/lib/screens/mobile/requests_page/request_response_tabs.dart @@ -19,13 +19,14 @@ class RequestResponseTabs extends StatelessWidget { child: EditorPaneRequestURLCard(), ), kVSpacer10, - RequestResponseTabbar( + SegmentedTabbar( controller: controller, + tabs: const [ + Tab(text: kLabelRequest), + Tab(text: kLabelResponse), + ], ), - Expanded( - child: RequestResponseTabviews( - controller: controller, - )) + Expanded(child: RequestResponseTabviews(controller: controller)) ], ); } diff --git a/lib/widgets/button_group_filled.dart b/lib/widgets/button_group_filled.dart new file mode 100644 index 00000000..24ae622d --- /dev/null +++ b/lib/widgets/button_group_filled.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class FilledButtonGroup extends StatelessWidget { + const FilledButtonGroup({super.key, required this.buttons}); + + final List buttons; + + Widget buildButton(ButtonData buttonData, {bool showLabel = true}) { + final icon = Icon(buttonData.icon, size: 20); + final label = Text( + buttonData.label, + style: kTextStyleButton, + ); + return Tooltip( + message: buttonData.tooltip, + child: FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size(44, 44), + padding: kPh12, + shape: const ContinuousRectangleBorder()), + onPressed: buttonData.onPressed, + label: showLabel + ? Row( + children: [ + icon, + kHSpacer4, + label, + ], + ) + : icon, + ), + ); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final showLabel = constraints.maxWidth > buttons.length * 110; + List buttonWidgets = buttons + .map((button) => buildButton(button, showLabel: showLabel)) + .toList(); + + List buttonsWithSpacers = []; + for (int i = 0; i < buttonWidgets.length; i++) { + buttonsWithSpacers.add(buttonWidgets[i]); + if (i < buttonWidgets.length - 1) { + buttonsWithSpacers.add(kHSpacer2); + } + } + return ClipRRect( + borderRadius: kBorderRadius20, + child: Row( + mainAxisSize: MainAxisSize.min, + children: buttonsWithSpacers, + ), + ); + }); + } +} diff --git a/lib/widgets/tabbar_request_response.dart b/lib/widgets/tabbar_segmented.dart similarity index 77% rename from lib/widgets/tabbar_request_response.dart rename to lib/widgets/tabbar_segmented.dart index 5a117120..bb58dc13 100644 --- a/lib/widgets/tabbar_request_response.dart +++ b/lib/widgets/tabbar_segmented.dart @@ -1,20 +1,27 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; -class RequestResponseTabbar extends StatelessWidget { - const RequestResponseTabbar({ +class SegmentedTabbar extends StatelessWidget { + const SegmentedTabbar({ super.key, required this.controller, + required this.tabs, + this.tabWidth = kSegmentedTabWidth, + this.tabHeight = kSegmentedTabHeight, }); final TabController controller; + final List tabs; + final double tabWidth; + final double tabHeight; @override Widget build(BuildContext context) { return Center( child: Container( - width: kReqResTabWidth, - height: kReqResTabHeight, + margin: kPh4, + width: tabWidth * tabs.length, + height: tabHeight, decoration: BoxDecoration( borderRadius: kBorderRadius20, border: Border.all( @@ -40,14 +47,7 @@ class RequestResponseTabbar extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), controller: controller, - tabs: const [ - Tab( - text: kLabelRequest, - ), - Tab( - text: kLabelResponse, - ), - ], + tabs: tabs, ), ), ), diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart index bdde7c47..014f0c86 100644 --- a/lib/widgets/table_request.dart +++ b/lib/widgets/table_request.dart @@ -20,6 +20,10 @@ class RequestDataTable extends StatelessWidget { final clrScheme = Theme.of(context).colorScheme; final List columns = [ + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), DataColumn2( label: Text(keyName ?? kNameField), ), @@ -30,6 +34,10 @@ class RequestDataTable extends StatelessWidget { DataColumn2( label: Text(valueName ?? kNameValue), ), + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), ]; final fieldDecoration = InputDecoration( @@ -52,6 +60,7 @@ class RequestDataTable extends StatelessWidget { .map( (MapEntry entry) => DataRow( cells: [ + const DataCell(kHSpacer5), DataCell( ReadOnlyTextField( initialValue: entry.key, @@ -67,6 +76,7 @@ class RequestDataTable extends StatelessWidget { decoration: fieldDecoration, ), ), + const DataCell(kHSpacer5), ], ), ) diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 9f0323aa..db9b860c 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'button_clear_response.dart'; export 'button_copy.dart'; export 'button_discord.dart'; +export 'button_group_filled.dart'; export 'button_repo.dart'; export 'button_save_download.dart'; export 'button_send.dart'; @@ -48,7 +49,7 @@ export 'splitview_drawer.dart'; export 'splitview_dashboard.dart'; export 'splitview_equal.dart'; export 'splitview_history.dart'; -export 'tabbar_request_response.dart'; +export 'tabbar_segmented.dart'; export 'table_map.dart'; export 'table_request.dart'; export 'tabs.dart'; From 5ad679cab34c60b6e40954388de8ce62304ecb83 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Sun, 21 Jul 2024 21:42:40 +0530 Subject: [PATCH 06/13] fix: history body view --- .../history_widgets/his_request_pane.dart | 64 +++++++++- .../request_pane/request_form_data.dart | 21 +--- lib/widgets/button_form_data_file.dart | 31 +++++ lib/widgets/editor.dart | 3 + lib/widgets/table_request.dart | 1 - lib/widgets/table_request_form.dart | 119 ++++++++++++++++++ lib/widgets/widgets.dart | 2 + 7 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 lib/widgets/button_form_data_file.dart create mode 100644 lib/widgets/table_request_form.dart diff --git a/lib/screens/history/history_widgets/his_request_pane.dart b/lib/screens/history/history_widgets/his_request_pane.dart index 6378a871..a2ad35b2 100644 --- a/lib/screens/history/history_widgets/his_request_pane.dart +++ b/lib/screens/history/history_widgets/his_request_pane.dart @@ -53,7 +53,69 @@ class HistoryRequestPane extends ConsumerWidget { rows: headersMap, keyName: kNameHeader, ), - const SizedBox(), + const HisRequestBody(), + ], + ); + } +} + +class HisRequestBody extends ConsumerWidget { + const HisRequestBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedHistoryModel = ref.watch(selectedHistoryRequestModelProvider); + final requestModel = selectedHistoryModel?.httpRequestModel; + final contentType = requestModel?.bodyContentType; + + return Column( + children: [ + kVSpacer5, + RichText( + text: TextSpan( + style: Theme.of(context).textTheme.labelLarge, + children: [ + const TextSpan( + text: "Content Type: ", + ), + TextSpan( + text: contentType?.name ?? "text", + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + )), + ], + ), + ), + kVSpacer5, + Expanded( + child: switch (contentType) { + ContentType.formdata => Padding( + padding: kPh4, + child: + RequestFormDataTable(rows: requestModel?.formData ?? [])), + // TODO: Fix JsonTextFieldEditor & plug it here + ContentType.json => Padding( + padding: kPt5o10, + child: TextFieldEditor( + key: Key("${selectedHistoryModel?.historyId}-json-body"), + fieldKey: + "${selectedHistoryModel?.historyId}-json-body-viewer", + initialValue: requestModel?.body, + readOnly: true, + ), + ), + _ => Padding( + padding: kPt5o10, + child: TextFieldEditor( + key: Key("${selectedHistoryModel?.historyId}-body"), + fieldKey: "${selectedHistoryModel?.historyId}-body-viewer", + initialValue: requestModel?.body, + readOnly: true, + ), + ), + }, + ) ], ); } diff --git a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart index 5228c660..9b0bb123 100644 --- a/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart +++ b/lib/screens/home_page/editor_pane/details_card/request_pane/request_form_data.dart @@ -124,17 +124,7 @@ class _FormDataBodyState extends ConsumerState { ), DataCell( formRows[index].type == FormDataType.file - ? ElevatedButton.icon( - icon: const Icon( - Icons.snippet_folder_rounded, - size: 20, - ), - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(kDataTableRowHeight), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), + ? FormDataFileButton( onPressed: () async { var pickedResult = await pickFile(); if (pickedResult != null && @@ -146,14 +136,7 @@ class _FormDataBodyState extends ConsumerState { _onFieldChange(selectedId!); } }, - label: Text( - (formRows[index].type == FormDataType.file && - formRows[index].value.isNotEmpty) - ? formRows[index].value.toString() - : kLabelSelectFile, - overflow: TextOverflow.ellipsis, - style: kFormDataButtonLabelTextStyle, - ), + initialValue: formRows[index].value, ) : CellField( keyId: "$selectedId-$index-form-v-$seed", diff --git a/lib/widgets/button_form_data_file.dart b/lib/widgets/button_form_data_file.dart new file mode 100644 index 00000000..262ea76b --- /dev/null +++ b/lib/widgets/button_form_data_file.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +class FormDataFileButton extends StatelessWidget { + const FormDataFileButton({super.key, this.onPressed, this.initialValue}); + + final VoidCallback? onPressed; + final String? initialValue; + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + icon: const Icon( + Icons.snippet_folder_rounded, + size: 20, + ), + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(kDataTableRowHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + onPressed: onPressed, + label: Text( + initialValue ?? kLabelSelectFile, + overflow: TextOverflow.ellipsis, + style: kFormDataButtonLabelTextStyle, + ), + ); + } +} diff --git a/lib/widgets/editor.dart b/lib/widgets/editor.dart index 736ffd83..5bdd2f82 100644 --- a/lib/widgets/editor.dart +++ b/lib/widgets/editor.dart @@ -9,11 +9,13 @@ class TextFieldEditor extends StatefulWidget { required this.fieldKey, this.onChanged, this.initialValue, + this.readOnly = false, }); final String fieldKey; final Function(String)? onChanged; final String? initialValue; + final bool readOnly; @override State createState() => _TextFieldEditorState(); } @@ -69,6 +71,7 @@ class _TextFieldEditorState extends State { keyboardType: TextInputType.multiline, expands: true, maxLines: null, + readOnly: widget.readOnly, style: kCodeStyle, textAlignVertical: TextAlignVertical.top, onChanged: widget.onChanged, diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart index 014f0c86..010aaa27 100644 --- a/lib/widgets/table_request.dart +++ b/lib/widgets/table_request.dart @@ -103,7 +103,6 @@ class RequestDataTable extends StatelessWidget { ), ), ), - kVSpacer40, ], ), ); diff --git a/lib/widgets/table_request_form.dart b/lib/widgets/table_request_form.dart new file mode 100644 index 00000000..a61c0cd7 --- /dev/null +++ b/lib/widgets/table_request_form.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:data_table_2/data_table_2.dart'; +import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/consts.dart'; + +class RequestFormDataTable extends StatelessWidget { + const RequestFormDataTable({ + super.key, + required this.rows, + this.keyName, + this.valueName, + }); + + final List rows; + final String? keyName; + final String? valueName; + + @override + Widget build(BuildContext context) { + final clrScheme = Theme.of(context).colorScheme; + + final List columns = [ + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), + DataColumn2( + label: Text(keyName ?? kNameField), + ), + const DataColumn2( + label: Text('='), + fixedWidth: 30, + ), + DataColumn2( + label: Text(valueName ?? kNameValue), + ), + const DataColumn2( + label: Text(''), + fixedWidth: 8, + ), + ]; + + final fieldDecoration = InputDecoration( + contentPadding: const EdgeInsets.only(bottom: 12), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.primary.withOpacity( + kHintOpacity, + ), + ), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: clrScheme.surfaceContainerHighest, + ), + ), + ); + + final List dataRows = rows + .map( + (FormDataModel entry) => DataRow( + cells: [ + const DataCell(kHSpacer5), + DataCell( + ReadOnlyTextField( + initialValue: entry.name, + decoration: fieldDecoration, + ), + ), + const DataCell( + Text('='), + ), + DataCell( + entry.type == FormDataType.file + ? Tooltip( + message: entry.value, + child: FormDataFileButton( + onPressed: () {}, + initialValue: entry.value, + ), + ) + : ReadOnlyTextField( + initialValue: entry.value, + decoration: fieldDecoration, + ), + ), + const DataCell(kHSpacer5), + ], + ), + ) + .toList(); + + return Container( + 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/widgets/widgets.dart b/lib/widgets/widgets.dart index db9b860c..5383916d 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -1,6 +1,7 @@ export 'button_clear_response.dart'; export 'button_copy.dart'; export 'button_discord.dart'; +export 'button_form_data_file.dart'; export 'button_group_filled.dart'; export 'button_repo.dart'; export 'button_save_download.dart'; @@ -51,6 +52,7 @@ export 'splitview_equal.dart'; export 'splitview_history.dart'; export 'tabbar_segmented.dart'; export 'table_map.dart'; +export 'table_request_form.dart'; export 'table_request.dart'; export 'tabs.dart'; export 'texts.dart'; From b2cd91274f759e73194d39140c5c1b4fe0e821dc Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 00:56:44 +0530 Subject: [PATCH 07/13] wip: manage history dialog --- lib/consts.dart | 18 +++- lib/models/settings_model.dart | 23 ++++- lib/providers/settings_providers.dart | 2 + lib/screens/envvar/environments_pane.dart | 2 +- lib/screens/history/history_pane.dart | 4 + .../history_widgets/his_bottombar.dart | 2 +- .../history_widgets/his_sidebar_header.dart | 16 +++- lib/screens/home_page/collection_pane.dart | 2 +- lib/widgets/dialog_history_retention.dart | 85 +++++++++++++++++++ lib/widgets/request_widgets.dart | 4 + lib/widgets/widgets.dart | 1 + 11 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 lib/widgets/dialog_history_retention.dart diff --git a/lib/consts.dart b/lib/consts.dart index ed2bc4e1..5e8790c0 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -109,6 +109,7 @@ const kPv2 = EdgeInsets.symmetric(vertical: 2); const kPv6 = EdgeInsets.symmetric(vertical: 6); const kPv8 = EdgeInsets.symmetric(vertical: 8); const kPv10 = EdgeInsets.symmetric(vertical: 10); +const kPv20 = EdgeInsets.symmetric(vertical: 20); const kPh2 = EdgeInsets.symmetric(horizontal: 2); const kPt28o8 = EdgeInsets.only(top: 28, left: 8.0, right: 8.0, bottom: 8.0); const kPt5o10 = @@ -116,9 +117,8 @@ const kPt5o10 = const kPh4 = EdgeInsets.symmetric(horizontal: 4); const kPh8 = EdgeInsets.symmetric(horizontal: 8); const kPh12 = EdgeInsets.symmetric(horizontal: 12); -const kPh20 = EdgeInsets.symmetric( - horizontal: 20, -); +const kPh20 = EdgeInsets.symmetric(horizontal: 20); +const kPh24 = EdgeInsets.symmetric(horizontal: 24); const kPh20t40 = EdgeInsets.only( left: 20, right: 20, @@ -178,6 +178,7 @@ const kHSpacer40 = SizedBox(width: 40); const kVSpacer5 = SizedBox(height: 5); const kVSpacer8 = SizedBox(height: 8); const kVSpacer10 = SizedBox(height: 10); +const kVSpacer16 = SizedBox(height: 16); const kVSpacer20 = SizedBox(height: 20); const kVSpacer40 = SizedBox(height: 40); @@ -331,6 +332,17 @@ class ButtonData { final String tooltip; } +enum HistoryRetentionPeriod { + oneWeek("1 Week", Icons.calendar_view_week_rounded), + oneMonth("1 Month", Icons.calendar_view_month_rounded), + threeMonths("3 Months", Icons.calendar_month_rounded), + forever("Forever", Icons.all_inclusive_rounded); + + const HistoryRetentionPeriod(this.label, this.icon); + final String label; + final IconData icon; +} + enum ItemMenuOption { edit("Rename"), delete("Delete"), diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 846bdf52..e82ffdca 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -13,6 +13,7 @@ class SettingsModel { this.saveResponses = true, this.promptBeforeClosing = true, this.activeEnvironmentId, + this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, }); final bool isDark; @@ -24,6 +25,7 @@ class SettingsModel { final bool saveResponses; final bool promptBeforeClosing; final String? activeEnvironmentId; + final HistoryRetentionPeriod historyRetentionPeriod; SettingsModel copyWith({ bool? isDark, @@ -35,6 +37,7 @@ class SettingsModel { bool? saveResponses, bool? promptBeforeClosing, String? activeEnvironmentId, + HistoryRetentionPeriod? historyRetentionPeriod, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -47,6 +50,8 @@ class SettingsModel { saveResponses: saveResponses ?? this.saveResponses, promptBeforeClosing: promptBeforeClosing ?? this.promptBeforeClosing, activeEnvironmentId: activeEnvironmentId ?? this.activeEnvironmentId, + historyRetentionPeriod: + historyRetentionPeriod ?? this.historyRetentionPeriod, ); } @@ -80,6 +85,18 @@ class SettingsModel { final saveResponses = data["saveResponses"] as bool?; final promptBeforeClosing = data["promptBeforeClosing"] as bool?; final activeEnvironmentId = data["activeEnvironmentId"] as String?; + final historyRetentionPeriodStr = data["historyRetentionPeriod"] as String?; + HistoryRetentionPeriod historyRetentionPeriod = + HistoryRetentionPeriod.oneWeek; + if (historyRetentionPeriodStr != null) { + try { + historyRetentionPeriod = + HistoryRetentionPeriod.values.byName(historyRetentionPeriodStr); + } catch (e) { + // pass + } + } + const sm = SettingsModel(); return sm.copyWith( @@ -92,6 +109,7 @@ class SettingsModel { saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod, ); } @@ -108,6 +126,7 @@ class SettingsModel { "saveResponses": saveResponses, "promptBeforeClosing": promptBeforeClosing, "activeEnvironmentId": activeEnvironmentId, + "historyRetentionPeriod": historyRetentionPeriod.name, }; } @@ -129,7 +148,8 @@ class SettingsModel { other.defaultCodeGenLang == defaultCodeGenLang && other.saveResponses == saveResponses && other.promptBeforeClosing == promptBeforeClosing && - other.activeEnvironmentId == activeEnvironmentId; + other.activeEnvironmentId == activeEnvironmentId && + other.historyRetentionPeriod == historyRetentionPeriod; } @override @@ -145,6 +165,7 @@ class SettingsModel { saveResponses, promptBeforeClosing, activeEnvironmentId, + historyRetentionPeriod, ); } } diff --git a/lib/providers/settings_providers.dart b/lib/providers/settings_providers.dart index 0fff037b..6293e04b 100644 --- a/lib/providers/settings_providers.dart +++ b/lib/providers/settings_providers.dart @@ -30,6 +30,7 @@ class ThemeStateNotifier extends StateNotifier { bool? saveResponses, bool? promptBeforeClosing, String? activeEnvironmentId, + HistoryRetentionPeriod? historyRetentionPeriod, }) async { state = state.copyWith( isDark: isDark, @@ -41,6 +42,7 @@ class ThemeStateNotifier extends StateNotifier { saveResponses: saveResponses, promptBeforeClosing: promptBeforeClosing, activeEnvironmentId: activeEnvironmentId, + historyRetentionPeriod: historyRetentionPeriod, ); await hiveHandler.saveSettings(state.toJson()); } diff --git a/lib/screens/envvar/environments_pane.dart b/lib/screens/envvar/environments_pane.dart index 340cc0e8..dd6b5af4 100644 --- a/lib/screens/envvar/environments_pane.dart +++ b/lib/screens/envvar/environments_pane.dart @@ -190,8 +190,8 @@ class EnvironmentItem extends ConsumerWidget { ref.read(activeEnvironmentIdStateProvider.notifier).state = id; }, onTap: () { - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); ref.read(selectedEnvironmentIdStateProvider.notifier).state = id; + ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); }, focusNode: ref.watch(nameTextFieldFocusNodeProvider), onChangedNameEditor: (value) { diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index 8b090b95..ba89c565 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -150,6 +150,10 @@ class _HistoryExpansionTileState extends ConsumerState ref .read(historyMetaStateNotifier.notifier) .loadHistoryRequest(item.first.historyId); + ref + .read(mobileScaffoldKeyStateProvider) + .currentState + ?.closeDrawer(); }, ), ); diff --git a/lib/screens/history/history_widgets/his_bottombar.dart b/lib/screens/history/history_widgets/his_bottombar.dart index 9945b7bc..da6b0ffe 100644 --- a/lib/screens/history/history_widgets/his_bottombar.dart +++ b/lib/screens/history/history_widgets/his_bottombar.dart @@ -82,7 +82,7 @@ class HistorySheetButton extends StatelessWidget { isScrollControlled: true, builder: (context) { return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 500), + constraints: const BoxConstraints(maxWidth: 400), child: const HistorRequestsScrollableSheet()); }, ); diff --git a/lib/screens/history/history_widgets/his_sidebar_header.dart b/lib/screens/history/history_widgets/his_sidebar_header.dart index e431c4b7..6af1e1e8 100644 --- a/lib/screens/history/history_widgets/his_sidebar_header.dart +++ b/lib/screens/history/history_widgets/his_sidebar_header.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; class HistorySidebarHeader extends ConsumerWidget { @@ -21,13 +22,22 @@ class HistorySidebarHeader extends ConsumerWidget { ), const Spacer(), IconButton( - tooltip: "Auto Delete Settings", + tooltip: "Manage History", style: IconButton.styleFrom( foregroundColor: Theme.of(context).colorScheme.primary, ), - onPressed: () {}, + onPressed: () { + showHistoryRetentionDialog( + context, + ref.read(settingsProvider.select( + (value) => value.historyRetentionPeriod)), (value) { + ref.read(settingsProvider.notifier).update( + historyRetentionPeriod: value, + ); + }); + }, icon: const Icon( - Icons.auto_delete_outlined, + Icons.manage_history_rounded, size: 20, ), ), diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 521fdad8..7c40ef3f 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -220,8 +220,8 @@ class RequestItem extends ConsumerWidget { selectedId: selectedId, editRequestId: editRequestId, onTap: () { - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); ref.read(selectedIdStateProvider.notifier).state = id; + ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); }, // onDoubleTap: () { // ref.read(selectedIdStateProvider.notifier).state = id; diff --git a/lib/widgets/dialog_history_retention.dart b/lib/widgets/dialog_history_retention.dart new file mode 100644 index 00000000..67f513ac --- /dev/null +++ b/lib/widgets/dialog_history_retention.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; + +showHistoryRetentionDialog( + BuildContext context, + HistoryRetentionPeriod historyRetentionPeriod, + Function(HistoryRetentionPeriod) onRetentionPeriodChange, +) { + HistoryRetentionPeriod selectedRetentionPeriod = historyRetentionPeriod; + + showDialog( + context: context, + builder: (context) { + return AlertDialog( + icon: const Icon(Icons.manage_history_rounded), + iconColor: Theme.of(context).colorScheme.primary, + title: const Text("Manage History"), + titleTextStyle: Theme.of(context).textTheme.titleLarge, + contentPadding: kPv20, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: kPh24, + child: Text( + "Select the duration for which you want to retain your request history", + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.8), + ), + ), + ), + kVSpacer10, + ...HistoryRetentionPeriod.values + .map((e) => RadioListTile( + title: Text( + e.label, + style: TextStyle( + color: selectedRetentionPeriod == e + ? Theme.of(context).colorScheme.primary + : null), + ), + secondary: Icon(e.icon, + color: selectedRetentionPeriod == e + ? Theme.of(context).colorScheme.primary + : Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.6)), + value: e, + groupValue: selectedRetentionPeriod, + onChanged: (value) { + if (value != null) { + selectedRetentionPeriod = value; + (context as Element).markNeedsBuild(); + } + }, + )) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () { + Navigator.pop(context); + }, + ), + TextButton( + child: const Text('Confirm'), + onPressed: () { + onRetentionPeriodChange(selectedRetentionPeriod); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); +} diff --git a/lib/widgets/request_widgets.dart b/lib/widgets/request_widgets.dart index 1e7b7ccf..5ad400aa 100644 --- a/lib/widgets/request_widgets.dart +++ b/lib/widgets/request_widgets.dart @@ -52,6 +52,10 @@ class _RequestPaneState extends State mainAxisAlignment: MainAxisAlignment.end, children: [ FilledButton.tonalIcon( + style: FilledButton.styleFrom( + padding: kPh12, + minimumSize: const Size(44, 44), + ), onPressed: widget.onPressedCodeButton, icon: Icon( widget.codePaneVisible diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 5383916d..fcb97a6b 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -15,6 +15,7 @@ export 'checkbox.dart'; export 'code_previewer.dart'; export 'codegen_previewer.dart'; export 'dialog_about.dart'; +export 'dialog_history_retention.dart'; export 'dialog_import.dart'; export 'dialog_rename.dart'; export 'drag_and_drop_area.dart'; From f3e3d30f849987995999230e15619ab48aa8a7f1 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 11:30:31 +0530 Subject: [PATCH 08/13] wip: history popup menu --- .../common_widgets/environment_dropdown.dart | 4 +- lib/screens/settings_page.dart | 64 +++++++------------ lib/widgets/popup_menu_codegen.dart | 4 +- lib/widgets/popup_menu_env.dart | 20 +++--- lib/widgets/popup_menu_history.dart | 57 +++++++++++++++++ lib/widgets/popup_menu_uri.dart | 4 +- lib/widgets/widgets.dart | 1 + 7 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 lib/widgets/popup_menu_history.dart diff --git a/lib/screens/common_widgets/environment_dropdown.dart b/lib/screens/common_widgets/environment_dropdown.dart index db012b32..cad6a17a 100644 --- a/lib/screens/common_widgets/environment_dropdown.dart +++ b/lib/screens/common_widgets/environment_dropdown.dart @@ -22,8 +22,8 @@ class EnvironmentDropdown extends ConsumerWidget { borderRadius: kBorderRadius8, ), child: EnvironmentPopupMenu( - activeEnvironment: environments?[activeEnvironment], - environments: environmentsList, + value: environments?[activeEnvironment], + items: environmentsList, onChanged: (value) { ref.read(activeEnvironmentIdStateProvider.notifier).state = value?.id; diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index c7426524..da00bfd2 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -81,27 +81,6 @@ class SettingsPage extends ConsumerWidget { }, items: kSupportedUriSchemes, ), - // DropdownButtonHideUnderline( - // child: DropdownButton( - // borderRadius: kBorderRadius8, - // onChanged: (value) { - // ref - // .read(settingsProvider.notifier) - // .update(defaultUriScheme: value); - // }, - // value: settings.defaultUriScheme, - // items: kSupportedUriSchemes - // .map>((String value) { - // return DropdownMenuItem( - // value: value, - // child: Padding( - // padding: kP10, - // child: Text(value), - // ), - // ); - // }).toList(), - // ), - // ), ), ), ListTile( @@ -123,26 +102,6 @@ class SettingsPage extends ConsumerWidget { }, items: CodegenLanguage.values, ), - // DropdownButtonHideUnderline( - // child: DropdownButton( - // borderRadius: kBorderRadius8, - // value: settings.defaultCodeGenLang, - // onChanged: (value) { - // ref - // .read(settingsProvider.notifier) - // .update(defaultCodeGenLang: value); - // }, - // items: CodegenLanguage.values.map((value) { - // return DropdownMenuItem( - // value: value, - // child: Padding( - // padding: kP10, - // child: Text(value.label), - // ), - // ); - // }).toList(), - // ), - // ), ), ), CheckboxListTile( @@ -186,6 +145,29 @@ class SettingsPage extends ConsumerWidget { ), ), ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('History Retention Period'), + subtitle: Text( + 'Your request history will be retained for ${settings.historyRetentionPeriod.label}'), + trailing: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.onSurface, + ), + borderRadius: kBorderRadius8, + ), + child: HistoryRetentionPopupMenu( + value: settings.historyRetentionPeriod, + onChanged: (value) { + ref + .read(settingsProvider.notifier) + .update(historyRetentionPeriod: value); + }, + items: HistoryRetentionPeriod.values, + ), + ), + ), ListTile( hoverColor: kColorTransparent, title: const Text('Clear Data'), diff --git a/lib/widgets/popup_menu_codegen.dart b/lib/widgets/popup_menu_codegen.dart index 6bb164f6..421d1ce7 100644 --- a/lib/widgets/popup_menu_codegen.dart +++ b/lib/widgets/popup_menu_codegen.dart @@ -1,6 +1,6 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; class CodegenPopupMenu extends StatelessWidget { const CodegenPopupMenu({ diff --git a/lib/widgets/popup_menu_env.dart b/lib/widgets/popup_menu_env.dart index 9b23ed9a..d61be5b4 100644 --- a/lib/widgets/popup_menu_env.dart +++ b/lib/widgets/popup_menu_env.dart @@ -1,24 +1,24 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; class EnvironmentPopupMenu extends StatelessWidget { const EnvironmentPopupMenu({ super.key, - this.activeEnvironment, + this.value, this.onChanged, - this.environments, + this.items, }); - final EnvironmentModel? activeEnvironment; + final EnvironmentModel? value; final void Function(EnvironmentModel? value)? onChanged; - final List? environments; + final List? items; final EnvironmentModel? noneEnvironmentModel = null; @override Widget build(BuildContext context) { - final activeEnvironmentName = getEnvironmentTitle(activeEnvironment?.name); + final valueName = getEnvironmentTitle(value?.name); final textClipLength = context.isCompactWindow ? 6 : 10; final double boxLength = context.isCompactWindow ? 100 : 130; return PopupMenuButton( @@ -34,7 +34,7 @@ class EnvironmentPopupMenu extends StatelessWidget { }, child: const Text("None"), ), - ...environments!.map((EnvironmentModel environment) { + ...items!.map((EnvironmentModel environment) { final name = getEnvironmentTitle(environment.name).clip(30); return PopupMenuItem( value: environment, @@ -55,9 +55,7 @@ class EnvironmentPopupMenu extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - activeEnvironment == null - ? "None" - : activeEnvironmentName.clip(textClipLength), + value == null ? "None" : valueName.clip(textClipLength), softWrap: false, ), const Icon( diff --git a/lib/widgets/popup_menu_history.dart b/lib/widgets/popup_menu_history.dart new file mode 100644 index 00000000..3f2af6ee --- /dev/null +++ b/lib/widgets/popup_menu_history.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; + +class HistoryRetentionPopupMenu extends StatelessWidget { + const HistoryRetentionPopupMenu({ + super.key, + required this.value, + required this.onChanged, + this.items, + }); + + final HistoryRetentionPeriod value; + final void Function(HistoryRetentionPeriod value) onChanged; + final List? items; + @override + Widget build(BuildContext context) { + final double boxLength = context.isCompactWindow ? 110 : 130; + return PopupMenuButton( + tooltip: "Select retention period", + surfaceTintColor: kColorTransparent, + constraints: BoxConstraints(minWidth: boxLength), + itemBuilder: (BuildContext context) { + return [ + ...items!.map((period) { + return PopupMenuItem( + value: period, + child: Text( + period.label, + softWrap: false, + overflow: TextOverflow.ellipsis, + ), + ); + }) + ]; + }, + onSelected: onChanged, + child: Container( + width: boxLength, + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value.label, + style: kTextStylePopupMenuItem, + ), + const Icon( + Icons.unfold_more, + size: 16, + ) + ], + ), + ), + ); + } +} diff --git a/lib/widgets/popup_menu_uri.dart b/lib/widgets/popup_menu_uri.dart index a6a3bb6c..177381b0 100644 --- a/lib/widgets/popup_menu_uri.dart +++ b/lib/widgets/popup_menu_uri.dart @@ -1,6 +1,6 @@ -import 'package:apidash/consts.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:flutter/material.dart'; +import 'package:apidash/extensions/extensions.dart'; +import 'package:apidash/consts.dart'; class URIPopupMenu extends StatelessWidget { const URIPopupMenu({ diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index fcb97a6b..06bc4bef 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -42,6 +42,7 @@ export 'menu_sidebar_top.dart'; export 'overlay_widget.dart'; export 'popup_menu_codegen.dart'; export 'popup_menu_env.dart'; +export 'popup_menu_history.dart'; export 'popup_menu_uri.dart'; export 'previewer.dart'; export 'request_widgets.dart'; From bed7ad347c4fd77e13b2e11e69b6b47e421647b7 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 21:19:33 +0530 Subject: [PATCH 09/13] feat: auto clear history --- lib/main.dart | 1 + lib/services/history_service.dart | 51 +++++++++++++++++++++++++++ lib/services/hive_services.dart | 2 +- lib/services/services.dart | 1 + lib/utils/history_utils.dart | 16 +++++++++ test/providers/ui_providers_test.dart | 3 ++ 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 lib/services/history_service.dart diff --git a/lib/main.dart b/lib/main.dart index 5ac878aa..d9730a03 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); GoogleFonts.config.allowRuntimeFetching = false; await openBoxes(); + await autoClearHistory(); if (kIsLinux) { await setupInitialWindow(); } diff --git a/lib/services/history_service.dart b/lib/services/history_service.dart new file mode 100644 index 00000000..d34d17fb --- /dev/null +++ b/lib/services/history_service.dart @@ -0,0 +1,51 @@ +import 'package:apidash/models/models.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; +import 'hive_services.dart'; + +Future autoClearHistory() async { + final settingsMap = hiveHandler.settings; + final retentionPeriod = settingsMap['historyRetentionPeriod']; + + HistoryRetentionPeriod historyRetentionPeriod = + HistoryRetentionPeriod.oneWeek; + if (retentionPeriod != null) { + historyRetentionPeriod = + HistoryRetentionPeriod.values.byName(retentionPeriod); + } + DateTime? retentionDate = getRetentionDate(historyRetentionPeriod); + + if (retentionDate == null) { + return; + } else { + List? historyIds = hiveHandler.getHistoryIds(); + List toRemoveIds = []; + + if (historyIds == null || historyIds.isEmpty) { + return; + } + + for (var historyId in historyIds) { + var jsonModel = hiveHandler.getHistoryMeta(historyId); + if (jsonModel != null) { + var jsonMap = Map.from(jsonModel); + HistoryMetaModel historyMetaModelFromJson = + HistoryMetaModel.fromJson(jsonMap); + if (historyMetaModelFromJson.timeStamp.isBefore(retentionDate)) { + toRemoveIds.add(historyId); + } + } + } + + if (toRemoveIds.isEmpty) { + return; + } + + for (var id in toRemoveIds) { + await hiveHandler.deleteHistoryRequest(id); + hiveHandler.deleteHistoryMeta(id); + } + hiveHandler.setHistoryIds( + historyIds..removeWhere((id) => toRemoveIds.contains(id))); + } +} diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index c6a3cc40..1a0f7ac0 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -97,7 +97,7 @@ class HiveHandler { String id, Map? historyRequestJsoon) => historyLazyBox.put(id, historyRequestJsoon); - Future deleteHistoryReqyest(String id) => historyLazyBox.delete(id); + Future deleteHistoryRequest(String id) => historyLazyBox.delete(id); Future clear() async { await dataBox.clear(); diff --git a/lib/services/services.dart b/lib/services/services.dart index a7cf03fd..7551de9b 100644 --- a/lib/services/services.dart +++ b/lib/services/services.dart @@ -1,3 +1,4 @@ export 'http_service.dart'; export 'hive_services.dart'; +export 'history_service.dart'; export 'window_services.dart'; diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index acbea853..c91a5e68 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -113,3 +113,19 @@ List getRequestGroup( requestGroup.sort((a, b) => b.timeStamp.compareTo(a.timeStamp)); return requestGroup; } + +DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) { + DateTime now = DateTime.now(); + DateTime today = DateTime(now.year, now.month, now.day); + + switch (retentionPeriod) { + case HistoryRetentionPeriod.oneWeek: + return today.subtract(const Duration(days: 7)); + case HistoryRetentionPeriod.oneMonth: + return today.subtract(const Duration(days: 30)); + case HistoryRetentionPeriod.threeMonths: + return today.subtract(const Duration(days: 90)); + default: + return null; + } +} diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 5854f768..1bd76c4d 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,4 +1,7 @@ import 'dart:io'; + +import 'package:spot/spot.dart'; +import 'package:apidash/consts.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/screens/common_widgets/common_widgets.dart'; import 'package:apidash/screens/dashboard.dart'; From 3265269c255ac8b9da349f1459c1277dfa4ad02d Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Mon, 22 Jul 2024 21:39:06 +0530 Subject: [PATCH 10/13] refactor: imports --- lib/screens/mobile/dashboard.dart | 6 +++--- lib/utils/history_utils.dart | 4 ++-- lib/widgets/card_history_request.dart | 4 ++-- lib/widgets/card_sidebar_history.dart | 4 ++-- lib/widgets/table_request.dart | 2 +- lib/widgets/table_request_form.dart | 3 ++- test/providers/ui_providers_test.dart | 1 - 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index a74e6094..8da58375 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -1,15 +1,15 @@ -import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; -import '../settings_page.dart'; -import 'navbar.dart'; import 'requests_page/requests_page.dart'; import '../envvar/environment_page.dart'; +import '../history/history_page.dart'; +import '../settings_page.dart'; import 'widgets/page_base.dart'; +import 'navbar.dart'; class MobileDashboard extends ConsumerStatefulWidget { const MobileDashboard({super.key}); diff --git a/lib/utils/history_utils.dart b/lib/utils/history_utils.dart index c91a5e68..cce87998 100644 --- a/lib/utils/history_utils.dart +++ b/lib/utils/history_utils.dart @@ -1,6 +1,6 @@ -import 'package:apidash/utils/convert_utils.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; +import 'convert_utils.dart'; DateTime stripTime(DateTime dateTime) { return DateTime(dateTime.year, dateTime.month, dateTime.day); @@ -116,7 +116,7 @@ List getRequestGroup( DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) { DateTime now = DateTime.now(); - DateTime today = DateTime(now.year, now.month, now.day); + DateTime today = stripTime(now); switch (retentionPeriod) { case HistoryRetentionPeriod.oneWeek: diff --git a/lib/widgets/card_history_request.dart b/lib/widgets/card_history_request.dart index a76e477c..228cbf5b 100644 --- a/lib/widgets/card_history_request.dart +++ b/lib/widgets/card_history_request.dart @@ -1,7 +1,7 @@ -import 'package:apidash/models/history_meta_model.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import 'texts.dart'; class HistoryRequestCard extends StatelessWidget { diff --git a/lib/widgets/card_sidebar_history.dart b/lib/widgets/card_sidebar_history.dart index be6387b3..9b1a9f20 100644 --- a/lib/widgets/card_sidebar_history.dart +++ b/lib/widgets/card_sidebar_history.dart @@ -1,7 +1,7 @@ -import 'package:apidash/models/history_meta_model.dart'; import 'package:flutter/material.dart'; -import 'package:apidash/consts.dart'; +import 'package:apidash/models/models.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import 'texts.dart' show MethodBox; class SidebarHistoryCard extends StatelessWidget { diff --git a/lib/widgets/table_request.dart b/lib/widgets/table_request.dart index 010aaa27..8c3d9055 100644 --- a/lib/widgets/table_request.dart +++ b/lib/widgets/table_request.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:data_table_2/data_table_2.dart'; -import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/consts.dart'; +import 'field_read_only.dart'; class RequestDataTable extends StatelessWidget { const RequestDataTable({ diff --git a/lib/widgets/table_request_form.dart b/lib/widgets/table_request_form.dart index a61c0cd7..91a1b39d 100644 --- a/lib/widgets/table_request_form.dart +++ b/lib/widgets/table_request_form.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:data_table_2/data_table_2.dart'; -import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; +import 'button_form_data_file.dart'; +import 'field_read_only.dart'; class RequestFormDataTable extends StatelessWidget { const RequestFormDataTable({ diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 1bd76c4d..81fb7656 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:spot/spot.dart'; import 'package:apidash/consts.dart'; import 'package:apidash/providers/providers.dart'; From dace495264f478f5e9e10f3c5498221b41311254 Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Tue, 23 Jul 2024 16:02:46 +0530 Subject: [PATCH 11/13] fix: ui tests --- lib/consts.dart | 6 ++- lib/providers/ui_providers.dart | 4 +- lib/screens/common_widgets/button_navbar.dart | 14 ++++--- lib/screens/dashboard.dart | 19 ++++----- lib/screens/envvar/environment_page.dart | 4 +- lib/screens/envvar/environments_pane.dart | 2 +- lib/screens/history/history_page.dart | 5 +-- lib/screens/history/history_pane.dart | 5 +-- lib/screens/home_page/collection_pane.dart | 4 +- lib/screens/home_page/home_page.dart | 24 +++++++----- lib/screens/mobile/dashboard.dart | 13 ++----- lib/screens/mobile/navbar.dart | 2 +- .../mobile/requests_page/requests_page.dart | 5 +-- lib/screens/settings_page.dart | 2 +- lib/utils/ui_utils.dart | 11 ++++++ lib/widgets/popup_menu_history.dart | 6 +-- lib/widgets/popup_menu_uri.dart | 2 +- test/models/settings_model_test.dart | 10 +++-- test/providers/ui_providers_test.dart | 39 ++++++++++++++++++- 19 files changed, 108 insertions(+), 69 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 5e8790c0..73de67f6 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -71,6 +71,10 @@ final kCodeStyle = TextStyle( fontFamilyFallback: kFontFamilyFallback, ); +final kHomeScaffoldKey = GlobalKey(); +final kEnvScaffoldKey = GlobalKey(); +final kHisScaffoldKey = GlobalKey(); + const kHintOpacity = 0.6; const kForegroundOpacity = 0.05; const kOverlayBackgroundOpacity = 0.5; @@ -82,7 +86,7 @@ const kFormDataButtonLabelTextStyle = TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ); -const kTextStylePopupMenuItem = TextStyle(fontSize: 16); +const kTextStylePopupMenuItem = TextStyle(fontSize: 14); final kButtonSidebarStyle = ElevatedButton.styleFrom(padding: kPh12); diff --git a/lib/providers/ui_providers.dart b/lib/providers/ui_providers.dart index 0478b040..d8076935 100644 --- a/lib/providers/ui_providers.dart +++ b/lib/providers/ui_providers.dart @@ -2,8 +2,8 @@ import 'package:apidash/consts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -final mobileScaffoldKeyStateProvider = StateProvider>( - (ref) => GlobalKey()); +final mobileScaffoldKeyStateProvider = + StateProvider>((ref) => kHomeScaffoldKey); final leftDrawerStateProvider = StateProvider((ref) => false); final navRailIndexStateProvider = StateProvider((ref) => 0); final selectedIdEditStateProvider = StateProvider((ref) => null); diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index f18c695c..bce7687f 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/utils/utils.dart'; class NavbarButton extends ConsumerWidget { const NavbarButton({ @@ -25,19 +26,20 @@ class NavbarButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); + final mobileScaffoldKeyNotifier = + ref.watch(mobileScaffoldKeyStateProvider.notifier); final bool isSelected = railIdx == buttonIdx; final Size size = isCompact ? const Size(56, 32) : const Size(65, 32); var onPress = isSelected ? null : () { if (buttonIdx != null) { - ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!; + final scaffoldKey = getScaffoldKey(buttonIdx!); + ref.watch(navRailIndexStateProvider.notifier).state = buttonIdx!; + mobileScaffoldKeyNotifier.state = scaffoldKey; if ((railIdx > 2 && buttonIdx! <= 2) || - !(ref - .read(mobileScaffoldKeyStateProvider) - .currentState - ?.isDrawerOpen ?? - true)) { + !(mobileScaffoldKey.currentState?.isDrawerOpen ?? true)) { ref.read(leftDrawerStateProvider.notifier).state = false; } } diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index c9b57475..c0f1532f 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,4 +1,3 @@ -import 'package:apidash/screens/history/history_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; @@ -7,6 +6,7 @@ import 'package:apidash/consts.dart'; import 'common_widgets/common_widgets.dart'; import 'envvar/environment_page.dart'; import 'home_page/home_page.dart'; +import 'history/history_page.dart'; import 'settings_page.dart'; class Dashboard extends ConsumerWidget { @@ -15,7 +15,6 @@ class Dashboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final railIdx = ref.watch(navRailIndexStateProvider); - final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); return Scaffold( body: SafeArea( child: Row( @@ -61,7 +60,7 @@ class Dashboard extends ConsumerWidget { ref.read(navRailIndexStateProvider.notifier).state = 2; }, icon: const Icon(Icons.history_outlined), - selectedIcon: const Icon(Icons.history), + selectedIcon: const Icon(Icons.history_rounded), ), Text( 'History', @@ -113,15 +112,11 @@ class Dashboard extends ConsumerWidget { child: IndexedStack( alignment: AlignmentDirectional.topCenter, index: railIdx, - children: [ - const HomePage(), - EnvironmentPage( - scaffoldKey: mobileScaffoldKey, - ), - HistoryPage( - scaffoldKey: mobileScaffoldKey, - ), - const SettingsPage(), + children: const [ + HomePage(), + EnvironmentPage(), + HistoryPage(), + SettingsPage(), ], ), ) diff --git a/lib/screens/envvar/environment_page.dart b/lib/screens/envvar/environment_page.dart index bbf1dcec..360b4551 100644 --- a/lib/screens/envvar/environment_page.dart +++ b/lib/screens/envvar/environment_page.dart @@ -12,10 +12,8 @@ import 'environment_editor.dart'; class EnvironmentPage extends ConsumerWidget { const EnvironmentPage({ super.key, - required this.scaffoldKey, }); - final GlobalKey scaffoldKey; @override Widget build(BuildContext context, WidgetRef ref) { final id = ref.watch(selectedEnvironmentIdStateProvider); @@ -23,7 +21,7 @@ class EnvironmentPage extends ConsumerWidget { selectedEnvironmentModelProvider.select((value) => value?.name))); if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, + scaffoldKey: kEnvScaffoldKey, mainContent: const EnvironmentEditor(), title: EditorTitle( title: name, diff --git a/lib/screens/envvar/environments_pane.dart b/lib/screens/envvar/environments_pane.dart index dd6b5af4..34d58e4b 100644 --- a/lib/screens/envvar/environments_pane.dart +++ b/lib/screens/envvar/environments_pane.dart @@ -191,7 +191,7 @@ class EnvironmentItem extends ConsumerWidget { }, onTap: () { ref.read(selectedEnvironmentIdStateProvider.notifier).state = id; - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); + kEnvScaffoldKey.currentState?.closeDrawer(); }, focusNode: ref.watch(nameTextFieldFocusNodeProvider), onChangedNameEditor: (value) { diff --git a/lib/screens/history/history_page.dart b/lib/screens/history/history_page.dart index 0f288fb7..d56c90f7 100644 --- a/lib/screens/history/history_page.dart +++ b/lib/screens/history/history_page.dart @@ -4,16 +4,15 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/providers/providers.dart'; import 'package:apidash/utils/utils.dart'; +import 'package:apidash/consts.dart'; import 'history_pane.dart'; import 'history_viewer.dart'; class HistoryPage extends ConsumerWidget { const HistoryPage({ super.key, - required this.scaffoldKey, }); - final GlobalKey scaffoldKey; @override Widget build(BuildContext context, WidgetRef ref) { final historyModel = ref.watch(selectedHistoryRequestModelProvider); @@ -22,7 +21,7 @@ class HistoryPage extends ConsumerWidget { : 'History'; if (context.isMediumWindow) { return DrawerSplitView( - scaffoldKey: scaffoldKey, + scaffoldKey: kHisScaffoldKey, mainContent: const HistoryViewer(), title: Text(title), leftDrawerContent: const HistoryPane(), diff --git a/lib/screens/history/history_pane.dart b/lib/screens/history/history_pane.dart index ba89c565..d76eac17 100644 --- a/lib/screens/history/history_pane.dart +++ b/lib/screens/history/history_pane.dart @@ -150,10 +150,7 @@ class _HistoryExpansionTileState extends ConsumerState ref .read(historyMetaStateNotifier.notifier) .loadHistoryRequest(item.first.historyId); - ref - .read(mobileScaffoldKeyStateProvider) - .currentState - ?.closeDrawer(); + kHisScaffoldKey.currentState?.closeDrawer(); }, ), ); diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 7c40ef3f..b30eded2 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -1,7 +1,7 @@ -import 'package:apidash/importer/importer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; +import 'package:apidash/importer/importer.dart'; import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; @@ -221,7 +221,7 @@ class RequestItem extends ConsumerWidget { editRequestId: editRequestId, onTap: () { ref.read(selectedIdStateProvider.notifier).state = id; - ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer(); + kHomeScaffoldKey.currentState?.closeDrawer(); }, // onDoubleTap: () { // ref.read(selectedIdStateProvider.notifier).state = id; diff --git a/lib/screens/home_page/home_page.dart b/lib/screens/home_page/home_page.dart index a7338a08..76509a08 100644 --- a/lib/screens/home_page/home_page.dart +++ b/lib/screens/home_page/home_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:apidash/widgets/widgets.dart'; +import 'package:apidash/extensions/extensions.dart'; +import '../mobile/requests_page/requests_page.dart'; import 'editor_pane/editor_pane.dart'; import 'collection_pane.dart'; @@ -8,15 +10,17 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Column( - children: [ - Expanded( - child: DashboardSplitView( - sidebarWidget: CollectionPane(), - mainWidget: RequestEditorPane(), - ), - ), - ], - ); + return context.isMediumWindow + ? const RequestResponsePage() + : const Column( + children: [ + Expanded( + child: DashboardSplitView( + sidebarWidget: CollectionPane(), + mainWidget: RequestEditorPane(), + ), + ), + ], + ); } } diff --git a/lib/screens/mobile/dashboard.dart b/lib/screens/mobile/dashboard.dart index 8da58375..0cef40ab 100644 --- a/lib/screens/mobile/dashboard.dart +++ b/lib/screens/mobile/dashboard.dart @@ -67,25 +67,18 @@ class PageBranch extends ConsumerWidget { final int pageIndex; @override Widget build(BuildContext context, WidgetRef ref) { - final scaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); switch (pageIndex) { case 1: - return EnvironmentPage( - scaffoldKey: scaffoldKey, - ); + return const EnvironmentPage(); case 2: - return HistoryPage( - scaffoldKey: scaffoldKey, - ); + return const HistoryPage(); case 3: return const PageBase( title: 'Settings', scaffoldBody: SettingsPage(), ); default: - return RequestResponsePage( - scaffoldKey: scaffoldKey, - ); + return const RequestResponsePage(); } } } diff --git a/lib/screens/mobile/navbar.dart b/lib/screens/mobile/navbar.dart index 5449f45f..b5402b88 100644 --- a/lib/screens/mobile/navbar.dart +++ b/lib/screens/mobile/navbar.dart @@ -52,7 +52,7 @@ class BottomNavBar extends ConsumerWidget { child: NavbarButton( railIdx: railIdx, buttonIdx: 2, - selectedIcon: Icons.history, + selectedIcon: Icons.history_rounded, icon: Icons.history_outlined, label: 'History', ), diff --git a/lib/screens/mobile/requests_page/requests_page.dart b/lib/screens/mobile/requests_page/requests_page.dart index 4164356c..fcd291b1 100644 --- a/lib/screens/mobile/requests_page/requests_page.dart +++ b/lib/screens/mobile/requests_page/requests_page.dart @@ -15,11 +15,8 @@ import 'request_response_tabs.dart'; class RequestResponsePage extends StatefulHookConsumerWidget { const RequestResponsePage({ super.key, - required this.scaffoldKey, }); - final GlobalKey scaffoldKey; - @override ConsumerState createState() => _RequestResponsePageState(); @@ -35,7 +32,7 @@ class _RequestResponsePageState extends ConsumerState final TabController requestResponseTabController = useTabController(initialLength: 2, vsync: this); return DrawerSplitView( - scaffoldKey: widget.scaffoldKey, + scaffoldKey: kHomeScaffoldKey, title: EditorTitle( title: name, onSelected: (ItemMenuOption item) { diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index da00bfd2..2ce155f2 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -149,7 +149,7 @@ class SettingsPage extends ConsumerWidget { hoverColor: kColorTransparent, title: const Text('History Retention Period'), subtitle: Text( - 'Your request history will be retained for ${settings.historyRetentionPeriod.label}'), + 'Your request history will be retained${settings.historyRetentionPeriod == HistoryRetentionPeriod.forever ? "" : " for"} ${settings.historyRetentionPeriod.label}'), trailing: Container( decoration: BoxDecoration( border: Border.all( diff --git a/lib/utils/ui_utils.dart b/lib/utils/ui_utils.dart index 3bf1fb98..764c821b 100644 --- a/lib/utils/ui_utils.dart +++ b/lib/utils/ui_utils.dart @@ -69,3 +69,14 @@ double? getJsonPreviewerMaxRootNodeWidth(double w) { } return w - 150; } + +GlobalKey getScaffoldKey(int railIdx) { + switch (railIdx) { + case 1: + return kEnvScaffoldKey; + case 2: + return kHisScaffoldKey; + default: + return kHomeScaffoldKey; + } +} diff --git a/lib/widgets/popup_menu_history.dart b/lib/widgets/popup_menu_history.dart index 3f2af6ee..72398bb3 100644 --- a/lib/widgets/popup_menu_history.dart +++ b/lib/widgets/popup_menu_history.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:apidash/extensions/extensions.dart'; import 'package:apidash/consts.dart'; class HistoryRetentionPopupMenu extends StatelessWidget { @@ -15,11 +14,11 @@ class HistoryRetentionPopupMenu extends StatelessWidget { final List? items; @override Widget build(BuildContext context) { - final double boxLength = context.isCompactWindow ? 110 : 130; + const double boxLength = 120; return PopupMenuButton( tooltip: "Select retention period", surfaceTintColor: kColorTransparent, - constraints: BoxConstraints(minWidth: boxLength), + constraints: const BoxConstraints(minWidth: boxLength), itemBuilder: (BuildContext context) { return [ ...items!.map((period) { @@ -44,6 +43,7 @@ class HistoryRetentionPopupMenu extends StatelessWidget { Text( value.label, style: kTextStylePopupMenuItem, + overflow: TextOverflow.ellipsis, ), const Icon( Icons.unfold_more, diff --git a/lib/widgets/popup_menu_uri.dart b/lib/widgets/popup_menu_uri.dart index 177381b0..5a62c97e 100644 --- a/lib/widgets/popup_menu_uri.dart +++ b/lib/widgets/popup_menu_uri.dart @@ -15,7 +15,7 @@ class URIPopupMenu extends StatelessWidget { final List? items; @override Widget build(BuildContext context) { - final double boxLength = context.isCompactWindow ? 90 : 130; + final double boxLength = context.isCompactWindow ? 90 : 110; return PopupMenuButton( tooltip: "Select URI Scheme", surfaceTintColor: kColorTransparent, diff --git a/test/models/settings_model_test.dart b/test/models/settings_model_test.dart index 41fea3b3..6df01335 100644 --- a/test/models/settings_model_test.dart +++ b/test/models/settings_model_test.dart @@ -14,6 +14,7 @@ void main() { saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, + historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, ); test('Testing toJson()', () { @@ -28,7 +29,8 @@ void main() { "defaultCodeGenLang": "curl", "saveResponses": true, "promptBeforeClosing": true, - 'activeEnvironmentId': null + "activeEnvironmentId": null, + "historyRetentionPeriod": "oneWeek", }; expect(sm.toJson(), expectedResult); }); @@ -45,7 +47,8 @@ void main() { "defaultCodeGenLang": "curl", "saveResponses": true, "promptBeforeClosing": true, - 'activeEnvironmentId': null + "activeEnvironmentId": null, + "historyRetentionPeriod": "oneWeek", }; expect(SettingsModel.fromJson(input), sm); }); @@ -61,6 +64,7 @@ void main() { saveResponses: false, promptBeforeClosing: true, activeEnvironmentId: null, + historyRetentionPeriod: HistoryRetentionPeriod.oneWeek, ); expect( sm.copyWith( @@ -72,7 +76,7 @@ void main() { test('Testing toString()', () { const expectedResult = - "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null}"; + "{isDark: false, alwaysShowCollectionPaneScrollbar: true, width: 300.0, height: 200.0, dx: 100.0, dy: 150.0, defaultUriScheme: http, defaultCodeGenLang: curl, saveResponses: true, promptBeforeClosing: true, activeEnvironmentId: null, historyRetentionPeriod: oneWeek}"; expect(sm.toString(), expectedResult); }); diff --git a/test/providers/ui_providers_test.dart b/test/providers/ui_providers_test.dart index 81fb7656..9b0653d3 100644 --- a/test/providers/ui_providers_test.dart +++ b/test/providers/ui_providers_test.dart @@ -12,6 +12,7 @@ import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart'; import 'package:apidash/screens/home_page/editor_pane/url_card.dart'; import 'package:apidash/screens/home_page/home_page.dart'; import 'package:apidash/screens/settings_page.dart'; +import 'package:apidash/screens/history/history_page.dart'; import 'package:apidash/services/hive_services.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:extended_text_field/extended_text_field.dart'; @@ -59,6 +60,7 @@ void main() { // Verify that the HomePage is displayed initially expect(find.byType(HomePage), findsOneWidget); expect(find.byType(EnvironmentPage), findsNothing); + expect(find.byType(HistoryPage), findsNothing); expect(find.byType(SettingsPage), findsNothing); }); @@ -79,9 +81,32 @@ void main() { // Verify that the EnvironmentPage is displayed expect(find.byType(HomePage), findsNothing); expect(find.byType(EnvironmentPage), findsOneWidget); + expect(find.byType(HistoryPage), findsNothing); expect(find.byType(SettingsPage), findsNothing); }); + testWidgets( + "Dashboard should display HistorPage when navRailIndexStateProvider is 2", + (WidgetTester tester) async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + navRailIndexStateProvider.overrideWith((ref) => 2), + ], + child: const Portal( + child: MaterialApp( + home: Dashboard(), + ), + ), + ), + ); + + // Verify that the SettingsPage is displayed + expect(find.byType(HomePage), findsNothing); + expect(find.byType(EnvironmentPage), findsNothing); + expect(find.byType(HistoryPage), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + }); testWidgets( "Dashboard should display SettingsPage when navRailIndexStateProvider is 3", (WidgetTester tester) async { @@ -101,6 +126,7 @@ void main() { // Verify that the SettingsPage is displayed expect(find.byType(HomePage), findsNothing); expect(find.byType(EnvironmentPage), findsNothing); + expect(find.byType(HistoryPage), findsNothing); expect(find.byType(SettingsPage), findsOneWidget); }); @@ -159,7 +185,7 @@ void main() { // Verify that the navRailIndexStateProvider still has the updated value final dashboard = tester.element(find.byType(Dashboard)); final container = ProviderScope.containerOf(dashboard); - expect(container.read(navRailIndexStateProvider), 2); + expect(container.read(navRailIndexStateProvider), 3); // Verify that the SettingsPage is still displayed after the rebuild expect(find.byType(SettingsPage), findsOneWidget); @@ -193,10 +219,19 @@ void main() { // Verify that the selected icon is the filled version (selectedIcon) expect(find.byIcon(Icons.computer_rounded), findsOneWidget); - // Go to SettingsPage + // Go to HistoryPage container.read(navRailIndexStateProvider.notifier).state = 2; await tester.pump(); + // Verify that the HistoryPage is displayed + expect(find.byType(HistoryPage), findsOneWidget); + // Verify that the selected icon is the filled version (selectedIcon) + expect(find.byIcon(Icons.history_rounded), findsOneWidget); + + // Go to SettingsPage + container.read(navRailIndexStateProvider.notifier).state = 3; + await tester.pump(); + // Verify that the SettingsPage is displayed expect(find.byType(SettingsPage), findsOneWidget); // Verify that the selected icon is the filled version (selectedIcon) From 1d063c19114e8613133a659383f5c9ca73f555bb Mon Sep 17 00:00:00 2001 From: DenserMeerkat Date: Thu, 25 Jul 2024 17:25:54 +0530 Subject: [PATCH 12/13] fix: scaffold drawer issue --- lib/screens/common_widgets/button_navbar.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/screens/common_widgets/button_navbar.dart b/lib/screens/common_widgets/button_navbar.dart index bce7687f..b9ea8e61 100644 --- a/lib/screens/common_widgets/button_navbar.dart +++ b/lib/screens/common_widgets/button_navbar.dart @@ -26,7 +26,6 @@ class NavbarButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final mobileScaffoldKey = ref.watch(mobileScaffoldKeyStateProvider); final mobileScaffoldKeyNotifier = ref.watch(mobileScaffoldKeyStateProvider.notifier); final bool isSelected = railIdx == buttonIdx; @@ -38,10 +37,7 @@ class NavbarButton extends ConsumerWidget { final scaffoldKey = getScaffoldKey(buttonIdx!); ref.watch(navRailIndexStateProvider.notifier).state = buttonIdx!; mobileScaffoldKeyNotifier.state = scaffoldKey; - if ((railIdx > 2 && buttonIdx! <= 2) || - !(mobileScaffoldKey.currentState?.isDrawerOpen ?? true)) { - ref.read(leftDrawerStateProvider.notifier).state = false; - } + ref.read(leftDrawerStateProvider.notifier).state = false; } onTap?.call(); }; From 464a1521c2cddcf0849d7ae0d91a0fc5bbb119d9 Mon Sep 17 00:00:00 2001 From: Ashita Prasad Date: Sat, 10 Aug 2024 18:33:42 +0530 Subject: [PATCH 13/13] move class --- lib/consts.dart | 14 -------------- lib/widgets/button_group_filled.dart | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/consts.dart b/lib/consts.dart index 73de67f6..09efd409 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -322,20 +322,6 @@ final kColorHttpMethodPut = Colors.amber.shade900; final kColorHttpMethodPatch = kColorHttpMethodPut; final kColorHttpMethodDelete = Colors.red.shade800; -class ButtonData { - ButtonData({ - required this.label, - required this.icon, - this.onPressed, - this.tooltip = "", - }); - - final String label; - final IconData icon; - final VoidCallback? onPressed; - final String tooltip; -} - enum HistoryRetentionPeriod { oneWeek("1 Week", Icons.calendar_view_week_rounded), oneMonth("1 Month", Icons.calendar_view_month_rounded), diff --git a/lib/widgets/button_group_filled.dart b/lib/widgets/button_group_filled.dart index 24ae622d..7e1de0db 100644 --- a/lib/widgets/button_group_filled.dart +++ b/lib/widgets/button_group_filled.dart @@ -1,6 +1,20 @@ import 'package:flutter/material.dart'; import 'package:apidash/consts.dart'; +class ButtonData { + ButtonData({ + required this.label, + required this.icon, + this.onPressed, + this.tooltip = "", + }); + + final String label; + final IconData icon; + final VoidCallback? onPressed; + final String tooltip; +} + class FilledButtonGroup extends StatelessWidget { const FilledButtonGroup({super.key, required this.buttons});