diff --git a/lib/consts.dart b/lib/consts.dart index 628598e47..a6047abae 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -485,3 +485,17 @@ const kMsgClearHistory = const kMsgClearHistorySuccess = 'History cleared successfully'; const kMsgClearHistoryError = 'Error clearing history'; const kMsgShareError = "Unable to share"; + +// Status Bar Constants +const kStatusBarHeight = 40.0; +const kStatusBarFontSize = 14.0; +const kStatusBarDefaultMessage = "Global Status Bar"; + +const kStatusBarExpandIconSize = 24.0; +const kStatusBarExpandedPadding = EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0); +const kStatusBarIconPaddingOffset = 32.0; +const kStatusBarFontWeight = FontWeight.w500; +const kStatusBarTextStyle = TextStyle( + fontSize: kStatusBarFontSize, + fontWeight: kStatusBarFontWeight, +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 29fc6e595..438226eba 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -3,3 +3,4 @@ export 'environment_providers.dart'; export 'history_providers.dart'; export 'settings_providers.dart'; export 'ui_providers.dart'; +export 'status_message_provider.dart'; \ No newline at end of file diff --git a/lib/providers/status_message_provider.dart b/lib/providers/status_message_provider.dart new file mode 100644 index 000000000..5b7ed44c5 --- /dev/null +++ b/lib/providers/status_message_provider.dart @@ -0,0 +1,52 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/utils/status_validator.dart'; +import 'package:apidash/consts.dart'; + +enum StatusMessageType { defaultType, info, warning, error } + +class StatusMessage { + final String message; + final StatusMessageType type; + + StatusMessage(this.message, this.type); +} + +final statusMessageProvider = + StateNotifierProvider((ref) { + return GlobalStatusBarManager(ref); +}); + +class GlobalStatusBarManager extends StateNotifier { + final Ref ref; + final StatusValidator _validator = StatusValidator(); + + GlobalStatusBarManager(this.ref) + : super(StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType)) { + ref.listen(selectedRequestModelProvider, (previous, next) { + if (next?.httpRequestModel != null) { + final httpModel = next!.httpRequestModel!; + final method = httpModel.method; + final body = httpModel.body; + final contentType = httpModel.bodyContentType; + + final newMessage = _validator.validateRequest(method, body, contentType: contentType); + // Only update if the new message is different + if (newMessage.message != state.message || newMessage.type != state.type) { + _updateStatusMessage(newMessage); + } + } else { + _resetStatusMessage(); + } + }); + } + + void _updateStatusMessage(StatusMessage newMessage) { // Updates the status message + state = newMessage; + } + + void _resetStatusMessage() { + state = StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType); + } +} \ No newline at end of file diff --git a/lib/screens/home_page/editor_pane/editor_pane.dart b/lib/screens/home_page/editor_pane/editor_pane.dart index 7ff67f5c1..36da4bd03 100644 --- a/lib/screens/home_page/editor_pane/editor_pane.dart +++ b/lib/screens/home_page/editor_pane/editor_pane.dart @@ -3,19 +3,24 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'editor_default.dart'; import 'editor_request.dart'; +import 'global_status_bar.dart'; class RequestEditorPane extends ConsumerWidget { - const RequestEditorPane({ - super.key, - }); + const RequestEditorPane({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - if (selectedId == null) { - return const RequestEditorDefault(); - } else { - return const RequestEditor(); - } + + return Column( + children: [ + Expanded( + child: selectedId == null + ? const RequestEditorDefault() + : const RequestEditor(), + ), + const GlobalStatusBar(), + ], + ); } } diff --git a/lib/screens/home_page/editor_pane/global_status_bar.dart b/lib/screens/home_page/editor_pane/global_status_bar.dart new file mode 100644 index 000000000..b3ccac215 --- /dev/null +++ b/lib/screens/home_page/editor_pane/global_status_bar.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; + +class GlobalStatusBar extends ConsumerStatefulWidget { + const GlobalStatusBar({super.key}); + + @override + ConsumerState createState() => _GlobalStatusBarState(); +} + +class _GlobalStatusBarState extends ConsumerState { + bool _isExpanded = false; + List _cachedLines = []; + String _lastMessage = ''; + + @override + Widget build(BuildContext context) { + final message = ref.watch(statusMessageProvider.select((s) => s.message)); + final type = ref.watch(statusMessageProvider.select((s) => s.type)); + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + _cachedLines = message != _lastMessage ? message.split('\n') : _cachedLines; + _lastMessage = message; + final needsExpansion = _cachedLines.length > 1; + + final color = switch (type) { + StatusMessageType.info => kColorSchemeSeed, + StatusMessageType.warning => kColorHttpMethodPut, + StatusMessageType.error => kColorDarkDanger, + _ => isDarkMode ? kColorWhite : kColorBlack, + }; + + final icon = switch (type) { + StatusMessageType.error => Icons.error_outline, + StatusMessageType.warning => Icons.warning_amber_outlined, + StatusMessageType.info => Icons.info_outline, + _ => null, + }; + + return Container( + width: double.infinity, + color: icon != null + ? color.withOpacity(kForegroundOpacity) + : isDarkMode + ? Theme.of(context).colorScheme.surface + : kColorWhite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: kPh12, + height: kStatusBarHeight, + child: Row( + children: [ + if (icon != null) ...[ + Icon(icon, size: kButtonIconSizeSmall, color: color), + kHSpacer8, + ], + Expanded( + child: Text( + _cachedLines.isNotEmpty ? _cachedLines.first : '', + style: kStatusBarTextStyle.copyWith(color: color), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (needsExpansion) + InkWell( + onTap: () => setState(() => _isExpanded = !_isExpanded), + customBorder: const CircleBorder(), + child: Icon( + _isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, + size: kStatusBarExpandIconSize, + color: color, + ), + ), + ], + ), + ), + if (_isExpanded && needsExpansion) + Container( + width: double.infinity, + padding: kStatusBarExpandedPadding.copyWith( + left: kStatusBarExpandedPadding.left + + (icon != null ? kStatusBarIconPaddingOffset : 0), + ), + child: Text( + _cachedLines.skip(1).join('\n'), + style: kStatusBarTextStyle.copyWith(color: color), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/utils/status_validator.dart b/lib/utils/status_validator.dart new file mode 100644 index 000000000..2ac8d9c57 --- /dev/null +++ b/lib/utils/status_validator.dart @@ -0,0 +1,50 @@ +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash/providers/providers.dart'; +import 'package:apidash/consts.dart'; +import 'dart:convert'; + +class StatusValidator { + StatusMessage validateRequest(HTTPVerb method, String? body, {ContentType? contentType}) { + // Check for GET requests with body + if (_isInvalidGetRequest(method, body)) { + return StatusMessage( + "GET requests cannot have a body. Remove the body or change the method to POST.", + StatusMessageType.warning, + ); + } + + //simple check for JSON validation for testing + if (contentType == ContentType.json && body != null && body.isNotEmpty) { + final jsonValidation = _validateJson(body); + if (jsonValidation != null) { + return jsonValidation; + } + } + + return StatusMessage(kStatusBarDefaultMessage, StatusMessageType.defaultType); + } + + bool _isInvalidGetRequest(HTTPVerb method, String? body) { + return method == HTTPVerb.get && body != null && body.isNotEmpty; + } + + StatusMessage? _validateJson(String jsonText) { + if (jsonText.trim().isEmpty) return null; + + try { + json.decode(jsonText); + return null; // Valid JSON + } catch (e) { + // Extract the error message + final errorMsg = e.toString(); + final simplifiedError = errorMsg.contains('FormatException') + ? 'Invalid JSON: ${errorMsg.split('FormatException: ').last}' + : 'Invalid JSON format'; + + return StatusMessage( + simplifiedError, + StatusMessageType.error, + ); + } + } +} \ No newline at end of file