Merge pull request #430 from DenserMeerkat/add-feature-history

feat: history of requests
This commit is contained in:
Ashita Prasad
2024-08-10 20:46:59 +05:30
committed by GitHub
82 changed files with 3182 additions and 351 deletions

View File

@ -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";

View File

@ -9,6 +9,7 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized();
GoogleFonts.config.allowRuntimeFetching = false;
await openBoxes();
await autoClearHistory();
if (kIsLinux) {
await setupInitialWindow();
}

View 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);
}

View 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;
}

View 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',
};

View 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);
}

View 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;
}

View 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(),
};

View File

@ -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';

View File

@ -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,
);
}
}

View File

@ -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;
}

View 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());
}
}

View File

@ -1,4 +1,5 @@
export 'collection_providers.dart';
export 'environment_providers.dart';
export 'history_providers.dart';
export 'settings_providers.dart';
export 'ui_providers.dart';

View File

@ -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());
}

View File

@ -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);

View File

@ -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,

View File

@ -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));

View File

@ -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';

View File

@ -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;

View File

@ -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();

View File

@ -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(),
],
),
)

View File

@ -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())
],
),
),
),
),

View File

@ -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,

View File

@ -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) {

View 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();
}
}

View 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(),
),
),
],
);
}
}

View 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(),
);
}
}

View 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),
),
),
),
),
);
}
}

View 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(),
),
),
);
}
}

View 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",
),
]);
}
}

View 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,
],
),
),
);
}
}

View 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,
),
),
},
)
],
);
}
}

View 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");
}
}

View 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(),
],
),
);
}
}

View 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,
),
),
)
],
),
),
);
});
}
}

View 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';

View File

@ -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;

View File

@ -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});

View File

@ -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",

View File

@ -27,7 +27,7 @@ class RequestEditor extends StatelessWidget {
),
)
: Padding(
padding: kIsMacOS || kIsWindows ? kPt24o8 : kP8,
padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8,
child: const Column(
children: [
RequestEditorTopBar(),

View File

@ -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(),
),
),
],
);
}
}

View File

@ -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();
}
}
}

View File

@ -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',

View File

@ -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(),
),
],
);
}
}

View File

@ -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) {

View File

@ -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'),

View 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)));
}
}

View File

@ -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();

View File

@ -1,3 +1,4 @@
export 'http_service.dart';
export 'hive_services.dart';
export 'history_service.dart';
export 'window_services.dart';

View File

@ -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 "";

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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';

View 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,
),
);
}
}

View 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,
),
);
});
}
}

View 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),
],
),
),
),
),
);
}
}

View 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,
)),
),
),
),
],
),
),
),
),
),
);
}
}

View 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);
},
),
],
);
},
);
}

View File

@ -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,

View File

@ -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(),
],
),
),
),
);

View File

@ -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,

View 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,
),
);
}
}

View File

@ -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({

View File

@ -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(

View 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,
)
],
),
),
);
}
}

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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(),
],
),
),

View 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();
}
}

View 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,
),
),
),
);
}
}

View 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,
),
),
),
],
),
);
}
}

View 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,
),
),
),
],
),
);
}
}

View File

@ -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,
),
);
}
}

View File

@ -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';

View File

@ -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:

View File

@ -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

View File

@ -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';

View File

@ -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';

View File

@ -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);
});

View File

@ -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)

View File

@ -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 = {