mirror of
https://github.com/foss42/apidash.git
synced 2025-06-27 19:07:40 +08:00
Merge pull request #430 from DenserMeerkat/add-feature-history
feat: history of requests
This commit is contained in:
@ -71,6 +71,10 @@ final kCodeStyle = TextStyle(
|
||||
fontFamilyFallback: kFontFamilyFallback,
|
||||
);
|
||||
|
||||
final kHomeScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final kEnvScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
final kHisScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
const kHintOpacity = 0.6;
|
||||
const kForegroundOpacity = 0.05;
|
||||
const kOverlayBackgroundOpacity = 0.5;
|
||||
@ -82,11 +86,12 @@ const kFormDataButtonLabelTextStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
const kTextStylePopupMenuItem = TextStyle(fontSize: 16);
|
||||
const kTextStylePopupMenuItem = TextStyle(fontSize: 14);
|
||||
|
||||
final kButtonSidebarStyle = ElevatedButton.styleFrom(padding: kPh12);
|
||||
|
||||
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));
|
||||
@ -99,23 +104,25 @@ 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 kPv20 = EdgeInsets.symmetric(vertical: 20);
|
||||
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);
|
||||
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,
|
||||
@ -147,6 +154,9 @@ const kPt8 = EdgeInsets.only(
|
||||
const kPt20 = EdgeInsets.only(
|
||||
top: 20,
|
||||
);
|
||||
const kPt24 = EdgeInsets.only(
|
||||
top: 24,
|
||||
);
|
||||
const kPt28 = EdgeInsets.only(
|
||||
top: 28,
|
||||
);
|
||||
@ -162,6 +172,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);
|
||||
@ -171,6 +182,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);
|
||||
|
||||
@ -185,8 +197,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,
|
||||
@ -310,6 +322,17 @@ final kColorHttpMethodPut = Colors.amber.shade900;
|
||||
final kColorHttpMethodPatch = kColorHttpMethodPut;
|
||||
final kColorHttpMethodDelete = Colors.red.shade800;
|
||||
|
||||
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"),
|
||||
@ -718,6 +741,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";
|
||||
|
@ -9,6 +9,7 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
GoogleFonts.config.allowRuntimeFetching = false;
|
||||
await openBoxes();
|
||||
await autoClearHistory();
|
||||
if (kIsLinux) {
|
||||
await setupInitialWindow();
|
||||
}
|
||||
|
22
lib/models/history_meta_model.dart
Normal file
22
lib/models/history_meta_model.dart
Normal file
@ -0,0 +1,22 @@
|
||||
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,
|
||||
required String requestId,
|
||||
@Default("") String name,
|
||||
required String url,
|
||||
required HTTPVerb method,
|
||||
required int responseStatus,
|
||||
required DateTime timeStamp,
|
||||
}) = _HistoryMetaModel;
|
||||
|
||||
factory HistoryMetaModel.fromJson(Map<String, Object?> json) =>
|
||||
_$HistoryMetaModelFromJson(json);
|
||||
}
|
282
lib/models/history_meta_model.freezed.dart
Normal file
282
lib/models/history_meta_model.freezed.dart
Normal file
@ -0,0 +1,282 @@
|
||||
// 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>(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<String, dynamic> json) {
|
||||
return _HistoryMetaModel.fromJson(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;
|
||||
int get responseStatus => throw _privateConstructorUsedError;
|
||||
DateTime get timeStamp => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$HistoryMetaModelCopyWith<HistoryMetaModel> 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 requestId,
|
||||
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? requestId = 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,
|
||||
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
|
||||
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 requestId,
|
||||
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? requestId = 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,
|
||||
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
|
||||
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,
|
||||
required this.requestId,
|
||||
this.name = "",
|
||||
required this.url,
|
||||
required this.method,
|
||||
required this.responseStatus,
|
||||
required this.timeStamp});
|
||||
|
||||
factory _$HistoryMetaModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$HistoryMetaModelImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String historyId;
|
||||
@override
|
||||
final String requestId;
|
||||
@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, requestId: $requestId, 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.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) &&
|
||||
(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, requestId, name, url,
|
||||
method, responseStatus, timeStamp);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$HistoryMetaModelImplCopyWith<_$HistoryMetaModelImpl> get copyWith =>
|
||||
__$$HistoryMetaModelImplCopyWithImpl<_$HistoryMetaModelImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$HistoryMetaModelImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
required final int responseStatus,
|
||||
required final DateTime timeStamp}) = _$HistoryMetaModelImpl;
|
||||
|
||||
factory _HistoryMetaModel.fromJson(Map<String, dynamic> json) =
|
||||
_$HistoryMetaModelImpl.fromJson;
|
||||
|
||||
@override
|
||||
String get historyId;
|
||||
@override
|
||||
String get requestId;
|
||||
@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;
|
||||
}
|
40
lib/models/history_meta_model.g.dart
Normal file
40
lib/models/history_meta_model.g.dart
Normal file
@ -0,0 +1,40 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'history_meta_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$HistoryMetaModelImpl _$$HistoryMetaModelImplFromJson(
|
||||
Map<String, dynamic> 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']),
|
||||
responseStatus: (json['responseStatus'] as num).toInt(),
|
||||
timeStamp: DateTime.parse(json['timeStamp'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$HistoryMetaModelImplToJson(
|
||||
_$HistoryMetaModelImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'historyId': instance.historyId,
|
||||
'requestId': instance.requestId,
|
||||
'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',
|
||||
};
|
23
lib/models/history_request_model.dart
Normal file
23
lib/models/history_request_model.dart
Normal file
@ -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<String, Object?> json) =>
|
||||
_$HistoryRequestModelFromJson(json);
|
||||
}
|
258
lib/models/history_request_model.freezed.dart
Normal file
258
lib/models/history_request_model.freezed.dart
Normal file
@ -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>(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<String, dynamic> 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<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$HistoryRequestModelCopyWith<HistoryRequestModel> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
27
lib/models/history_request_model.g.dart
Normal file
27
lib/models/history_request_model.g.dart
Normal file
@ -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<String, Object?>.from(json['metaData'] as Map)),
|
||||
httpRequestModel: HttpRequestModel.fromJson(
|
||||
Map<String, Object?>.from(json['httpRequestModel'] as Map)),
|
||||
httpResponseModel: HttpResponseModel.fromJson(
|
||||
Map<String, Object?>.from(json['httpResponseModel'] as Map)),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$HistoryRequestModelImplToJson(
|
||||
_$HistoryRequestModelImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'historyId': instance.historyId,
|
||||
'metaData': instance.metaData.toJson(),
|
||||
'httpRequestModel': instance.httpRequestModel.toJson(),
|
||||
'httpResponseModel': instance.httpResponseModel.toJson(),
|
||||
};
|
@ -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';
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
@ -256,12 +282,29 @@ class CollectionStateNotifier
|
||||
httpResponseModel: responseModel,
|
||||
isWorking: false,
|
||||
);
|
||||
String newHistoryId = getNewUuid();
|
||||
HistoryRequestModel model = HistoryRequestModel(
|
||||
historyId: newHistoryId,
|
||||
metaData: HistoryMetaModel(
|
||||
historyId: newHistoryId,
|
||||
requestId: id,
|
||||
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;
|
||||
}
|
||||
|
||||
|
91
lib/providers/history_providers.dart
Normal file
91
lib/providers/history_providers.dart
Normal file
@ -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<String?>((ref) => null);
|
||||
|
||||
final selectedRequestGroupIdStateProvider = StateProvider<String?>((ref) {
|
||||
final selectedHistoryId = ref.watch(selectedHistoryIdStateProvider);
|
||||
final historyMetaState = ref.read(historyMetaStateNotifier);
|
||||
if (selectedHistoryId == null) {
|
||||
return null;
|
||||
}
|
||||
return getHistoryRequestKey(historyMetaState![selectedHistoryId]!);
|
||||
});
|
||||
|
||||
final selectedHistoryRequestModelProvider =
|
||||
StateProvider<HistoryRequestModel?>((ref) => null);
|
||||
|
||||
final historySequenceProvider =
|
||||
StateProvider<Map<DateTime, List<HistoryMetaModel>>?>((ref) {
|
||||
final historyMetas = ref.watch(historyMetaStateNotifier);
|
||||
return getTemporalGroups(historyMetas?.values.toList());
|
||||
});
|
||||
|
||||
final StateNotifierProvider<HistoryMetaStateNotifier,
|
||||
Map<String, HistoryMetaModel>?> historyMetaStateNotifier =
|
||||
StateNotifierProvider((ref) => HistoryMetaStateNotifier(ref, hiveHandler));
|
||||
|
||||
class HistoryMetaStateNotifier
|
||||
extends StateNotifier<Map<String, HistoryMetaModel>?> {
|
||||
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<String>? historyIds = hiveHandler.getHistoryIds();
|
||||
if (historyIds == null || historyIds.isEmpty) {
|
||||
state = null;
|
||||
return false;
|
||||
} else {
|
||||
Map<String, HistoryMetaModel> historyMetaMap = {};
|
||||
for (var historyId in historyIds) {
|
||||
var jsonModel = hiveHandler.getHistoryMeta(historyId);
|
||||
if (jsonModel != null) {
|
||||
var jsonMap = Map<String, Object?>.from(jsonModel);
|
||||
var historyMetaModelFromJson = HistoryMetaModel.fromJson(jsonMap);
|
||||
historyMetaMap[historyId] = historyMetaModelFromJson;
|
||||
}
|
||||
}
|
||||
state = historyMetaMap;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadHistoryRequest(String id) async {
|
||||
var jsonModel = await hiveHandler.getHistoryRequest(id);
|
||||
if (jsonModel != null) {
|
||||
var jsonMap = Map<String, Object?>.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<String> updatedHistoryKeys =
|
||||
state == null ? [id] : [...state!.keys, id];
|
||||
hiveHandler.setHistoryIds(updatedHistoryKeys);
|
||||
hiveHandler.setHistoryMeta(id, model.metaData.toJson());
|
||||
await hiveHandler.setHistoryRequest(id, model.toJson());
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export 'collection_providers.dart';
|
||||
export 'environment_providers.dart';
|
||||
export 'history_providers.dart';
|
||||
export 'settings_providers.dart';
|
||||
export 'ui_providers.dart';
|
||||
|
@ -30,6 +30,7 @@ class ThemeStateNotifier extends StateNotifier<SettingsModel> {
|
||||
bool? saveResponses,
|
||||
bool? promptBeforeClosing,
|
||||
String? activeEnvironmentId,
|
||||
HistoryRetentionPeriod? historyRetentionPeriod,
|
||||
}) async {
|
||||
state = state.copyWith(
|
||||
isDark: isDark,
|
||||
@ -41,6 +42,7 @@ class ThemeStateNotifier extends StateNotifier<SettingsModel> {
|
||||
saveResponses: saveResponses,
|
||||
promptBeforeClosing: promptBeforeClosing,
|
||||
activeEnvironmentId: activeEnvironmentId,
|
||||
historyRetentionPeriod: historyRetentionPeriod,
|
||||
);
|
||||
await hiveHandler.saveSettings(state.toJson());
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ import 'package:apidash/consts.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final mobileScaffoldKeyStateProvider = StateProvider<GlobalKey<ScaffoldState>>(
|
||||
(ref) => GlobalKey<ScaffoldState>());
|
||||
final mobileScaffoldKeyStateProvider =
|
||||
StateProvider<GlobalKey<ScaffoldState>>((ref) => kHomeScaffoldKey);
|
||||
final leftDrawerStateProvider = StateProvider<bool>((ref) => false);
|
||||
final navRailIndexStateProvider = StateProvider<int>((ref) => 0);
|
||||
final selectedIdEditStateProvider = StateProvider<String?>((ref) => null);
|
||||
final environmentFieldEditStateProvider = StateProvider<String?>((ref) => null);
|
||||
final codePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
||||
final historyCodePaneVisibleStateProvider = StateProvider<bool>((ref) => false);
|
||||
final saveDataStateProvider = StateProvider<bool>((ref) => false);
|
||||
final clearDataStateProvider = StateProvider<bool>((ref) => false);
|
||||
final hasUnsavedChangesProvider = StateProvider<bool>((ref) => false);
|
||||
|
@ -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,24 +26,26 @@ class NavbarButton extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
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) {
|
||||
final scaffoldKey = getScaffoldKey(buttonIdx!);
|
||||
ref.watch(navRailIndexStateProvider.notifier).state = buttonIdx!;
|
||||
mobileScaffoldKeyNotifier.state = scaffoldKey;
|
||||
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 > 1 && buttonIdx! <= 1) {
|
||||
ref.read(leftDrawerStateProvider.notifier).state = false;
|
||||
}
|
||||
}
|
||||
onTap?.call();
|
||||
},
|
||||
onTap: onPress,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -56,19 +59,7 @@ class NavbarButton extends ConsumerWidget {
|
||||
: TextButton.styleFrom(
|
||||
fixedSize: size,
|
||||
),
|
||||
onPressed: isSelected
|
||||
? null
|
||||
: () {
|
||||
if (buttonIdx != null) {
|
||||
ref.read(navRailIndexStateProvider.notifier).state =
|
||||
buttonIdx!;
|
||||
if (railIdx > 1 && buttonIdx! <= 1) {
|
||||
ref.read(leftDrawerStateProvider.notifier).state =
|
||||
false;
|
||||
}
|
||||
}
|
||||
onTap?.call();
|
||||
},
|
||||
onPressed: onPress,
|
||||
child: Icon(
|
||||
isSelected ? selectedIcon : icon,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
|
@ -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));
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -6,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 {
|
||||
@ -14,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(
|
||||
@ -54,18 +54,18 @@ class Dashboard extends ConsumerWidget {
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
kVSpacer10,
|
||||
// IconButton(
|
||||
// isSelected: railIdx == 2,
|
||||
// onPressed: () {
|
||||
// ref.read(navRailIndexStateProvider.notifier).state = 2;
|
||||
// },
|
||||
// icon: const Icon(Icons.history_outlined),
|
||||
// selectedIcon: const Icon(Icons.history),
|
||||
// ),
|
||||
// Text(
|
||||
// 'History',
|
||||
// style: Theme.of(context).textTheme.labelSmall,
|
||||
// ),
|
||||
IconButton(
|
||||
isSelected: railIdx == 2,
|
||||
onPressed: () {
|
||||
ref.read(navRailIndexStateProvider.notifier).state = 2;
|
||||
},
|
||||
icon: const Icon(Icons.history_outlined),
|
||||
selectedIcon: const Icon(Icons.history_rounded),
|
||||
),
|
||||
Text(
|
||||
'History',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
@ -90,7 +90,7 @@ class Dashboard extends ConsumerWidget {
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: NavbarButton(
|
||||
railIdx: railIdx,
|
||||
buttonIdx: 2,
|
||||
buttonIdx: 3,
|
||||
selectedIcon: Icons.settings,
|
||||
icon: Icons.settings_outlined,
|
||||
label: 'Settings',
|
||||
@ -112,12 +112,11 @@ class Dashboard extends ConsumerWidget {
|
||||
child: IndexedStack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
index: railIdx,
|
||||
children: [
|
||||
const HomePage(),
|
||||
EnvironmentPage(
|
||||
scaffoldKey: mobileScaffoldKey,
|
||||
),
|
||||
const SettingsPage(),
|
||||
children: const [
|
||||
HomePage(),
|
||||
EnvironmentPage(),
|
||||
HistoryPage(),
|
||||
SettingsPage(),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ class EnvironmentEditor extends ConsumerWidget {
|
||||
padding: context.isMediumWindow
|
||||
? kPb10
|
||||
: (kIsMacOS || kIsWindows)
|
||||
? kPt24o8
|
||||
? kPt28o8
|
||||
: kP8,
|
||||
child: Column(
|
||||
children: [
|
||||
@ -65,34 +65,43 @@ 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,
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
color: kColorTransparent,
|
||||
surfaceTintColor: kColorTransparent,
|
||||
shape: context.isMediumWindow
|
||||
? null
|
||||
: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
),
|
||||
borderRadius: kBorderRadius12,
|
||||
),
|
||||
borderRadius: kBorderRadius12,
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
kHSpacer40,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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())
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -12,10 +12,8 @@ import 'environment_editor.dart';
|
||||
class EnvironmentPage extends ConsumerWidget {
|
||||
const EnvironmentPage({
|
||||
super.key,
|
||||
required this.scaffoldKey,
|
||||
});
|
||||
|
||||
final GlobalKey<ScaffoldState> 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,
|
||||
|
@ -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;
|
||||
kEnvScaffoldKey.currentState?.closeDrawer();
|
||||
},
|
||||
focusNode: ref.watch(nameTextFieldFocusNodeProvider),
|
||||
onChangedNameEditor: (value) {
|
||||
|
100
lib/screens/history/history_details.dart
Normal file
100
lib/screens/history/history_details.dart
Normal file
@ -0,0 +1,100 @@
|
||||
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 'package:apidash/screens/common_widgets/common_widgets.dart';
|
||||
import 'history_widgets/history_widgets.dart';
|
||||
|
||||
class HistoryDetails extends StatefulHookConsumerWidget {
|
||||
const HistoryDetails({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<HistoryDetails> createState() => _HistoryDetailsState();
|
||||
}
|
||||
|
||||
class _HistoryDetailsState extends ConsumerState<HistoryDetails>
|
||||
with TickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedHistoryRequest =
|
||||
ref.watch(selectedHistoryRequestModelProvider);
|
||||
final metaData = selectedHistoryRequest?.metaData;
|
||||
|
||||
final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider);
|
||||
|
||||
final TabController controller =
|
||||
useTabController(initialLength: 3, vsync: this);
|
||||
|
||||
return selectedHistoryRequest != null
|
||||
? 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) ...[
|
||||
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(),
|
||||
const CodePane(
|
||||
isHistoryRequest: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const HistoryPageBottombar()
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: kPh4,
|
||||
child: RequestDetailsCard(
|
||||
child: EqualSplitView(
|
||||
leftWidget: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: HistoryRequestPane(
|
||||
isCompact: isCompact,
|
||||
),
|
||||
),
|
||||
const HistoryPageBottombar(),
|
||||
],
|
||||
),
|
||||
rightWidget: codePaneVisible
|
||||
? const CodePane(isHistoryRequest: true)
|
||||
: const HistoryResponsePane(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
kVSpacer8,
|
||||
]
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
44
lib/screens/history/history_page.dart
Normal file
44
lib/screens/history/history_page.dart
Normal file
@ -0,0 +1,44 @@
|
||||
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 '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,
|
||||
});
|
||||
|
||||
@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: kHisScaffoldKey,
|
||||
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: [
|
||||
Expanded(
|
||||
child: DashboardSplitView(
|
||||
sidebarWidget: HistoryPane(),
|
||||
mainWidget: HistoryViewer(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
160
lib/screens/history/history_pane.dart
Normal file
160
lib/screens/history/history_pane.dart
Normal file
@ -0,0 +1,160 @@
|
||||
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/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({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Padding(
|
||||
padding: (!context.isMediumWindow && kIsMacOS ? kPt24 : kPt8) +
|
||||
(context.isMediumWindow ? kPb70 : EdgeInsets.zero),
|
||||
child: const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
HistorySidebarHeader(),
|
||||
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<DateTime>? 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.separated(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom),
|
||||
controller: scrollController,
|
||||
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,
|
||||
initiallyExpanded: index == 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryExpansionTile extends StatefulHookConsumerWidget {
|
||||
const HistoryExpansionTile({
|
||||
super.key,
|
||||
required this.requestGroups,
|
||||
required this.date,
|
||||
this.initiallyExpanded = false,
|
||||
});
|
||||
|
||||
final Map<String, List<HistoryMetaModel>> requestGroups;
|
||||
final DateTime date;
|
||||
final bool initiallyExpanded;
|
||||
|
||||
@override
|
||||
ConsumerState<HistoryExpansionTile> createState() =>
|
||||
_HistoryExpansionTileState();
|
||||
}
|
||||
|
||||
class _HistoryExpansionTileState extends ConsumerState<HistoryExpansionTile>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
initialValue: widget.initiallyExpanded ? 1.0 : 0.0,
|
||||
);
|
||||
final animation = Tween(begin: 0.0, end: 0.25).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.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
},
|
||||
trailing: const SizedBox.shrink(),
|
||||
tilePadding: kPh8,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
collapsedBackgroundColor: colorScheme.surfaceContainerLow,
|
||||
initiallyExpanded: widget.initiallyExpanded,
|
||||
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);
|
||||
kHisScaffoldKey.currentState?.closeDrawer();
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
161
lib/screens/history/history_requests.dart
Normal file
161
lib/screens/history/history_requests.dart
Normal file
@ -0,0 +1,161 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.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:apidash/consts.dart';
|
||||
|
||||
class HistoryRequests extends ConsumerWidget {
|
||||
const HistoryRequests({
|
||||
super.key,
|
||||
this.scrollController,
|
||||
this.onSelect,
|
||||
});
|
||||
|
||||
final ScrollController? scrollController;
|
||||
final Function()? onSelect;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedRequestId = ref.watch(selectedHistoryIdStateProvider);
|
||||
final selectedRequest = ref.watch(selectedHistoryRequestModelProvider);
|
||||
final historyMetas = ref.watch(historyMetaStateNotifier);
|
||||
final requestGroup = getRequestGroup(
|
||||
historyMetas?.values.toList(), selectedRequest?.metaData);
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
padding: kPh4,
|
||||
children: [
|
||||
kVSpacer10,
|
||||
...requestGroup.map((request) => Padding(
|
||||
padding: kPv2 + kPh4,
|
||||
child: HistoryRequestCard(
|
||||
id: request.historyId,
|
||||
model: request,
|
||||
isSelected: selectedRequestId == request.historyId,
|
||||
onTap: () {
|
||||
ref.read(selectedHistoryIdStateProvider.notifier).state =
|
||||
request.historyId;
|
||||
ref
|
||||
.read(historyMetaStateNotifier.notifier)
|
||||
.loadHistoryRequest(request.historyId);
|
||||
onSelect?.call();
|
||||
},
|
||||
),
|
||||
)),
|
||||
kVSpacer10,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistorRequestsScrollableSheet extends StatefulWidget {
|
||||
const HistorRequestsScrollableSheet({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<HistorRequestsScrollableSheet> createState() =>
|
||||
_HistorRequestsScrollableSheetState();
|
||||
}
|
||||
|
||||
class _HistorRequestsScrollableSheetState
|
||||
extends State<HistorRequestsScrollableSheet> {
|
||||
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<DragUpdateDetails> 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
35
lib/screens/history/history_viewer.dart
Normal file
35
lib/screens/history/history_viewer.dart
Normal file
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
47
lib/screens/history/history_widgets/his_action_buttons.dart
Normal file
47
lib/screens/history/history_widgets/his_action_buttons.dart
Normal file
@ -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",
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
106
lib/screens/history/history_widgets/his_bottombar.dart
Normal file
106
lib/screens/history/history_widgets/his_bottombar.dart
Normal file
@ -0,0 +1,106 @@
|
||||
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({
|
||||
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 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: 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: 400),
|
||||
child: const HistorRequestsScrollableSheet());
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: isCompact
|
||||
? icon
|
||||
: const Row(
|
||||
children: [
|
||||
Text(
|
||||
"Show All",
|
||||
style: kTextStyleButton,
|
||||
),
|
||||
kHSpacer5,
|
||||
icon,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
122
lib/screens/history/history_widgets/his_request_pane.dart
Normal file
122
lib/screens/history/history_widgets/his_request_pane.dart
Normal file
@ -0,0 +1,122 @@
|
||||
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 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,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
48
lib/screens/history/history_widgets/his_response_pane.dart
Normal file
48
lib/screens/history/history_widgets/his_response_pane.dart
Normal file
@ -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");
|
||||
}
|
||||
}
|
60
lib/screens/history/history_widgets/his_sidebar_header.dart
Normal file
60
lib/screens/history/history_widgets/his_sidebar_header.dart
Normal file
@ -0,0 +1,60 @@
|
||||
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 {
|
||||
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: "Manage History",
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
showHistoryRetentionDialog(
|
||||
context,
|
||||
ref.read(settingsProvider.select(
|
||||
(value) => value.historyRetentionPeriod)), (value) {
|
||||
ref.read(settingsProvider.notifier).update(
|
||||
historyRetentionPeriod: value,
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.manage_history_rounded,
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
65
lib/screens/history/history_widgets/his_url_card.dart
Normal file
65
lib/screens/history/history_widgets/his_url_card.dart
Normal file
@ -0,0 +1,65 @@
|
||||
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: ReadOnlyTextField(
|
||||
initialValue: url,
|
||||
style: kCodeStyle.copyWith(
|
||||
fontSize: fontSize,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
5
lib/screens/history/history_widgets/history_widgets.dart
Normal file
5
lib/screens/history/history_widgets/history_widgets.dart
Normal file
@ -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';
|
@ -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';
|
||||
@ -220,8 +220,8 @@ class RequestItem extends ConsumerWidget {
|
||||
selectedId: selectedId,
|
||||
editRequestId: editRequestId,
|
||||
onTap: () {
|
||||
ref.read(mobileScaffoldKeyStateProvider).currentState?.closeDrawer();
|
||||
ref.read(selectedIdStateProvider.notifier).state = id;
|
||||
kHomeScaffoldKey.currentState?.closeDrawer();
|
||||
},
|
||||
// onDoubleTap: () {
|
||||
// ref.read(selectedIdStateProvider.notifier).state = id;
|
||||
|
@ -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});
|
||||
|
@ -124,17 +124,7 @@ class _FormDataBodyState extends ConsumerState<FormDataWidget> {
|
||||
),
|
||||
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<FormDataWidget> {
|
||||
_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",
|
||||
|
@ -27,7 +27,7 @@ class RequestEditor extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: kIsMacOS || kIsWindows ? kPt24o8 : kP8,
|
||||
padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8,
|
||||
child: const Column(
|
||||
children: [
|
||||
RequestEditorTopBar(),
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,12 @@ 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});
|
||||
@ -39,7 +40,7 @@ class _MobileDashboardState extends ConsumerState<MobileDashboard> {
|
||||
),
|
||||
if (context.isMediumWindow)
|
||||
AnimatedPositioned(
|
||||
bottom: railIdx > 1
|
||||
bottom: railIdx > 2
|
||||
? 0
|
||||
: isLeftDrawerOpen
|
||||
? 0
|
||||
@ -66,27 +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,
|
||||
);
|
||||
// case 2:
|
||||
// // TODO: Implement history page
|
||||
// return const PageBase(
|
||||
// title: 'History',
|
||||
// scaffoldBody: SizedBox(),
|
||||
// );
|
||||
return const EnvironmentPage();
|
||||
case 2:
|
||||
return const HistoryPage();
|
||||
case 3:
|
||||
return const PageBase(
|
||||
title: 'Settings',
|
||||
scaffoldBody: SettingsPage(),
|
||||
);
|
||||
default:
|
||||
return RequestResponsePage(
|
||||
scaffoldKey: scaffoldKey,
|
||||
);
|
||||
return const RequestResponsePage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,19 +48,19 @@ class BottomNavBar extends ConsumerWidget {
|
||||
label: 'Variables',
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// child: NavbarButton(
|
||||
// railIdx: railIdx,
|
||||
// buttonIdx: 2,
|
||||
// selectedIcon: Icons.history,
|
||||
// icon: Icons.history_outlined,
|
||||
// label: 'History',
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
child: NavbarButton(
|
||||
railIdx: railIdx,
|
||||
buttonIdx: 2,
|
||||
selectedIcon: Icons.history_rounded,
|
||||
icon: Icons.history_outlined,
|
||||
label: 'History',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: NavbarButton(
|
||||
railIdx: railIdx,
|
||||
buttonIdx: 3,
|
||||
selectedIcon: Icons.settings,
|
||||
icon: Icons.settings_outlined,
|
||||
label: 'Settings',
|
||||
|
@ -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';
|
||||
@ -18,84 +19,34 @@ 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))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 <Widget>[
|
||||
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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
@ -16,11 +15,8 @@ import 'request_response_tabs.dart';
|
||||
class RequestResponsePage extends StatefulHookConsumerWidget {
|
||||
const RequestResponsePage({
|
||||
super.key,
|
||||
required this.scaffoldKey,
|
||||
});
|
||||
|
||||
final GlobalKey<ScaffoldState> scaffoldKey;
|
||||
|
||||
@override
|
||||
ConsumerState<RequestResponsePage> createState() =>
|
||||
_RequestResponsePageState();
|
||||
@ -36,7 +32,7 @@ class _RequestResponsePageState extends ConsumerState<RequestResponsePage>
|
||||
final TabController requestResponseTabController =
|
||||
useTabController(initialLength: 2, vsync: this);
|
||||
return DrawerSplitView(
|
||||
scaffoldKey: widget.scaffoldKey,
|
||||
scaffoldKey: kHomeScaffoldKey,
|
||||
title: EditorTitle(
|
||||
title: name,
|
||||
onSelected: (ItemMenuOption item) {
|
||||
|
@ -81,27 +81,6 @@ class SettingsPage extends ConsumerWidget {
|
||||
},
|
||||
items: kSupportedUriSchemes,
|
||||
),
|
||||
// DropdownButtonHideUnderline(
|
||||
// child: DropdownButton<String>(
|
||||
// borderRadius: kBorderRadius8,
|
||||
// onChanged: (value) {
|
||||
// ref
|
||||
// .read(settingsProvider.notifier)
|
||||
// .update(defaultUriScheme: value);
|
||||
// },
|
||||
// value: settings.defaultUriScheme,
|
||||
// items: kSupportedUriSchemes
|
||||
// .map<DropdownMenuItem<String>>((String value) {
|
||||
// return DropdownMenuItem<String>(
|
||||
// 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<CodegenLanguage>(
|
||||
// borderRadius: kBorderRadius8,
|
||||
// value: settings.defaultCodeGenLang,
|
||||
// onChanged: (value) {
|
||||
// ref
|
||||
// .read(settingsProvider.notifier)
|
||||
// .update(defaultCodeGenLang: value);
|
||||
// },
|
||||
// items: CodegenLanguage.values.map((value) {
|
||||
// return DropdownMenuItem<CodegenLanguage>(
|
||||
// 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${settings.historyRetentionPeriod == HistoryRetentionPeriod.forever ? "" : " 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'),
|
||||
|
51
lib/services/history_service.dart
Normal file
51
lib/services/history_service.dart
Normal file
@ -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<void> 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<String>? historyIds = hiveHandler.getHistoryIds();
|
||||
List<String> toRemoveIds = [];
|
||||
|
||||
if (historyIds == null || historyIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var historyId in historyIds) {
|
||||
var jsonModel = hiveHandler.getHistoryMeta(historyId);
|
||||
if (jsonModel != null) {
|
||||
var jsonMap = Map<String, Object?>.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)));
|
||||
}
|
||||
}
|
@ -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<void> openBoxes() async {
|
||||
@ -13,6 +18,8 @@ Future<void> 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<void> deleteEnvironment(String id) => environmentBox.delete(id);
|
||||
|
||||
dynamic getHistoryIds() => historyMetaBox.get(kHistoryBoxIds);
|
||||
Future<void> setHistoryIds(List<String>? ids) =>
|
||||
historyMetaBox.put(kHistoryBoxIds, ids);
|
||||
|
||||
dynamic getHistoryMeta(String id) => historyMetaBox.get(id);
|
||||
Future<void> setHistoryMeta(
|
||||
String id, Map<String, dynamic>? historyMetaJson) =>
|
||||
historyMetaBox.put(id, historyMetaJson);
|
||||
|
||||
Future<void> deleteHistoryMeta(String id) => historyMetaBox.delete(id);
|
||||
|
||||
Future<dynamic> getHistoryRequest(String id) async =>
|
||||
await historyLazyBox.get(id);
|
||||
Future<void> setHistoryRequest(
|
||||
String id, Map<String, dynamic>? historyRequestJsoon) =>
|
||||
historyLazyBox.put(id, historyRequestJsoon);
|
||||
|
||||
Future<void> deleteHistoryRequest(String id) => historyLazyBox.delete(id);
|
||||
|
||||
Future clear() async {
|
||||
await dataBox.clear();
|
||||
await environmentBox.clear();
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'http_service.dart';
|
||||
export 'hive_services.dart';
|
||||
export 'history_service.dart';
|
||||
export 'window_services.dart';
|
||||
|
@ -1,10 +1,25 @@
|
||||
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 humanizeTime(DateTime? time) {
|
||||
if (time == null) {
|
||||
return "";
|
||||
}
|
||||
return DateFormat('hh:mm:ss a').format(time);
|
||||
}
|
||||
|
||||
String humanizeDuration(Duration? duration) {
|
||||
if (duration == null) {
|
||||
return "";
|
||||
|
131
lib/utils/history_utils.dart
Normal file
131
lib/utils/history_utils.dart
Normal file
@ -0,0 +1,131 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
} 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<DateTime, List<HistoryMetaModel>> temporalGroups) {
|
||||
if (temporalGroups.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
List<DateTime> keys = temporalGroups.keys.toList();
|
||||
keys.sort((a, b) => b.compareTo(a));
|
||||
return temporalGroups[keys.first]!.first.historyId;
|
||||
}
|
||||
|
||||
DateTime getDateTimeKey(List<DateTime> 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<DateTime, List<HistoryMetaModel>> getTemporalGroups(
|
||||
List<HistoryMetaModel>? models) {
|
||||
Map<DateTime, List<HistoryMetaModel>> temporalGroups = {};
|
||||
if (models?.isEmpty ?? true) {
|
||||
return temporalGroups;
|
||||
}
|
||||
for (HistoryMetaModel model in models!) {
|
||||
List<DateTime> 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<String, List<HistoryMetaModel>> getRequestGroups(
|
||||
List<HistoryMetaModel>? models) {
|
||||
Map<String, List<HistoryMetaModel>> 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;
|
||||
}
|
||||
|
||||
List<HistoryMetaModel> getRequestGroup(
|
||||
List<HistoryMetaModel>? models, HistoryMetaModel? selectedModel) {
|
||||
List<HistoryMetaModel> 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;
|
||||
}
|
||||
|
||||
DateTime? getRetentionDate(HistoryRetentionPeriod retentionPeriod) {
|
||||
DateTime now = DateTime.now();
|
||||
DateTime today = stripTime(now);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -69,3 +69,14 @@ double? getJsonPreviewerMaxRootNodeWidth(double w) {
|
||||
}
|
||||
return w - 150;
|
||||
}
|
||||
|
||||
GlobalKey<ScaffoldState> getScaffoldKey(int railIdx) {
|
||||
switch (railIdx) {
|
||||
case 1:
|
||||
return kEnvScaffoldKey;
|
||||
case 2:
|
||||
return kHisScaffoldKey;
|
||||
default:
|
||||
return kHomeScaffoldKey;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
31
lib/widgets/button_form_data_file.dart
Normal file
31
lib/widgets/button_form_data_file.dart
Normal file
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
74
lib/widgets/button_group_filled.dart
Normal file
74
lib/widgets/button_group_filled.dart
Normal file
@ -0,0 +1,74 @@
|
||||
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});
|
||||
|
||||
final List<ButtonData> 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<Widget> buttonWidgets = buttons
|
||||
.map((button) => buildButton(button, showLabel: showLabel))
|
||||
.toList();
|
||||
|
||||
List<Widget> 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,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
65
lib/widgets/card_history_request.dart
Normal file
65
lib/widgets/card_history_request.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.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 {
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
109
lib/widgets/card_sidebar_history.dart
Normal file
109
lib/widgets/card_sidebar_history.dart
Normal file
@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.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 {
|
||||
const SidebarHistoryCard({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.models,
|
||||
required this.method,
|
||||
this.isSelected = false,
|
||||
this.requestGroupSize = 1,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final List<HistoryMetaModel> models;
|
||||
final HTTPVerb method;
|
||||
final bool isSelected;
|
||||
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;
|
||||
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 > 9
|
||||
? "9+"
|
||||
: requestGroupSize.toString(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
85
lib/widgets/dialog_history_retention.dart
Normal file
85
lib/widgets/dialog_history_retention.dart
Normal file
@ -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<HistoryRetentionPeriod>(
|
||||
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: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('Confirm'),
|
||||
onPressed: () {
|
||||
onRetentionPeriodChange(selectedRetentionPeriod);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -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<TextFieldEditor> createState() => _TextFieldEditorState();
|
||||
}
|
||||
@ -69,6 +71,7 @@ class _TextFieldEditorState extends State<TextFieldEditor> {
|
||||
keyboardType: TextInputType.multiline,
|
||||
expands: true,
|
||||
maxLines: null,
|
||||
readOnly: widget.readOnly,
|
||||
style: kCodeStyle,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
onChanged: widget.onChanged,
|
||||
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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,
|
||||
|
30
lib/widgets/field_read_only.dart
Normal file
30
lib/widgets/field_read_only.dart
Normal file
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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({
|
||||
|
@ -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<EnvironmentModel>? environments;
|
||||
final List<EnvironmentModel>? 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(
|
||||
|
57
lib/widgets/popup_menu_history.dart
Normal file
57
lib/widgets/popup_menu_history.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.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<HistoryRetentionPeriod>? items;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double boxLength = 120;
|
||||
return PopupMenuButton(
|
||||
tooltip: "Select retention period",
|
||||
surfaceTintColor: kColorTransparent,
|
||||
constraints: const 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,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Icon(
|
||||
Icons.unfold_more,
|
||||
size: 16,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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({
|
||||
@ -15,7 +15,7 @@ class URIPopupMenu extends StatelessWidget {
|
||||
final List<String>? 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,
|
||||
|
@ -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<Widget> children;
|
||||
final List<bool> showIndicators;
|
||||
final bool? showViewCodeButton;
|
||||
|
||||
@override
|
||||
State<RequestPane> createState() => _RequestPaneState();
|
||||
@ -29,28 +32,19 @@ class RequestPane extends StatefulWidget {
|
||||
|
||||
class _RequestPaneState extends State<RequestPane>
|
||||
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,
|
||||
@ -58,6 +52,10 @@ class _RequestPaneState extends State<RequestPane>
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
style: FilledButton.styleFrom(
|
||||
padding: kPh12,
|
||||
minimumSize: const Size(44, 44),
|
||||
),
|
||||
onPressed: widget.onPressedCodeButton,
|
||||
icon: Icon(
|
||||
widget.codePaneVisible
|
||||
@ -76,10 +74,11 @@ class _RequestPaneState extends State<RequestPane>
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
TabBar(
|
||||
key: Key(widget.selectedId!),
|
||||
controller: _controller,
|
||||
controller: controller,
|
||||
overlayColor: kColorTransparentState,
|
||||
labelPadding: kPh2,
|
||||
onTap: widget.onTapTabBar,
|
||||
@ -101,7 +100,7 @@ class _RequestPaneState extends State<RequestPane>
|
||||
kVSpacer5,
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _controller,
|
||||
controller: controller,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: widget.children,
|
||||
),
|
||||
@ -109,10 +108,4 @@ class _RequestPaneState extends State<RequestPane>
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
65
lib/widgets/splitview_history.dart
Normal file
65
lib/widgets/splitview_history.dart
Normal file
@ -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<HistorySplitView> {
|
||||
final MultiSplitViewController _controller = MultiSplitViewController(
|
||||
areas: [
|
||||
Area(id: "sidebar", min: 200, size: 250, 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();
|
||||
}
|
||||
}
|
56
lib/widgets/tabbar_segmented.dart
Normal file
56
lib/widgets/tabbar_segmented.dart
Normal file
@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
|
||||
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<Widget> tabs;
|
||||
final double tabWidth;
|
||||
final double tabHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
margin: kPh4,
|
||||
width: tabWidth * tabs.length,
|
||||
height: tabHeight,
|
||||
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: tabs,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
110
lib/widgets/table_request.dart
Normal file
110
lib/widgets/table_request.dart
Normal file
@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import 'field_read_only.dart';
|
||||
|
||||
class RequestDataTable extends StatelessWidget {
|
||||
const RequestDataTable({
|
||||
super.key,
|
||||
required this.rows,
|
||||
this.keyName,
|
||||
this.valueName,
|
||||
});
|
||||
|
||||
final Map<String, String> rows;
|
||||
final String? keyName;
|
||||
final String? valueName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final clrScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final List<DataColumn> 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<DataRow> dataRows = rows.entries
|
||||
.map<DataRow>(
|
||||
(MapEntry<String, String> entry) => DataRow(
|
||||
cells: <DataCell>[
|
||||
const DataCell(kHSpacer5),
|
||||
DataCell(
|
||||
ReadOnlyTextField(
|
||||
initialValue: entry.key,
|
||||
decoration: fieldDecoration,
|
||||
),
|
||||
),
|
||||
const DataCell(
|
||||
Text('='),
|
||||
),
|
||||
DataCell(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
120
lib/widgets/table_request_form.dart
Normal file
120
lib/widgets/table_request_form.dart
Normal file
@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:data_table_2/data_table_2.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({
|
||||
super.key,
|
||||
required this.rows,
|
||||
this.keyName,
|
||||
this.valueName,
|
||||
});
|
||||
|
||||
final List<FormDataModel> rows;
|
||||
final String? keyName;
|
||||
final String? valueName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final clrScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final List<DataColumn> 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<DataRow> dataRows = rows
|
||||
.map<DataRow>(
|
||||
(FormDataModel entry) => DataRow(
|
||||
cells: <DataCell>[
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,21 @@
|
||||
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';
|
||||
export 'button_send.dart';
|
||||
export 'card_history_request.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';
|
||||
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';
|
||||
@ -27,6 +32,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';
|
||||
@ -36,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';
|
||||
@ -44,7 +51,11 @@ export 'snackbars.dart';
|
||||
export 'splitview_drawer.dart';
|
||||
export 'splitview_dashboard.dart';
|
||||
export 'splitview_equal.dart';
|
||||
export 'tables.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';
|
||||
export 'uint8_audio_player.dart';
|
||||
|
@ -704,6 +704,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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
|
||||
@ -63,6 +63,7 @@ dependencies:
|
||||
hooks_riverpod: ^2.5.1
|
||||
flutter_hooks: ^0.20.5
|
||||
flutter_portal: ^1.1.4
|
||||
intl: ^0.19.0
|
||||
multi_trigger_autocomplete:
|
||||
git:
|
||||
url: https://github.com/foss42/multi_trigger_autocomplete.git
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -6,13 +6,13 @@ 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';
|
||||
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';
|
||||
@ -60,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);
|
||||
});
|
||||
|
||||
@ -80,11 +81,12 @@ 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 SettingsPage when navRailIndexStateProvider is 2",
|
||||
"Dashboard should display HistorPage when navRailIndexStateProvider is 2",
|
||||
(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
@ -102,6 +104,29 @@ void main() {
|
||||
// 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 {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
navRailIndexStateProvider.overrideWith((ref) => 3),
|
||||
],
|
||||
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), findsNothing);
|
||||
expect(find.byType(SettingsPage), findsOneWidget);
|
||||
});
|
||||
|
||||
@ -125,7 +150,7 @@ void main() {
|
||||
// Verify that the navRailIndexStateProvider is updated
|
||||
final dashboard = tester.element(find.byType(Dashboard));
|
||||
final container = ProviderScope.containerOf(dashboard);
|
||||
expect(container.read(navRailIndexStateProvider), 2);
|
||||
expect(container.read(navRailIndexStateProvider), 3);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
@ -160,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);
|
||||
@ -194,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)
|
||||
|
@ -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<String, String> mapInput = {
|
||||
|
Reference in New Issue
Block a user