wip: history panes

This commit is contained in:
DenserMeerkat
2024-07-20 22:05:08 +05:30
parent 8eadd7f8fa
commit f8ede1edc8
26 changed files with 473 additions and 71 deletions

View File

@ -9,6 +9,7 @@ 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

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

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
class HistoryRequestPane extends ConsumerWidget {
const HistoryRequestPane({
super.key,
this.isCompact = false,
});
final bool isCompact;
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedId = ref.watch(selectedHistoryIdStateProvider);
final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider);
final headersMap = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.headersMap)) ??
{};
final headerLength = headersMap.length;
final paramsMap = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.paramsMap)) ??
{};
final paramLength = paramsMap.length;
final hasBody = ref.watch(selectedHistoryRequestModelProvider
.select((value) => value?.httpRequestModel.hasBody)) ??
false;
return RequestPane(
selectedId: selectedId,
codePaneVisible: codePaneVisible,
onPressedCodeButton: () {
ref.read(historyCodePaneVisibleStateProvider.notifier).state =
!codePaneVisible;
},
showViewCodeButton: !isCompact,
showIndicators: [
paramLength > 0,
headerLength > 0,
hasBody,
],
children: [
RequestDataTable(
rows: paramsMap,
keyName: kNameURLParam,
),
RequestDataTable(
rows: headersMap,
keyName: kNameHeader,
),
const SizedBox(),
],
);
}
}

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

