Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/consts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
1 change: 1 addition & 0 deletions lib/providers/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
52 changes: 52 additions & 0 deletions lib/providers/status_message_provider.dart
Original file line number Diff line number Diff line change
@@ -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<GlobalStatusBarManager, StatusMessage>((ref) {
return GlobalStatusBarManager(ref);
});

class GlobalStatusBarManager extends StateNotifier<StatusMessage> {
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);
}
}
21 changes: 13 additions & 8 deletions lib/screens/home_page/editor_pane/editor_pane.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
],
);
}
}
99 changes: 99 additions & 0 deletions lib/screens/home_page/editor_pane/global_status_bar.dart
Original file line number Diff line number Diff line change
@@ -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<GlobalStatusBar> createState() => _GlobalStatusBarState();
}

class _GlobalStatusBarState extends ConsumerState<GlobalStatusBar> {
bool _isExpanded = false;
List<String> _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),
),
),
],
),
);
}
}
50 changes: 50 additions & 0 deletions lib/utils/status_validator.dart
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
}