mirror of
https://github.com/foss42/apidash.git
synced 2025-05-17 22:36:16 +08:00
feat: history of requests
This commit is contained in:
@ -168,6 +168,7 @@ const kPb15 = EdgeInsets.only(
|
||||
const kPb70 = EdgeInsets.only(
|
||||
bottom: 70,
|
||||
);
|
||||
const kHSpacer2 = SizedBox(width: 2);
|
||||
const kHSpacer4 = SizedBox(width: 4);
|
||||
const kHSpacer5 = SizedBox(width: 5);
|
||||
const kHSpacer10 = SizedBox(width: 10);
|
||||
@ -191,8 +192,8 @@ const kRandMax = 100000;
|
||||
const kSuggestionsMenuWidth = 300.0;
|
||||
const kSuggestionsMenuMaxHeight = 200.0;
|
||||
|
||||
const kReqResTabWidth = 280.0;
|
||||
const kReqResTabHeight = 32.0;
|
||||
const kSegmentedTabWidth = 140.0;
|
||||
const kSegmentedTabHeight = 32.0;
|
||||
|
||||
const kDataTableScrollbarTheme = ScrollbarThemeData(
|
||||
crossAxisMargin: -4,
|
||||
@ -316,6 +317,20 @@ final kColorHttpMethodPut = Colors.amber.shade900;
|
||||
final kColorHttpMethodPatch = kColorHttpMethodPut;
|
||||
final kColorHttpMethodDelete = Colors.red.shade800;
|
||||
|
||||
class ButtonData {
|
||||
ButtonData({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.tooltip = "",
|
||||
});
|
||||
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
final String tooltip;
|
||||
}
|
||||
|
||||
enum ItemMenuOption {
|
||||
edit("Rename"),
|
||||
delete("Delete"),
|
||||
@ -724,6 +739,8 @@ const kLabelSave = "Save";
|
||||
const kLabelDownload = "Download";
|
||||
const kLabelSaving = "Saving";
|
||||
const kLabelSaved = "Saved";
|
||||
const kLabelCode = "Code";
|
||||
const kLabelDuplicate = "Duplicate";
|
||||
// Request Pane
|
||||
const kLabelRequest = "Request";
|
||||
const kLabelHideCode = "Hide Code";
|
||||
|
@ -9,6 +9,7 @@ part 'history_meta_model.g.dart';
|
||||
class HistoryMetaModel with _$HistoryMetaModel {
|
||||
const factory HistoryMetaModel({
|
||||
required String historyId,
|
||||
required String requestId,
|
||||
@Default("") String name,
|
||||
required String url,
|
||||
required HTTPVerb method,
|
||||
|
@ -21,6 +21,7 @@ HistoryMetaModel _$HistoryMetaModelFromJson(Map<String, dynamic> json) {
|
||||
/// @nodoc
|
||||
mixin _$HistoryMetaModel {
|
||||
String get historyId => throw _privateConstructorUsedError;
|
||||
String get requestId => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get url => throw _privateConstructorUsedError;
|
||||
HTTPVerb get method => throw _privateConstructorUsedError;
|
||||
@ -41,6 +42,7 @@ abstract class $HistoryMetaModelCopyWith<$Res> {
|
||||
@useResult
|
||||
$Res call(
|
||||
{String historyId,
|
||||
String requestId,
|
||||
String name,
|
||||
String url,
|
||||
HTTPVerb method,
|
||||
@ -62,6 +64,7 @@ class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel>
|
||||
@override
|
||||
$Res call({
|
||||
Object? historyId = null,
|
||||
Object? requestId = null,
|
||||
Object? name = null,
|
||||
Object? url = null,
|
||||
Object? method = null,
|
||||
@ -73,6 +76,10 @@ class _$HistoryMetaModelCopyWithImpl<$Res, $Val extends HistoryMetaModel>
|
||||
? _value.historyId
|
||||
: historyId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
requestId: null == requestId
|
||||
? _value.requestId
|
||||
: requestId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
@ -107,6 +114,7 @@ abstract class _$$HistoryMetaModelImplCopyWith<$Res>
|
||||
@useResult
|
||||
$Res call(
|
||||
{String historyId,
|
||||
String requestId,
|
||||
String name,
|
||||
String url,
|
||||
HTTPVerb method,
|
||||
@ -126,6 +134,7 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res>
|
||||
@override
|
||||
$Res call({
|
||||
Object? historyId = null,
|
||||
Object? requestId = null,
|
||||
Object? name = null,
|
||||
Object? url = null,
|
||||
Object? method = null,
|
||||
@ -137,6 +146,10 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res>
|
||||
? _value.historyId
|
||||
: historyId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
requestId: null == requestId
|
||||
? _value.requestId
|
||||
: requestId // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
@ -166,6 +179,7 @@ class __$$HistoryMetaModelImplCopyWithImpl<$Res>
|
||||
class _$HistoryMetaModelImpl implements _HistoryMetaModel {
|
||||
const _$HistoryMetaModelImpl(
|
||||
{required this.historyId,
|
||||
required this.requestId,
|
||||
this.name = "",
|
||||
required this.url,
|
||||
required this.method,
|
||||
@ -178,6 +192,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel {
|
||||
@override
|
||||
final String historyId;
|
||||
@override
|
||||
final String requestId;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String name;
|
||||
@override
|
||||
@ -191,7 +207,7 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HistoryMetaModel(historyId: $historyId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)';
|
||||
return 'HistoryMetaModel(historyId: $historyId, requestId: $requestId, name: $name, url: $url, method: $method, responseStatus: $responseStatus, timeStamp: $timeStamp)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -201,6 +217,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel {
|
||||
other is _$HistoryMetaModelImpl &&
|
||||
(identical(other.historyId, historyId) ||
|
||||
other.historyId == historyId) &&
|
||||
(identical(other.requestId, requestId) ||
|
||||
other.requestId == requestId) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.url, url) || other.url == url) &&
|
||||
(identical(other.method, method) || other.method == method) &&
|
||||
@ -212,8 +230,8 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel {
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, historyId, name, url, method, responseStatus, timeStamp);
|
||||
int get hashCode => Object.hash(runtimeType, historyId, requestId, name, url,
|
||||
method, responseStatus, timeStamp);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -233,6 +251,7 @@ class _$HistoryMetaModelImpl implements _HistoryMetaModel {
|
||||
abstract class _HistoryMetaModel implements HistoryMetaModel {
|
||||
const factory _HistoryMetaModel(
|
||||
{required final String historyId,
|
||||
required final String requestId,
|
||||
final String name,
|
||||
required final String url,
|
||||
required final HTTPVerb method,
|
||||
@ -245,6 +264,8 @@ abstract class _HistoryMetaModel implements HistoryMetaModel {
|
||||
@override
|
||||
String get historyId;
|
||||
@override
|
||||
String get requestId;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get url;
|
||||
|
@ -10,6 +10,7 @@ _$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']),
|
||||
@ -21,6 +22,7 @@ Map<String, dynamic> _$$HistoryMetaModelImplToJson(
|
||||
_$HistoryMetaModelImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'historyId': instance.historyId,
|
||||
'requestId': instance.requestId,
|
||||
'name': instance.name,
|
||||
'url': instance.url,
|
||||
'method': _$HTTPVerbEnumMap[instance.method]!,
|
||||
|
@ -159,6 +159,32 @@ class CollectionStateNotifier
|
||||
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||
}
|
||||
|
||||
void duplicateFromHistory(HistoryRequestModel historyRequestModel) {
|
||||
final newId = getNewUuid();
|
||||
|
||||
var itemIds = ref.read(requestSequenceProvider);
|
||||
var currentModel = historyRequestModel;
|
||||
final newModel = RequestModel(
|
||||
id: newId,
|
||||
name: "${currentModel.metaData.name} (history)",
|
||||
httpRequestModel: currentModel.httpRequestModel,
|
||||
responseStatus: currentModel.metaData.responseStatus,
|
||||
message: kResponseCodeReasons[currentModel.metaData.responseStatus],
|
||||
httpResponseModel: currentModel.httpResponseModel,
|
||||
isWorking: false,
|
||||
sendingTime: null,
|
||||
);
|
||||
|
||||
itemIds.insert(0, newId);
|
||||
var map = {...state!};
|
||||
map[newId] = newModel;
|
||||
state = map;
|
||||
|
||||
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
||||
ref.read(selectedIdStateProvider.notifier).state = newId;
|
||||
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
||||
}
|
||||
|
||||
void update(
|
||||
String id, {
|
||||
HTTPVerb? method,
|
||||
@ -261,6 +287,7 @@ class CollectionStateNotifier
|
||||
historyId: newHistoryId,
|
||||
metaData: HistoryMetaModel(
|
||||
historyId: newHistoryId,
|
||||
requestId: id,
|
||||
name: requestModel.name,
|
||||
url: substitutedHttpRequestModel.url,
|
||||
method: substitutedHttpRequestModel.method,
|
||||
|
@ -27,22 +27,27 @@ class NavbarButton extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final bool isSelected = railIdx == buttonIdx;
|
||||
final Size size = isCompact ? const Size(56, 32) : const Size(65, 32);
|
||||
var onPress = isSelected
|
||||
? null
|
||||
: () {
|
||||
if (buttonIdx != null) {
|
||||
ref.read(navRailIndexStateProvider.notifier).state = buttonIdx!;
|
||||
if ((railIdx > 2 && buttonIdx! <= 2) ||
|
||||
!(ref
|
||||
.read(mobileScaffoldKeyStateProvider)
|
||||
.currentState
|
||||
?.isDrawerOpen ??
|
||||
true)) {
|
||||
ref.read(leftDrawerStateProvider.notifier).state = false;
|
||||
}
|
||||
}
|
||||
onTap?.call();
|
||||
};
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: isSelected
|
||||
? null
|
||||
: () {
|
||||
if (buttonIdx != null) {
|
||||
ref.read(navRailIndexStateProvider.notifier).state =
|
||||
buttonIdx!;
|
||||
if (railIdx > 2 && buttonIdx! <= 2) {
|
||||
ref.read(leftDrawerStateProvider.notifier).state = false;
|
||||
}
|
||||
}
|
||||
onTap?.call();
|
||||
},
|
||||
onTap: onPress,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
@ -56,19 +61,7 @@ class NavbarButton extends ConsumerWidget {
|
||||
: TextButton.styleFrom(
|
||||
fixedSize: size,
|
||||
),
|
||||
onPressed: isSelected
|
||||
? null
|
||||
: () {
|
||||
if (buttonIdx != null) {
|
||||
ref.read(navRailIndexStateProvider.notifier).state =
|
||||
buttonIdx!;
|
||||
if (railIdx > 2 && buttonIdx! <= 2) {
|
||||
ref.read(leftDrawerStateProvider.notifier).state =
|
||||
false;
|
||||
}
|
||||
}
|
||||
onTap?.call();
|
||||
},
|
||||
onPressed: onPress,
|
||||
child: Icon(
|
||||
isSelected ? selectedIcon : icon,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
|
@ -25,7 +25,7 @@ class _HistoryDetailsState extends ConsumerState<HistoryDetails>
|
||||
final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider);
|
||||
|
||||
final TabController controller =
|
||||
useTabController(initialLength: 2, vsync: this);
|
||||
useTabController(initialLength: 3, vsync: this);
|
||||
|
||||
return selectedHistoryRequest != null
|
||||
? LayoutBuilder(
|
||||
@ -42,28 +42,45 @@ class _HistoryDetailsState extends ConsumerState<HistoryDetails>
|
||||
)),
|
||||
kVSpacer10,
|
||||
if (isCompact) ...[
|
||||
RequestResponseTabbar(
|
||||
SegmentedTabbar(
|
||||
controller: controller,
|
||||
tabs: const [
|
||||
Tab(text: kLabelRequest),
|
||||
Tab(text: kLabelResponse),
|
||||
Tab(text: kLabelCode),
|
||||
],
|
||||
),
|
||||
kVSpacer10,
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller,
|
||||
children: [
|
||||
HistoryRequestPane(
|
||||
isCompact: isCompact,
|
||||
),
|
||||
const HistoryResponsePane(),
|
||||
],
|
||||
))
|
||||
child: TabBarView(
|
||||
controller: controller,
|
||||
children: [
|
||||
HistoryRequestPane(
|
||||
isCompact: isCompact,
|
||||
),
|
||||
const HistoryResponsePane(),
|
||||
const CodePane(
|
||||
isHistoryRequest: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const HistoryPageBottombar()
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: kPh4,
|
||||
child: RequestDetailsCard(
|
||||
child: EqualSplitView(
|
||||
leftWidget: HistoryRequestPane(
|
||||
isCompact: isCompact,
|
||||
leftWidget: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: HistoryRequestPane(
|
||||
isCompact: isCompact,
|
||||
),
|
||||
),
|
||||
const HistoryPageBottombar(),
|
||||
],
|
||||
),
|
||||
rightWidget: codePaneVisible
|
||||
? const CodePane(isHistoryRequest: true)
|
||||
|
@ -6,7 +6,6 @@ import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/utils/utils.dart';
|
||||
import 'history_pane.dart';
|
||||
import 'history_viewer.dart';
|
||||
import 'history_widgets/history_widgets.dart';
|
||||
|
||||
class HistoryPage extends ConsumerWidget {
|
||||
const HistoryPage({
|
||||
@ -23,14 +22,14 @@ class HistoryPage extends ConsumerWidget {
|
||||
: 'History';
|
||||
if (context.isMediumWindow) {
|
||||
return DrawerSplitView(
|
||||
scaffoldKey: scaffoldKey,
|
||||
mainContent: const HistoryViewer(),
|
||||
title: Text(title),
|
||||
leftDrawerContent: const HistoryPane(),
|
||||
actions: const [SizedBox(width: 16)],
|
||||
onDrawerChanged: (value) =>
|
||||
ref.read(leftDrawerStateProvider.notifier).state = value,
|
||||
bottomNavigationBar: const HistoryPageBottombar());
|
||||
scaffoldKey: scaffoldKey,
|
||||
mainContent: const HistoryViewer(),
|
||||
title: Text(title),
|
||||
leftDrawerContent: const HistoryPane(),
|
||||
actions: const [SizedBox(width: 16)],
|
||||
onDrawerChanged: (value) =>
|
||||
ref.read(leftDrawerStateProvider.notifier).state = value,
|
||||
);
|
||||
}
|
||||
return const Column(
|
||||
children: [
|
||||
|
@ -65,6 +65,7 @@ class HistoryList extends HookConsumerWidget {
|
||||
child: HistoryExpansionTile(
|
||||
date: sortedHistoryKeys[index],
|
||||
requestGroups: requestGroups,
|
||||
initiallyExpanded: index == 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -78,10 +79,12 @@ class HistoryExpansionTile extends StatefulHookConsumerWidget {
|
||||
super.key,
|
||||
required this.requestGroups,
|
||||
required this.date,
|
||||
this.initiallyExpanded = false,
|
||||
});
|
||||
|
||||
final Map<String, List<HistoryMetaModel>> requestGroups;
|
||||
final DateTime date;
|
||||
final bool initiallyExpanded;
|
||||
|
||||
@override
|
||||
ConsumerState<HistoryExpansionTile> createState() =>
|
||||
@ -95,8 +98,9 @@ class _HistoryExpansionTileState extends ConsumerState<HistoryExpansionTile>
|
||||
final animationController = useAnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
initialValue: widget.initiallyExpanded ? 1.0 : 0.0,
|
||||
);
|
||||
final animation = Tween(begin: 0.25, end: 0.0).animate(animationController);
|
||||
final animation = Tween(begin: 0.0, end: 0.25).animate(animationController);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final selectedGroupId = ref.watch(selectedRequestGroupIdStateProvider);
|
||||
return ExpansionTile(
|
||||
@ -122,16 +126,16 @@ class _HistoryExpansionTileState extends ConsumerState<HistoryExpansionTile>
|
||||
),
|
||||
onExpansionChanged: (value) {
|
||||
if (value) {
|
||||
animationController.reverse();
|
||||
} else {
|
||||
animationController.forward();
|
||||
} else {
|
||||
animationController.reverse();
|
||||
}
|
||||
},
|
||||
trailing: const SizedBox.shrink(),
|
||||
tilePadding: kPh8,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
collapsedBackgroundColor: colorScheme.surfaceContainerLow,
|
||||
initiallyExpanded: true,
|
||||
initiallyExpanded: widget.initiallyExpanded,
|
||||
childrenPadding: kPv8 + kPe4,
|
||||
children: widget.requestGroups.values.map((item) {
|
||||
return Padding(
|
||||
|
47
lib/screens/history/history_widgets/his_action_buttons.dart
Normal file
47
lib/screens/history/history_widgets/his_action_buttons.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/widgets/widgets.dart';
|
||||
import 'package:apidash/models/models.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
|
||||
class HistoryActionButtons extends ConsumerWidget {
|
||||
const HistoryActionButtons({super.key, this.historyRequestModel});
|
||||
|
||||
final HistoryRequestModel? historyRequestModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final collectionStateNotifier = ref.watch(collectionStateNotifierProvider);
|
||||
final isAvailable = collectionStateNotifier?.values.any((element) =>
|
||||
element.id == historyRequestModel?.metaData.requestId) ??
|
||||
false;
|
||||
final requestId = historyRequestModel?.metaData.requestId;
|
||||
return FilledButtonGroup(buttons: [
|
||||
ButtonData(
|
||||
icon: Icons.copy_rounded,
|
||||
label: kLabelDuplicate,
|
||||
onPressed: requestId != null
|
||||
? () {
|
||||
ref
|
||||
.read(collectionStateNotifierProvider.notifier)
|
||||
.duplicateFromHistory(historyRequestModel!);
|
||||
ref.read(navRailIndexStateProvider.notifier).state = 0;
|
||||
}
|
||||
: null,
|
||||
tooltip: "Duplicate Request",
|
||||
),
|
||||
ButtonData(
|
||||
icon: Icons.north_east_rounded,
|
||||
label: kLabelRequest,
|
||||
onPressed: isAvailable && requestId != null
|
||||
? () {
|
||||
ref.read(selectedIdStateProvider.notifier).state = requestId;
|
||||
ref.read(navRailIndexStateProvider.notifier).state = 0;
|
||||
}
|
||||
: null,
|
||||
tooltip: isAvailable ? "Go to Request" : "Couldn't find Request",
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:apidash/extensions/extensions.dart';
|
||||
import 'package:apidash/providers/providers.dart';
|
||||
import 'package:apidash/utils/utils.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
import '../history_requests.dart';
|
||||
import 'his_action_buttons.dart';
|
||||
|
||||
class HistoryPageBottombar extends ConsumerWidget {
|
||||
const HistoryPageBottombar({
|
||||
@ -16,59 +19,87 @@ class HistoryPageBottombar extends ConsumerWidget {
|
||||
final requestGroup = getRequestGroup(
|
||||
historyMetas?.values.toList(), selectedRequestModel?.metaData);
|
||||
final requestCount = requestGroup.length;
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: Container(
|
||||
height: 60 + MediaQuery.paddingOf(context).bottom,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 1,
|
||||
),
|
||||
|
||||
return Container(
|
||||
height: 60 + MediaQuery.paddingOf(context).bottom,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
requestCount > 1
|
||||
? Badge(
|
||||
label: Text(
|
||||
requestCount > 9 ? '9 +' : requestCount.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(maxWidth: 500),
|
||||
child: const HistorRequestsScrollableSheet());
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.keyboard_arrow_up_rounded,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
child: context.isMediumWindow
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
HistoryActionButtons(historyRequestModel: selectedRequestModel),
|
||||
HistorySheetButton(requestCount: requestCount)
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: HistoryActionButtons(
|
||||
historyRequestModel: selectedRequestModel)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistorySheetButton extends StatelessWidget {
|
||||
const HistorySheetButton({
|
||||
super.key,
|
||||
required this.requestCount,
|
||||
});
|
||||
|
||||
final int requestCount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCompact = context.isCompactWindow;
|
||||
const icon = Icon(Icons.keyboard_arrow_up_rounded);
|
||||
return Badge(
|
||||
isLabelVisible: requestCount > 1,
|
||||
label: Text(
|
||||
requestCount > 9 ? '9+' : requestCount.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
child: FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(44, 44),
|
||||
padding: isCompact ? kP4 : const EdgeInsets.fromLTRB(16, 12, 8, 12),
|
||||
),
|
||||
onPressed: requestCount > 1
|
||||
? () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: const HistorRequestsScrollableSheet());
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: isCompact
|
||||
? icon
|
||||
: const Row(
|
||||
children: [
|
||||
Text(
|
||||
"Show All",
|
||||
style: kTextStyleButton,
|
||||
),
|
||||
kHSpacer5,
|
||||
icon,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -19,13 +19,14 @@ class RequestResponseTabs extends StatelessWidget {
|
||||
child: EditorPaneRequestURLCard(),
|
||||
),
|
||||
kVSpacer10,
|
||||
RequestResponseTabbar(
|
||||
SegmentedTabbar(
|
||||
controller: controller,
|
||||
tabs: const [
|
||||
Tab(text: kLabelRequest),
|
||||
Tab(text: kLabelResponse),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: RequestResponseTabviews(
|
||||
controller: controller,
|
||||
))
|
||||
Expanded(child: RequestResponseTabviews(controller: controller))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
60
lib/widgets/button_group_filled.dart
Normal file
60
lib/widgets/button_group_filled.dart
Normal file
@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
|
||||
class FilledButtonGroup extends StatelessWidget {
|
||||
const FilledButtonGroup({super.key, required this.buttons});
|
||||
|
||||
final List<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,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,20 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:apidash/consts.dart';
|
||||
|
||||
class RequestResponseTabbar extends StatelessWidget {
|
||||
const RequestResponseTabbar({
|
||||
class SegmentedTabbar extends StatelessWidget {
|
||||
const SegmentedTabbar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.tabs,
|
||||
this.tabWidth = kSegmentedTabWidth,
|
||||
this.tabHeight = kSegmentedTabHeight,
|
||||
});
|
||||
|
||||
final TabController controller;
|
||||
final List<Widget> tabs;
|
||||
final double tabWidth;
|
||||
final double tabHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: kReqResTabWidth,
|
||||
height: kReqResTabHeight,
|
||||
margin: kPh4,
|
||||
width: tabWidth * tabs.length,
|
||||
height: tabHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: kBorderRadius20,
|
||||
border: Border.all(
|
||||
@ -40,14 +47,7 @@ class RequestResponseTabbar extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
controller: controller,
|
||||
tabs: const <Widget>[
|
||||
Tab(
|
||||
text: kLabelRequest,
|
||||
),
|
||||
Tab(
|
||||
text: kLabelResponse,
|
||||
),
|
||||
],
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
),
|
@ -20,6 +20,10 @@ class RequestDataTable extends StatelessWidget {
|
||||
final clrScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final List<DataColumn> columns = [
|
||||
const DataColumn2(
|
||||
label: Text(''),
|
||||
fixedWidth: 8,
|
||||
),
|
||||
DataColumn2(
|
||||
label: Text(keyName ?? kNameField),
|
||||
),
|
||||
@ -30,6 +34,10 @@ class RequestDataTable extends StatelessWidget {
|
||||
DataColumn2(
|
||||
label: Text(valueName ?? kNameValue),
|
||||
),
|
||||
const DataColumn2(
|
||||
label: Text(''),
|
||||
fixedWidth: 8,
|
||||
),
|
||||
];
|
||||
|
||||
final fieldDecoration = InputDecoration(
|
||||
@ -52,6 +60,7 @@ class RequestDataTable extends StatelessWidget {
|
||||
.map<DataRow>(
|
||||
(MapEntry<String, String> entry) => DataRow(
|
||||
cells: <DataCell>[
|
||||
const DataCell(kHSpacer5),
|
||||
DataCell(
|
||||
ReadOnlyTextField(
|
||||
initialValue: entry.key,
|
||||
@ -67,6 +76,7 @@ class RequestDataTable extends StatelessWidget {
|
||||
decoration: fieldDecoration,
|
||||
),
|
||||
),
|
||||
const DataCell(kHSpacer5),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
export 'button_clear_response.dart';
|
||||
export 'button_copy.dart';
|
||||
export 'button_discord.dart';
|
||||
export 'button_group_filled.dart';
|
||||
export 'button_repo.dart';
|
||||
export 'button_save_download.dart';
|
||||
export 'button_send.dart';
|
||||
@ -48,7 +49,7 @@ export 'splitview_drawer.dart';
|
||||
export 'splitview_dashboard.dart';
|
||||
export 'splitview_equal.dart';
|
||||
export 'splitview_history.dart';
|
||||
export 'tabbar_request_response.dart';
|
||||
export 'tabbar_segmented.dart';
|
||||
export 'table_map.dart';
|
||||
export 'table_request.dart';
|
||||
export 'tabs.dart';
|
||||
|
Reference in New Issue
Block a user