@ -49,9 +49,8 @@ class HistoryURLCard extends StatelessWidget {
),
isCompact ? kHSpacer10 : kHSpacer20,
Expanded(
child: RawTextField(
readOnly: true,
controller: TextEditingController(text: url),
child: ReadOnlyTextField(
initialValue: url,
style: kCodeStyle.copyWith(
fontSize: fontSize,
),

View File

@ -4,7 +4,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
import './details_pane/url_card.dart';
import 'package:apidash/screens/common_widgets/common_widgets.dart';
import 'details_pane/url_card.dart';
import 'details_pane/his_request_pane.dart';
import 'details_pane/his_response_pane.dart';
class HistoryDetails extends StatefulHookConsumerWidget {
const HistoryDetails({super.key});
@ -21,22 +24,62 @@ class _HistoryDetailsState extends ConsumerState<HistoryDetails>
ref.watch(selectedHistoryRequestModelProvider);
final metaData = selectedHistoryRequest?.metaData;
final codePaneVisible = ref.watch(historyCodePaneVisibleStateProvider);
final TabController controller =
useTabController(initialLength: 2, vsync: this);
return selectedHistoryRequest != null
? Column(
? LayoutBuilder(
builder: (context, constraints) {
final isCompact = constraints.maxWidth < kMediumWindowWidth;
return Column(
children: [
kVSpacer5,
Padding(
padding: kP4,
padding: kPh4,
child: HistoryURLCard(
method: metaData!.method, url: metaData.url)),
method: metaData!.method,
url: metaData.url,
)),
kVSpacer10,
if (isCompact) ...[
RequestResponseTabbar(
controller: controller,
),
kVSpacer10,
Expanded(
child: TabBarView(
controller: controller,
children: [
HistoryRequestPane(
isCompact: isCompact,
),
const HistoryResponsePane(),
],
))
] else ...[
Expanded(
child: Padding(
padding: kPh4,
child: RequestDetailsCard(
child: EqualSplitView(
leftWidget: HistoryRequestPane(
isCompact: isCompact,
),
rightWidget: codePaneVisible
? const CodePane(isHistoryRequest: true)
: const HistoryResponsePane(),
),
),
),
),
kVSpacer8,
]
],
);
},
)
: const Text("No Request Selected");
: const SizedBox.shrink();
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/extensions/extensions.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/utils/utils.dart';
import 'history_pane.dart';
import 'history_viewer.dart';
@ -16,11 +17,14 @@ class HistoryPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyModel = ref.watch(selectedHistoryRequestModelProvider);
final title = historyModel != null
? getHistoryRequestName(historyModel.metaData)
: 'History';
if (context.isMediumWindow) {
return DrawerSplitView(
scaffoldKey: scaffoldKey,
mainContent: const HistoryViewer(),
title: Text(historyModel?.historyId ?? 'History'),
title: Text(title),
leftDrawerContent: const HistoryPane(),
actions: const [SizedBox(width: 16)],
onDrawerChanged: (value) =>

View File

@ -56,6 +56,7 @@ class HistoryList extends HookConsumerWidget {
title: Text(
humanizeDate(date),
),
initiallyExpanded: true,
children: requestGroups.values.map((item) {
return Padding(
padding: kPv2 + kPh4,
@ -79,9 +80,11 @@ class HistoryList extends HookConsumerWidget {
);
}).toList()
: [
const Text(
const Center(
child: Text(
'No history',
style: TextStyle(color: Colors.grey),
),
)
],
),

View File

@ -1,3 +1,4 @@
import 'package:apidash/consts.dart';
import 'package:flutter/material.dart';
import 'package:apidash/providers/history_providers.dart';
import 'package:apidash/utils/history_utils.dart';
@ -10,23 +11,29 @@ class HistoryRequests extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedRequestId = ref.watch(selectedHistoryIdStateProvider);
final selectedRequest = ref.read(selectedHistoryRequestModelProvider);
final historyMetas = ref.read(historyMetaStateNotifier);
final selectedRequest = ref.watch(selectedHistoryRequestModelProvider);
final historyMetas = ref.watch(historyMetaStateNotifier);
final requestGroup = getRequestGroup(
historyMetas?.values.toList(), selectedRequest?.metaData);
return Column(
children: requestGroup
.map((request) => SidebarHistoryCard(
children: [
kVSpacer20,
...requestGroup.map((request) => Padding(
padding: kPv2 + kPh4,
child: HistoryRequestCard(
id: request.historyId,
method: request.method,
model: request,
isSelected: selectedRequestId == request.historyId,
onTap: () {
ref.read(selectedHistoryIdStateProvider.notifier).state =
request.historyId;
ref
.read(historyMetaStateNotifier.notifier)
.loadHistoryRequest(request.historyId);
},
models: [request],
),
))
.toList(),
],
);
}
}

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

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

View File

@ -13,6 +13,13 @@ String humanizeDate(DateTime? date) {
return DateFormat('MMMM d, yyyy').format(date);
}
String humanizeTime(DateTime? time) {
if (time == null) {
return "";
}
return DateFormat('hh:mm:ss a').format(time);
}
String humanizeDuration(Duration? duration) {
if (duration == null) {
return "";

View File

@ -1,10 +1,22 @@
import 'package:apidash/models/models.dart';
import 'package:apidash/utils/convert_utils.dart';
import 'package:apidash/models/models.dart';
import 'package:apidash/consts.dart';
DateTime stripTime(DateTime dateTime) {
return DateTime(dateTime.year, dateTime.month, dateTime.day);
}
RequestModel getRequestModelFromHistoryModel(HistoryRequestModel model) {
return RequestModel(
id: model.historyId,
name: model.metaData.name,
responseStatus: model.httpResponseModel.statusCode,
message: kResponseCodeReasons[model.httpResponseModel.statusCode],
httpRequestModel: model.httpRequestModel,
httpResponseModel: model.httpResponseModel,
);
}
String getHistoryRequestName(HistoryMetaModel model) {
if (model.name.isNotEmpty) {
return model.name;

View File

@ -0,0 +1,65 @@
import 'package:apidash/models/history_meta_model.dart';
import 'package:flutter/material.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/utils/utils.dart';
import 'texts.dart';
class HistoryRequestCard extends StatelessWidget {
const HistoryRequestCard({
super.key,
required this.id,
required this.model,
this.isSelected = false,
this.onTap,
});
final String id;
final HistoryMetaModel model;
final bool isSelected;
final Function()? onTap;
@override
Widget build(BuildContext context) {
final Color color = Theme.of(context).colorScheme.surface;
final Color colorVariant =
Theme.of(context).colorScheme.surfaceContainerHighest.withOpacity(0.5);
final Color surfaceTint = Theme.of(context).colorScheme.primary;
return Card(
shape: const ContinuousRectangleBorder(borderRadius: kBorderRadius12),
elevation: isSelected ? 1 : 0,
surfaceTintColor: isSelected ? surfaceTint : null,
color: isSelected
? Theme.of(context).colorScheme.brightness == Brightness.dark
? colorVariant
: color
: color,
margin: EdgeInsets.zero,
child: InkWell(
onTap: onTap,
borderRadius: kBorderRadius6,
hoverColor: colorVariant,
focusColor: colorVariant.withOpacity(0.5),
child: Padding(
padding: kPv6 + kPh8,
child: SizedBox(
height: 20,
child: Row(
children: [
Expanded(
child: Text(
humanizeTime(model.timeStamp),
softWrap: false,
overflow: TextOverflow.fade,
style: kCodeStyle,
),
),
kHSpacer4,
StatusCode(statusCode: model.responseStatus),
],
),
),
),
),
);
}
}

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,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,
@ -76,10 +70,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 +96,7 @@ class _RequestPaneState extends State<RequestPane>
kVSpacer5,
Expanded(
child: TabBarView(
controller: _controller,
controller: controller,
physics: const NeverScrollableScrollPhysics(),
children: widget.children,
),
@ -109,10 +104,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(
showClearButton
? ClearResponseButton(
onPressed: onClearResponse,
)
: const SizedBox.shrink(),
],
),
),

View File

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:data_table_2/data_table_2.dart';
import 'package:apidash/widgets/widgets.dart';
import 'package:apidash/consts.dart';
class RequestDataTable extends StatelessWidget {
const RequestDataTable({
super.key,
required this.rows,
this.keyName,
this.valueName,
});
final Map<String, String> rows;
final String? keyName;
final String? valueName;
@override
Widget build(BuildContext context) {
final clrScheme = Theme.of(context).colorScheme;
final List<DataColumn> columns = [
DataColumn2(
label: Text(keyName ?? kNameField),
),
const DataColumn2(
label: Text('='),
fixedWidth: 30,
),
DataColumn2(
label: Text(valueName ?? kNameValue),
),
];
final fieldDecoration = InputDecoration(
contentPadding: const EdgeInsets.only(bottom: 12),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: clrScheme.primary.withOpacity(
kHintOpacity,
),
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: clrScheme.surfaceContainerHighest,
),
),
);
final List<DataRow> dataRows = rows.entries
.map<DataRow>(
(MapEntry<String, String> entry) => DataRow(
cells: <DataCell>[
DataCell(
ReadOnlyTextField(
initialValue: entry.key,
decoration: fieldDecoration,
),
),
const DataCell(
Text('='),
),
DataCell(
ReadOnlyTextField(
initialValue: entry.value,
decoration: fieldDecoration,
),
),
],
),
)
.toList();
return Container(
margin: kP10,
child: Column(
children: [
Expanded(
child: Theme(
data: Theme.of(context)
.copyWith(scrollbarTheme: kDataTableScrollbarTheme),
child: DataTable2(
columnSpacing: 12,
dividerThickness: 0,
horizontalMargin: 0,
headingRowHeight: 0,
dataRowHeight: kDataTableRowHeight,
bottomMargin: kDataTableBottomPadding,
isVerticalScrollBarVisible: true,
columns: columns,
rows: dataRows,
),
),
),
kVSpacer40,
],
),
);
}
}

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

@ -4,6 +4,7 @@ export 'button_discord.dart';
export 'button_repo.dart';
export 'button_save_download.dart';
export 'button_send.dart';
export 'card_history_request.dart';
export 'card_request_details.dart';
export 'card_sidebar_environment.dart';
export 'card_sidebar_history.dart';
@ -28,6 +29,7 @@ export 'field_cell.dart';
export 'field_header.dart';
export 'field_json_search.dart';
export 'field_raw.dart';
export 'field_read_only.dart';
export 'field_url.dart';
export 'intro_message.dart';
export 'json_previewer.dart';
@ -46,9 +48,9 @@ export 'splitview_drawer.dart';
export 'splitview_dashboard.dart';
export 'splitview_equal.dart';
export 'splitview_history.dart';
export 'suggestions_menu.dart';
export 'tabbar_request_response.dart';
export 'tables.dart';
export 'table_map.dart';
export 'table_request.dart';
export 'tabs.dart';
export 'texts.dart';
export 'uint8_audio_player.dart';

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

@ -1,12 +1,9 @@
import 'dart:io';
import 'package:spot/spot.dart';
import 'package:apidash/consts.dart';
import 'package:apidash/providers/providers.dart';
import 'package:apidash/screens/common_widgets/common_widgets.dart';
import 'package:apidash/screens/dashboard.dart';
import 'package:apidash/screens/envvar/environment_page.dart';
import 'package:apidash/screens/home_page/collection_pane.dart';
import 'package:apidash/screens/home_page/editor_pane/details_card/code_pane.dart';
import 'package:apidash/screens/home_page/editor_pane/details_card/response_pane.dart';
import 'package:apidash/screens/home_page/editor_pane/editor_default.dart';
import 'package:apidash/screens/home_page/editor_pane/editor_pane.dart';

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